kevy4.0

全文検索(KIND text

textはインデックスエンジンの3番目のkindです。ほかのインデックスと同じように宣言すれば、フィールドの生バイト列がトークン化され、シャードごとの転置セグメントになります。セグメントはすべての書き込みと同期して維持されます(ドリフトゼロ。range/uniqueと同じ、構成上の派生物という規律です)。別立ての検索サーバーも、取り込みパイプラインも、アナライザの設定もありません。1つのverbがインデックスを宣言し、以降のすべての書き込みがそれを正確に保ちます。

IDX.CREATE posts ON PREFIX post: FIELD body TYPE str KIND text
IDX.QUERY posts MATCH "rust 全文检索" LIMIT 10 [FIELDS title body]

クイックスタート(サーバー)

行は宣言したプレフィックス配下のハッシュキーであり、インデックス対象の値は宣言された1つのフィールドです。rangeインデックスとまったく同じです(indexes.md)。

kevy-cli -p 6004 IDX.CREATE posts ON PREFIX post: FIELD body TYPE str KIND text
kevy-cli -p 6004 HSET post:1 title "intro" body "kevy is a pure-Rust key-value store"
kevy-cli -p 6004 HSET post:2 title "search" body "dictionary-free CJK full-text search"
kevy-cli -p 6004 IDX.QUERY posts MATCH "full-text rust" LIMIT 10

応答はkey, scoreの組のフラットな配列で、良いものが先に来ます。FIELDSを足すと、各ヒットを所有するシャード上で、指定したハッシュフィールドを同じ呼び出しの中でhydrateします(2回目の往復はありません)。

kevy-cli -p 6004 IDX.QUERY posts MATCH "search" LIMIT 10 FIELDS title

補助verbは、ほかのkindと同じようにtextインデックスにも効きます。

MATCHLIMIT(1000以下)とFIELDSを受け取ります。CURSOR形式は存在しません(理由は「マッチとランキング」を参照)。

クイックスタート(組み込み)

プロセス内でも同じエンジンです。textのkindはtext cargoフィーチャの背後にあります(デフォルトで有効。コンパイルから外すと、IndexKind::Textでのidx_createKevyError::Unsupportedを返します)。

use kevy_embedded::{Config, IndexKind, IndexValType, Store};

fn main() -> kevy_embedded::KevyResult<()> {
    let store = Store::open(Config::default())?;

    store.idx_create(b"posts", b"post:", b"body", IndexValType::Str,
                     IndexKind::Text)?;   // builds synchronously
    store.hset(b"post:1", &[
        (b"title".as_slice(), b"intro".as_slice()),
        (b"body".as_slice(),
         b"kevy is a pure-Rust key-value store".as_slice()),
    ])?;

    // BM25-ranked hits, best first: Vec<(key, score)>.
    let hits = store.idx_match(b"posts", b"rust store", 10)?;
    for (key, score) in &hits {
        println!("{} {score}", String::from_utf8_lossy(key));
    }
    Ok(())
}

idx_matchKevyResult<Vec<(Vec<u8>, f64)>>を返します。インデックスが見つからなければKevyError::NotFoundがそれを名指しします。組み込みにFIELDSのhydrationはありません——プロセス内にいるのですから、フィールドはhgetで読んでください。組み込みのビルドは同期的です。idx_createはインデックスが提供可能になった時点で返ります(ポーリングすべき-INDEXBUILDINGの局面はありません)。

トークン化(辞書不要)

bigram方式がCJK対応のすべてです。出荷すべき辞書もなければ、古くなる辞書もありません。混在スクリプトの文書("Rust 入门")は、両方の半分をそれぞれの規則でインデックスします。代償は、1文字のCJKクエリにおける再現率のノイズです(それらは孤立文字として出力されたものにしかマッチしません)。2文字以上でクエリしてください。

マッチとランキング

MATCHクエリトークン上のORセマンティクスで、BM25でランクづけします(k1=1.2、b=0.75、非負idfのバリアント)。より多くの、そしてより稀なクエリ語にマッチする文書ほど上位に来ます。語の頻度は飽和し、長い文書は正規化により下げられます。

宣言された近似が2つあります。

フレーズクエリなし、ブール構文なし、ハイライトなし——それらはクエリエンジンへの坂道です(意図的にスコープ外です)。それらが必要なら、あなたが説明しているのは検索エンジンです。kevyのtext kindは「宣言されたフィールドに対するランクづけされた参照」で止まります。

ハイブリッド検索(BM25 + KNN)

同じコーパス上のtextインデックスとANNインデックスは、reciprocal-rank fusionによってサーバーサイドで融合します。

IDX.QUERY HYBRID posts MATCH "rust storage" embs KNN <f32-le-blob>
    [LIMIT n] [RRFK k] [EF ef] [FIELDS f…]

各ヒットの融合スコアは、2つの結果リストにわたるΣ 1/(k + rank_i)です(RRFKのデフォルトは60——標準的な融合定数です。生のスコアではなく順位を使うので、BM25と距離を互いに較正する必要がありません)。KNN側はvector-search.mdを参照してください。HYBRIDはサーバーのverbです。組み込みの呼び出し側はidx_matchidx_knnを走らせ、プロセス内で融合してください。

ライフサイクルと整合性

どのインデックスkindとも同じ包絡線です(indexes.md)。

パフォーマンス

top-Kの評価にはMaxScoreによる枝刈りを使います(稀な語から先に処理し、より一般的なリストは、新しい文書をtop Kへ押し上げられなくなった時点から候補ごとのプローブに切り替わります)。postingsはdoc-idの転置リストで、2通りにインパクト順序づけされています。tfのバケットは降順、各バケットの内側では疎なlog2 dlバンドが昇順です。単一語のクエリ(枝刈りの相手となる2本目のリストがない場合)は、下端がk番目の床を超えられない最初のバンドでちょうど停止します。おかげで最も一般的な語ですら、20万文書でおよそ0.1msで答えます(postingsの全走査だった頃はおよそ6msでした)。postingsが1件だけのトークン(Zipfのロングテール)はインラインにとどまり、ヒープを消費しません。

計測された包絡線です(領収書はbenchツリーにあります)。

書き込み側は標準的なインデックス税です。マッチするインデックス1つにつき、書き込みごとにハッシュフィールド読み出し1回とセグメント更新1回。空のカタログのコストは、書き込みごとに1回の分岐しない分岐です。

サイジング

bytes ≈ Σ_token (token_len + 48) + postings × 64 + Σ_doc (key_len + text_len + 72)(文書は元のテキストを保持するので、更新は自分のトークンだけをちょうど取り除けます)。IDX.VERIFY / IDX.LISTがライブで報告します(textのkindではentries/bytes/postings/tokens)。bench/textgate.shがこの式を実RSSの増分に対してゲートします。

再構築は標準のバックフィルの骨組みに乗ります(サーバーではtick増分、組み込みでは同期)。

関連項目