Skip to content
Published on

Elasticsearch と OpenSearch、Lucene の内部 — Inverted Index、BM25、Sharding、Vector Search、Hybrid RAG まで (2025)

Authors

"Search is not a feature. It's a philosophy of how humans interact with information." — Doug Cutting (Lucene 作者, 1999)

1999 年、Doug Cutting が Java で Lucene を作ったとき、彼は「誰もが自分のデータに Google 級の検索を持てるべきだ」と信じていた。26 年経った今、Elasticsearch、OpenSearch、Solr はすべて Lucene の上に立つ。ログ解析、商品検索、オートコンプリート、そして 2024 年からは RAG (Retrieval-Augmented Generation) の中核インフラまで。

しかし「Elasticsearch を使う」と「Lucene を理解する」は天と地の差である。本稿は検索の本質から 2025 年の Hybrid Search まで一気通貫の地図である。


1. RDB の LIKE '%keyword%' はなぜダメか

線形スキャンの壁

SELECT * FROM articles WHERE content LIKE '%postgresql%';
  • インデックス使用不可 (先頭 %)
  • すべての行の text フィールドをフルスキャン
  • 1 億ドキュメントで数十分

Postgres GIN も部分的に解決するが、トークン化/言語解析の制限、スコアリングの困難、オートコンプリートやタイポ補正の生態系の弱さ、分散検索の弱さがある。専用検索エンジンが必要な理由だ。


2. Inverted Index — 検索の数学的心臓

基本アイデア

Document 1: "The quick brown fox"
Document 2: "The lazy brown dog"
Document 3: "Foxes and dogs"

単語 から ドキュメントリスト に反転すると:

brown  -> [1, 2]
dog    -> [2]
dogs   -> [3]
fox    -> [1]
foxes  -> [3]
lazy   -> [2]
quick  -> [1]
the    -> [1, 2]

クエリ "brown dog" は AND 演算で [2]。数十億ドキュメントでも O(logN)O(\log N) に近い速度。

Lucene の実装単位

  1. Term Dictionary — 全 term のソート済み辞書 (FST)
  2. Postings List — term ごとのドキュメントリスト + 頻度/位置
  3. Stored Fields — 原文 (圧縮)
  4. Doc Values — 集計/ソート用のカラムナ格納
  5. Norms — フィールド長の正規化値

FST — 接頭辞共有でメモリ節約

"fox, foxes, foxy" の共通接頭辞を有限状態トランスデューサで圧縮。数千万 term を数 MB に収める。オートコンプリートの中核構造でもある。


3. Segment — Lucene の不変性原則

Immutable Segment

Lucene では一度書かれたファイルは変更されない。これが Lucene を速く安全にする。

  • Append only — 新規ドキュメントは新しい Segment を作る
  • Delete は tombstone を残すのみ
  • Update は delete + insert
  • 小さな Segment は merge され大きな Segment になる

Segment の構成ファイル

_0.cfs     — composite file
_0.cfe     — entry point
_0.si      — segment info
_0.fdt/fdx — field data
_0.tim/tip — term dictionary
_0.doc/pos — postings
_0.dvm/dvd — doc values
_0.liv     — live docs

Merge ポリシー

  • 小さな Segment が多い -> 検索が遅い
  • 巨大 Segment -> merge コスト爆発 (I/O 嵐)
  • 既定は TieredMergePolicy — サイズ別階層で merge

Refresh / Flush / Commit

用語意味タイミング
Refreshメモリバッファを検索可能な Segment にする既定 1 秒ごと
FlushSegment をディスクに fsync自動 (メモリ閾値)
Committranslog 含む完全永続化頻度は低い

「Near Real-Time Search」の秘密: Refresh は 1 秒、Flush は後で。これが Elasticsearch のトレードマーク「1 秒の遅延」だ。

Translog

  • すべての書き込みは translog に先に記録
  • ノード再起動時に再生
  • index.translog.durability: request (毎回 fsync、遅いが損失 0) vs async (定期、既定 5 秒)

