アレクサスキル『図書館サーチ』を公開しました

こんにちは。

半年ほどAlexaスキルをコツコツ作っています。

今月も1件スキルが公開されました!その名も『図書館サーチ』です。

f:id:WorldWorldWorld:20180820213111p:plain 図書館サーチ

図書館サーチでできることや仕組みをご紹介します。

図書館サーチでできること

読みたい本が、近所のどの図書館で借りられるかを教えてくれます。

「アレクサ、図書館サーチでハリーポッターとアズカバンの囚人を探して」

検索した結果はアレクサアプリにも表示されます。

『図書館サーチ』の仕組み

このスキルはAPI寄せ集め組み合わせてできています。使っているAPIは3つ。

以下の図に従って簡単に説明していきます。 f:id:WorldWorldWorld:20180821202315p:plain

①②ユーザーが喋ったキーワードを元に、Amazon Product Advertising APIから候補となる本を探す

このスキルにおける一番の難点は検索対象の本の絞り込みです。

最終的に図書館から蔵書状況を検索するカーリルAPIでは、特定のISBNコードが必要です。そのためには検索する本を一冊に絞らなければなりません。

今回は深いことを考えずに「ユーザーの喋ったタイトルでAmazon検索をかけて、一番最初に当てはまった本」という乱暴な方法取りました。


Amazon検索をプログラム上で実施するにはAmazon Product Advertising APIを利用します。

https://docs.aws.amazon.com/ja_jp/AWSECommerceService/latest/DG/Welcome.htmldocs.aws.amazon.com

以前、rubyAPIを試したとき、公式のライブラリがめちゃくちゃ使いづらかった記憶があるのですが、nodejsでは amazon-product-api - npmというとっても簡単なライブラリがありました。

www.npmjs.com

ただしなぜか、単一であるはずのプロパティも全て配列状で返ってくる仕様になっているので、そこだけ注意が必要です。

こんな感じで実行するとjson形式で結果を返してくれます。

const amazon = require('amazon-product-api');
const amazon_client = amazon.createClient({
    awsId: process.env.AWS_ID, // AWSで取得したトークンID
    awsSecret: process.env.AWS_SECRET, //AWSで取得したアクセスシークレット
    awsTag: process.env.AWS_TAG // AmazonアソシエイトのID
});

const query = {
    Keywords: booktitle, // ユーザーが喋った本のタイトル
    searchIndex: "Books",  // 検索対象カテゴリ
    responseGroup: "ItemAttributes",  // 取得する情報
    domain: 'webservices.amazon.co.jp' // 検索するドメイン(国)
}

// 検索の実行
const amazon_search_result = await amazon_client.itemSearch(query);

// 1件目のISBNコードを取得
const isbn = amazon_search_result[i].ItemAttributes[0]['ISBN'][0];

③④郵便番号をもとに位置情報(緯度、経度)を取得する

「近所の図書館」を探すには、そもそも自分=Amazon Echoがどこにいるのかを知る必要があります。

Amazon Echoではあらかじめ住所情報を設定することができ、図書館サーチでは住所情報の中の郵便番号を利用します。
f:id:WorldWorldWorld:20180820220230p:plain

住所情報、そして郵便番号を取得するコードは以下のとおり。

const { requestEnvelope, serviceClientFactory } = handlerInput;
const { deviceId } = requestEnvelope.context.System.device;
const deviceAddressServiceClient = serviceClientFactory.getDeviceAddressServiceClient();
const address = await deviceAddressServiceClient.getFullAddress(deviceId); // 住所情報を取得
const adress.postalCode; // 住所情報から郵便番号を取得

次に、この郵便番号が指す緯度・経度を求めます。それにはHeartRailsから提供されているGeoAPIを使います。 geoapi.heartrails.com

このAPI、キーの取得が不要で無制限に使えます。すごい。使い方もとても簡単です。

// Amazon Echoに設定された郵便番号をGetパラメータに指定
const geodata = await axios.get(`http://geoapi.heartrails.com/api/json?method=searchByPostal&postal=${address.postalCode}`);

// 取得した緯度・経度を「,」で結合(郵便番号では複数の位置情報候補が見つかるので、とりあえず先頭のものを利用。
)
const geocode = `${geodata.data.response.location[0].x},${geodata.data.response.location[0].y}`;

⑤⑥カーリルの図書館APIで近所の図書館を探す

