MNTSQで検索エンジニアをしている溝口です。 今回はElasticsearchでハイライト処理を行う際に利用するUnifiedHighlighterの挙動について簡単に調べる機会があったので、それを簡単に記事にしました。
ハイライト処理とは
検索結果一覧が表示された際に、以下のようにヒットした該当箇所が強調表示される機能のことです。(以下のスクリーンショットだと検索キーワードの「ハイライト」が太文字になっていると思います。)
この機能を実現するために、Elasticsearchでは以下の3種類のハイライターが利用可能です。
- PlainHighlighter
- FastVectorHighlighter
- UnifiedHighlighter
それぞれ特徴は異なりますが、今回はハイライトするにあたってそれぞれのハイライターが必要とするリソースにフォーカスします。
PlainHighlighter
かつてデフォルトだったハイライターです。ハイライト処理をするためにテキストを再解析します。 必要なリソースは少ないですが、テキストの再解析を行うため、ハイライト処理にかかる時間は長くなりがちです。
FastVectorHighlighter
名前の通り高速なハイライト処理が可能ですが、ハイライト処理をするためにTermVectorという属性を必要とします。TermVectorはディスクに保存する必要があるので、必要なディスク容量は大きくなります。
UnifiedHighlighter
最も新しいハイライターで現在のデフォルトです。かつて存在したPostingsHighlighterというハイライターを改善して作られました。ハイライト処理をする際に以下を適切に選択して処理の高速化をはかります。
- POSTINGS (posting listを利用する)
- TERM_VECTORS (FastVectorHighlighterと同じく、TermVectorを利用する)
- ANALYSIS (PlainHighlighterと同じく、ハイライト時にテキストを再解析する)
- POSTINGS_WITH_TERM_VECTORS (posting listとTermVectorの両方を利用する)
さて、前置きが長くなりましたが、ElasticsearchのReferenceを読むと、上記のようにそれぞれのハイライターを使う場合にどのようなフィールドのオプションを指定すれば良いかが書いてあります。
https://www.elastic.co/guide/en/elasticsearch/reference/7.11/highlighting.html#offsets-strategy
しかしながら、上記のうちUnifiedHighlighterのPOSTINGS_WITH_TERM_VECTORSの記述が見当たらないことに気づきます。
そのため、今回はElasticsearchからUnifiedHighlighterを使うときのOffset strategy(ハイライトを高速化するときに必要な追加の情報)で、
- そもそもPOSTINGS_WITH_TERM_VECTORSは利用可能なのか
を明らかにし、それに加えて
- それぞれのOffset strategyを利用した場合のディスク使用量と性能の向上度合い
を簡単に計測してみようと思います。
Setup
以下の条件でデータを登録して、indexサイズ、ハイライトの平均処理時間を簡単にみてみたいと思います。
Elasticsearchは以下のDockerfileでanalysis-kuromojiだけ入れて、イメージをbuildし、そのイメージを元にしたコンテナを以下のコマンドで起動します。
Dockerfile
FROM docker.elastic.co/elasticsearch/elasticsearch:7.11.2 RUN /usr/share/elasticsearch/bin/elasticsearch-plugin install analysis-kuromoji
起動コマンド
$ docker run -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" 6571a46f9f37
検証に使うデータには、日本語Wikipediaのダンプデータから適当に10万件の本文情報のみを抜き取ったものを使います。 利用するindex名と、調査対象のフィールドの設定差分は以下の通りです。
- 共通
- analyzer = kuromoji
- wiki_text_a (上述のANALYSISに相当)
- index_options = positions
- term_vector = no
- wiki_text_p (上述のPOSTINGSに相当)
- index_options = offsets
- term_vector = no
- wiki_text_t (上述のTERM_VECTORSに相当)
- index_options = positions
- term_vector = with_positions_offsets
- wiki_text_pt (上述のPOSTINGS_WITH_TERM_VECTORSに相当)
- index_options = offsets
- term_vector = yes
一例を挙げるとこんな感じです。
$ curl -XPUT localhost:9200/wiki_text_a -H 'Content-Type: application/json' -d '{ "mappings": { "properties": { "text": { "type": "text", "analyzer": "kuromoji", "index_options": "positions", "term_vector": "no" } } }, "settings": { "number_of_shards": 1, "number_of_replicas": 0 } }'
なお、TermVectorを有効にする時の指定がwith_positions_offsetsとyesになっているなど、微妙な違いがありますが、これはElasticsearchではTermVectors/TermPositions/TermOffsetsという異なる情報をterm_vectorという属性にまとめていることに起因しています。 Offset strategyがPOSTINGS_WITH_TERM_VECTORSの場合には、TermVectorsのみを、TERM_VECTORSの場合にはTermVectors/TermPositions/TermOffsetsの3種を全て利用するので、上記のような設定になっています。
indexingが終わったら、以下の通りそれぞれのindexをforce mergeします。
$ curl -XPOST "localhost:9200/wiki*/_forcemerge?max_num_segments=1" $ curl -XGET "localhost:9200/_cat/segments?v" index shard prirep ip segment generation docs.count docs.deleted size size.memory committed searchable version compound wiki_text_a 0 p 172.17.0.2 _2 2 100000 0 389.1mb 1172 true true 8.7.0 false wiki_text_p 0 p 172.17.0.2 _2 2 100000 0 471.8mb 1172 true true 8.7.0 false wiki_text_pt 0 p 172.17.0.2 _3 3 100000 0 584.6mb 3004 true true 8.7.0 false wiki_text_t 0 p 172.17.0.2 _2 2 100000 0 638.7mb 3004 true true 8.7.0 false
結果の取りまとめ前ですが、上記でとりわけ目につくのは、wiki_test_t(term_vector: with_positions_offsets)を指定しているインデックスが、デフォルトのindexオプションのものの1.6倍にもなっていることです。 なお、wiki_test_pt(postings + term vector)は、上述の通りterm_vectorの一部の属性のみを追加しているので、それよりは大きくなっていないことが確認できます。
ハイライト性能は、jmeterを使って簡易に測ります。 今回は簡単に以下の設定で測りたいと思います。
- スレッド数: 2
- Ramp-Up期間: 1
- ループ回数: 10000
ベンチマークに使う検索キーワードはindexに含まれているtermで出現回数が多い&3文字以上のものを10000件ピックして使うことにしました。
クエリには以下を使います。(${text}の部分は10000件のtermが代入されます)
{ "query": { "match": { "text": "${text}" } }, "size": 10, "_source": ["id"], "highlight": { "fields": { "text": {} }, "type": "unified" } }
また、日本語検索ではmatchクエリが使われることはあまりないので、上記の他にmatch_phrase_prefixクエリでも計測してみます。
結果
結果をまとめると以下のようになりました。
matchクエリ
index名 | Throughput | Average | Median | 95% Line | 99% Line |
---|---|---|---|---|---|
wiki_test_a | 77.1 | 25 | 22 | 49 | 71 |
wiki_test_p | 226.6 | 8 | 8 | 11 | 13 |
wiki_test_t | 208.6 | 9 | 8 | 13 | 14 |
wiki_test_pt | 220.1 | 8 | 8 | 12 | 14 |
ANALYSISとそれ以外では大きな違いが見受けられますが、これならPOSTINGSが最も優秀そうです。
match_phrase_prefixクエリ
index名 | Throughput | Average | Median | 95% Line | 99% Line |
---|---|---|---|---|---|
wiki_test_a | 74.6 | 26 | 23 | 43 | 51 |
wiki_test_p | 73.6 | 26 | 24 | 44 | 52 |
wiki_test_t | 205.9 | 9 | 8 | 13 | 14 |
wiki_test_pt | 195.6 | 9 | 9 | 14 | 16 |
matchクエリの時とは一転して、POSTINGSはANALYSISと大して変わらない性能となってしまいました。
まとめ
ElasticsearchにおけるUnifiedHighlighterとindexの属性指定の関係をまとめて、簡単な性能比較をしてみました。
結果としては、 1. POSTINGS_WITH_TERM_VECTORSは有効 2. 多く情報を保存することでハイライト処理は高速になる。一方で、必要となるディスク領域が大きくなっていく 3. 利用するクエリに応じてどの情報を保存するべきかを判断すべき
ということが確認できました。
今回は簡単にディスク容量とハイライトの速度の関係をまとめてみました。それぞれのStrategyにおける使用メモリ量なども機会があれば記事にできればと思います。
この記事を書いた人

溝口泰史
MNTSQ社で検索エンジニアをしています。