4. BM25 — TF-IDF を置き換えたスコア

TF-IDF の限界

tf-idf(t,d)=tf(t,d)×logNdf(t)\text{tf-idf}(t, d) = \text{tf}(t, d) \times \log\frac{N}{\text{df}(t)}

長い文書ほど tf が増え、スコアが膨張する。

BM25 の式

score(d,q)=tqIDF(t)f(t,d)(k1+1)f(t,d)+k1(1b+bdavgdl)\text{score}(d, q) = \sum_{t \in q} \text{IDF}(t) \cdot \frac{f(t, d) \cdot (k_1 + 1)}{f(t, d) + k_1 \cdot (1 - b + b \cdot \frac{|d|}{\text{avgdl}})}

重要な変更:

  • Saturation — 同じ単語が多くても増加が鈍化 (k11.2k_1 \approx 1.2)
  • 長さ正規化 — 文書長を平均と比較しペナルティ/ボーナス (b0.75b \approx 0.75)

なぜ BM25 が既定か

  • 20 年以上の経験的実績
  • パラメータ 2 つで調整可能
  • Lucene 既定 (2016-)

商品名やクエリログ分析では b=0.3 程度まで下げるのが一般的。


5. Analyzer — トークン化の芸術

3 段パイプライン

  1. Character Filter — HTML 除去、文字置換
  2. Tokenizer — 文を単語に分割
  3. Token Filter — 小文字化、ステミング、Synonym、ストップワード

韓国語の地獄

英語は空白でトークン化しやすい。韓国語は膠着語 + 語尾変化 + 助詞。「검색했다/검색한다/검색은/검색을」すべてが「검색」にマッチしなければならない。nori (韓国語)、kuromoji (日本語)、ik (中国語) が代表的な Analyzer。

nori の例

POST _analyze
{
  "analyzer": "nori",
  "text": "Elasticsearch는 검색엔진입니다"
}

-> "elasticsearch", "는", "검색", "엔진", "입니다"

助詞/語尾を除去: "filter": ["nori_part_of_speech"]

Synonym 展開

"shoe, sneaker, runner"
-> "shoe" クエリで sneaker/runner もマッチ

検索品質の半分は Synonym 辞書にかかっている。構築は地味だが ROI 最高。


6. Shard と Replica — 分散の基本

Primary Shard

  • インデックスは複数 primary shard に分割
  • shard = hash(_routing) % number_of_primary_shards
  • _routing の既定は _id

Replica Shard

  • Primary の複製
  • 検索性能の拡張 + 障害対策

制限と原則

  • primary 数はインデックス作成後に変更不可 (routing が壊れる)
  • 対応: reindex または alias + 新インデックス
  • shard サイズの経験則: 10-50 GB
  • 過剰な shard -> cluster state 爆発

Cluster State と Split Brain

  • master ノードが cluster メタデータを管理
  • 同時に複数 master が立つと split brain
  • 7.0 (2020) で合意アルゴリズムを書き直し (Raft-like)

7. Query DSL — JSON の迷宮

主なクエリ

クエリ用途
matchAnalyzer 経由のフルテキストマッチ
termAnalyzer を通さず完全一致 (keyword)
match_phrase順序保持フレーズ
multi_match複数フィールド同時検索
boolAND/OR/NOT 合成
range範囲
function_scoreカスタムスコア
rank_featureブースト用フィールド

bool の 4 節

{
  "bool": {
    "must": [],
    "should": [],
    "filter": [],
    "must_not": []
  }
}

filter を積極的に使う — キャッシュされ高速。スコアリングは match、固定条件は filter

Term vs Match — 最頻出の罠

{"term": {"name": "User Name"}}
{"match": {"name": "User Name"}}

text フィールドには match、keyword フィールドには term

Aggregation

