表題の通り、Elasticsearch 8.4 から待望の近似近傍探索と従来の検索を組み合わたハイブリッド検索が可能になったらしいので、試してみました。

Elascticsearch 8 で導入された近似近傍探索について

Elasticsearch 公式の記事1がわかりやすく近似近傍探索について語られています。 また、日本語では@pakio さんの紹介記事2も非常にわかりやすいので、そちらも御覧ください。

嬉しいけど物足りない点

公式の資料3や@pakio さんの資料でも触れられていますが、

  • You can’t currently use the Query DSL to filter documents for an approximate kNN search. If you need to filter the documents, consider using exact kNN instead.

Elasticsearch の Query DSL との併用不可というのが物足りない点でした。

端的に説明すると Elasticsearch 8 で利用可能になった近似近傍探索は、あくまでベクトル間のみの近似近傍探索のみできるのであって、従来の Elasticsearch の検索機能(term や filter)と近似近傍探索を組み合わせて検索できないということです。

Vespa の開発者の Jo さんも同様の点4について触れていました。

The most surprising part of the announcement is that they won’t allow combining the nearest neighbor search with standard query terms and filters. I think that will be disappointing for many who have been waiting for this feature. That concludes this thread, thoughts 👇🧵 5/5

なぜならベクトルだけの素朴な近似近傍探索なら、正直ベクトル検索エンジンのQdrantMilvusValdでも事足ります。 検索エンジンの Elasticsearch に求められるのは、従来のフィルタリング機能と近似近傍探索を組み合わせたり、term による検索と近似近傍探索を組み合わせなど素朴な近似近傍探索では実現できない点を補える事ができれば近似近傍探索の実用可能性がグンと広がるので、ちょっと物足りないねというのが正直な感想でした。

それを実現するための関連するチケット5などはあったのでいつか実現してほしいなと思っていましたが、Linkedin の投稿で Elasticsearch のエンジニアの方が遂に実現できたよ!!と紹介しており6 、早速試してみたいと思います。

2022/10/30 追記: Elasticsearch version 8.2.0 | Elasticsearch Guide [8.2] | Elastic のリリースで

Integrate filtering support for ANN #84734 (issue: #81788)

と書かれており、ANN でのフィルタリングサポートは Elasticsearch 8.2 からサポートされていたみたいです。 なので正確には Elasticsearch 8.4 からは検索クエリが使えるようになったということですね。

ハイブリッド検索

Elasticsearch の公式ドキュメントはこちら7

このドキュメントを基に、

  • 従来の検索機能
  • 近似近傍探索のみによる検索結果
  • 上記ふたつを組み合わせた検索結果

を一画面で表示できる Web アプリを作ったので結果を比較してみたいと思います。

データやインデキシング自体は前回作成した、Elasticsearch の近似近傍探索を使って、ドラえもんのひみつ道具検索エンジンを作ってみたを基に今回もコードは全て公開しています。release tag はv0.2 にしています。

https://github.com/hurutoriya/doraemon-himitsu-dogu-search/releases/tag/v0.2.0

前回からの変更点を主に書いていきます。

日本語検索の設定

Docker ファイルで日本語用の analyzer であるkuromojiを追加し

1
2
3
4
5
FROM docker.elastic.co/elasticsearch/elasticsearch:8.4.3
# set the specific password for Elasticsearch
RUN echo "elastic" | bin/elasticsearch-keystore add "bootstrap.password" -xf
RUN bin/elasticsearch-plugin install analysis-kuromoji
RUN bin/elasticsearch-plugin install analysis-icu

