1. 前書き
こんにちは。
サイオステクノロジーの田川です。
今回は、Elasticsearch で実際に日本語でのベクトル検索を行ってみたいと思います。
対象者
- Elastic Cloud のアカウントを持っている人(トライアルライセンスを含む)
- Elasticsearch の初心者~中級者
できるようになること
- Elasticsearch で日本語のベクトル検索(ANN)を行えるようになる。
前提条件
- Elastic Cloud (version: 8.15.0)
- Elastic Cloud 上に日本語でのベクトル検索の準備が完了している。
前回の記事に日本語用のベクトル検索の準備作業を記載しています。
(2024年10月28日時点の情報を元に記載しています。)
2. ベクトル検索の種類
前回、ベクトル検索を行うための準備を行いました。
今回は、実際にベクトル検索を行ってみたいと思いますが、
その前に Elasticsearch における密ベクトルに対する検索の種類について説明したいと思います。
Elasticsearch では密ベクトルに対し2種類の検索方法が用意されています。
- 厳密に、クエリに最も近いベクトルを検索する方法(Exact kNN)
- 厳密ではないが、クエリにある程度近いものを高速に検索する方法(Approximate kNN(ANN))
今回は、ある程度近いものを取得できればよいので、後者の方法(ANN)を利用します。
(*脚注1)1
3. ベクトル検索の実行
いくつか検索してみます。
3.1 ベクトル検索(その1)
クエリ1: “桃太郎が鬼が島へ向かった際の乗り物は”
まず、キーワード検索をしてみます。
(前回のブログ記事の中で、momotaro_v3 インデックスに桃太郎の本文をベクトル値を含めて登録済です。)
GET /momotaro_v3/_search
{
"_source": false,
"fields": ["chunk_no", "content"],
"query": {
"match": {
"content": "桃太郎が鬼が島へ向かった際の乗り物は"
}
}
}
上記のキーワード検索を行っても、top 10 以内に「船」という情報は見つかりません。
次にANNベクトル検索を行ってみます。
Elasticsearch で ANNベクトル検索を行いたい場合、”knn” を利用します。
(*脚注2)2
GET /momotaro_v3/_search
{
"_source": false,
"fields": ["chunk_no", "content"],
"knn": {
"field": "text_embedding.predicted_value",
"k": "10",
"num_candidates": "100",
"query_vector_builder": {
"text_embedding": {
"model_id": ".multilingual-e5-small_linux-x86_64",
"model_text": "桃太郎が鬼が島へ向かった際の乗り物は"
}
}
}
}
(*脚注3)3
結果
...
第3位
"chunk_no" : [ 133 ],
"content": " 桃太郎は...また船に乗りました。..."
第4位
"chunk_no" : [ 113 ],
"content": " 桃太郎も...船の進むにしたがって..."
...
第9位
"chunk_no": [ 105 ],
"content": " 桃太郎と、...この船に乗り込みました。"
いくつか「船」を示唆する情報がヒットしています。
ここで注目なのは、検索クエリである
“桃太郎が鬼が島へ向かった際の乗り物は”
から算出されるベクトル値を明示していないことです。
ベクトル検索では、検索クエリのベクトル値に近いベクトル値を持つドキュメントを検索するので、
検索クエリのベクトル値が必要なはずです。
にもかかわらずベクトル検索を行うことができます。
これは、
"query_vector_builder": {
"text_embedding": {
"model_id": "モデルid",
"model_text": "検索したいクエリ"
}
}
の部分に入力された情報を元に Elasticsearch が
“検索したいクエリ” → ベクトルの生成
を内部的に行ってくれているためです。
このように、Elasticsearch では、ドキュメントの登録時だけでなく、
検索時もベクトル化の処理を実装することなく、ベクトル検索を行うことができます。
3.2 ベクトル検索(その2)
別の質問をしてみます。
クエリ2: “桃から産まれたのは”
まず、キーワード検索をしてみます。
GET /momotaro_v3/_search
{
"_source": false,
"fields": ["chunk_no", "content"],
"query": {
"match": {
"content": "桃から産まれたのは?"
}
}
}
第5位
"chunk_no": [ 40 ],
"content": " そして桃の中から生まれた子だというので、この子に桃太郎という名をつけました。"
ヒットはしましたが、5位に表示されました。
ベクトル検索をしてみます。
GET /momotaro_v3/_search
{
"_source": false,
"fields": ["chunk_no", "content"],
"knn": {
"field": "text_embedding.predicted_value",
"k": "10",
"num_candidates": "100",
"query_vector_builder": {
"text_embedding": {
"model_id": ".multilingual-e5-small_linux-x86_64",
"model_text": "桃から産まれたのは?"
}
}
}
}
第1位
"chunk_no": [ 40 ],
"content": " そして桃の中から生まれた子だというので、この子に桃太郎という名をつけました。"
1位に表示されました。
キーワード検索では、”産まれる” と “生まれる” は全く別の単語という扱いのため、上位にヒットしませんでしたが、
ベクトル検索では、”産まれる” と “生まれる” が近いベクトルを持っているため、
上位にヒットしたものと考えられます。
3.3 ベクトル検索(その3)
ベクトル検索のちょっと変わった使い方としては、別の言語による検索も(ある程度)可能です。
クエリ3: “Who was born from a peach ?”
キーワード検索では、全くヒットしません。
元の文章が日本語で書かれているのに英語で検索を行っているためです。
一方、ベクトル検索では、第3位に表示されました。
第3位
"chunk_no": [ 40 ],
"content": " そして桃の中から生まれた子だというので、この子に桃太郎という名をつけました。"
“peach” と “桃”、”born” と “生まれる” が近いものとして捉えられているためだと思われます。
4. まとめ
今回、ANNによるベクトル検索を行い、ある程度期待した結果を得ることができました。
しかも、独自にベクトル化の処理を実装する必要はありませんでした。
ただし、ベクトル検索は万能ではありません。キーワード検索の方が適している場合もあります。
次回は、キーワード検索とベクトル検索の両方を行うハイブリッド検索を行ってみたいと思います。
- Elasticsearch は、超大規模なデータであっても動作することを念頭に、
時間やメモリを消費して厳密な値を求めることよりも、
近似値であっても、より速く、より省メモリーで算出する傾向にあるように感じます。
ANN だけでなく、統計情報の中央値などの算出に関しても、
(厳密な値ではないかもしれませんが)近似値をより早く算出するような動きになっています。
(厳密な値を求めようとして、莫大な時間がかかってタイムアウトになったり、
莫大なメモリーを必要として Out of memory になってエラーになることは避け,
近似値であっても、限られたリソース(時間、メモリ)内で答えを出そうとします。)
どうしても最近傍探索を行いたいという方は、下記を参考にしてみてください。
cosineSimilarity 関数を利用しますが、ANN検索よりもかなり手間がかかります。
https://www.elastic.co/guide/en/elasticsearch/reference/current/knn-search.html#exact-knn
https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-script-score-query.html
https://www.elastic.co/jp/blog/text-similarity-search-with-vectors-in-elasticsearch ↩︎ - Elasticsearch で、ANN検索を行う場合、”knn” パラメータを指定して検索を行います。
(ANNなのに”knn”を指定するのでややこしいのですが、そういう風になっている、と理解してください。) ↩︎ - “.multilingual-e5-small_linux-x86_64” では、検索クエリのプレフィックスとして
“query: ” を付けた方がいいという仕様のようですが、
今回は、サンプルなので、プレフィックスは付けていません。
https://huggingface.co/intfloat/multilingual-e5-small#faq ↩︎