検索結果改善支援ツールQuepidでElasticsearchの結果を評価してみる

# はじめに

検索結果の改善を支援するためのツールに [Quepid](https://quepid.com/) （キューピッド）というものがあります。
Quepidのソースコードは[GitHub](https://github.com/o19s/quepid)で公開されており、2019年に初回コミットがあり現在も継続的に改修が行われています（Quepid自体は2014年頃にはあったよう [link](https://opensourceconnections.com/blog/2014/06/10/what-is-search-relevancy/)）

Quepidという存在については数年前から知っていたものの、具体的に何が出来るのか、評価・改善をどう行うのかは分かっていなかったため、今回Quepidについて調査してその内容をまとめます。
もし有用そうであれば、検索結果を今後人手で評価する時に、利用または参考にしたいと思っています。

![](https://quepid.com/images/screenshot-home.png)
[引用元](https://quepid.com/why-quepid/)

# 本記事で分かること

最初にQuepidについての概要について触れた後、ローカル環境でQuepidを起動して最終的に検索結果を評価するところまで試します。

* Quepid概要やコア概念
* ローカル環境でのQuepidの起動方法
* ローカル環境でQuepidとElasticsearchを連携して検索結果を評価する方法
  * ケース（Elasticsearchとの連携、評価クエリの追加）作成
  * 検索結果の評価

:::note info
本記事は動作させることを主目的にしているため、検索結果の評価用スコアを出すところまでの流れを主に記述します。
そのため、具体的な改善方法や評価方法に主眼を置いた記述はありません。
:::


# Quepid概要

検索エンジンの結果の品質を評価・改善するためのオープンソースのツールです。
検索エンジンとして今回はElasticsearchを用いますが、SolrやOpenSearch、Algoriaなど他エンジンとの連携もサポートしています。

Quepidの特徴としては以下が挙げられます。

特徴|内容
---|---
検索結果の評価|検索クエリに対する結果が適切かどうかを手動で評価する
クエリやスコアリングのチューニング|クエリやスコアリングアルゴリズムを調整しながら、検索結果のランキングを改善する
定量評価|PrecisionやRecall、nDCGなどの評価指標の計算が可能
チームでのコラボレーション|複数のユーザーが同じチームに参加することで評価を共有出来るため、チーム全体で検索精度の改善が行える


# Quepidのコア概念

[Core Concepts](https://quepid.com/docs/#getting-started-overview)にQuepidで重要な概念（用語として捉えて良さそう）がまとめられていました。
以下に本記事で主に利用する用語を列挙しました。

用語|定義
---|---
Case|単一検索エンジンにおけるクエリと関連性設定
Query|ケース内のクエリは、キーワードやその他の検索条件とそれに対応する検索結果セットを指す
Result|クエリ内の個々の結果ドキュメントを指す。これらが評価されクエリの累積スコアとなる。
Rating| 個々の検索結果の関連性を示すスコア。各評価の解釈はクエリやケースで使用するスコアラーに依存
Scorer|クエリ結果を評価するための指標を指す（e.g. AP, RR, CG, DCG, NDCGなど）。カスタムスコアラーを作成することも可能

これらを作成したり操作したりすることで、検索結果のスコアを算出することが出来ます。本記事で評価指標としてnDCGを用いて検索結果のnDCGスコアを算出します。

その他に、本記事ではほとんど登場しないですが、 `ブック（プロジェクト管理、複数ケースの一元管理）`や `チーム（ユーザーグループ）` などQuepidで重要となる用語があります。


# Quepid起動前の事前準備

ここから実際に手を動かしていきます。

今回QuepidとElasticsearchを連携して結果を評価します。Quepidを起動する前に、Elasticsearchにデータ登録するところまで実施し、検索リクエストを行えるよう事前に準備します。

詳細は割愛しますが、以下の手順で行いました。

1. Elasticsearchをローカル環境に起動する
2. インデックス `esci-products` を作成する
3. インデックス `esci-products` に149422件のデータを登録する
* 登録データは、商品検索用データセット[esci-data](https://github.com/amazon-science/esci-data)を利用
* 「localeが"jp"のデータ」かつ「titleとdescriptionが両方とも存在するデータ」に絞って登録

※登録するにあたり [search-research](https://github.com/johtani/search-research) を参考にさせて頂きました。

マッピング定義は以下を明示的に定義しています。

```json
{
  "mappings": {
    "properties": {
      "product_id": {
        "type": "keyword"
      },
      "product_title": {
        "type": "keyword",
        "ignore_above": 256,
        "fields": {
          "ja": {
            "type": "text",
            "analyzer": "kuromoji"
          },
          "en": {
            "type": "text",
            "analyzer": "standard"
          }
        }
      },
      "product_description": {
        "type": "keyword",
        "ignore_above": 256,
        "fields": {
          "ja": {
            "type": "text",
            "analyzer": "kuromoji"
          },
          "en": {
            "type": "text",
            "analyzer": "standard"
          }
        }
      },
      "product_bullet_point": {
        "type": "keyword",
        "ignore_above": 256,
        "fields": {
          "ja": {
            "type": "text",
            "analyzer": "kuromoji"
          },
          "en": {
            "type": "text",
            "analyzer": "standard"
          }
        }
      },
      "product_brand": {
        "type": "keyword"
      },
      "product_color": {
        "type": "keyword"
      },
      "product_locale": {
        "type": "keyword"
      },
      "products_dense_vector": {
        "type": "dense_vector",
        "dims": 512,
        "index": true,
        "similarity": "cosine"
      },
      "type": {
        "type": "keyword"
      },
      "image": {
        "type": "keyword",
        "index": false,
        "doc_values": false
      }
    }
  },
  "settings": {}
}
```




:::note info
**ElasticsearchのCORS設定について**

Elasticsearch側にCORS設定がないためエラーになるケースがあります。
エラーになった場合は、elasticsearch.ymlに以下を追記して起動し直してください。

```
http.cors.enabled: true
http.cors.allow-origin: "*"
http.cors.allow-headers: "X-Requested-With, Content-Type, Content-Length, Authorization"
http.cors.allow-methods: "OPTIONS, HEAD, GET, POST, PUT"
```

:::




# ローカル環境でのQuepidの起動方法

次にQuepidをセットアップしていきます。

Quepid関連のコンテンを立ち上げるために、[quepid](https://github.com/o19s/quepid)リポジトリの `docker-compose.yml`, `docker-compose.prod.yml`や[Installation-Guide](https://github.com/o19s/quepid/wiki/Installation-Guide)の情報を参考にしました。

GitHubにあるdocker-compose.ymlを用いてコンテナ起動しようとしましたが、自分の環境では動きませんでした。そのため、ymlファイルを少し修正した以下を用いて起動しました。

```yml:compose.yml
services:
  app:
    container_name: quepid_app
    image: o19s/quepid:latest
    environment:
      - PORT=3000
      - RACK_ENV=production
      - RAILS_ENV=production
      - DATABASE_URL=mysql2://root:password@mysql:3306/quepid
      - FORCE_SSL=false
      - MAX_THREADS=2
      - WEB_CONCURRENCY=2
      - SECRET_KEY_BASE=some_value_needed_here
      - RAILS_LOG_TO_STDOUT=true
      - RAILS_SERVE_STATIC_FILES=true
      - EMAIL_MARKETING_MODE=false
      - QUEPID_DEFAULT_SCORER=nDCG@10 #デフォルト評価指標用の環境変数
      - COMMUNAL_SCORERS_ONLY=false
      - QUERY_LIST_SORTABLE=true
    ports:
      - 3000:3000
    links:
      - mysql
    depends_on:
      - mysql

  mysql:
    container_name: quepid_db
    image: mysql:8.3.0
    ports:
      - 3306:3306
    environment:
      - MYSQL_ROOT_PASSWORD=password
```

今回は検索結果を評価するために必要最低限のコンテナのみ（app, MySQL）を起動しています。また、Elasticsearchは別で起動していますが、上記の `compose.yml` に含めても問題ありません。

上記内容のcompose.ymlファイルを作成後、以下の順にコマンドを実行します。

```
% docker compose up -d mysql
% docker compose run --rm app bin/rake db:setup ### appコンテナを起動する前にmysqlにデータベースquepidが存在している必要があるため
% docker compose up -d app
```

コンテナ起動後の構成は以下のようになります。MySQLはQuepid上で追加更新した情報を格納するために利用します。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129694/4d5b5f8f-8ffc-1c56-6247-f5af7dcb230c.png" width=500></img>


コンテナが全て正常に起動した後 http://localhost:3000 にアクセスすると以下のような画面が表示されます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129694/d5f31998-4fb2-5e24-bdeb-0f6eac8c50e4.png" width=500></img>

最初にアカウントを作成します。今回の用途はローカル環境用のため、作成できれば問題ないと思います。作成すると以下の画面に遷移します。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129694/0c00b859-0193-3302-e2fb-46b68824ac40.png" width=500></img>



# ケース作成

アカウント作成後、ケースを作成します。
ここからようやく検索結果を評価する準備に入っていきます。

手順等は以下リンクによくまとまっていますので参考に。

* https://quepid.com/docs/
* https://quepid.com/tutorials/

「Relevancy Case」の作成から以下の順に必要な情報を入力してケースを作成します。

No|内容
---|---
1|ウェルカムのポップアップが表示されるのでContinueをクリック<br>!<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129694/c5438f18-815f-06e7-83cf-265c679aecfc.png" width=500></img>
2|ケース名「ESCI Search」を入力してContinueをクリック<br>!<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129694/c0723146-db97-5723-017a-c192b526d860.png" width=500></img>
3|接続先の検索エンジンを設定します。URL(`http://localhost:9200/esci-products/_search`)など各種必要な情報を入力してContinueをクリック<br>!<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129694/8b478297-8b28-91b0-ea1d-5563b652b186.png" width=500></img>
4|結果を評価する時に表示する情報を入力してContinueをクリック。 `thumb:` や `image:` で結果に画像を表示することも可能なようです。<br>!<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129694/9224125b-a171-4983-0dab-32b5fa1812b7.png" width=500></img>
5|評価したいクエリを追加してContinueをクリック<br>!<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129694/08b8419f-1ab1-4623-825f-326fe18aa233.png" width=500></img>
6|ケース作成完了<br><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129694/4b6ed741-b614-d373-325b-c16cfe4687ee.png" width=500></img>

元の画面に戻って何度かリロードすると、以下のようなElasticsearchから取得した件数が表示されます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129694/ada36b6b-7821-024d-7fb9-01e50249dc29.png" width=500></img>

折りたたみ表示を展開するとElasticsearchのレスポンス情報が表示されていたり、今回は評価指標としてnDCGを選択したため、4段階で検索結果アイテムを評価出来ることが分かります。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129694/5aae01e4-6cb8-21f2-1d8c-9d39759edab8.png" width=500></img>



# 検索結果の評価

「Tune Relevance」というリンクをクリックすると画面の右側に「Query Sandbox」が表示されます。
「Query Sandbox」ではElasticsearchにリクエストするElasticsearchクエリを確認することが出来ます。デフォルトでは`multi_matchクエリ`の`type:best_fields`で 検索対象が`タイトルフィールド`のクエリになっているようです。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129694/b82d0ad2-8186-69c8-c3e6-96844b1feeb8.png" width=500></img>

試しにdescriptionも検索対象に加えた後、`Rerun My Searches!` をクリックして再検索を行ってみました。検索対象のフィールドを増やしたことで、ヒット件数が増えたことを確認しました。

検索結果のTOP10を評価してみます。4段階で良しなに評価してみました。
すると `nDCGスコア0.78` が表示されました。
人手での評価がやはりネックですが、検索結果のスコアがすぐに算出・可視化されました。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129694/a31b721f-486f-f934-30ee-7d657cd471da.png" width=500></img>


この後、試しにbest_fieldsからmost_fieldsに変更して再検索してみましたが、スコアはすぐには出ませんでした。おそらく検索結果のTOP10が変わってしまい、まだ評価されていない商品（文書）がTOP10に並んでしまったためと思われます。
そのため、お試しで改善を確認するにはなかなか難しいと感じました。もしかしたら、最初は数件のデータで試したほうが良いかもしれません。


**＜検索チューニングポイント＞**

チューニングポイントとしては以下が挙げられます。評価した商品（文書）が出てくればこのあたりの検証も行いやすそうです。

* クエリ修正
* ランキングアルゴリズム、スコアリングの調整
* パラメータチューニング（インデックス設定や検索パラメータ調整など）



# まとめ

検索結果の改善を支援するツールQuepidについて調査してみました。
評価指標を選択して検索結果を人手で評価してチューニングして再評価するというサイクルを回せそうな可能性を感じましたが、使いこなすにはまだまだ知見が必要だと思いました。

検索結果の個々の商品（文書）を人手で評価する時に、実サービス同等の検索結果UIを表示したいというような要望もありそうですが、カスタマイズ出来るかは分からずとなります。

もっと容易に検索改善を試す方法があるのか、今回使っていない機能にどのようなものがあるのか等については時間がある時に調査したいと思います。