Elasticsearch の mapping は zozo さんの記事8を参考にして以下のように書いてみました。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
{
  "settings": {
    "analysis": {
      "analyzer": {
        "custome_ja_analyzer": {
          "type": "custom",
          "char_filter": ["icu_normalizer"],
          "tokenizer": "kuromoji_tokenizer",
          "filter": [
            "kuromoji_baseform",
            "kuromoji_part_of_speech",
            "ja_stop",
            "kuromoji_number",
            "kuromoji_stemmer"
          ]
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "vector": {
        "type": "dense_vector",
        "dims": 768,
        "index": true,
        "similarity": "l2_norm"
      },
      "name": {
        "type": "text",
        "analyzer": "custome_ja_analyzer"
      },
      "yomi": {
        "type": "text",
        "analyzer": "custome_ja_analyzer"
      },
      "description": {
        "type": "text",
        "analyzer": "custome_ja_analyzer"
      }
    }
  }
}

kuromoji のおかげで前処理を行わなくても、日本語で解析ができるようになります。

実際に起動してインデックスを構築した、Elasticsearch が動く Docker にリクエストを投げてみると、analyzer によって処理がされます。 検索時にはこれらの tokens が転置インデックス上で検索されます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
curl --cacert http_ca.crt -u elastic:elastic -X POST "https://localhost:9200/himitsu_dogu/_analyze?pretty" -H 'Content-Type: application/json' -d'{"analyzer": "custome_ja_analyzer","text":"ドラえもんのひみつ道具はどこでもドア以外にもたくさん存在する"}'
{
  "tokens" : [
    {
      "token" : "ドラえもん",
      "start_offset" : 0,
      "end_offset" : 5,
      "type" : "word",
      "position" : 0
    },
    {
      "token" : "ひむ",
      "start_offset" : 6,
      "end_offset" : 8,
      "type" : "word",
      "position" : 2
    },
    {
      "token" : "道具",
      "start_offset" : 9,
      "end_offset" : 11,
      "type" : "word",
      "position" : 4
    },
    {
      "token" : "どこ",
      "start_offset" : 12,
      "end_offset" : 14,
      "type" : "word",
      "position" : 6
    },
    {
      "token" : "ドア",
      "start_offset" : 16,
      "end_offset" : 18,
      "type" : "word",
      "position" : 8
    },
    {
      "token" : "以外",
      "start_offset" : 18,
      "end_offset" : 20,
      "type" : "word",
      "position" : 9
    },
    {
      "token" : "たくさん",
      "start_offset" : 22,
      "end_offset" : 26,
      "type" : "word",
      "position" : 12
    },
    {
      "token" : "存在",
      "start_offset" : 26,
      "end_offset" : 28,
      "type" : "word",
      "position" : 13
    }
  ]
}

実際に簡単な multi match query(複数フィールドでの一致検索)の結果はこちら

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
$ curl --cacert http_ca.crt -u elastic:elastic -X POST "https://localhost:9200/himitsu_dogu/_search?pretty" -H 'Content-Type: application/json' -d'{"size": 1, "query": {"multi_match" : {"query":"どこでもドア", "fields": [ "name", "description" ] }}}'

{
  "took" : 2,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 44,
      "relation" : "eq"
    },
    "max_score" : 11.774845,
    "hits" : [
      {
        "_index" : "himitsu_dogu",
        "_id" : "816",
        "_score" : 11.774845,
        "_source" : {
          "id" : 816,
          "name" : "どこでもドア",
          "yomi" : "どこでもどあ",
          "description" : "このドアを開けるだけで、行きたいところへ、どこへでも行くことができる。",
          "vector" : [
            -0.23546676337718964,
            ...
            -0.37538981437683105
          ]
        }
      }
    ]
  }
}

これで、Elasticsearch で日本語検索が問題なく出来ていることが確認できたので、ハイブリッド検索についての説明に

結果比較

比較対象

multi match query, ANN, multi match + ANN の三種類のクエリで検索結果を定性的に比較します。 Python 上ではクエリは以下のような形式で書いてみました。TOP_Kは 10 で、keywordは入力クエリです。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
multimatch_query: dict = {
		"size": TOP_K,
		"query": {"multi_match": {"query": keyword, "fields": ["name", "description"]}},
}
ann_query: dict = {
		"knn": {
				"field": "vector",
				"query_vector": sentence_embeddings[0],
				"k": TOP_K,
				"num_candidates": 100,
		}
}
# NOTE: score = match score + ANN score
hybrid_query: dict = {
		"size": TOP_K,
		"query": {"multi_match": {"query":keyword, "fields": ["name", "description"]}},
		"knn": {
				"field": "vector",
				"query_vector": sentence_embeddings[0],
				"k": TOP_K,
				"num_candidates": 100,
		},
}
  • multimatch_query: ひみつ道具の名前と説明文の複数フィールドでマッチするドキュメントを検索する
  • ann_query: ひみつ道具の名前と説明文を空白区切りで結合された文章をJapaneseSententsBERT でベクトル化し、近似近傍探索を行う
  • hybrid_query: 上記2つのクエリのスコアを足し合わせた結果(match score + ANN score)を検索結果として返す。
    • 公式ドキュメントでは、boostを使うことで係数を書けて調整する方法も紹介されています。

実際にデモアプリの Streamlit 上では、上記3つのクエリの検索結果が side by side で比較できるようにしてみました。 これで、各検索手法で結果がどう異なっているか一目瞭然ですね。

streamlit side by side result

Streamlit で得られた結果を表として以下にまとめました。

クエリ: AI

