プログラミング
Lumeの仕組み: 検索のプリミティブ
How Lume Works: The Retrieval Primitives (deepbluedynamics.com)
要約
Lumeは、GitHubでオープンに開発されているRust製のハイブリッド検索エンジンです。このシステムは、BM25、GTR-T5ベクトル、およびエンティティグラフという3つの独立したプリミティブを使ってドキュメントをランク付けし、クエリから証拠までの全ステップを検査可能にすることを目指しています。ローカルファースト、レイヤード、監査可能という設計原則に基づき、エージェントシステムにおける検索を「魔法のステップ」として扱わないことを目指しています。
全文翻訳
DeepBlue Dynamics / Signal Log / lume-retrieval-primitivesLumeは、Steve Harrisと私がgithub.com/DeepBlueDynamics/lumeでオープンに開発しているRust製のハイブリッド検索エンジンです。これは、小さなCLIとMCPサーバーで構成され、BSD-3ライセンスで提供されています。そして、「エージェントが質問をしたとき、クエリから証拠までのすべてのステップは検査可能であるべきだ」という頑固なアイデアに基づいて構築されています。LumeはMarkdown、ソースコード、PDF(小さなPythonエクストラクター経由)をインデックス化し、フィールド認識BM25、Shivvr経由の稠密なGTR-T5ベクトル、および重要度スコア付きエンティティグラフという3つの独立したプリミティブでそれらをランク付けします。語彙コアとグラフは完全にマシン上で実行されます。稠密なベクトルのみが外部に呼び出され、そのエンドポイントはデフォルトでlocalhostになっています。不透明な「ランキングを返す検索ボックス」はありません。すべてのスコアには名前、ファイル、および調整可能なパラメータがあります。この記事では、Lumeの検索コアをエンドツーエンドで、現在のツリーへの行レベルの参照とともに解説します。もしあなたがエージェントシステムを構築していて、検索を魔法のステップとして扱うことにうんざりしているなら、これはあなたのためです。A few principles up front, because they explain the design:Local-first. Lexical search and the entity graph run entirely on your machine. Dense vectors are fetched from Shivvr through SHIVVR_BASE_URL, which defaults to a local endpoint.Layered, not monolithic. BM25, semantic, and graph are independent signals with their own scores. The blend is one line; each input is replaceable.Auditable. The engine prints what it pruned, what it ranked, and why it rejected the rest.設計を説明するため、いくつかの原則を最初に挙げます。ローカルファースト。語彙検索とエンティティグラフは完全にマシン上で実行されます。稠密なベクトルはSHIVVR_BASE_URL(デフォルトはローカルエンドポイント)を通じてShivvrからフェッチされます。レイヤード、モノリシックではない。BM25、セマンティック、およびグラフは、それぞれ独自のスコアを持つ独立したシグナルです。ブレンドは1行で記述され、各入力は置き換え可能です。監査可能。エンジンは、何がプルーニングされ、何がランク付けされ、なぜ残りが拒否されたかを出力します。0. 検索の単位:セクションLumeはMarkdownをインデックス化し、#ヘッダーでセクションに分割します(src/bm25.rs:211のparse_markdown)。セクション(src/bm25.rs:106)は、すべてがランク付けされる原子です。pub struct Section { pub title: String, pub body: String, pub line_number: usize, pub filename: Option<String>, pub entities: Vec<String>, // resolved named entities, for the graph }タイトルと本文は、それぞれ独立したフィールドであり、独立した統計情報を持っています。この区別は、スコアリングに即座に現れます。インデックス全体は、Bm25Index(src/bm25.rs:147)としてメモリに保存されます。これには、フィールドごとの用語頻度マップ、ドキュメント頻度、フィールド長、ローリングビットマップポスティングリスト、素数/ゲーデルシグネチャフィルター、およびグラフに供給されるエンティティポスティングリストが含まれます。1. プリミティブ:フィールド認識BM25語彙コアは、3つの選択可能なバリアントを持つフィールド認識BM25です。チューニングのデフォルト(src/bm25.rs:125のBm25Params)は意図的にクラシックです。Self { k1: 1.2, b: 0.75, delta: 1.0, title_weight: 2.0, body_weight: 1.0 }k1は用語頻度の飽和を制御し、bは長さの正規化を制御します。唯一意見が分かれる選択はtitle_weight: 2.0です。これは、タイトルでのヒットが、本文でのヒットよりも、調整係数が適用される前に2倍貢献することを意味します。これは有用ですが、クエリトークンが広範な場合、章のタイトルを過度に重視する可能性があります。これを法律ではなく、調整可能なノブとして扱ってください。IDFは標準的な平滑化された形式で、0でフロアリングされ、各用語の貢献はフィールドごとに計算され、その後フィールドウェイトで合計されます(src/bm25.rs:728のcalculate_bm25_term_score)。let len_normalization = 1.0 - b + b * (doc_len / avgdl);match variant { SearchVariant::Classic => idf * (tf * (k1 + 1.0)) / (tf + k1 * len_normalization), SearchVariant::Plus => idf * ((tf*(k1+1.0))/(tf + k1*len_normalization) + params.delta), SearchVariant::L => { let s = tf / len_normalization; idf * (s*(k1+1.0))/(s + k1) }, } // total_score += title_weight * title_score + body_weight * body_score; (src/bm25.rs:635)Classicは教科書通りのBM25です。Plusは、マッチした用語が何も貢献しないことを防ぐためにデルタフロアを追加し、BM25の長いドキュメントに対する過剰なペナルティに対抗します。Lは、長さの正規化を飽和の内側に移動させ、非常に長いドキュメントを平滑化します。LumeはデフォルトでClassicを実行します(src/main.rs:1430)。2. 2段階プルーニング:ローリングユニオン、次にゲーデルシグネチャすべてのクエリに対して、本の1,926のセクションすべてをBM25スコアリングする必要はありません。Lumeの検索(src/bm25.rs:445)は2段階です。ステージ1 — 候補の収集。クエリ用語のローリングビットマップポスティングリストを結合します。これはいくつかのビットセットOR演算であり、コーパスを任意のクエリ用語を含むセクションに瞬時に絞り込みます。// src/bm25.rs:460let mut candidate_set = MiniRoaring::new();let mut first = true;for q_tok in &query_tokens { if let Some(list) = self.posting_lists.get(&q_tok.bytes) { if first { candidate_set = list.clone(); first = false; } else { candidate_set = candidate_set.union(list); } } }ステージ1b — ゲーデルタグシグネチャプルーニング。クエリタガーがエンティティを認識する場合、各候補セクションは素因数分解されたシグネチャフィルターに対して検証されます(src/fast_retrieval.rs:449のPrimeFilter::test_tag_prime、src/bm25.rs:538で評価)。各既知のタグ出力は素数にマップされ、セクションのタグシグネチャはそのタグの素数の積であるため、包含は割り算によってチェックされます。不明なクエリタグは意図的にダミーの素数を受け取り、クローズに失敗します。失敗した候補は、より重いスコアリングの前にTagSignatureMismatchとして破棄されます。ステージ2 — 重いスコアリングは生存者のみに対して実行されます。そして、エンジンは標準エラー出力でファネルの形状を通知します(src/bm25.rs:557)。[Two-Stage Pruning] Pruned candidate space from 1926 to 302 (roaring generated: 609) sections in 54.70µsCandidates: 609Ranked: 302Rejected: TagSignatureMismatch: 307...このアカウンティングは装飾ではありません。クエリが間違ったものを返したときに最初に読むべきものです。3. クエリ衛生:ストップワードと調整2つの小さなプリミティブが品質に大きな影響を与えます。クエリ側のストップワードフィルタリング(src/bm25.rs:98のfilter_query_stopwords)。機能語や疑問詞はクエリからのみ除去され、インデックスからは除去されません。これがないと、「how does Dantès know Mercédès」というクエリはhow/does/knowによって支配され、無関係なセクション(「How a Gardener…」というタイトルの章など)にマッチしてしまいます。セーフティネットとして、すべてのトークンがストップワードの場合(「how are you」)、元のトークンが保持されるため、結果が得られます。調整係数(src/bm25.rs:638)。異なるクエリ用語により多くマッチするドキュメントは、単一の共通用語を繰り返すドキュメントよりも優れているべきです。Lumeはスコアをカバレッジベースの係数で乗算します。let coverage = matched_terms.len() as f64 / num_distinct as f64;let coord = COORD_FLOOR + (1.0 - COORD_FLOOR) * coverage; // COORD_FLOOR = 0.5total_score *= coord;したがって、3つのクエリ用語すべてにマッチするセクションはスコアの100%を維持し、3つの用語のうち1つにマッチするセクションは約⅔を維持します。単一用語のクエリの場合、coverage == 1.0なので、通常のルックアップは影響を受けません。これは優しい後押しであり、厳密なANDではありません。4. プリミティブ:稠密なベクトル(ローカルGTR-T5)語彙検索は語彙のギャップを埋めることができません。「starved to death」(餓死)と「gastroenteritis」(胃腸炎)などです。それがセマンティックプリミティブの仕事です。Lumeは、Shivvr経由でテキストを768次元のGTR-T5ベクトルに埋め込みます。デフォルトのベースURLはhttp://localhost:8085(src/hybrid.rs:777)で、リクエストにはサービス・トークン(src/hybrid.rs:784)が依然として必要です。専用の埋め込みエンドポイントはないため、embed_text(src/hybrid.rs:43)は一時的なスクラッチストアに取り込み、応答から直接ベクトルを読み取ります。// 768-d GTR-T5 ("organize") vector, asserted on the way out:if emb.len() != 768 { return Err(format!("Expected 768-d GTR-T5 vector, got {}", emb.len())); }インデックス作成時、セクションはセマンティックセッションにプッシュされます(src/hybrid.rs:581のensure_semantic_session)。巧妙な点は増分性です。各セクションにはコンテンツハッシュ(src/hybrid.rs:407のsection_hash)が与えられ、行番号は意図的に除外されます。これにより、セクションを移動しても再埋め込みが強制されません。再インデックス作成はハッシュで差分を取り、変更されたものだけを補充します。一致するコーパスフィンガープリントは何も操作しません。クエリ結果もキャッシュされます(.lume-semantic-cache.json)。クエリ時、query_semantic_search(src/hybrid.rs:637)はShivvrにn=60の近傍を要求します。インデックスがセマンティックベクトルなしで構築された場合、またはトークンが不足している場合、検索はクリーンに語彙BM25にダウングレードされ、その旨が通知されます。アルファ>0のリクエストが語彙のみのインデックスに対して行われた場合、語彙のみの検索が実行されたことが通知されます(src/main.rs:1389)。5. プリミティブ:セマンティック知識グラフ(共起ではなく重要度)3番目のシグナルは構造的です。Lume