Elasticsearch シリーズ
内容 リンク Elasticsearch 基本操作 https://blog.yexca.net/archives/226 Elasticsearch クエリ操作 この記事 RestClient 基本操作 https://blog.yexca.net/archives/228 RestClient クエリ操作 https://blog.yexca.net/archives/229 Elasticsearch データ集計 https://blog.yexca.net/archives/231 Elasticsearch オートコンプリート https://blog.yexca.net/archives/232 Elasticsearch データ同期 https://blog.yexca.net/archives/234 Elasticsearch クラスター https://blog.yexca.net/archives/235
前回の記事では主にESのデータ保存機能について話したけど、ESが一番得意なのはやっぱり検索とデータ分析なんだ。
ESのクエリも相変わらずJSON形式のDSLで実装するよ。
クエリの分類
よくあるクエリの種類はこんな感じ。
- 全件検索: 全てのデータを検索するやつ。だいたいテストで使うね。例:
match_all - 全文検索 (full text) クエリ: アナライザーを使ってユーザー入力の内容を分かち書きして、転置インデックスからマッチするものを探すよ。例:
match_querymulti_match_query
- 厳密検索: 正確なターム値に基づいてデータを検索するやつ。だいたい
keyword、数値、日付、booleanなどのフィールドタイプを探す時に使うよ。例:idsrangeterm
- 地理情報 (geo) クエリ: 緯度経度に基づいて検索するやつ。例:
geo_distancegeo_bounding_box
- 複合クエリ (compound): 上記の簡単なクエリ条件を組み合わせて、検索条件をまとめるやつ。例:
boolfunction_score
クエリの文法はだいたい同じだよ:
| |
全件検索
クエリタイプは match_all で、検索条件はないよ。
| |
全文検索クエリ
ユーザーの入力内容を分かち書きするよ。検索ボックスでの検索によく使われるね。タームを使ってマッチさせるから、検索対象のフィールドは分かち書きできる text タイプじゃないといけないんだ。
よくあるのは:
match: 単一フィールド検索multi_match: 複数フィールド検索。どれか一つのフィールドが条件に合えば検索条件に合うとみなされるよ。
match クエリの文法:
| |
multi_match の文法
| |
以前インデックスを作成した時に、
brand、name、businessの値をcopy_toを使ってallフィールドにコピーしたから、上記の2つのクエリの結果は同じになるよ。でも、検索フィールドが増えれば増えるほどパフォーマンスへの影響が大きくなるから、
copy_toを使って、単一フィールドで検索するのがおすすめだよ。
厳密検索
厳密検索は検索条件を分かち書きしないよ。よくあるのは:
term: タームの厳密な値に基づいて検索range: 値の範囲に基づいて検索
term クエリ
検索条件は分かち書きされないタームじゃないといけないよ。入力値と完全に一致した場合にのみ条件に合うんだ。
文法:
| |
例
| |
range クエリ
範囲検索だよ。だいたい数値タイプの範囲で絞り込みをする時に使うね。例えば、価格や日付の範囲でフィルタリングする時とか。
文法:
| |
例
| |
地理座標クエリ
これは要するに緯度経度に基づいて検索するやつだよ。公式ドキュメントはこちら: https://www.elastic.co/guide/en/elasticsearch/reference/current/geo-queries.html
よくあるシーン: 周囲のホテル、タクシー、人、グルメを検索する時とか。
矩形範囲検索
geo_bounding_box クエリ。特定の矩形範囲内にある座標を持つ全てのドキュメントを検索するよ。
左上と右下の2つの点の座標を指定する必要があるんだ。
| |
近隣検索
距離検索 (geo_distance) とも呼ばれるね。指定した中心点から特定の距離以内にある全てのドキュメントを検索するよ。
| |
例: 周囲 (31.21, 121.5) の15km以内にあるホテルを検索するよ。
| |
複合クエリ
他のシンプルなクエリを組み合わせて、より複雑な検索ロジックを実現するんだ。よくあるのは2種類:
function score: スコア計算関数クエリ。ドキュメントの関連性スコアを制御して、ドキュメントのランキングを調整できるよ。bool query: ブールクエリ。論理関係を使って複数の他のクエリを組み合わせ、複雑な検索を実現するよ。
関連性スコア
match クエリを使うと、ドキュメントの結果は検索タームとの関連度に基づいてスコア (_score) がつけられて、結果はスコアの高い順に並んで返ってくるんだ。こんな感じ:
| |
ESでは、初期にはTF-IDFアルゴリズムがスコア計算に使われていたんだ。