カーリルの図書館APIは図書館そのものの検索や、各図書館での蔵書状況を調べることができるAPIです。利用にはAPIキーの取得が必要ですが、無料で使うことができます。

図書館 API | カーリル

まずは先ほどGeoAPIで取得した緯度・経度を使って、近所の図書館を10件取得します。

//緯度・経度を「,」で連結した値(geocode)をGetパラメータに渡して呼び出す
const calilresponse = await axios.get(`http://api.calil.jp/library?appkey=${カーリルで取得したAPIキー}&limit=10&format=json&geocode=${}&callback= `);

// 見つかった図書館の一覧
const librarydata = calilresponse.data;

⑦⑧図書館ごとに本の貸出状況を調べる

カーリルの図書館APIでは、図書館をあるグループで識別するsystemidと本のisbnコードを指定することで、その図書館での本の貸出状況を調べることができます。

だたし注意点として、カーリルAPIにリクエストを送るとその先で図書館ごとの個別のシステムへ問い合わせが行われており、レスポンス時間に差があります。レスポンスに時間がかかる図書館では、一旦「問い合わせ中」の結果が返ってくるため、APIの呼び出し元では再帰的な問い合わせが必要です。

// カーリルAPIで貸出状況を検索する
// 問い合わせが終わらなければ最大3回まで、2秒おきに検索する。
const recursiveBookSearch = async function(isbn,systemid,count=0,session="") {
    let url = `http://api.calil.jp/check?appkey=${カーリルで取得したAPIキー}&isbn=${isbn}&systemid=${systemid}&format=json&callback=no`;
    if(session) {
        url = url + `&session=${session}`;
    }
    const reserveddata = (await axios.get(url)).data;
    if(reserveddata .continue && count < 3) {
        return await new Promise(resolve=>{
            setTimeout(()=>{
                resolve(recursiveBookSearch(isbn,systemid,count+1,reserveddata .session));
            },2000);
        });
    } else{
        return reserveddata ;
    }
};

// JSON配列からsystemidを順に取得
const systemid = librarydata[i].systemid;
// 再帰処理を呼び出す
const reserveddata = await recursiveBookSearch(isbn,systemid);

reserveddataにはsystemidにぶら下がる個別の図書館(libkey)の貸出状況と、systemidごとの貸出システムのURLが格納されているので、これらをAlexaの応答カードに書いてレスポンスを返します。

//systemidにぶら下がる個別の図書館ごとの貸出状況
const libkeystatus = reserveddata.books[isbn][systemid].libkey;
//systemidの貸出状況URL
const reserveurl = reserveddata.books[isbn][systemid].reserveurl;

コード全文はGitHubで。
GitHub - quotto/library-skill

改善したいこと

改善したいことは大きく3つあります。

本の絞り込み

1つ目が検索対象となる本の正確な絞り込み。

断片的なキーワードでは候補となる本はたくさんあるし、フルタイトルであってもハードカーバーや文庫本など、複数の選択肢が発生します。

しかしそれらを一つずつ音声で確認していくのは非常に煩雑であり、なかなか解決方法が思いつかないのが正直なところです。

レスポンス

2つ目は応答性能の向上。

前述の通りカーリルAPIでは、最終的に各図書館システムへ問い合わせを行うため、システムによって応答速度にばらつきがあります。

そのため、このスキルでは検索を終えるために最大1分ほど時間を要することがあります。

音声インターフェースで1分の待ち時間は致命的ですね。これもなかなか解決方法が思いつかないのですが、自身でキャッシュを持つなど、改善を試みたいです。

提供する情報を増やす

3つ目がアレクサアプリへのレスポンス表示です。

現状では「探した本のタイトル」「図書館名」「URL」ですが、

  • 本の画像も載せたい:これは扱う画像(Amazonの商品画像)の権利の問題がクリアーできませんでした。
  • URLをクリックできるようにしたい:ブラウザからは問題ないのですが、アレクサアプリからはURLのリンクが貼れません。標準機能のレスポンスではリンクが埋められているので、何か方法があるのかも・・・?
  • 図書館の位置情報とか提供したい:URLとの関連で、見つかった図書館の位置情報(GoogleMapのリンクとか)を埋め込んで提供できれば少し使い勝手が良くなるのではないかと思います。



以上、Alexaスキル『図書館サーチ』のご紹介でした!