Elixir Livebook で Algolia からデータ検索してみる

## はじめに

Algolia は簡単に使える全文検索サービスです

https://www.algolia.com/

ざっくり言うと、テキスト検索や条件による絞り込み機能を自分の Web サイトに簡単に組み込めます

例えば、以下のようなオンラインストア風画面を簡単に実装できます

![algolia_temp.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/db7fd08b-f290-7343-aa7c-c0f733d19271.png)

とりあえず少ないデータでやる分には無料で試せます

以下は Algolia のデモサイトです

https://instantsearchjs.netlify.app/examples/react/e-commerce/search/

https://instantsearchjs.netlify.app/examples/react/tourism/

大体どんなことをするものか分かったと思います

というわけで、 Elixir から Algolia のデータを検索してみます

今回も Livebook を使います

https://livebook.dev/

ゆくゆくは LiveView に組み込んでいきます

実装したノートブックはこちら

https://github.com/RyoWakabayashi/elixir-learning/blob/main/livebooks/algolia/algolia_search.livemd

## Algolia のアカウント作成

:::note info
2023/02/17 現在の方法です
:::

Algolia のトップページ右上の START FREE をクリックします

![スクリーンショット 2023-02-17 15.04.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/bf0eb0ea-4178-4deb-d98a-0b7cff7a84f8.png)

メールアドレス、パスワードを入力し、利用規約への同意、ロボットではないことの証明をしてサインアップします

![スクリーンショット 2023-02-17 15.05.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/8cb43c68-f895-30d9-a002-1c49c46e2186.png)

必要な項目を埋めていきます

![スクリーンショット 2023-02-17 15.07.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/03826ea0-472d-c7eb-df1b-02a7898a0e6f.png)

メール検証をするように言われるので、自分のメールアドレスに届いている検証用メールから Verify Your Email のボタンをクリックします

![スクリーンショット 2023-02-17 15.28.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/354ea081-776e-59ff-7dab-fe191f7c3330.png)

ダッシュボードが表示されればアカウント作成は完了です

![スクリーンショット 2023-02-17 15.34.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/3a6cec00-6c3f-a096-5344-17b278598bfb.png)

## データの準備

今回はお試し用に Algolia の公開データセットを使います

https://github.com/algolia/datasets

色々データはありますが、ecommerce = Eコマース（電子商取引） = EC = オンラインストア用データを使ってみます

https://github.com/algolia/datasets/tree/master/ecommerce-federated

以下のデータ本体（products.json）とデータの設定（products_configuration.json）をダウンロードしておきます

https://github.com/algolia/datasets/raw/master/ecommerce-federated/products.json

https://github.com/algolia/datasets/raw/master/ecommerce-federated/products_configuration.json

## インデックスの作成

Algolia に戻って、インデックス（データを入れておくもの）を作成して先程用意したデータを格納します

ダッシュボードの左メニュー下側にあるロケットアイコン :rocket: をクリックします

すると、下画像のように最初のインデックスを作る画面が表示されます

products と入力して Create index をクリックします

![スクリーンショット 2023-02-17 15.43.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/ecef02c0-c2eb-9c3c-3dd9-ddcf90c41422.png)

続いて、作成したインデックスにデータをインポートします

Upload your records |> Upload file と選択し、用意しておいた products.json をアップロードします

![スクリーンショット 2023-02-17 15.49.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/455224eb-a287-3df1-326e-a623f335c1af.png)

データがインポートされるとインデックスの画面に遷移します（遷移しなかったらダッシュボードの一覧下にインデックスへのリンクがあるのでそこから遷移します）

画面中央あたりにある　Manage index |> Import Configuration をクリックします

![スクリーンショット 2023-02-17 15.52.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/5a37b6c3-6b69-6648-076a-96784a161886.png)

用意しておいた products_configuration.json を選択し、 Settings と Rule の両方にチェックを付けてテキストボックスに IMPORT を入力してから Import Configuration をクリックします

![スクリーンショット 2023-02-17 15.52.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/c1bafab2-0d7f-b9d8-716c-283a4da3f3b9.png)

これでデータと設定が準備できました

## APIキーの取得

左メニュー上側の家アイコン :house: からダッシュボードに戻ります

画面中央あたりにある API Keys をクリックします

![スクリーンショット 2023-02-17 15.34.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/3a6cec00-6c3f-a096-5344-17b278598bfb.png)

API キーの画面に遷移します

![スクリーンショット 2023-02-17 16.11.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/08ce6580-bca4-f35e-4fb6-009d1fc842f7.png)

ここで表示される　Application ID と Admin API Key を後で使います

## Algolia 用の Elixir モジュール

elixir algolia で Google 検索すると、二つのモジュールがヒットします

https://github.com/sikanhe/algolia-elixir

https://github.com/WTTJ/algoliax

2023年2月17日現在、それぞれ最終更新日は以下のようになっていました

- algolia-elixir 5年前
- Algoliax 6ヶ月前

というわけで、比較的更新されている Algoliax の方を使います

## セットアップ

Livebook で新しいノートブックを開き、セットアップを実行します

```elixir
Mix.install([
  {:algoliax, "~> 0.7"},
  {:req, "~> 0.3"},
  {:kino, "~> 0.8"}
])
```