TF-IDFアルゴリズムには欠点があって、タームの頻度が高くなるほどドキュメントのスコアも高くなって、単一のタームがドキュメントに与える影響が大きすぎたんだ。5.1バージョン以降は、アルゴリズムがBM25アルゴリズムに変わり、単一のタームのスコアには上限が設けられるようになったよ。

スコア計算関数クエリ
スコア計算関数はかなり合理的だけど、プロダクトが求めているものと必ずしも一致するとは限らないんだ。関連性スコアを制御したい場合は、ESの function_score クエリを使って、ドキュメントの関連性スコアを変更し、新しく得られたスコアに基づいてソートする必要があるよ。
構造:
| |
元のクエリ: この条件に基づいてドキュメントを検索し、BM25アルゴリズムに基づいてドキュメントにスコアをつけるよ。これが元のスコア (query score) だね。
フィルタ条件: この条件に合致するドキュメントのみが再スコアリングされるよ。
スコア計算関数: フィルタ条件に合致するドキュメントは、この関数に基づいて計算されて、関数スコアが得られるんだ。4種類の関数があるよ:
weight: 関数の結果が定数になる。field_value_factor: ドキュメント内の特定のフィールド値を関数の結果として使う。random_score: 乱数を関数の結果として使う。script_score: カスタムのスコア計算関数アルゴリズム。
演算モード: スコア計算関数の結果と、元のクエリの関連性スコアの間の演算方法。こんな感じ:
multiply: 乗算replace:function scoreでquery scoreを置き換える- その他、
sum、avg、max、minなど
例: 「如家」というブランドのホテルを上位に表示させるよ。
| |
ブールクエリ
ブールクエリは一つまたは複数のクエリ句の組み合わせで、それぞれの句がサブクエリになっているんだ。組み合わせ方はこんな感じ:
must: 各サブクエリに必ずマッチする必要がある。「AND」みたいなものだね。should: サブクエリに選択的にマッチする。「OR」みたいなものだよ。must_not: 必ずマッチしないこと。スコア計算には参加しない。「NOT」みたいなものだね。filter: 必ずマッチすること。スコア計算には参加しない。
例えば、ホテルを検索する時に、地域、ブランド、価格などのフィールドで絞り込みたい場合、それぞれのフィールドで検索条件や方法が違うから、複数の異なるクエリが必要になるよね。それらを組み合わせるにはboolクエリを使うんだ。
スコア計算に参加するフィールドは、検索パフォーマンスが悪くなるよ。多条件検索の時は、次のことをおすすめするね:
- 検索ボックスのキーワード検索は全文検索だから、
mustクエリを使って、スコア計算に参加させる。 - その他のフィルタ条件は、
filterクエリを使って、スコア計算には参加させない。
文法:
| |
例: 名前が「如家」を含み、価格が400以下で、座標 (31.21, 121.5) の周囲10km以内にあるホテルを検索するよ。
| |
検索結果の処理
検索で得られた結果は、ソート、ページネーション、ハイライトができるよ。
ソート
ESはデフォルトでは関連性スコアに基づいてソートするけど、カスタム方式で検索結果をソートすることもできるんだ。ソートできるフィールドタイプは、keyword タイプ、数値タイプ、地理座標タイプ、日付タイプなどがあるよ。
通常フィールドのソート
keyword、数値、日付タイプのソートの文法はだいたい同じだよ。
文法:
| |
ソート条件は配列になっていて、複数のソート条件を書けるよ。宣言された順に、最初の条件が同じ場合は2番目の条件で、というように続くんだ。
例: ホテルデータをユーザー評価の降順で、評価が同じ場合は価格の昇順でソートするよ。
| |
地理座標ソート
文法:
| |
例: ホテルを距離でソートするよ (位置を 31.034661, 121.612282 と仮定するね)。
高徳で緯度経度を取得するにはここ: https://lbs.amap.com/demo/jsapi-v2/example/map/click-to-get-lnglat/
| |
ページネーション
ESはデフォルトでTop10のデータしか返さないから、もっと多くのデータを検索したい場合はページネーションのパラメータを変更する必要があるよ。ESは from、size パラメータを変更して、返却するページング結果を制御するんだ:
from: 何番目のドキュメントから始めるかsize: 合計でいくつのドキュメントを検索するか
MySQLの
limit ?,?みたいなものだね。
基本ページネーション
基本的な文法は以下の通りだよ。
| |
ディープページネーションの問題
もし990〜1000番目のデータを検索したいなら、クエリはこうなるよ。
| |
でもESの仕組み上、ページネーションする時はまず0〜1000件を検索して、それから990〜1000件目を切り取って表示するんだ。
ESがシングルノードモードなら大きな影響はないけど、クラスター構成でデプロイされている場合、1000件検索するからといって各ノードが200件ずつ検索するわけじゃないんだ。なぜなら、Aノードの200件がBノードでは1000位以下になる可能性もあるからね。
上位1000件を取得するためには、各ノードがTop1000を検索し、それらをまとめて再ランキングして切り取る必要があるんだ。
もしTop10000、あるいはそれ以上を検索しようとすると、メモリとCPUに非常に大きな負荷がかかるから、ESは from + size が10000を超えるリクエストを禁止しているよ。
ディープページネーションに対しては、ESが2つの解決策を提供しているよ: https://www.elastic.co/guide/en/elasticsearch/reference/current/paginate-search-results.html
search after: ページネーション時にソートが必要で、原理は前回のソート値から次のページのデータを検索するんだ。公式が推奨する方法だね。scroll: 原理はソートされたドキュメントIDのスナップショットをメモリに保存するんだけど、公式はもう推奨していないよ。
ページネーションまとめ
from + size:- 利点: ランダムなページ移動をサポートしているよ。
- 欠点: ディープページネーションの問題があるのと、デフォルトの検索上限 (
from + size) が10000なんだ。 - 使うシーン: 百度、京東、Google、淘宝みたいなランダムなページ移動がある検索だね。
after search:- 利点: 検索上限がないよ (1回の検索で
sizeが10000を超えなければね)。 - 欠点: 次のページへ順次検索するだけで、ランダムなページ移動はできないんだ。
- 使うシーン: ランダムなページ移動が必要ない検索、例えばスマホで下にスクロールするページ移動とか。
- 利点: 検索上限がないよ (1回の検索で
scroll:- 利点: 検索上限がないよ (1回の検索で
sizeが10000を超えなければね)。 - 欠点: 余分なメモリ消費があるし、検索結果はリアルタイムじゃないんだ。
- 使うシーン: 大量のデータ取得や移行。ES7.1からは非推奨になってて、
after searchがおすすめだよ。
- 利点: 検索上限がないよ (1回の検索で
ハイライト
検索エンジンでコンテンツを検索する時、キーワードが赤色になって目立つことがあるよね。それがハイライト表示で、通常はドキュメント内の全てのキーワードに <em> タグを追加して、そのタグにCSSスタイルを設定するんだ。
文法:
| |
注意点:
- ハイライトはキーワードに対して行われるから、検索条件にはキーワードが必要で、範囲検索はできないよ。
- デフォルトでは、ハイライトするフィールドは検索で指定したフィールドと一致している必要があるよ。そうじゃないとハイライトされないんだ。
- 検索対象ではないフィールドをハイライトしたい場合は、
required_field_match=falseというプロパティを追加する必要があるよ。
例: 検索結果で、名前の部分をハイライトするよ。
| |
結果の highlight 部分に、タグが追加された後の結果が表示されているね。
検索結果処理のまとめ
DSLクエリは大きなJSONオブジェクトで、以下を含むよ:
query: クエリfrom、size: ページネーション条件sort: ソート条件highlight: ハイライト条件
総合的な例
| |