Elasticsearch 検索と関連性チューニング:マッピング・クエリ・集計・パフォーマンス
Elasticsearch を本気でチューニングすべき理由
Elasticsearch の「そのまま使える」体験は demo には十分です。しかしドキュメントが数億件、QPS が数千を超えると、チューニングされていない mapping とクエリは一気にクラスタを沈めます。書き込み停滞、クエリのスパイク、ヒープ OOM へ。チューニングの本質は書き込み時に重い計算を済ませ、クエリ時には必要な仕事だけをすることです。
| 次元 | チューニング前 | チューニング後 |
|---|---|---|
| フィールド型 | 全て text か keyword |
検索/集計需要で正確に分割 |
| フィルタ条件 | must 内に |
filter コンテキストへ |
| シャード数 | 適当に 5/10 | データ量とノードから逆算 |
| バルク書き込み | 単発 _doc |
bulk + 適切なバッチ |
| 関連性 | デフォルト BM25 | boost / 業務重みを併用 |
トークン化と Analyzer:検索品質の土台
analyzer は「ユーザーが探す語」と「文書にある語」が一致するかを決めます。中国語は特に正しいトークナイザが必要です。
{
"settings": {
"analysis": {
"analyzer": {
"my_cn": {
"tokenizer": "ik_max_word",
"filter": ["lowercase"]
}
}
}
},
"mappings": {
"properties": {
"title": { "type": "text", "analyzer": "my_cn" },
"status": { "type": "keyword" }
}
}
}
mapping やクエリ DSL を調査するときは、リクエスト本体を JSON フォーマッタ で整形してから見ると、深いネストのオブジェクトでも混乱しません。
text と keyword:検索と集計のトレードオフ
text:トークン化される。全文検索に向くが、完全一致や集計には不向き。keyword:トークン化されない。完全一致・ソート・terms 集計に向くが全文検索不可。
一般的な手法は multi-fields で両方を残すことです:
{
"mappings": {
"properties": {
"city": {
"type": "text",
"fields": {
"raw": { "type": "keyword" }
}
}
}
}
}
検索は city(トークン化)、集計/ソートは city.raw(完全一致)を使います。
bool クエリ:filter と query の雲泥の差
最も誤用されやすい点です。filter コンテキストは関連性スコアを計算せず、結果はキャッシュ可能です。query コンテキストはスコアを計算します。「○/×」条件(ステータス、時間範囲、カテゴリ)は全て filter に入れます。
{
"query": {
"bool": {
"must": [
{ "match": { "title": "手机" } }
],
"filter": [
{ "term": { "status": "on" } },
{ "range": { "price": { "gte": 100, "lte": 5000 } } }
]
}
}
}
must 内の match はスコアに寄与し、filter 内の term/range は高速でノードレベルキャッシュから再利用されます。
集計:分析をエンジンにプッシュダウン
データをアプリ層に引き戻して集計するのではなく、ES 集計を直接使います:
{
"size": 0,
"aggs": {
"by_category": {
"terms": { "field": "category.raw", "size": 10 }
},
"price_stats": {
"stats": { "field": "price" }
}
}
}
size: 0 はヒット文書を返さず集計結果のみを返すため、帯域を大きく節約します。
インデックス設定:シャードとレプリカ
シャードは多いほど良いわけではありません。目安:
- 単シャードを 10GB ~ 50GB に収める。
- シャード数 ≈ 全データ量 / 目標シャードサイズ。
- レプリカ数 = 読みスループット需要 / 単ノード能力。高可用のため最低 1。
{
"settings": {
"number_of_shards": 3,
"number_of_replicas": 1,
"refresh_interval": "30s"
}
}
refresh_interval を 30s に大きくすると、近リアルタイム更新による書き込み増幅を大幅に下げられます(ログ系に最適)。
バルク書き込みとキャッシュ
単発ではなく bulk
curl -XPOST "localhost:9200/orders/_bulk" -H 'Content-Type: application/json' --data-binary @bulk.jsonl
バッチは 5MB ~ 15MB 程度が目安。大きすぎるとキュー滞留を招きます。
キャッシュを賢く使う
- Node query cache:
filter結果をノード単位でキャッシュ。 - Shard request cache:集計 /
size:0リクエストの結果をキャッシュ。 - Fielddata / doc_values:ソート・集計は列存に依存。
textを直接集計しない。
関連性チューニング(BM25)
デフォルト BM25 で十分ですが、業務重みを重ねられます:
{
"query": {
"bool": {
"should": [
{ "match": { "title": { "query": "手机", "boost": 3 } } },
{ "match": { "description": { "query": "手机", "boost": 1 } } }
]
}
}
}
タイトル一致の重みは説明の 3 倍です。function_score で時間減衰や販売数などの外部因子も導入できます。
API 認証(ES へ書き込む Service Token 等)は JWT デコード ツールで payload の有効期限や権限クレームを即座に確認できます。
よくある質問 FAQ
Q1:なぜ text フィールドで terms 集計がエラーになるのですか?
text はトークン化されるため正確な集計ができません。.raw(keyword)サブフィールドを使ってください。
Q2:filter と must はどちらが速いですか?
filter の方が速く、スコア計算もせずキャッシュ可能です。「硬い条件」は優先して filter へ。
Q3:シャード数はいくつにすべきですか?
全データ量から逆算し、単シャード 10〜50GB に。多すぎるとクラスタのメタデータ負荷が跳ね上がります。
Q4:書き込みが遅いときの調査方法は?
単発書き込み(bulk に切替)、refresh_interval が小さすぎないか、レプリカが多すぎないかを確認。
Q5:中国語検索の品質が悪い場合は?
IK / pinyin などの中国語トークナイザに変え、検索フィールドに該当 analyzer をマッピングで指定します。
おすすめツール
Elasticsearch 開発では、以下の ツール が役立ちます:
- JSON フォーマッタ — mapping と DSL リクエスト本体の整理
- SQL フォーマッタ — SQL にならって ES クエリを設計
- Base64 エンコード — 文書 ID / バイナリペイロードの処理
- JWT デコード — 書き込み認証トークンのクレーム確認
Elasticsearch のパフォーマンスは、書き込み前の mapping と analyzer 設計が七分、クエリ時の filter/集計のトレードオフが三分。済ませるべき計算は前もって済ませ、キャッシュすべきものはキャッシュしておけば、数億件のデータでもミリ秒応答を保てます。
ブラウザローカルツールを無料で試す →