[Req](https://github.com/wojtekmach/req) はデータセットの設定情報をダウンロードしてくるのに使います

[Kino](https://github.com/livebook-dev/kino) はデータを見やすく表示するために使います

本来であれば `Mix.install` に `config` を以下のように設定します

```elixir
config: [
    algoliax: [
      api_key: <APIキー>,
      application_id: <アプリケーションID>
    ]
  ]
```

しかし、 Livebook に秘密情報などを残したくないので、後から環境変数で指定します

## 認証情報の設定

アプリケーションIDは見えても良い情報なので、 `Kino.Input.text` でテキストボックスを作成します

```elixir
application_id = Kino.Input.text("Application ID")
```

表示されたテキストボックスに Algolia の API キー画面からアプリケーションIDをコピー＆ペーストします

次のコードを実行することで環境変数にアプリケーションIDをセットします

```elixir
application_id
|> Kino.Input.read()
|> then(&System.put_env("ALGOLIA_APPLICATION_ID", &1))
```

APIキーは秘密情報なので、 `Kino.Input.password` で入力値が隠れるテキストボックスを作成します

Algolia の API キー画面から Admin API KEY をコピー＆ペーストします

次のコードを実行することで環境変数にAPIキーをセットします

```elixir
api_key
|> Kino.Input.read()
|> then(&System.put_env("ALGOLIA_API_KEY", &1))
```

## インデックス設定の取得

[Algoliax の README](https://github.com/WTTJ/algoliax) に従うと、インデックスにアクセスするためのモジュールを定義するようになっています

> ## Usage
>
> ```elixir
> 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 でも読み込みます

```elixir
configuration =
  "https://raw.githubusercontent.com/algolia/datasets/master/ecommerce-federated/products_configuration.json"
  |> Req.get!()
  |> then(&Jason.decode!(&1.body))
```

Algoliax で使える形に整形します

```elixir
settings =
  configuration["settings"]
  |> Enum.into([], fn {key, value} ->
    {
      key |> Inflex.underscore() |> String.to_atom(),
      value
    }
  end)
```

結果は以下のようになります

```elixir
[
  advanced_syntax: true,
  alternatives_as_exact: ["ignorePlurals", "singleWordSynonym"],
  attribute_for_distinct: nil,
  ...
  snippet_ellipsis_text: "",
  typo_tolerance: "strict",
  unretrievable_attributes: nil
]
```

## モジュールの定義

インデックス名を `:products` に指定して `Products` モジュールを定義します

```elixir
defmodule Products do
  use Algoliax.Indexer,
    index_name: :products,
    algolia: settings
end
```

## 検索

ここまで準備すると後は単純で、 `search` にテキストを入れるだけで検索できます

```elixir
{:ok, %Algoliax.Response{response: response}} = Products.search("Nike")

response
```

このとき、デバッグ情報としてインデックスの設定を上書きしているのが見えます（初回実行時のみ）

```elixir
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"}
```

結果は以下のようになります

```elixir
%{
  "exhaustive" => %{"nbHits" => true, "typo" => true},
  "exhaustiveNbHits" => true,
  "exhaustiveTypo" => true,
  "hits" => [
  ...
  "serverTimeMS" => 3
}
```

非常に項目が多いので、一部を切り出します

- hitsPerPage: 1ページあたりの件数
- nbHits: ヒットした件数
- nbPages: ページ数
- page: 現在のページ番号

```elixir
{
  response["hitsPerPage"],
  response["nbHits"],
  response["nbPages"],
  response["page"],
}
```

結果は以下のようになります

```elixir
{20, 339, 17, 0}
```

ページ指定する場合は以下のようにします

```elixir
Products.search("Shirt", %{page: 1})
```

また、1ページあたりの件数を指定することもできます

```elixir
Products.search("Shirt", %{hits_per_page: 100})
```

ヒットしたデータの1件を見てみましょう

項目が非常に多いので、ツリー表示します

```elixir
response["hits"]
|> List.first()
|> Kino.Tree.new()
```

結果は以下のようになります

![スクリーンショット 2023-02-17 17.01.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/337ad8c9-d8e6-3592-912f-12b16085ddff.png)

`brand` や `name` などはヒットした商品の属性情報です

`_` から始まる情報はメタデータになっています

### ハイライト

`_highlightResult` には、検索したテキストがどの項目にヒットしたのか、という情報が格納されています

対象はインデックスの設定情報で `attributes_to_highlight` に指定した項目です

```elixir
response["hits"]
|> List.first()
|> then(& &1["_highlightResult"])
```

値は以下のようになっています

```elixir
%{
  "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` に指定した項目です

```elixir
response["hits"]
|> Enum.filter(& &1["_snippetResult"]["description"]["value"] != "")
|> List.first()
|> then(& &1["_snippetResult"])
```

値は以下のようになります

```elixir
%{
  "description" => %{
    "matchLevel" => "full",
    "value" => "Ralph Lauren has interpreted the slim-fit <em>shirt</em> with an"
  }
}
```

`description` に含まれる `shirt` という単語が完全一致しています

## まとめ

Algolia からデータ検索することができました

もっとモジュールが充実するように、とにかく使ってみて Issue を上げたりしたいですね