はじめに
Algolia は簡単に使える全文検索サービスです
ざっくり言うと、テキスト検索や条件による絞り込み機能を自分の Web サイトに簡単に組み込めます
例えば、以下のようなオンラインストア風画面を簡単に実装できます
とりあえず少ないデータでやる分には無料で試せます
以下は Algolia のデモサイトです
大体どんなことをするものか分かったと思います
というわけで、 Elixir から Algolia のデータを検索してみます
今回も Livebook を使います
ゆくゆくは LiveView に組み込んでいきます
実装したノートブックはこちら
Algolia のアカウント作成
2023/02/17 現在の方法です
Algolia のトップページ右上の START FREE をクリックします
メールアドレス、パスワードを入力し、利用規約への同意、ロボットではないことの証明をしてサインアップします
必要な項目を埋めていきます
メール検証をするように言われるので、自分のメールアドレスに届いている検証用メールから Verify Your Email のボタンをクリックします
ダッシュボードが表示されればアカウント作成は完了です
データの準備
今回はお試し用に Algolia の公開データセットを使います
色々データはありますが、ecommerce = Eコマース(電子商取引) = EC = オンラインストア用データを使ってみます
以下のデータ本体(products.json)とデータの設定(products_configuration.json)をダウンロードしておきます
インデックスの作成
Algolia に戻って、インデックス(データを入れておくもの)を作成して先程用意したデータを格納します
ダッシュボードの左メニュー下側にあるロケットアイコン をクリックします
すると、下画像のように最初のインデックスを作る画面が表示されます
products と入力して Create index をクリックします
続いて、作成したインデックスにデータをインポートします
Upload your records |> Upload file と選択し、用意しておいた products.json をアップロードします
データがインポートされるとインデックスの画面に遷移します(遷移しなかったらダッシュボードの一覧下にインデックスへのリンクがあるのでそこから遷移します)
画面中央あたりにある Manage index |> Import Configuration をクリックします
用意しておいた products_configuration.json を選択し、 Settings と Rule の両方にチェックを付けてテキストボックスに IMPORT を入力してから Import Configuration をクリックします
これでデータと設定が準備できました
APIキーの取得
左メニュー上側の家アイコン からダッシュボードに戻ります
画面中央あたりにある API Keys をクリックします
API キーの画面に遷移します
ここで表示される Application ID と Admin API Key を後で使います
Algolia 用の Elixir モジュール
elixir algolia で Google 検索すると、二つのモジュールがヒットします
2023年2月17日現在、それぞれ最終更新日は以下のようになっていました
- algolia-elixir 5年前
- Algoliax 6ヶ月前
というわけで、比較的更新されている Algoliax の方を使います
セットアップ
Livebook で新しいノートブックを開き、セットアップを実行します
Mix.install([
{:algoliax, "~> 0.7"},
{:req, "~> 0.3"},
{:kino, "~> 0.8"}
])
Req はデータセットの設定情報をダウンロードしてくるのに使います
Kino はデータを見やすく表示するために使います
本来であれば Mix.install
に config
を以下のように設定します
config: [
algoliax: [
api_key: <APIキー>,
application_id: <アプリケーションID>
]
]
しかし、 Livebook に秘密情報などを残したくないので、後から環境変数で指定します
認証情報の設定
アプリケーションIDは見えても良い情報なので、 Kino.Input.text
でテキストボックスを作成します
application_id = Kino.Input.text("Application ID")
表示されたテキストボックスに Algolia の API キー画面からアプリケーションIDをコピー&ペーストします
次のコードを実行することで環境変数にアプリケーションIDをセットします
application_id
|> Kino.Input.read()
|> then(&System.put_env("ALGOLIA_APPLICATION_ID", &1))
APIキーは秘密情報なので、 Kino.Input.password
で入力値が隠れるテキストボックスを作成します
Algolia の API キー画面から Admin API KEY をコピー&ペーストします
次のコードを実行することで環境変数にAPIキーをセットします
api_key
|> Kino.Input.read()
|> then(&System.put_env("ALGOLIA_API_KEY", &1))
インデックス設定の取得
Algoliax の README に従うと、インデックスにアクセスするためのモジュールを定義するようになっています
Usage
defmodule People do use Algoliax.Indexer, index_name: :algoliax_people, object_id: :reference, algolia: [ attributes_for_faceting: ["age"], searchable_attributes: ["full_name"], custom_ranking: ["desc(updated_at)"] ] defstruct reference: nil, last_name: nil, first_name: nil, age: nil end
ここで引数 algolia
にインデックスの設定情報を入れています
このモジュールで指定しているインデックス設定と、 Algolia 上のインデックス設定が異なる場合、 Algoliax はモジュールの内容で Algolia の内容を上書きに行きます
もし引数 algolia
を指定しない場合、設定が全て nil になって消されてしまいます
なので、必ず正しい設定情報を指定する必要があります
また、設定情報を読み書きできる権限が必要なため、 Admin API KEY を使う必要があります
というわけで、先程ダウンロードしていた products_configuration.json を Elixir でも読み込みます
configuration =
"https://raw.githubusercontent.com/algolia/datasets/master/ecommerce-federated/products_configuration.json"
|> Req.get!()
|> then(&Jason.decode!(&1.body))
Algoliax で使える形に整形します
settings =
configuration["settings"]
|> Enum.into([], fn {key, value} ->
{
key |> Inflex.underscore() |> String.to_atom(),
value
}
end)
結果は以下のようになります
[
advanced_syntax: true,
alternatives_as_exact: ["ignorePlurals", "singleWordSynonym"],
attribute_for_distinct: nil,
...
snippet_ellipsis_text: "",
typo_tolerance: "strict",
unretrievable_attributes: nil
]
モジュールの定義
インデックス名を :products
に指定して Products
モジュールを定義します
defmodule Products do
use Algoliax.Indexer,
index_name: :products,
algolia: settings
end
検索
ここまで準備すると後は単純で、 search
にテキストを入れるだけで検索できます
{:ok, %Algoliax.Response{response: response}} = Products.search("Nike")
response
このとき、デバッグ情報としてインデックスの設定を上書きしているのが見えます(初回実行時のみ)
07:31:01.027 [debug] CONFIGURE_INDEX: PUT https://xxxxxxxxxx.algolia.net/1/indexes/products/settings, body: %{"queryLanguages" => ["en"], "removeWordsIfNoResults" => "none", "snippetEllipsisText" => "", ...}
07:31:01.783 [debug] GET_SETTINGS: GET https://xxxxxxxxxx-dsn.algolia.net/1/indexes/products/settings
07:31:02.146 [debug] SEARCH: POST https://xxxxxxxxxx.algolia.net/1/indexes/products/query, body: %{query: "Shirt"}
結果は以下のようになります
%{
"exhaustive" => %{"nbHits" => true, "typo" => true},
"exhaustiveNbHits" => true,
"exhaustiveTypo" => true,
"hits" => [
...
"serverTimeMS" => 3
}
非常に項目が多いので、一部を切り出します
- hitsPerPage: 1ページあたりの件数
- nbHits: ヒットした件数
- nbPages: ページ数
- page: 現在のページ番号
{
response["hitsPerPage"],
response["nbHits"],
response["nbPages"],
response["page"],
}
結果は以下のようになります
{20, 339, 17, 0}
ページ指定する場合は以下のようにします
Products.search("Shirt", %{page: 1})
また、1ページあたりの件数を指定することもできます
Products.search("Shirt", %{hits_per_page: 100})
ヒットしたデータの1件を見てみましょう
項目が非常に多いので、ツリー表示します
response["hits"]
|> List.first()
|> Kino.Tree.new()
結果は以下のようになります
brand
や name
などはヒットした商品の属性情報です
_
から始まる情報はメタデータになっています
ハイライト
_highlightResult
には、検索したテキストがどの項目にヒットしたのか、という情報が格納されています
対象はインデックスの設定情報で attributes_to_highlight
に指定した項目です
response["hits"]
|> List.first()
|> then(& &1["_highlightResult"])
値は以下のようになっています
%{
"brand" => %{"matchLevel" => "none", "matchedWords" => [], "value" => "Polo Ralph Lauren"},
"list_categories" => [
%{"matchLevel" => "none", "matchedWords" => [], "value" => "Men"},
%{"matchLevel" => "none", "matchedWords" => [], "value" => "Clothing"},
%{
"fullyHighlighted" => false,
"matchLevel" => "full",
"matchedWords" => ["shirt"],
"value" => "<em>Shirt</em>s"
}
],
"name" => %{
"fullyHighlighted" => false,
"matchLevel" => "full",
"matchedWords" => ["shirt"],
"value" => "<em>Shirt</em> Polo Ralph Lauren pink"
}
}
brand
ブランド名ではヒットしていません
list_categories
カテゴリリストの中では Shirts
の Shirt
の部分がヒットしています
name
商品名は Shirt Polo Ralph Lauren pink
の中から Shirt
の部分がヒットしています
スニペット
_snippetResult
にも、検索したテキストがどの項目にヒットしたのか、という情報が格納されています
対象はインデックスの設定情報で attributes_to_snippet
に指定した項目です
response["hits"]
|> Enum.filter(& &1["_snippetResult"]["description"]["value"] != "")
|> List.first()
|> then(& &1["_snippetResult"])
値は以下のようになります
%{
"description" => %{
"matchLevel" => "full",
"value" => "Ralph Lauren has interpreted the slim-fit <em>shirt</em> with an"
}
}
description
に含まれる shirt
という単語が完全一致しています
まとめ
Algolia からデータ検索することができました
もっとモジュールが充実するように、とにかく使ってみて Issue を上げたりしたいですね