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

- Name
- Youngju Kim
- @fjvbn20031
"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]。数十億ドキュメントでも に近い速度。
Lucene の実装単位
- Term Dictionary — 全 term のソート済み辞書 (FST)
- Postings List — term ごとのドキュメントリスト + 頻度/位置
- Stored Fields — 原文 (圧縮)
- Doc Values — 集計/ソート用のカラムナ格納
- 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 秒ごと |
| Flush | Segment をディスクに fsync | 自動 (メモリ閾値) |
| Commit | translog 含む完全永続化 | 頻度は低い |
「Near Real-Time Search」の秘密: Refresh は 1 秒、Flush は後で。これが Elasticsearch のトレードマーク「1 秒の遅延」だ。
Translog
- すべての書き込みは translog に先に記録
- ノード再起動時に再生
index.translog.durability:request(毎回 fsync、遅いが損失 0) vsasync(定期、既定 5 秒)
4. BM25 — TF-IDF を置き換えたスコア
TF-IDF の限界
長い文書ほど tf が増え、スコアが膨張する。
BM25 の式
重要な変更:
- Saturation — 同じ単語が多くても増加が鈍化 ()
- 長さ正規化 — 文書長を平均と比較しペナルティ/ボーナス ()
なぜ BM25 が既定か
- 20 年以上の経験的実績
- パラメータ 2 つで調整可能
- Lucene 既定 (2016-)
商品名やクエリログ分析では b=0.3 程度まで下げるのが一般的。
5. Analyzer — トークン化の芸術
3 段パイプライン
- Character Filter — HTML 除去、文字置換
- Tokenizer — 文を単語に分割
- 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 の迷宮
主なクエリ
| クエリ | 用途 |
|---|---|
match | Analyzer 経由のフルテキストマッチ |
term | Analyzer を通さず完全一致 (keyword) |
match_phrase | 順序保持フレーズ |
multi_match | 複数フィールド同時検索 |
bool | AND/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
| 観点 | BM25 | Vector |
|---|---|---|
| 完全一致 (SKU) | 強 | 弱 |
| 意味類似 | 弱 | 強 |
| 希少語 (専門用語) | 強 | 弱 |
| タイポ | 弱 | 中 |
| 多言語 | 弱 | 強 |
両方が必要だ。
RRF — Reciprocal Rank Fusion
- 順位のみで結合
- スケール差に無関係 (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 8 | Elastic License 2 / SSPL | Elastic |
| Elasticsearch 8.14+ | AGPL 追加 (2024.8) | Elastic (回復の試み) |
| OpenSearch 2.x | Apache 2.0 | AWS -> 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-shard/1-replica で数十 TB のインデックス
_id手動指定による routing 偏り- インデックス/shard が多すぎる
- JVM heap 64 GB 設定 (Compressed OOPs 超過)
- Deep pagination (
from=10000) — scroll/search_after を使う - analyzed text フィールドに term クエリ
- クエリごとに cluster health check
- 大量削除の頻繁な実行 —
_forcemerge必要 - snapshot 無し
- 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