{
  "aggs": {
    "by_category": {
      "terms": {"field": "category"},
      "aggs": {
        "avg_price": {"avg": {"field": "price"}}
      }
    }
  }
}

SQL の GROUP BY に相当。ログ解析/ダッシュボードに必須。


8. Vector Search — 2022 年以降の革命

なぜ Vector か

  • BM25 は単語マッチ — 「子犬」と「犬」を別物とみなす
  • Embedding は意味ベース — 類似意味を自動接続

ES/OpenSearch の kNN

Elasticsearch 8.0 (2022) から native kNN。Lucene 9.0 の HNSW 実装を活用。

{
  "mappings": {
    "properties": {
      "title_vector": {
        "type": "dense_vector",
        "dims": 768,
        "index": true,
        "similarity": "cosine"
      }
    }
  }
}
{
  "knn": {
    "field": "title_vector",
    "query_vector": [0.1, 0.2],
    "k": 10,
    "num_candidates": 100
  }
}

HNSW パラメータ

  • m: 各ノードの隣接数 (通常 16)
  • ef_construction: インデックス時の探索幅 (100-200)
  • ef_search: クエリ時の探索幅 (recall vs 速度)

Quantization

  • int8 quantization — 4 倍節約、精度損失 1% 程度
  • BBQ (Better Binary Quantization) — Lucene 10 (2024 末)、32 倍節約
  • 2025 年の RAG では事実上の標準

9. Hybrid Search — RAG 時代の答え

BM25 vs Vector

観点BM25Vector
完全一致 (SKU)
意味類似
希少語 (専門用語)
タイポ
多言語

両方が必要だ。

RRF — Reciprocal Rank Fusion

RRF(d)=i1k+ranki(d)\text{RRF}(d) = \sum_i \frac{1}{k + \text{rank}_i(d)}

  • 順位のみで結合
  • スケール差に無関係 (BM25 と cosine が違ってよい)
  • k = 60 が経験的に最良

ES の rrf retriever

{
  "retriever": {
    "rrf": {
      "retrievers": [
        {"standard": {"query": {"match": {"content": "query"}}}},
        {"knn": {"field": "vec", "query_vector": [], "k": 50}}
      ],
      "rank_window_size": 50,
      "rank_constant": 60
    }
  }
}

Cross-Encoder Reranker

  • BM25/kNN で上位 100 件を取得
  • Cross-Encoder (BERT 系) で再順位付け
  • ES/OpenSearch で 2024 年からネイティブ対応 (Cohere、E5、BGE)

10. 2021 年ライセンス戦争 — OpenSearch 誕生

背景

  • AWS が Elasticsearch を managed service として販売
  • Elastic は「AWS が上流貢献せず利益だけ」と激怒
  • 2021 年 1 月、Elasticsearch 7.11 を SSPL/Elastic License の二重ライセンスへ移行
  • AWS は即座にフォーク: OpenSearch

結果

  • Elastic: ライセンスで AWS と競争し収益を防衛
  • AWS: OpenSearch を Linux Foundation へ移管 (2024 年 9 月)
  • コミュニティ: 分裂

2024-2025 の現状

製品ライセンス主導
Elasticsearch 8Elastic License 2 / SSPLElastic
Elasticsearch 8.14+AGPL 追加 (2024.8)Elastic (回復の試み)
OpenSearch 2.xApache 2.0AWS -> Linux Foundation

選択ガイド

  • AWS managed 中心 -> OpenSearch
  • 最新 ML/Vector/ESQL -> Elasticsearch
  • オープンソース純正主義 -> OpenSearch
  • Elastic Agent/Fleet 生態系 -> Elasticsearch

11. 運用の地獄 — よくある障害

JVM Heap

  • 32 GB を超えない (Compressed OOPs の境界)
  • Heap の 50% で警告、75% で危険
  • Old Gen GC は検索を止める (stop-the-world)

Circuit Breaker