multi machスコアひみつ道具の名前説明ANNスコアひみつ道具の名前説明hybrid(match score + ANN scoreスコアひみつ道具の名前説明
15.3606エーアイアイ高性能(せいのう)の AI(エーアイ/人工知能)が搭載(とうさい)されているサル型ロボット。一から教えてあげると、なんでも自分で考えて行動する。10.0027なんでもデリバリーバックパックデリバリー(配達)用のひみつ道具。中は四次元になっているため、いくらでも配達品を入れることができ、ゆれてもこわれることなく運べる。大きなポケットには、目的地まで自動運転してくれ、ひとこぎで10メートル進む自転車が入っている。運転しながら食べたり飲んだりできる装置(そうち)も付いており、雨がふれば自動でカサもさしてくれる。15.3606エーアイアイ高性能(せいのう)の AI(エーアイ/人工知能)が搭載(とうさい)されているサル型ロボット。一から教えてあげると、なんでも自分で考えて行動する。
20.0027なりきりケイドロセット本格的なおにごっこが楽しめる未来の遊び道具。通信もできる警察(けいさつ)手帳型のバッジや『強力におい追跡鼻(ついせきばな)』、身につけると体力が三割増しになる『泥棒風呂敷(どろぼうふろしき)』、『牢屋(ろうや)シール』などが入っている。20.0027なんでもデリバリーバックパックデリバリー(配達)用のひみつ道具。中は四次元になっているため、いくらでも配達品を入れることができ、ゆれてもこわれることなく運べる。大きなポケットには、目的地まで自動運転してくれ、ひとこぎで10メートル進む自転車が入っている。運転しながら食べたり飲んだりできる装置(そうち)も付いており、雨がふれば自動でカサもさしてくれる。
30.0026地下工事マシンこの機械に設計図を描いた設計紙を入れ、コンクリートボンベを乗せてバルブを開くと、その設計図通りの空間を地下に作ってくれる。30.0027なりきりケイドロセット本格的なおにごっこが楽しめる未来の遊び道具。通信もできる警察(けいさつ)手帳型のバッジや『強力におい追跡鼻(ついせきばな)』、身につけると体力が三割増しになる『泥棒風呂敷(どろぼうふろしき)』、『牢屋(ろうや)シール』などが入っている。
40.0026ミチサキステッキどちらの道を進めばいいか迷ったときに使う道具。正しい道の方向に倒れて、道を教えてくれる。40.0026地下工事マシンこの機械に設計図を描いた設計紙を入れ、コンクリートボンベを乗せてバルブを開くと、その設計図通りの空間を地下に作ってくれる。
50.0026ウラオモテックスこれを体に貼ると、裏でコソコソやっていたことや心の中で思っていることを、みんなの前で大っぴらにやってしまう。50.0026ミチサキステッキどちらの道を進めばいいか迷ったときに使う道具。正しい道の方向に倒れて、道を教えてくれる。
60.0026かべ景色きりかえ機かべを通して、どこの景色でも見ることができる。実際(じっさい)にうつし出した場所とつながるので、かべから出入りもできるようになる。60.0026ウラオモテックスこれを体に貼ると、裏でコソコソやっていたことや心の中で思っていることを、みんなの前で大っぴらにやってしまう。
70.0026クロマキーセットグリーンバックやブルーバックを背景(はいけい)にして、セットのカメラで撮影(さつえい)すると、映(うつ)したものを実際(じっさい)の景色にはめこむことができる。70.0026かべ景色きりかえ機かべを通して、どこの景色でも見ることができる。実際(じっさい)にうつし出した場所とつながるので、かべから出入りもできるようになる。
80.0026断層ビジョン地中や機械の内部、人間の体の中など、外から見えないところを画面に映し出し、調べることができる。映し出したところに付属の旗を立てると、実際のその場所にも旗が立ち、目印になる。80.0026クロマキーセットグリーンバックやブルーバックを背景(はいけい)にして、セットのカメラで撮影(さつえい)すると、映(うつ)したものを実際(じっさい)の景色にはめこむことができる。
90.0026ゆっくり反射ぞうきんこのぞうきんで鏡やガラスを拭くと過去が映る。拭けば拭くほど、映る時間が過去に戻っていく。90.0026断層ビジョン地中や機械の内部、人間の体の中など、外から見えないところを画面に映し出し、調べることができる。映し出したところに付属の旗を立てると、実際のその場所にも旗が立ち、目印になる。
100.0026とうしめがねムシめがねのようだが、これをつかってモノを見ると、中のようすがすけて見える。100.0026ゆっくり反射ぞうきんこのぞうきんで鏡やガラスを拭くと過去が映る。拭けば拭くほど、映る時間が過去に戻っていく。

ANNでは検索対象になっていなかった秘密道具である「エーアイアイ」をmulti matchクエリでは検索できており、ハイブリッド検索によってmulti matchANNがお互いを補うあうことで、主観的な評価ですが、検索結果になっていることが分かります。

クエリ: ガリバー

multi matchスコアひみつ道具の名前説明ANNスコアひみつ道具の名前説明hybrid(match score + ANN scoreスコアひみつ道具の名前説明
16.9183ガリバートンネル大きな入り口から入って、小さな出口から出ると、からだが小さくなるトンネル。逆にくぐりぬけると、もとに戻る。10.0029ガリバーロープ巨大なものをかんたんにしばりつけることができるロープ。16.9211ガリバーロープ巨大なものをかんたんにしばりつけることができるロープ。
26.9183ガリバーロープ巨大なものをかんたんにしばりつけることができるロープ。20.0027なりきりケイドロセット本格的なおにごっこが楽しめる未来の遊び道具。通信もできる警察(けいさつ)手帳型のバッジや『強力におい追跡鼻(ついせきばな)』、身につけると体力が三割増しになる『泥棒風呂敷(どろぼうふろしき)』、『牢屋(ろうや)シール』などが入っている。26.9183ガリバートンネル大きな入り口から入って、小さな出口から出ると、からだが小さくなるトンネル。逆にくぐりぬけると、もとに戻る。
35.8304箱庭シリーズ 湖大自然のミニチュアシリーズのひとつ。スモールライトやガリバートンネルで小さくなれば、この中で遊ぶことができる。30.0027スーパーダンごっこふろしき未来の子どもたちがスーパーヒーローごっこで使うオモチャのふろしき。これを身につけると、低空ながら飛ぶことができ、空気銃の弾(たま)もはね返し、ものを透(す)かして見ることもできる。35.8304箱庭シリーズ 湖大自然のミニチュアシリーズのひとつ。スモールライトやガリバートンネルで小さくなれば、この中で遊ぶことができる。
44.6887ミニハウスエネルギーのムダをはぶくために作られた、ミニハウス。乾電池一本でクーラーや冷蔵庫などの家電製品をすべて動かすことができる。ガリバートンネルで小さくなって使う。40.0026ゴキブリトレとれビッグゴキブリをとる道具を、人間用に大きくしたもの。入ったら最後、ねんちゃくシートにつかまってしまう。44.6887ミニハウスエネルギーのムダをはぶくために作られた、ミニハウス。乾電池一本でクーラーや冷蔵庫などの家電製品をすべて動かすことができる。ガリバートンネルで小さくなって使う。
54.5612ムシムシ操縦ハンドルこのハンドルを自分が乗りたい虫に向けて、真ん中のボタンを押すと、その虫の背中にハンドルがくっつき、背中に乗って操縦することができる。ただし、ガリバートンネルなどで身体を小さくしてから使う。50.0026実景ひきよせ額縁額縁の横にあるダイヤルを回すと、海でも雪山でもジャングルでも、実際にある景色をひきよせることができる。額縁のガラスを外してくぐれば、その景色の場所に行くこともできる。54.5612ムシムシ操縦ハンドルこのハンドルを自分が乗りたい虫に向けて、真ん中のボタンを押すと、その虫の背中にハンドルがくっつき、背中に乗って操縦することができる。ただし、ガリバートンネルなどで身体を小さくしてから使う。
60.0026ぼんのうボウシこれを頭にかぶると、その人の煩悩(ぼんのう)が雲のようなものに映像(えいぞう)となってうかび上がり、釣鐘(つりがね)のかたちにまとまる。それを撞木(しゅもく)型のハンマーでたたくと、それらの煩悩が追い払(はら)われる。60.0027なりきりケイドロセット本格的なおにごっこが楽しめる未来の遊び道具。通信もできる警察(けいさつ)手帳型のバッジや『強力におい追跡鼻(ついせきばな)』、身につけると体力が三割増しになる『泥棒風呂敷(どろぼうふろしき)』、『牢屋(ろうや)シール』などが入っている。
70.0025ねがい七夕ロケット専用(せんよう)の短冊(たんざく)に願いごとを書いて笹(ささ)につけ、笹ごとロケットで宇宙(うちゅう)に飛ばすと、一年間その願いごとをかなえてくれる。短冊に書いたことと反対のことがかなう「うら七夕ロケット」もある。 【うら七夕ロケット】 専用(せんよう)の短冊(たんざく)に願いごとを書いて笹(ささ)につけ、笹ごとロケットで宇宙(うちゅう)に飛ばすと、一年間、短冊に書いたことと反対のことがかなう。70.0027スーパーダンごっこふろしき未来の子どもたちがスーパーヒーローごっこで使うオモチャのふろしき。これを身につけると、低空ながら飛ぶことができ、空気銃の弾(たま)もはね返し、ものを透(す)かして見ることもできる。
80.0025フエルミラー増やしたいものをこのカガミにうつすと、本物になって出てくる。80.0026ゴキブリトレとれビッグゴキブリをとる道具を、人間用に大きくしたもの。入ったら最後、ねんちゃくシートにつかまってしまう。
90.0025ふわふわぐすりこれを口に入れると、体の中にガスができて、空気より軽くなるため、空を歩いたり、飛んだりすることができる。90.0026実景ひきよせ額縁額縁の横にあるダイヤルを回すと、海でも雪山でもジャングルでも、実際にある景色をひきよせることができる。額縁のガラスを外してくぐれば、その景色の場所に行くこともできる。
100.0025乗りものぐつこれに両足を入れてボタンを押すと、車やジェット機、潜水艦(せんすいかん)など、いろいろな乗りものになる。100.0026ぼんのうボウシこれを頭にかぶると、その人の煩悩(ぼんのう)が雲のようなものに映像(えいぞう)となってうかび上がり、釣鐘(つりがね)のかたちにまとまる。それを撞木(しゅもく)型のハンマーでたたくと、それらの煩悩が追い払(はら)われる。

multi matchクエリでは、「ガリバー」とう単語が全てマッチし検索結果としては申し分ありません。 ANNは「ガリバー」というクエリによって、「ガリバーロープ」はヒットしていますが「ガリバートンネル」はなぜか出てきていません。 ハイブリッド検索によって、上位 5 個はmulti matchクエリによる適合率(precision)が高いひみつ道具がランク付けされ、その後 ANN の結果が混ざってきています。正直「ガリバー」という単語から想起するようなひみつ道具とはあまり感じませんが、特徴ベクトルの閾値などで上手く枝刈りさえしておけば、質は担保しつつ多様性のある結果が確保できそうです。

実際はこんなに単純な multi match計算ではなく、BM25 で計算されたスコアを導入したりなど工夫すべき点は多々ありますが、ハイブリッド検索の可能性を感じることができました。

特徴ベクトルのチューニングや、既存のクエリとの組み合わせるためのチューニングなどやることは増えそうですが、これは夢が広がりますね。 実際にはスコアを組み合わせたりするのもそうですが、ANN の結果にフィルタリングするほうが実務的には簡単に役立つことが多そうな気がしました。

たとえば、特定のカテゴリ内で ANN するだけでも、 「この商品に見た目が似ている商品はこちらです」を提示する際に画像としては似ているが商品としては全く違う商品(iPhone と iPhone ケースなど)を分けるのも簡単にできちゃいますしね。

まとめ

このハイブリッド検索によって近似近傍探索のスコアと従来の検索結果のスコアをかけあわせたり、フィルタリングしたりなど真の意味で近似近傍探索が Elasticsearch で活用可能になったのではないのでしょうか?


  1. Introducing approximate nearest neighbor search in Elasticsearch 8.0 | Elastic Blog ↩︎

  2. 8.0 からの kNN はどう変わったのか / How kNN search changed in the Elasticsearch 8.0 - Speaker Deck ↩︎

  3. k-nearest neighbor (kNN) search | Elasticsearch Guide [8.0] | Elastic ↩︎

  4. https://twitter.com/jobergum/status/1491366596665016321 Jo さんの皮肉な検索エンジン批評は自分の中で名物になっている ↩︎

  5. ↩︎
  6. Elasticsearch 8.4 introduces a hybrid search by Mayya Sharipova Elasticsearch 8.4 introduces a hybrid search: ability to combine results from knn search with traditional search features (queries, aggs etc) and all this under a single familiar _search API. ↩︎

  7. https://www.elastic.co/guide/en/elasticsearch/reference/8.4/knn-search.html#_combine_approximate_knn_with_other_features ↩︎

  8. Elasticsearch で日本語検索を扱うためのマッピング定義 ZOZO TECH BLOG ↩︎

See Also

Support

記事をお読みくださりありがとうございます。このウェブサイトの運営を支援していただける方を募集しています。 もしよろしければ、下のボタンからサポート(投げ銭)していただけると、ブログ執筆、情報発信のモチベーションに繋がります✨