circuit_breaking_exception: Data too large
  • 既定 60-70%
  • 拒否されたクエリを再試行しない — 悪化する

Hot Shard

  • 特定 shard にクエリ集中
  • 原因: routing キーの偏り
  • 対応: _routing 調整、shard 数増加

Shard 爆発

  • ノード当たり 1000+ shard で cluster state 過負荷
  • ILM: rollover、shrink、delete を活用

Snapshot と Restore

  • S3/GCS/Azure Blob リポジトリ
  • 増分バックアップ
  • 大規模 cluster では restore に数時間

12. Ingest パイプライン

  • Logstash — Input -> Filter -> Output、JVM ベースで重い
  • Beats — Filebeat/Metricbeat/Packetbeat、Go ベースで軽量
  • Elastic Agent + Fleet — 単一エージェント、中央管理
  • OpenTelemetry — 2024 年から OTel -> ES が一級対応、Collector が Logstash 代替

Ingest Node Pipeline

{
  "processors": [
    {"set": {"field": "indexed_at", "value": "{{_ingest.timestamp}}"}},
    {"grok": {"field": "message", "patterns": ["%{COMBINEDAPACHELOG}"]}}
  ]
}

13. ES|QL — SQL の帰還 (2024)

Elastic 長年の念願、SQL 風クエリ言語。

FROM logs-*
| WHERE status >= 500
| STATS count = COUNT(*) BY host
| SORT count DESC
| LIMIT 10
  • パイプベース (Splunk SPL、Kusto KQL からの着想)
  • JSON Query DSL の複雑さを排除
  • 分析クエリに圧倒的に便利

OpenSearch も 2024 年に PPL (Piped Processing Language) で類似機能を追加。


14. 検索品質の評価

  • nDCG — 上位結果の関連度を log 加重
  • Precision / Recall — トレードオフ
  • Learning to Rank — クリックログから LambdaMART 学習
  • A/B Test — オフライン指標 と オンライン成功は別物

15. アンチパターン TOP 10

  1. 既定 1-shard/1-replica で数十 TB のインデックス
  2. _id 手動指定による routing 偏り
  3. インデックス/shard が多すぎる
  4. JVM heap 64 GB 設定 (Compressed OOPs 超過)
  5. Deep pagination (from=10000) — scroll/search_after を使う
  6. analyzed text フィールドに term クエリ
  7. クエリごとに cluster health check
  8. 大量削除の頻繁な実行 — _forcemerge 必要
  9. snapshot 無し
  10. Vector のみで BM25 を捨てる — Hybrid がほぼ常勝

16. チェックリスト

  • 用途分離 (logs/search/vector/analytics)
  • shard サイズ 10-50 GB
  • ILM policy (rollover、hot/warm/cold)
  • snapshot の定期実行
  • heap 32 GB 以下、off-heap はファイルキャッシュ
  • Circuit Breaker アラート
  • Analyzer 選定 (韓国語は nori)
  • Synonym 辞書の構築/管理
  • Hybrid Search の検討 (BM25 + vector + RRF)
  • CTR、nDCG、0-result 率の監視
  • bool filter でキャッシュ活用
  • ES|QL/PPL で分析クエリを読みやすく

終わりに — Lucene の肩の上の巨人

Elasticsearch、OpenSearch、Solr — 名は違えど、すべて Lucene という巨人の肩に立つ。その Lucene は 1999 年、Doug Cutting が「検索を民主化する」ために書いた 1 つの Java ライブラリから始まった。

2025 年の検索は、ログを探し、商品を探し、コンテンツを推薦し、LLM のコンテキストになり、オートコンプリートとタイポ補正を提供する。どこにでもあるが、全員が正しく理解しているわけではない。Inverted Index と Segment、BM25 と Vector Search、Shard と Routing が腑に落ちた瞬間、あなたは検索を使う人から設計する人になる。


"A good search engine doesn't just find what you typed. It finds what you meant." — Peter Norvig