Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Laurus

Rust 向けの高速で多機能なハイブリッド検索ライブラリ。

Laurus は、Lexical 検索(転置インデックス(Inverted Index)によるキーワードマッチング)と Vector 検索(エンベディングによるセマンティック類似度検索)を単一のエンジンに統合した、純 Rust ライブラリです。外部サーバーを必要とせず、Rust アプリケーションに直接組み込んで使用できます。

主な機能

機能説明
Lexical 検索BM25 スコアリングを備えた転置インデックスによる全文検索
Vector 検索Flat、HNSW、IVF インデックスを用いた近似最近傍探索(ANN)
ハイブリッド検索Lexical と Vector の検索結果を融合アルゴリズム(RRF、WeightedSum)で統合
テキスト解析プラガブルなアナライザパイプライン — トークナイザ、フィルタ、ステマー、シノニム
エンベディングCandle(ローカル BERT/CLIP)、OpenAI API、カスタムエンベッダをビルトインサポート
ストレージプラガブルなバックエンド — インメモリ、ファイルベース、メモリマップド
Query DSLLexical、Vector、ハイブリッド検索のための人間が読みやすいクエリ構文
純 Rustコアに C/C++ 依存なし — 安全でポータブル、ビルドも容易

仕組み

graph LR
    subgraph Your Application
        D["Document"]
        Q["Query"]
    end

    subgraph Laurus Engine
        SCH["Schema"]
        AN["Analyzer"]
        EM["Embedder"]
        LI["Lexical Index\n(Inverted Index)"]
        VI["Vector Index\n(HNSW / Flat / IVF)"]
        FU["Fusion\n(RRF / WeightedSum)"]
    end

    D --> SCH
    SCH --> AN --> LI
    SCH --> EM --> VI
    Q --> LI --> FU
    Q --> VI --> FU
    FU --> R["Ranked Results"]
  1. スキーマを定義する — フィールドとその型(text、integer、vector など)を宣言します
  2. Engine を構築する — テキスト用のアナライザと Vector 用のエンベッダを接続します
  3. ドキュメントをインデックスする — Engine が各フィールドを適切なインデックスに自動的に振り分けます
  4. 検索する — Lexical、Vector、またはハイブリッドクエリを実行し、ランク付けされた結果を取得します

ドキュメントマップ

セクション学べること
はじめにLaurus をインストールし、数分で最初の検索を実行する
アーキテクチャEngine のコンポーネントとデータフローを理解する
コアコンセプトスキーマ、テキスト解析、エンベディング、ストレージ
インデクシング転置インデックスと Vector インデックスの内部動作
検索クエリの種類、Vector 検索、ハイブリッド融合
Query DSLすべての検索タイプに対応した人間が読みやすいクエリ構文
ライブラリ (laurus)Engine の内部構造、スコアリング、ファセット、拡張性
CLI (laurus-cli)インデックス管理と検索のためのコマンドラインツール
サーバー (laurus-server)HTTP Gateway を備えた gRPC サーバー
開発ガイドLaurus のビルド、テスト、コントリビュート

クイックサンプル

use std::sync::Arc;
use laurus::{Document, Engine, Schema, SearchRequestBuilder, Result};
use laurus::lexical::{TextOption, TermQuery};
use laurus::storage::memory::MemoryStorage;

#[tokio::main]
async fn main() -> Result<()> {
    // 1. Storage
    let storage = Arc::new(MemoryStorage::new(Default::default()));

    // 2. Schema
    let schema = Schema::builder()
        .add_text_field("title", TextOption::default())
        .add_text_field("body", TextOption::default())
        .add_default_field("body")
        .build();

    // 3. Engine
    let engine = Engine::builder(storage, schema).build().await?;

    // 4. Index a document
    let doc = Document::builder()
        .add_text("title", "Hello Laurus")
        .add_text("body", "A fast search library for Rust")
        .build();
    engine.add_document("doc-1", doc).await?;
    engine.commit().await?;

    // 5. Search
    let request = SearchRequestBuilder::new()
        .lexical_search_request(
            laurus::LexicalSearchRequest::new(
                Box::new(TermQuery::new("body", "rust"))
            )
        )
        .limit(10)
        .build();
    let results = engine.search(request).await?;

    for r in &results {
        println!("{}: score={:.4}", r.id, r.score);
    }
    Ok(())
}

ライセンス

Laurus は MIT ライセンス のもとで提供されています。

アーキテクチャ

このページでは、Laurus の内部構造について説明します。アーキテクチャを理解することで、スキーマ設計、Analyzer の選択、検索戦略についてより適切な判断ができるようになります。

プロジェクト構成

Laurus は Cargo workspace として 3 つのクレートで構成されています。

graph TB
    CLI["laurus-cli\n(Binary Crate)\nCLI + REPL"]
    SRV["laurus-server\n(Library + Binary)\ngRPC Server + HTTP Gateway"]
    LIB["laurus\n(Library Crate)\nCore Search Engine"]

    CLI --> LIB
    SRV --> LIB
クレート種類説明
laurusLibraryコア検索エンジン – Lexical 検索、Vector 検索、ハイブリッド検索
laurus-cliBinaryインデックス管理と検索のためのコマンドラインインターフェース
laurus-serverLibrary + Binaryオプションの HTTP/JSON ゲートウェイ付き gRPC サーバー

各クレートの詳細については以下を参照してください。

全体概要

Laurus は単一の Engine を中心に構成されており、4 つの内部コンポーネントを統括します。

graph TB
    subgraph Engine
        SCH["Schema"]
        LS["LexicalStore\n(Inverted Index)"]
        VS["VectorStore\n(HNSW / Flat / IVF)"]
        DL["DocumentLog\n(WAL + Document Storage)"]
    end

    Storage["Storage (trait)\nMemory / File / File+Mmap"]

    LS --- Storage
    VS --- Storage
    DL --- Storage
コンポーネント役割
Schemaフィールドとその型を宣言し、各フィールドのルーティング先を決定する
LexicalStoreキーワード検索のための転置インデックス(Inverted Index)(BM25 スコアリング)
VectorStore類似度検索のためのベクトルインデックス(Flat、HNSW、または IVF)
DocumentLog耐久性のための WAL(Write-Ahead Log)+ ドキュメントストレージ

3 つのストアはすべて単一の Storage バックエンドを共有し、キープレフィックス(lexical/vector/documents/)によって分離されています。

Engine のライフサイクル

Engine の構築

EngineBuilder が各パーツから Engine を組み立てます。

#![allow(unused)]
fn main() {
let engine = Engine::builder(storage, schema)
    .analyzer(analyzer)      // optional: for text fields
    .embedder(embedder)      // optional: for vector fields
    .build()
    .await?;
}
sequenceDiagram
    participant User
    participant EngineBuilder
    participant Engine

    User->>EngineBuilder: new(storage, schema)
    User->>EngineBuilder: .analyzer(analyzer)
    User->>EngineBuilder: .embedder(embedder)
    User->>EngineBuilder: .build().await
    EngineBuilder->>EngineBuilder: split_schema()
    Note over EngineBuilder: Separate fields into\nLexicalIndexConfig\n+ VectorIndexConfig
    EngineBuilder->>Engine: Create LexicalStore
    EngineBuilder->>Engine: Create VectorStore
    EngineBuilder->>Engine: Create DocumentLog
    EngineBuilder->>Engine: Recover from WAL
    EngineBuilder-->>User: Engine ready

build() 時に Engine は以下の処理を行います。

  1. スキーマの分割 — Lexical フィールドは LexicalIndexConfig へ、Vector フィールドは VectorIndexConfig へ振り分けられる
  2. プレフィックス付きストレージの作成 — 各コンポーネントが分離された名前空間を取得する(lexical/vector/documents/
  3. ストアの初期化LexicalStoreVectorStore がそれぞれの設定で構築される
  4. WAL からの復旧 — 前回のセッションからの未コミット操作を再生する

スキーマの分割

Schema には Lexical フィールドと Vector フィールドの両方が含まれています。ビルド時に split_schema() がこれらを分離します。

graph LR
    S["Schema\ntitle: Text\nbody: Text\ncategory: Text\npage: Integer\ncontent_vec: HNSW"]

    S --> LC["LexicalIndexConfig\ntitle: TextOption\nbody: TextOption\ncategory: TextOption\npage: IntegerOption\n_id: KeywordAnalyzer"]

    S --> VC["VectorIndexConfig\ncontent_vec: HnswOption\n(dim=384, m=16, ef=200)"]

主なポイント:

  • 予約フィールド _id は常に KeywordAnalyzer(完全一致)で Lexical 設定に追加される
  • PerFieldAnalyzer はフィールドごとの Analyzer 設定をラップする。単純な StandardAnalyzer を渡した場合、すべてのテキストフィールドのデフォルトとなる
  • PerFieldEmbedder も Vector フィールドに対して同様に動作する

インデクシングのデータフロー

engine.add_document(id, doc) を呼び出した場合の処理:

sequenceDiagram
    participant User
    participant Engine
    participant WAL as DocumentLog (WAL)
    participant Lexical as LexicalStore
    participant Vector as VectorStore

    User->>Engine: add_document("doc-1", doc)
    Engine->>WAL: Append to WAL
    Engine->>Engine: Assign internal ID (u64)

    loop For each field in document
        alt Lexical field (text, integer, etc.)
            Engine->>Lexical: Analyze + index field
        else Vector field
            Engine->>Vector: Embed + index field
        end
    end

    Note over Engine: Document is buffered\nbut NOT yet searchable

    User->>Engine: commit()
    Engine->>Lexical: Flush segments to storage
    Engine->>Vector: Flush segments to storage
    Engine->>WAL: Truncate WAL
    Note over Engine: Documents are\nnow searchable

主なポイント:

  • WAL 優先: すべての書き込みは、インメモリ構造を変更する前にログに記録される
  • デュアルインデクシング: 各フィールドはスキーマに基づいて Lexical ストアまたは Vector ストアのいずれかにルーティングされる
  • コミットが必要: ドキュメントは commit() の後にのみ検索可能になる

検索のデータフロー

engine.search(request) を呼び出した場合の処理:

sequenceDiagram
    participant User
    participant Engine
    participant Lexical as LexicalStore
    participant Vector as VectorStore
    participant Fusion

    User->>Engine: search(request)

    opt Filter query present
        Engine->>Lexical: Execute filter query
        Lexical-->>Engine: Allowed document IDs
    end

    par Lexical search
        Engine->>Lexical: Execute lexical query
        Lexical-->>Engine: Ranked hits (BM25)
    and Vector search
        Engine->>Vector: Execute vector query
        Vector-->>Engine: Ranked hits (similarity)
    end

    alt Both lexical and vector results
        Engine->>Fusion: Fuse results (RRF or WeightedSum)
        Fusion-->>Engine: Merged ranked list
    end

    Engine->>Engine: Apply offset + limit
    Engine-->>User: Vec of SearchResult

検索パイプラインは 3 つのステージで構成されています。

  1. フィルタ(オプション) — Lexical インデックスに対してフィルタクエリを実行し、許可されたドキュメント ID のセットを取得する
  2. 検索 — Lexical クエリと Vector クエリを並列に実行する
  3. フュージョン — 両方のクエリタイプが存在する場合、RRF(デフォルト、k=60)または WeightedSum を使用して結果をマージする

ストレージアーキテクチャ

すべてのコンポーネントは単一の Storage trait 実装を共有しますが、キープレフィックスを使用してデータを分離します。

graph TB
    Engine --> PS1["PrefixedStorage\nprefix: 'lexical/'"]
    Engine --> PS2["PrefixedStorage\nprefix: 'vector/'"]
    Engine --> PS3["PrefixedStorage\nprefix: 'documents/'"]

    PS1 --> S["Storage Backend\n(Memory / File / File+Mmap)"]
    PS2 --> S
    PS3 --> S
バックエンド説明最適な用途
MemoryStorageすべてのデータをメモリ上に保持テスト、小規模データセット、一時的な利用
FileStorage標準的なファイル I/O一般的な本番利用
FileStorage (mmap)メモリマップドファイル(use_mmap = true大規模データセット、読み取り負荷の高いワークロード

フィールドごとのディスパッチ

PerFieldAnalyzer が提供されている場合、Engine はフィールド固有の Analyzer に解析処理をディスパッチします。同様のパターンが PerFieldEmbedder にも適用されます。

graph LR
    PFA["PerFieldAnalyzer"]
    PFA -->|"title"| KA["KeywordAnalyzer"]
    PFA -->|"body"| SA["StandardAnalyzer"]
    PFA -->|"description"| JA["JapaneseAnalyzer"]
    PFA -->|"_id"| KA2["KeywordAnalyzer\n(always)"]
    PFA -->|other fields| DEF["Default Analyzer\n(StandardAnalyzer)"]

これにより、同一 Engine 内で異なるフィールドに異なる解析戦略を使用できます。

まとめ

項目詳細
コア構造体Engine — すべての操作を統括する
ビルダーEngineBuilder — Storage + Schema + Analyzer + Embedder から Engine を組み立てる
スキーマ分割Lexical フィールド → LexicalIndexConfig、Vector フィールド → VectorIndexConfig
書き込みパスWAL → インメモリバッファ → commit() → 永続ストレージ
読み取りパスクエリ → 並列 Lexical/Vector 検索 → フュージョン → ランク付き結果
ストレージ分離PrefixedStorage による lexical/vector/documents/ プレフィックス
フィールドごとのディスパッチPerFieldAnalyzerPerFieldEmbedder がフィールド固有の実装にルーティングする

次のステップ

はじめに

Laurus へようこそ! このセクションでは、ライブラリのインストールから最初の検索の実行までをガイドします。

作成するもの

このガイドを終えると、以下の機能を持つ検索エンジンが動作するようになります:

  • テキストドキュメントのインデックス
  • キーワード(Lexical)検索の実行
  • セマンティック(Vector)検索の実行
  • ハイブリッド検索による両者の統合

前提条件

  • Rust 1.85 以降(edition 2024)
  • Cargo(Rust に同梱)
  • Tokio ランタイム(Laurus は非同期 API を使用します)

ステップ

  1. インストール — Laurus をプロジェクトに追加し、Feature Flags を選択する
  2. クイックスタート — 5 つのステップで完全な検索エンジンを構築する

ワークフロー概要

Laurus を使った検索アプリケーションの構築は、一貫したパターンに従います:

graph LR
    A["1. Create\nStorage"] --> B["2. Define\nSchema"]
    B --> C["3. Build\nEngine"]
    C --> D["4. Index\nDocuments"]
    D --> E["5. Search"]
ステップ内容
Storage の作成データの保存先を選択する — インメモリ、ディスク、メモリマップド
Schema の定義フィールドとその型(text、integer、vector など)を宣言する
Engine の構築アナライザ(テキスト用)とエンベッダ(Vector 用)を接続する
ドキュメントのインデックスドキュメントを追加すると、Engine がフィールドを適切なインデックスに振り分ける
検索Lexical、Vector、またはハイブリッドクエリを実行し、ランク付けされた結果を取得する

インストール

プロジェクトへの Laurus の追加

Cargo.tomllaurustokio(非同期ランタイム)を追加します:

[dependencies]
laurus = "0.1.0"
tokio = { version = "1", features = ["full"] }

Feature Flags

Laurus はデフォルトで最小限の機能セットで提供されます。必要に応じて追加の機能を有効にしてください:

Feature説明ユースケース
(default)コアライブラリ(Lexical 検索、ストレージ、アナライザ — エンベディングなし)キーワード検索のみ
embeddings-candleHugging Face Candle によるローカル BERT エンベディング外部 API 不要の Vector 検索
embeddings-openaiOpenAI API エンベディング(text-embedding-3-small 等)クラウドベースの Vector 検索
embeddings-multimodalCandle による CLIP エンベディング(テキスト + 画像)マルチモーダル(テキスト→画像)検索
embeddings-all上記すべてのエンベディング機能全エンベディング対応

Lexical 検索のみ(エンベディング不要):

[dependencies]
laurus = "0.1.0"

ローカルモデルによる Vector 検索(API キー不要):

[dependencies]
laurus = { version = "0.1.0", features = ["embeddings-candle"] }

OpenAI による Vector 検索:

[dependencies]
laurus = { version = "0.1.0", features = ["embeddings-openai"] }

すべての機能:

[dependencies]
laurus = { version = "0.1.0", features = ["embeddings-all"] }

インストールの確認

Laurus が正しくコンパイルされることを確認するために、最小限のプログラムを作成します:

use laurus::Result;

#[tokio::main]
async fn main() -> Result<()> {
    println!("Laurus version: {}", laurus::VERSION);
    Ok(())
}
cargo run

バージョンが表示されれば、クイックスタートに進む準備が整っています。

クイックスタート

このチュートリアルでは、5 つのステップで完全な検索エンジンを構築する方法を説明します。最後には、ドキュメントをインデックスしてキーワード検索ができるようになります。

ステップ 1 — Storage の作成

Storage は Laurus がインデックスデータを保存する場所を決定します。開発やテストには MemoryStorage を使用します:

#![allow(unused)]
fn main() {
use std::sync::Arc;
use laurus::storage::memory::MemoryStorage;
use laurus::Storage;

let storage: Arc<dyn Storage> = Arc::new(
    MemoryStorage::new(Default::default())
);
}

ヒント: 本番環境では FileStorage(オプションで use_mmap によるメモリマップド I/O)の使用を検討してください。詳細はストレージを参照してください。

ステップ 2 — Schema の定義

Schema はドキュメント内のフィールドと、各フィールドのインデックス方法を宣言します:

#![allow(unused)]
fn main() {
use laurus::Schema;
use laurus::lexical::TextOption;

let schema = Schema::builder()
    .add_text_field("title", TextOption::default())
    .add_text_field("body", TextOption::default())
    .add_default_field("body")  // used when no field is specified in a query
    .build();
}

各フィールドには型があります。主な型は以下の通りです:

メソッドフィールド型値の例
add_text_fieldText(全文検索可能)"Hello world"
add_integer_field64 ビット整数42
add_float_field64 ビット浮動小数点数3.14
add_boolean_fieldブール値true / false
add_datetime_fieldUTC 日時2024-01-15T10:30:00Z
add_hnsw_fieldVector(HNSW インデックス)[0.1, 0.2, ...]
add_flat_fieldVector(Flat インデックス)[0.1, 0.2, ...]

全一覧はスキーマとフィールドを参照してください。

ステップ 3 — Engine の構築

Engine は Storage、Schema、ランタイムコンポーネントを統合します:

#![allow(unused)]
fn main() {
use laurus::Engine;

let engine = Engine::builder(storage, schema)
    .build()
    .await?;
}

テキストフィールドのみを使用する場合、デフォルトの StandardAnalyzer が自動的に適用されます。解析のカスタマイズや Vector エンベディングの追加については、アーキテクチャを参照してください。

ステップ 4 — ドキュメントのインデックス

DocumentBuilder でドキュメントを作成し、Engine に追加します:

#![allow(unused)]
fn main() {
use laurus::Document;

// Each document needs a unique external ID (string)
let doc = Document::builder()
    .add_text("title", "Introduction to Rust")
    .add_text("body", "Rust is a systems programming language focused on safety and performance.")
    .build();
engine.add_document("doc-1", doc).await?;

let doc = Document::builder()
    .add_text("title", "Python for Data Science")
    .add_text("body", "Python is widely used in machine learning and data analysis.")
    .build();
engine.add_document("doc-2", doc).await?;

let doc = Document::builder()
    .add_text("title", "Web Development with JavaScript")
    .add_text("body", "JavaScript powers interactive web applications and server-side code with Node.js.")
    .build();
engine.add_document("doc-3", doc).await?;

// Commit to make documents searchable
engine.commit().await?;
}

重要: ドキュメントは commit() が呼ばれるまで検索可能になりません。

ステップ 5 — 検索

SearchRequestBuilder とクエリを使ってインデックスを検索します:

#![allow(unused)]
fn main() {
use laurus::{SearchRequestBuilder, LexicalSearchRequest};
use laurus::lexical::TermQuery;

// Search for "rust" in the "body" field
let request = SearchRequestBuilder::new()
    .lexical_search_request(
        LexicalSearchRequest::new(
            Box::new(TermQuery::new("body", "rust"))
        )
    )
    .limit(10)
    .build();

let results = engine.search(request).await?;

for result in &results {
    println!("ID: {}, Score: {:.4}", result.id, result.score);
    if let Some(doc) = &result.document {
        if let Some(title) = doc.get("title") {
            println!("  Title: {:?}", title);
        }
    }
}
}

完全なサンプル

以下は、コピー・ペーストしてそのまま実行できる完全なプログラムです:

use std::sync::Arc;
use laurus::{
    Document, Engine, LexicalSearchRequest,
    Result, Schema, SearchRequestBuilder,
};
use laurus::lexical::{TextOption, TermQuery};
use laurus::storage::memory::MemoryStorage;

#[tokio::main]
async fn main() -> Result<()> {
    // 1. Storage
    let storage = Arc::new(MemoryStorage::new(Default::default()));

    // 2. Schema
    let schema = Schema::builder()
        .add_text_field("title", TextOption::default())
        .add_text_field("body", TextOption::default())
        .add_default_field("body")
        .build();

    // 3. Engine
    let engine = Engine::builder(storage, schema).build().await?;

    // 4. Index documents
    for (id, title, body) in [
        ("doc-1", "Introduction to Rust", "Rust is a systems programming language focused on safety."),
        ("doc-2", "Python for Data Science", "Python is widely used in machine learning."),
        ("doc-3", "Web Development", "JavaScript powers interactive web applications."),
    ] {
        let doc = Document::builder()
            .add_text("title", title)
            .add_text("body", body)
            .build();
        engine.add_document(id, doc).await?;
    }
    engine.commit().await?;

    // 5. Search
    let request = SearchRequestBuilder::new()
        .lexical_search_request(
            LexicalSearchRequest::new(
                Box::new(TermQuery::new("body", "rust"))
            )
        )
        .limit(10)
        .build();

    let results = engine.search(request).await?;
    for r in &results {
        println!("{}: score={:.4}", r.id, r.score);
    }

    Ok(())
}

次のステップ

サンプル

laurus/examples/ ディレクトリには、ライブラリのさまざまな機能を示す実行可能なサンプルが含まれています。

サンプルの実行

# Feature Flags なしでサンプルを実行
cargo run --example <name>

# Feature Flags を指定してサンプルを実行
cargo run --example <name> --features <flag>

利用可能なサンプル

quickstart

基本的なワークフローを示す最小限のサンプルです: Storage の作成、Schema の定義、Engine の構築、ドキュメントのインデックス、検索を行います。

cargo run --example quickstart

デモ内容: インメモリストレージ、TextOptionTermQueryLexicalSearchRequest

すべての Lexical クエリ型を示す包括的なサンプルです。Builder API と QueryParser DSL の両方を使用します。

cargo run --example lexical_search

デモ内容: TermQueryPhraseQueryFuzzyQueryWildcardQueryNumericRangeQueryGeoQueryBooleanQuerySpanQuery

モックエンベッダを使用した Vector 検索のサンプルです。フィルタ付き Vector 検索や DSL 構文も含みます。

cargo run --example vector_search

デモ内容: PerFieldEmbedderVectorSearchRequestBuilder、フィルタ付き検索、DSL 構文(field:~"query"

異なる融合アルゴリズムを用いた Lexical 検索と Vector 検索の統合サンプルです。

cargo run --example hybrid_search

デモ内容: Lexical のみ、Vector のみ、ハイブリッド検索。RRFWeightedSum の両方の融合アルゴリズム。Builder API と DSL。

search_with_candle

Hugging Face Candle を使用した実際の BERT エンベディングによる Vector 検索です。初回実行時にモデルが自動的にダウンロードされます(約 80 MB)。

cargo run --example search_with_candle --features embeddings-candle

必要条件: embeddings-candle Feature Flag

デモ内容: CandleBertEmbeddersentence-transformers/all-MiniLM-L6-v2、384 次元)

search_with_openai

OpenAI Embeddings API を使用した Vector 検索です。

export OPENAI_API_KEY=your-api-key
cargo run --example search_with_openai --features embeddings-openai

必要条件: embeddings-openai Feature Flag、OPENAI_API_KEY 環境変数

デモ内容: OpenAIEmbeddertext-embedding-3-small、1536 次元)

CLIP モデルを使用したマルチモーダル(テキスト + 画像)検索です。

cargo run --example multimodal_search --features embeddings-multimodal

必要条件: embeddings-multimodal Feature Flag

デモ内容: CandleClipEmbedder、ファイルシステムからの画像インデックス、テキスト→画像クエリおよび画像→画像クエリ

synonym_graph_filter

解析時のトークン展開のための SynonymGraphFilter のデモです。

cargo run --example synonym_graph_filter

デモ内容: シノニム辞書の作成、シノニムによるトークン展開、ブーストの適用、トークンの position および position_length 属性

ヘルパーモジュール: common.rs

common.rs ファイルは、サンプルで使用される共通ユーティリティを提供します:

  • memory_storage() – インメモリストレージインスタンスの作成
  • per_field_analyzer() – 特定のフィールドに KeywordAnalyzer を設定した PerFieldAnalyzer の作成
  • MockEmbedder – 実際のモデルなしで Vector 検索をテストするためのモック Embedder 実装

スキーマとフィールド

Schema はドキュメントの構造を定義します。どのフィールドが存在し、各フィールドがどのようにインデクシングされるかを指定します。Schema は Engine にとって唯一の情報源です。

CLI で使用される TOML ファイル形式については、スキーマフォーマットリファレンスを参照してください。

Schema

Schema は名前付きフィールドのコレクションです。各フィールドはLexical フィールド(キーワード検索用)または Vector フィールド(類似度検索用)のいずれかです。

#![allow(unused)]
fn main() {
use laurus::Schema;
use laurus::lexical::TextOption;
use laurus::lexical::core::field::IntegerOption;
use laurus::vector::HnswOption;

let schema = Schema::builder()
    .add_text_field("title", TextOption::default())
    .add_text_field("body", TextOption::default())
    .add_integer_field("year", IntegerOption::default())
    .add_hnsw_field("embedding", HnswOption::default())
    .add_default_field("body")
    .build();
}

デフォルトフィールド

add_default_field() は、クエリがフィールド名を明示的に指定しない場合に検索対象となるフィールドを指定します。これは Query DSL パーサーで使用されます。

フィールドタイプ

graph TB
    FO["FieldOption"]

    FO --> T["Text"]
    FO --> I["Integer"]
    FO --> FL["Float"]
    FO --> B["Boolean"]
    FO --> DT["DateTime"]
    FO --> G["Geo"]
    FO --> BY["Bytes"]

    FO --> FLAT["Flat"]
    FO --> HNSW["HNSW"]
    FO --> IVF["IVF"]

Lexical フィールド

Lexical フィールドは転置インデックス(Inverted Index)を使用してインデクシングされ、キーワードベースのクエリをサポートします。

タイプRust 型SchemaBuilder メソッド説明
TextTextOptionadd_text_field()全文検索可能。Analyzer によりトークン化される
IntegerIntegerOptionadd_integer_field()64 ビット符号付き整数。範囲クエリをサポート
FloatFloatOptionadd_float_field()64 ビット浮動小数点数。範囲クエリをサポート
BooleanBooleanOptionadd_boolean_field()true / false
DateTimeDateTimeOptionadd_datetime_field()UTC タイムスタンプ。範囲クエリをサポート
GeoGeoOptionadd_geo_field()緯度/経度のペア。半径検索とバウンディングボックスクエリをサポート
BytesBytesOptionadd_bytes_field()バイナリデータ

Text フィールドオプション

TextOption はテキストのインデクシング方法を制御します。

#![allow(unused)]
fn main() {
use laurus::lexical::TextOption;

// Default: indexed + stored + term vectors (all true)
let opt = TextOption::default();

// Customize
let opt = TextOption::default()
    .indexed(true)
    .stored(true)
    .term_vectors(true);
}
オプションデフォルト説明
indexedtrueフィールドが検索可能かどうか
storedtrue元の値が取得用に保存されるかどうか
term_vectorstrueターム位置が保存されるかどうか(フレーズクエリやハイライトに必要)

Vector フィールド

Vector フィールドは近似最近傍(ANN: Approximate Nearest Neighbor)検索のためのベクトルインデックスを使用してインデクシングされます。

タイプRust 型SchemaBuilder メソッド説明
FlatFlatOptionadd_flat_field()ブルートフォース線形スキャン。正確な結果
HNSWHnswOptionadd_hnsw_field()Hierarchical Navigable Small World グラフ。高速な近似検索
IVFIvfOptionadd_ivf_field()Inverted File Index。クラスタベースの近似検索

HNSW フィールドオプション(最も一般的)

#![allow(unused)]
fn main() {
use laurus::vector::HnswOption;
use laurus::vector::core::distance::DistanceMetric;

let opt = HnswOption {
    dimension: 384,                          // vector dimensions
    distance: DistanceMetric::Cosine,        // distance metric
    m: 16,                                   // max connections per layer
    ef_construction: 200,                    // construction search width
    base_weight: 1.0,                        // default scoring weight
    quantizer: None,                         // optional quantization
};
}

パラメータの詳細なガイダンスについては、Vector インデクシングを参照してください。

Document

Document は名前付きフィールド値のコレクションです。DocumentBuilder を使用してドキュメントを構築します。

#![allow(unused)]
fn main() {
use laurus::Document;

let doc = Document::builder()
    .add_text("title", "Introduction to Rust")
    .add_text("body", "Rust is a systems programming language.")
    .add_integer("year", 2024)
    .add_float("rating", 4.8)
    .add_boolean("published", true)
    .build();
}

ドキュメントのインデクシング

Engine はドキュメントを追加するための 2 つのメソッドを提供しており、それぞれ異なるセマンティクスを持ちます。

メソッド動作ユースケース
put_document(id, doc)Upsert — 同じ ID のドキュメントが存在する場合、置き換えられる標準的なドキュメントインデクシング
add_document(id, doc)Append — 新しいチャンクとしてドキュメントを追加。同じ ID で複数のチャンクを持てるチャンク分割されたドキュメント(例: 段落に分割された長い記事)
#![allow(unused)]
fn main() {
// Upsert: replaces any existing document with id "doc1"
engine.put_document("doc1", doc).await?;

// Append: adds another chunk under the same id "doc1"
engine.add_document("doc1", chunk2).await?;

// Always commit after indexing
engine.commit().await?;
}

ドキュメントの取得

get_documents を使用して、外部 ID でドキュメント(チャンクを含む)を取得します。

#![allow(unused)]
fn main() {
let docs = engine.get_documents("doc1").await?;
for doc in &docs {
    if let Some(title) = doc.get("title") {
        println!("Title: {:?}", title);
    }
}
}

ドキュメントの削除

外部 ID を共有するすべてのドキュメントとチャンクを削除します。

#![allow(unused)]
fn main() {
engine.delete_documents("doc1").await?;
engine.commit().await?;
}

ドキュメントのライフサイクル

graph LR
    A["Build Document"] --> B["put/add_document()"]
    B --> C["WAL"]
    C --> D["commit()"]
    D --> E["Searchable"]
    E --> F["get_documents()"]
    E --> G["delete_documents()"]

重要: ドキュメントは commit() が呼び出されるまで検索可能になりません。

DocumentBuilder メソッド

メソッド値の型説明
add_text(name, value)Stringテキストフィールドを追加
add_integer(name, value)i64整数フィールドを追加
add_float(name, value)f64浮動小数点数フィールドを追加
add_boolean(name, value)boolブールフィールドを追加
add_datetime(name, value)DateTime<Utc>日時フィールドを追加
add_vector(name, value)Vec<f32>事前計算済みベクトルフィールドを追加
add_geo(name, lat, lon)(f64, f64)地理座標フィールドを追加
add_bytes(name, data)Vec<u8>バイナリデータを追加
add_field(name, value)DataValue任意の値型を追加

DataValue

DataValue は Laurus におけるフィールド値を表す統合列挙型です。

#![allow(unused)]
fn main() {
pub enum DataValue {
    Null,
    Bool(bool),
    Int64(i64),
    Float64(f64),
    Text(String),
    Bytes(Vec<u8>, Option<String>),  // (data, optional MIME type)
    Vector(Vec<f32>),
    DateTime(DateTime<Utc>),
    Geo(f64, f64),          // (latitude, longitude)
}
}

DataValue は一般的な型に対して From<T> を実装しているため、.into() 変換が使用できます。

#![allow(unused)]
fn main() {
use laurus::DataValue;

let v: DataValue = "hello".into();       // Text
let v: DataValue = 42i64.into();         // Int64
let v: DataValue = 3.14f64.into();       // Float64
let v: DataValue = true.into();          // Bool
let v: DataValue = vec![0.1f32, 0.2].into(); // Vector
}

予約フィールド

_id フィールドは Laurus の内部使用のために予約されています。外部ドキュメント ID を格納し、常に KeywordAnalyzer(完全一致)でインデクシングされます。スキーマに追加する必要はありません。自動的に管理されます。

動的フィールド管理

稼働中のエンジンに対して、フィールドの追加および削除を動的に行えます。 フィールドの型変更はサポートされていません。

フィールドの追加

Engine::add_field() を使用すると、稼働中のエンジンにフィールドを動的に追加できます。

Lexical フィールドの追加

let updated_schema = engine.add_field(
    "category",
    FieldOption::Text(TextOption::default()),
).await?;

Vector フィールドの追加

let updated_schema = engine.add_field(
    "embedding",
    FieldOption::Flat(FlatOption::default().dimension(384)),
).await?;

既存のドキュメントには影響がありません(新しいフィールドの値が存在しないだけです)。

フィールドの削除

Engine::delete_field() を使用すると、稼働中のエンジンからフィールドを動的に削除できます。

let updated_schema = engine.delete_field("category").await?;

フィールド削除時の動作は以下の通りです。

  • スキーマからフィールド定義が削除されます。
  • default_fields に含まれている場合、そこからも削除されます。
  • フィールドに紐づくアナライザーおよびエンベッダーの登録が解除されます。
  • 既にインデックスされたデータは物理的に残りますが、スキーマから削除されたフィールドにはアクセスできなくなります。

共通の注意事項

返却された Schema は呼び出し側で永続化する必要があります(例: schema.toml への書き出し)。

スキーマ設計のヒント

  1. Lexical フィールドと Vector フィールドを分離する — フィールドは Lexical か Vector のいずれかであり、両方にはなりません。ハイブリッド検索には、別々のフィールドを作成してください(例: テキスト用に body、ベクトル用に body_vec)。

  2. 完全一致フィールドには KeywordAnalyzer を使用する — カテゴリ、ステータス、タグフィールドは PerFieldAnalyzer 経由で KeywordAnalyzer を使用し、トークン化を避けてください。

  3. 適切なベクトルインデックスを選択する — ほとんどの場合は HNSW、小規模データセットには Flat、非常に大規模なデータセットには IVF を使用してください。詳細は Vector インデクシングを参照。

  4. デフォルトフィールドを設定する — Query DSL を使用する場合、デフォルトフィールドを設定することで、ユーザーは body:hello の代わりに hello と記述できます。

  5. スキーマジェネレータを使用するlaurus create schema を実行して、手書きの代わりにインタラクティブにスキーマ TOML ファイルを構築できます。詳細は CLI コマンドを参照。

テキスト解析

テキスト解析(Text Analysis)は、生のテキストを検索可能なトークンに変換するプロセスです。ドキュメントがインデクシングされる際、Analyzer がテキストフィールドを個々のタームに分割します。クエリが実行される際も、同じ Analyzer がクエリテキストを処理し、一貫性を確保します。

解析パイプライン

graph LR
    Input["Raw Text\n'The quick brown FOX jumps!'"]
    CF["UnicodeNormalizationCharFilter"]
    T["Tokenizer\nSplit into words"]
    F1["LowercaseFilter"]
    F2["StopFilter"]
    F3["StemFilter"]
    Output["Terms\n'quick', 'brown', 'fox', 'jump'"]

    Input --> CF --> T --> F1 --> F2 --> F3 --> Output

解析パイプラインは以下で構成されます。

  1. Char Filter — トークン化の前に文字レベルで生テキストを正規化する
  2. Tokenizer — テキストを生トークン(単語、文字、n-gram)に分割する
  3. Token Filter — トークンの変換、削除、展開を行う(小文字化、ストップワード除去、ステミング、同義語展開)

Analyzer トレイト

すべての Analyzer は Analyzer トレイトを実装します。

#![allow(unused)]
fn main() {
pub trait Analyzer: Send + Sync + Debug {
    fn analyze(&self, text: &str) -> Result<TokenStream>;
    fn name(&self) -> &str;
    fn as_any(&self) -> &dyn Any;
}
}

TokenStreamBox<dyn Iterator<Item = Token> + Send> であり、トークンの遅延イテレータです。

Token には以下のフィールドが含まれます。

フィールド説明
textStringトークンテキスト
positionusize元テキスト内の位置
start_offsetusize元テキスト内の開始バイトオフセット
end_offsetusize元テキスト内の終了バイトオフセット
position_incrementusize前のトークンからの距離
position_lengthusizeトークンのスパン(同義語の場合は 1 より大きい)
boostf32トークンレベルのスコアリング重み
stoppedboolストップワードとしてマークされているかどうか
metadataOption<TokenMetadata>追加のトークンメタデータ

組み込み Analyzer

StandardAnalyzer

デフォルトの Analyzer です。ほとんどの西洋言語に適しています。

パイプライン: RegexTokenizer(Unicode 単語境界) → LowercaseFilterStopFilter(128 個の一般的な英語ストップワード)

#![allow(unused)]
fn main() {
use laurus::analysis::analyzer::standard::StandardAnalyzer;

let analyzer = StandardAnalyzer::default();
// "The Quick Brown Fox" → ["quick", "brown", "fox"]
// ("The" is removed by stop word filtering)
}

JapaneseAnalyzer

日本語テキストの分割に形態素解析を使用します。

パイプライン: UnicodeNormalizationCharFilter(NFKC) → JapaneseIterationMarkCharFilterLinderaTokenizerLowercaseFilterStopFilter(日本語ストップワード)

#![allow(unused)]
fn main() {
use laurus::analysis::analyzer::japanese::JapaneseAnalyzer;

let analyzer = JapaneseAnalyzer::new()?;
// "東京都に住んでいる" → ["東京", "都", "に", "住ん", "で", "いる"]
}

KeywordAnalyzer

入力全体を単一のトークンとして扱います。トークン化や正規化は行いません。

#![allow(unused)]
fn main() {
use laurus::analysis::analyzer::keyword::KeywordAnalyzer;

let analyzer = KeywordAnalyzer::new();
// "Hello World" → ["Hello World"]
}

完全一致が必要なフィールド(カテゴリ、タグ、ステータスコード)に使用してください。

SimpleAnalyzer

フィルタリングなしでテキストをトークン化します。元の大文字小文字とすべてのトークンが保持されます。解析パイプラインを完全に制御したい場合や、Tokenizer を単独でテストしたい場合に便利です。

パイプライン: ユーザー指定の Tokenizer のみ(Char Filter なし、Token Filter なし)

#![allow(unused)]
fn main() {
use laurus::analysis::analyzer::simple::SimpleAnalyzer;
use laurus::analysis::tokenizer::regex::RegexTokenizer;
use std::sync::Arc;

let tokenizer = Arc::new(RegexTokenizer::new()?);
let analyzer = SimpleAnalyzer::new(tokenizer);
// "Hello World" → ["Hello", "World"]
// (no lowercasing, no stop word removal)
}

Tokenizer のテストや、別のステップで手動で Token Filter を適用したい場合に使用してください。

EnglishAnalyzer

英語に特化した Analyzer です。トークン化、小文字化、一般的な英語ストップワードの除去を行います。

パイプライン: RegexTokenizer(Unicode 単語境界) → LowercaseFilterStopFilter(128 個の一般的な英語ストップワード)

#![allow(unused)]
fn main() {
use laurus::analysis::analyzer::language::english::EnglishAnalyzer;

let analyzer = EnglishAnalyzer::new()?;
// "The Quick Brown Fox" → ["quick", "brown", "fox"]
// ("The" is removed by stop word filtering, remaining tokens are lowercased)
}

PipelineAnalyzer

任意の Char Filter、Tokenizer、Token Filter のシーケンスを組み合わせてカスタムパイプラインを構築します。

#![allow(unused)]
fn main() {
use laurus::analysis::analyzer::pipeline::PipelineAnalyzer;
use laurus::analysis::char_filter::unicode_normalize::{
    NormalizationForm, UnicodeNormalizationCharFilter,
};
use laurus::analysis::tokenizer::regex::RegexTokenizer;
use laurus::analysis::token_filter::lowercase::LowercaseFilter;
use laurus::analysis::token_filter::stop::StopFilter;
use laurus::analysis::token_filter::stem::StemFilter;

let analyzer = PipelineAnalyzer::new(Arc::new(RegexTokenizer::new()?))
    .add_char_filter(Arc::new(UnicodeNormalizationCharFilter::new(NormalizationForm::NFKC)))
    .add_filter(Arc::new(LowercaseFilter::new()))
    .add_filter(Arc::new(StopFilter::new()))
    .add_filter(Arc::new(StemFilter::new()));  // Porter stemmer
}

PerFieldAnalyzer

PerFieldAnalyzer を使用すると、同一 Engine 内で異なるフィールドに異なる Analyzer を割り当てることができます。

graph LR
    PFA["PerFieldAnalyzer"]
    PFA -->|"title"| KW["KeywordAnalyzer"]
    PFA -->|"body"| STD["StandardAnalyzer"]
    PFA -->|"description_ja"| JP["JapaneseAnalyzer"]
    PFA -->|other fields| DEF["Default\n(StandardAnalyzer)"]
#![allow(unused)]
fn main() {
use std::sync::Arc;
use laurus::analysis::analyzer::standard::StandardAnalyzer;
use laurus::analysis::analyzer::keyword::KeywordAnalyzer;
use laurus::analysis::analyzer::per_field::PerFieldAnalyzer;

// Default analyzer for fields not explicitly configured
let per_field = PerFieldAnalyzer::new(
    Arc::new(StandardAnalyzer::default())
);

// Use KeywordAnalyzer for exact-match fields
per_field.add_analyzer("category", Arc::new(KeywordAnalyzer::new()));
per_field.add_analyzer("status", Arc::new(KeywordAnalyzer::new()));

let engine = Engine::builder(storage, schema)
    .analyzer(Arc::new(per_field))
    .build()
    .await?;
}

注意: _id フィールドは設定に関係なく、常に KeywordAnalyzer で解析されます。

Char Filter

Char Filter は Tokenizer に渡される前の生入力テキストに対して動作します。Unicode 正規化、文字マッピング、パターンベースの置換などの文字レベルの正規化を行います。これにより、Tokenizer がクリーンで正規化されたテキストを受け取ることが保証されます。

すべての Char Filter は CharFilter トレイトを実装します。

#![allow(unused)]
fn main() {
pub trait CharFilter: Send + Sync {
    fn filter(&self, input: &str) -> (String, Vec<Transformation>);
    fn name(&self) -> &'static str;
}
}

Transformation レコードは文字位置がどのようにシフトしたかを記述し、Engine がトークン位置を元テキストにマッピングできるようにします。

Char Filter説明
UnicodeNormalizationCharFilterUnicode 正規化(NFC、NFD、NFKC、NFKD)
MappingCharFilterマッピング辞書に基づいて文字シーケンスを置換
PatternReplaceCharFilter正規表現パターンに一致する文字を置換
JapaneseIterationMarkCharFilter日本語の踊り字を基本文字に展開

UnicodeNormalizationCharFilter

入力テキストに Unicode 正規化を適用します。検索用途では NFKC が推奨されます。互換文字と合成形式の両方を正規化するためです。

#![allow(unused)]
fn main() {
use laurus::analysis::char_filter::unicode_normalize::{
    NormalizationForm, UnicodeNormalizationCharFilter,
};

let filter = UnicodeNormalizationCharFilter::new(NormalizationForm::NFKC);
// "Sony" (fullwidth) → "Sony" (halfwidth)
// "㌂" → "アンペア"
}
形式説明
NFC正準分解後に正準合成
NFD正準分解
NFKC互換分解後に正準合成
NFKD互換分解

MappingCharFilter

辞書を使用して文字シーケンスを置換します。Aho-Corasick アルゴリズム(最左最長一致)によりマッチングが行われます。

#![allow(unused)]
fn main() {
use std::collections::HashMap;
use laurus::analysis::char_filter::mapping::MappingCharFilter;

let mut mapping = HashMap::new();
mapping.insert("ph".to_string(), "f".to_string());
mapping.insert("qu".to_string(), "k".to_string());

let filter = MappingCharFilter::new(mapping)?;
// "phone queue" → "fone keue"
}

PatternReplaceCharFilter

正規表現パターンのすべての出現箇所を固定文字列で置換します。

#![allow(unused)]
fn main() {
use laurus::analysis::char_filter::pattern_replace::PatternReplaceCharFilter;

// Remove hyphens
let filter = PatternReplaceCharFilter::new(r"-", "")?;
// "123-456-789" → "123456789"

// Normalize numbers
let filter = PatternReplaceCharFilter::new(r"\d+", "NUM")?;
// "Year 2024" → "Year NUM"
}

JapaneseIterationMarkCharFilter

日本語の踊り字を基本文字に展開します。漢字()、ひらがな()、カタカナ()の踊り字をサポートします。

#![allow(unused)]
fn main() {
use laurus::analysis::char_filter::japanese_iteration_mark::JapaneseIterationMarkCharFilter;

let filter = JapaneseIterationMarkCharFilter::new(
    true,  // normalize kanji iteration marks
    true,  // normalize kana iteration marks
);
// "佐々木" → "佐佐木"
// "いすゞ" → "いすず"
}

パイプラインでの Char Filter の使用

PipelineAnalyzeradd_char_filter() で Char Filter を追加します。複数の Char Filter は追加された順序で適用され、すべて Tokenizer の実行前に処理されます。

#![allow(unused)]
fn main() {
use std::sync::Arc;
use laurus::analysis::analyzer::pipeline::PipelineAnalyzer;
use laurus::analysis::char_filter::unicode_normalize::{
    NormalizationForm, UnicodeNormalizationCharFilter,
};
use laurus::analysis::char_filter::pattern_replace::PatternReplaceCharFilter;
use laurus::analysis::tokenizer::regex::RegexTokenizer;
use laurus::analysis::token_filter::lowercase::LowercaseFilter;

let analyzer = PipelineAnalyzer::new(Arc::new(RegexTokenizer::new()?))
    .add_char_filter(Arc::new(
        UnicodeNormalizationCharFilter::new(NormalizationForm::NFKC),
    ))
    .add_char_filter(Arc::new(
        PatternReplaceCharFilter::new(r"-", "")?,
    ))
    .add_filter(Arc::new(LowercaseFilter::new()));
// "Tokyo-2024" → NFKC → "Tokyo-2024" → remove hyphens → "Tokyo2024" → tokenize → lowercase → ["tokyo2024"]
}

Tokenizer

Tokenizer説明
RegexTokenizerUnicode 単語境界で分割。空白と句読点で区切る
UnicodeWordTokenizerUnicode 単語境界で分割
WhitespaceTokenizer空白のみで分割
WholeTokenizer入力全体を単一のトークンとして返す
LinderaTokenizer日本語形態素解析(Lindera/MeCab)
NgramTokenizer設定可能なサイズの n-gram トークンを生成

Token Filter

フィルタ説明
LowercaseFilterトークンを小文字に変換
StopFilter一般的な単語を除去(“the”、“is”、“a”)
StemFilter単語を語幹に縮約(“running” → “run”)
SynonymGraphFilter同義語辞書でトークンを展開
BoostFilterトークンのブースト値を調整
LimitFilterトークン数を制限
StripFilterトークンの先頭/末尾の空白を除去
FlattenGraphFilterトークングラフをフラット化(同義語展開用)
RemoveEmptyFilter空トークンを除去

同義語展開

SynonymGraphFilter は同義語辞書を使用してタームを展開します。

#![allow(unused)]
fn main() {
use laurus::analysis::synonym::dictionary::SynonymDictionary;
use laurus::analysis::token_filter::synonym_graph::SynonymGraphFilter;

let mut dict = SynonymDictionary::new(None)?;
dict.add_synonym_group(vec!["ml".into(), "machine learning".into()]);
dict.add_synonym_group(vec!["ai".into(), "artificial intelligence".into()]);

// keep_original=true means original token is preserved alongside synonyms
let filter = SynonymGraphFilter::new(dict, true)
    .with_boost(0.8);  // synonyms get 80% weight
}

boost パラメータは、元のトークンに対する同義語の重みを制御します。値 0.8 は、同義語のマッチが完全一致のスコアの 80% を寄与することを意味します。

Embedding

Embedding は、テキスト(または画像)を意味的な情報を捉えた密なベクトル(数値ベクトル)に変換します。類似した意味を持つ 2 つのテキストは、ベクトル空間内で近い位置のベクトルを生成するため、類似度ベースの検索が可能になります。

Embedder トレイト

すべての Embedder は Embedder トレイトを実装します。

#![allow(unused)]
fn main() {
#[async_trait]
pub trait Embedder: Send + Sync + Debug {
    async fn embed(&self, input: &EmbedInput<'_>) -> Result<Vector>;
    async fn embed_batch(&self, inputs: &[EmbedInput<'_>]) -> Result<Vec<Vector>>;
    fn supported_input_types(&self) -> Vec<EmbedInputType>;
    fn name(&self) -> &str;
    fn as_any(&self) -> &dyn Any;
}
}

embed() メソッドは VectorVec<f32> をラップした構造体)を返します。

EmbedInput は 2 つのモダリティをサポートします。

バリアント説明
EmbedInput::Text(&str)テキスト入力
EmbedInput::Bytes(&[u8], Option<&str>)バイナリ入力(オプションの MIME タイプ付き、画像用)

組み込み Embedder

CandleBertEmbedder

Hugging Face Candle を使用して BERT モデルをローカルで実行します。API キーは不要です。

Feature flag: embeddings-candle

#![allow(unused)]
fn main() {
use laurus::CandleBertEmbedder;

// Downloads model on first run (~80MB)
let embedder = CandleBertEmbedder::new(
    "sentence-transformers/all-MiniLM-L6-v2"  // model name
)?;
// Output: 384-dimensional vector
}
プロパティ
モデルsentence-transformers/all-MiniLM-L6-v2
次元数384
実行環境ローカル(CPU)
初回ダウンロード約 80 MB

OpenAIEmbedder

OpenAI Embeddings API を呼び出します。API キーが必要です。

Feature flag: embeddings-openai

#![allow(unused)]
fn main() {
use laurus::OpenAIEmbedder;

let embedder = OpenAIEmbedder::new(
    api_key,
    "text-embedding-3-small".to_string()
).await?;
// Output: 1536-dimensional vector
}
プロパティ
モデルtext-embedding-3-small(または任意の OpenAI モデル)
次元数1536(text-embedding-3-small の場合)
実行環境リモート API 呼び出し
必要条件OPENAI_API_KEY 環境変数

CandleClipEmbedder

マルチモーダル(テキスト + 画像)Embedding のために CLIP モデルをローカルで実行します。

Feature flag: embeddings-multimodal

#![allow(unused)]
fn main() {
use laurus::CandleClipEmbedder;

let embedder = CandleClipEmbedder::new(
    "openai/clip-vit-base-patch32"
)?;
// Text or images → 512-dimensional vector
}
プロパティ
モデルopenai/clip-vit-base-patch32
次元数512
入力タイプテキストおよび画像
ユースケーステキストから画像への検索、画像から画像への検索

PrecomputedEmbedder

Embedding 計算を行わず、事前計算済みのベクトルを直接使用します。ベクトルが外部で生成される場合に便利です。

#![allow(unused)]
fn main() {
use laurus::PrecomputedEmbedder;

let embedder = PrecomputedEmbedder::new();  // no parameters needed
}

PrecomputedEmbedder を使用する場合、ドキュメントには Embedding 用のテキストではなく、ベクトルを直接指定します。

#![allow(unused)]
fn main() {
let doc = Document::builder()
    .add_vector("embedding", vec![0.1, 0.2, 0.3, ...])
    .build();
}

PerFieldEmbedder

PerFieldEmbedder は Embedding リクエストをフィールド固有の Embedder にルーティングします。

graph LR
    PFE["PerFieldEmbedder"]
    PFE -->|"text_vec"| BERT["CandleBertEmbedder\n(384 dim)"]
    PFE -->|"image_vec"| CLIP["CandleClipEmbedder\n(512 dim)"]
    PFE -->|other fields| DEF["Default Embedder"]
#![allow(unused)]
fn main() {
use std::sync::Arc;
use laurus::PerFieldEmbedder;

let bert = Arc::new(CandleBertEmbedder::new("...")?);
let clip = Arc::new(CandleClipEmbedder::new("...")?);


let per_field = PerFieldEmbedder::new(bert.clone());
per_field.add_embedder("text_vec", bert.clone());
per_field.add_embedder("image_vec", clip.clone());

let engine = Engine::builder(storage, schema)
    .embedder(Arc::new(per_field))
    .build()
    .await?;
}

これは以下の場合に特に有用です。

  • 異なる Vector フィールドに異なるモデルが必要な場合(例: テキスト用に BERT、画像用に CLIP)
  • 異なるフィールドが異なるベクトル次元を持つ場合
  • ローカル Embedder とリモート Embedder を混在させたい場合

Embedding の使用方法

インデクシング時

Vector フィールドにテキスト値を追加すると、Engine が自動的に Embedding を生成します。

#![allow(unused)]
fn main() {
let doc = Document::builder()
    .add_text("text_vec", "Rust is a systems programming language")
    .build();
engine.add_document("doc-1", doc).await?;
// The embedder converts the text to a vector before indexing
}

検索時

テキストで検索すると、Engine がクエリテキストも同様に Embedding 化します。

#![allow(unused)]
fn main() {
// Builder API
let request = VectorSearchRequestBuilder::new()
    .add_text("text_vec", "systems programming")
    .build();

// Query DSL
let request = vector_parser.parse(r#"text_vec:~"systems programming""#).await?;
}

どちらのアプローチも、インデクシング時と同じ Embedder を使用してクエリテキストを Embedding 化するため、一貫したベクトル空間が保証されます。

Feature Flag まとめ

各 Embedder は Cargo.toml で特定の Feature Flag を有効にする必要があります。

EmbedderFeature Flag依存関係
CandleBertEmbedderembeddings-candlecandle-core, candle-nn, candle-transformers, hf-hub, tokenizers
OpenAIEmbedderembeddings-openaireqwest
CandleClipEmbedderembeddings-multimodalimage + embeddings-candle
PrecomputedEmbedder(なし – 常に利用可能)

embeddings-all Feature ですべての Embedding 機能を一括で有効にできます。詳細は Feature Flags を参照してください。

Embedder の選択

シナリオ推奨 Embedder
クイックプロトタイピング、オフライン利用CandleBertEmbedder
高精度が求められる本番環境OpenAIEmbedder
テキスト + 画像検索CandleClipEmbedder
外部パイプラインからの事前計算済みベクトルPrecomputedEmbedder
フィールドごとに複数モデルを使用他の Embedder をラップした PerFieldEmbedder

ストレージ

Laurus はプラガブルなストレージレイヤーを使用し、インデックスデータの永続化方法と保存場所を抽象化します。すべてのコンポーネント(Lexical インデックス、Vector インデックス、ドキュメントログ)は単一のストレージバックエンドを共有します。

Storage トレイト

すべてのバックエンドは Storage トレイトを実装します。

#![allow(unused)]
fn main() {
pub trait Storage: Send + Sync + Debug {
    fn loading_mode(&self) -> LoadingMode;
    fn open_input(&self, name: &str) -> Result<Box<dyn StorageInput>>;
    fn create_output(&self, name: &str) -> Result<Box<dyn StorageOutput>>;
    fn file_exists(&self, name: &str) -> bool;
    fn delete_file(&self, name: &str) -> Result<()>;
    fn list_files(&self) -> Result<Vec<String>>;
    fn file_size(&self, name: &str) -> Result<u64>;
    // ... additional methods
}
}

このインターフェースはファイル指向です。すべてのデータ(インデックスセグメント、メタデータ、WAL エントリ、ドキュメント)は名前付きファイルとして保存され、ストリーミング StorageInput / StorageOutput ハンドルを通じてアクセスされます。

ストレージバックエンド

MemoryStorage

すべてのデータがメモリ上に保持されます。高速でシンプルですが、耐久性はありません。

#![allow(unused)]
fn main() {
use std::sync::Arc;
use laurus::Storage;
use laurus::storage::memory::MemoryStorage;

let storage: Arc<dyn Storage> = Arc::new(
    MemoryStorage::new(Default::default())
);
}
プロパティ
耐久性なし(プロセス終了時にデータ消失)
速度最速
ユースケーステスト、プロトタイピング、一時的なデータ

FileStorage

標準的なファイルシステムベースの永続化です。各キーがディスク上のファイルにマッピングされます。

#![allow(unused)]
fn main() {
use std::sync::Arc;
use laurus::Storage;
use laurus::storage::file::{FileStorage, FileStorageConfig};

let config = FileStorageConfig::new("/tmp/laurus-data");
let storage: Arc<dyn Storage> = Arc::new(FileStorage::new("/tmp/laurus-data", config)?);
}
プロパティ
耐久性完全(ディスクに永続化)
速度中程度(ディスク I/O)
ユースケース一般的な本番利用

メモリマッピング付き FileStorage

FileStorageuse_mmap 設定フラグによるメモリマップドファイルアクセスをサポートします。有効にすると、OS がメモリとディスク間のページングを管理します。

#![allow(unused)]
fn main() {
use std::sync::Arc;
use laurus::Storage;
use laurus::storage::file::{FileStorage, FileStorageConfig};

let mut config = FileStorageConfig::new("/tmp/laurus-data");
config.use_mmap = true;  // enable memory-mapped I/O
let storage: Arc<dyn Storage> = Arc::new(FileStorage::new("/tmp/laurus-data", config)?);
}
プロパティ
耐久性完全(ディスクに永続化)
速度高速(OS 管理のメモリマッピング)
ユースケース大規模データセット、読み取り負荷の高いワークロード

StorageFactory

設定を使用してストレージを作成することもできます。

#![allow(unused)]
fn main() {
use laurus::storage::{StorageConfig, StorageFactory};
use laurus::storage::memory::MemoryStorageConfig;

let storage = StorageFactory::create(
    StorageConfig::Memory(MemoryStorageConfig::default())
)?;
}

PrefixedStorage

Engine は PrefixedStorage を使用して、単一のストレージバックエンド内でコンポーネントを分離します。

graph TB
    E["Engine"]
    E --> P1["PrefixedStorage\nprefix = 'lexical/'"]
    E --> P2["PrefixedStorage\nprefix = 'vector/'"]
    E --> P3["PrefixedStorage\nprefix = 'documents/'"]
    P1 --> S["Storage Backend"]
    P2 --> S
    P3 --> S

Lexical ストアがキー segments/seg-001.dict を書き込む場合、実際には基盤バックエンドでは lexical/segments/seg-001.dict として保存されます。これにより、コンポーネント間のキー衝突が防止されます。

PrefixedStorage を自分で作成する必要はありません。EngineBuilder が自動的に処理します。

ColumnStorage

主要なストレージバックエンドに加えて、Laurus はフィールドレベルの高速アクセスのための ColumnStorage レイヤーを提供します。これはファセッティング、ソート、集計などの操作で内部的に使用され、ドキュメント全体をデシリアライズせずに個々のフィールド値にアクセスすることが重要な場合に利用されます。

ColumnValue

ColumnValue は単一の格納されたカラム値を表します。

バリアント説明
String(String)UTF-8 テキスト
I32(i32)32 ビット符号付き整数
I64(i64)64 ビット符号付き整数
U32(u32)32 ビット符号なし整数
U64(u64)64 ビット符号なし整数
F32(f32)32 ビット浮動小数点数
F64(f64)64 ビット浮動小数点数
Bool(bool)ブール値
DateTime(i64)Unix タイムスタンプ(秒)
Null値なし

ColumnStorage は Engine が内部的に管理するため、直接操作する必要はありません。

バックエンドの選択

要因MemoryStorageFileStorageFileStorage (mmap)
耐久性なし完全完全
読み取り速度最速中程度高速
書き込み速度最速中程度中程度
メモリ使用量データサイズに比例少ないOS 管理
最大データサイズRAM による制限ディスクによる制限ディスク + アドレス空間による制限
最適な用途テスト、小規模データセット一般的な利用大規模な読み取り負荷の高いデータセット

推奨事項

  • 開発 / テスト: ファイルクリーンアップなしで高速に反復するために MemoryStorage を使用
  • 本番(一般): 信頼性の高い永続化のために FileStorage を使用
  • 本番(大規模): 大規模なインデックスがあり OS のページキャッシュを活用したい場合は FileStorageuse_mmap = true を使用

次のステップ

インデキシング(Indexing)

このセクションでは、Laurus がデータを内部的にどのように格納・整理するかについて説明します。インデキシングレイヤーを理解することで、適切なフィールドタイプの選択やパフォーマンスチューニングに役立ちます。

トピック

Lexical インデキシング

転置インデックス(Inverted Index)を使用したテキスト、数値、地理フィールドのインデキシング方法について説明します。

  • 転置インデックスの構造(Term Dictionary、Posting Lists)
  • 数値範囲クエリのための BKD ツリー
  • セグメントファイルとそのフォーマット
  • BM25 スコアリング

Vector インデキシング

近似最近傍探索(Approximate Nearest Neighbor Search)のためのベクトルフィールドのインデキシング方法について説明します。

  • インデックスタイプ: Flat、HNSW、IVF
  • パラメータチューニング(m、ef_construction、n_clusters、n_probe)
  • 距離メトリクス(Cosine、Euclidean、DotProduct)
  • 量子化(Quantization): SQ8、PQ

Lexical インデキシング

Lexical インデキシングは、キーワードベースの検索を支える仕組みです。ドキュメントのテキストフィールドがインデキシングされると、Laurus は転置インデックス(Inverted Index) を構築します。これは、タームからそのタームを含むドキュメントへのマッピングを行うデータ構造です。

Lexical インデキシングの仕組み

sequenceDiagram
    participant Doc as Document
    participant Analyzer
    participant Writer as IndexWriter
    participant Seg as Segment

    Doc->>Analyzer: "The quick brown fox"
    Analyzer->>Analyzer: Tokenize + Filter
    Analyzer-->>Writer: ["quick", "brown", "fox"]
    Writer->>Writer: Buffer in memory
    Writer->>Seg: Flush to segment on commit()

ステップごとの流れ

  1. 解析(Analyze): テキストが設定されたアナライザー(トークナイザー + フィルター)を通過し、正規化されたタームのストリームが生成される
  2. バッファリング(Buffer): タームはフィールドごとに整理され、インメモリの書き込みバッファに格納される
  3. コミット(Commit): commit() の呼び出し時に、バッファがストレージ上の新しいセグメントにフラッシュされる

転置インデックス(Inverted Index)

転置インデックスは、基本的にタームからドキュメントリストへのマップです。

graph LR
    subgraph "Term Dictionary"
        T1["'brown'"]
        T2["'fox'"]
        T3["'quick'"]
        T4["'rust'"]
    end

    subgraph "Posting Lists"
        P1["doc_1, doc_3"]
        P2["doc_1"]
        P3["doc_1, doc_2"]
        P4["doc_2, doc_3"]
    end

    T1 --> P1
    T2 --> P2
    T3 --> P3
    T4 --> P4
コンポーネント説明
Term Dictionaryインデックス内のすべてのユニークなタームのソート済みリスト。高速なプレフィックス検索をサポート
Posting Lists各タームに対する、ドキュメント ID とメタデータ(ターム頻度、位置情報)のリスト
Doc Values数値フィールドや日付フィールドでのソート/フィルター操作のためのカラム指向ストレージ

Posting List の内容

Posting List の各エントリには以下の情報が含まれます。

フィールド説明
Document ID内部 u64 識別子
Term Frequencyそのドキュメント内でタームが出現する回数
Positions(オプション)ドキュメント内でタームが出現する位置(フレーズクエリに必要)
Weightこのポスティングのスコアウェイト

数値フィールドと日付フィールド

整数、浮動小数点数、日時フィールドは、BKD ツリー を使用してインデキシングされます。BKD ツリーは範囲クエリに最適化された空間分割データ構造です。

graph TB
    Root["BKD Root"]
    Root --> L["values < 50"]
    Root --> R["values >= 50"]
    L --> LL["values < 25"]
    L --> LR["25 <= values < 50"]
    R --> RL["50 <= values < 75"]
    R --> RR["values >= 75"]

BKD ツリーにより、price:[10 TO 100]date:[2024-01-01 TO 2024-12-31] のような範囲クエリを効率的に評価できます。

地理フィールド(Geo Fields)

地理フィールドは緯度/経度のペアを格納します。以下のクエリをサポートする空間データ構造を使用してインデキシングされます。

  • 半径クエリ(Radius queries): 中心点から N キロメートル以内のすべてのポイントを検索
  • バウンディングボックスクエリ(Bounding box queries): 矩形領域内のすべてのポイントを検索

セグメント(Segments)

Lexical インデックスはセグメントに分割されて構成されます。各セグメントはイミュータブルで自己完結型のミニインデックスです。

graph TB
    LI["Lexical Index"]
    LI --> S1["Segment 0"]
    LI --> S2["Segment 1"]
    LI --> S3["Segment 2"]

    S1 --- F1[".dict (terms)"]
    S1 --- F2[".post (postings)"]
    S1 --- F3[".bkd (numerics)"]
    S1 --- F4[".docs (doc store)"]
    S1 --- F5[".dv (doc values)"]
    S1 --- F6[".meta (metadata)"]
    S1 --- F7[".lens (field lengths)"]
ファイル拡張子内容
.dictTerm Dictionary(ソート済みターム + メタデータオフセット)
.postPosting Lists(ドキュメント ID、ターム頻度、位置情報)
.bkd数値フィールドおよび日付フィールドの BKD ツリーデータ
.docs格納されたフィールド値(元のドキュメント内容)
.dvソートおよびフィルタリング用の Doc Values
.metaセグメントメタデータ(ドキュメント数、ターム数など)
.lensフィールド長の正規化値(BM25 スコアリング用)

セグメントのライフサイクル

  1. 作成(Create): commit() が呼び出されるたびに新しいセグメントが作成される
  2. 検索(Search): すべてのセグメントが並列に検索され、結果がマージされる
  3. マージ(Merge): 定期的に、複数の小さなセグメントがより大きなセグメントにマージされ、クエリパフォーマンスが向上する
  4. 削除(Delete): ドキュメントが削除された場合、物理的に削除されるのではなく、削除ビットマップに ID が追加される(Deletions & Compaction を参照)

BM25 スコアリング

Laurus は Lexical 検索結果のスコアリングに BM25 アルゴリズムを使用します。BM25 は以下の要素を考慮します。

  • ターム頻度(Term Frequency, TF): ドキュメント内でタームが出現する頻度(多いほど良いが、収穫逓減あり)
  • 逆文書頻度(Inverse Document Frequency, IDF): 全ドキュメントにおけるタームの希少性(希少なほど重要)
  • フィールド長の正規化(Field Length Normalization): 短いフィールドは長いフィールドに対してブーストされる

計算式:

score(q, d) = IDF(q) * (TF(q, d) * (k1 + 1)) / (TF(q, d) + k1 * (1 - b + b * |d| / avgdl))

k1 = 1.2b = 0.75 がデフォルトのチューニングパラメータです。

SIMD 最適化

ベクトル距離計算では、利用可能な場合に SIMD(Single Instruction, Multiple Data)命令が活用され、ベクトル検索における類似度計算が大幅に高速化されます。

コード例

use std::sync::Arc;
use laurus::{Document, Engine, Schema};
use laurus::lexical::TextOption;
use laurus::lexical::core::field::IntegerOption;
use laurus::storage::memory::MemoryStorage;

#[tokio::main]
async fn main() -> laurus::Result<()> {
    let storage = Arc::new(MemoryStorage::new(Default::default()));
    let schema = Schema::builder()
        .add_text_field("title", TextOption::default())
        .add_text_field("body", TextOption::default())
        .add_integer_field("year", IntegerOption::default())
        .build();

    let engine = Engine::builder(storage, schema).build().await?;

    // Index documents
    engine.add_document("doc-1", Document::builder()
        .add_text("title", "Rust Programming")
        .add_text("body", "Rust is a systems programming language.")
        .add_integer("year", 2024)
        .build()
    ).await?;

    // Commit to flush segments to storage
    engine.commit().await?;

    Ok(())
}

次のステップ

Vector インデキシング

Vector インデキシングは、類似性ベースの検索を支える仕組みです。ドキュメントのベクトルフィールドがインデキシングされると、Laurus はエンベディングベクトルを専用のインデックス構造に格納し、高速な近似最近傍探索(Approximate Nearest Neighbor, ANN)を可能にします。

Vector インデキシングの仕組み

sequenceDiagram
    participant Doc as Document
    participant Embedder
    participant Normalize as Normalizer
    participant Index as Vector Index

    Doc->>Embedder: "Rust is a systems language"
    Embedder-->>Normalize: [0.12, -0.45, 0.78, ...]
    Normalize->>Normalize: L2 normalize
    Normalize-->>Index: [0.14, -0.52, 0.90, ...]
    Index->>Index: Insert into index structure

ステップごとの流れ

  1. エンベディング(Embed): テキスト(または画像)が、設定されたエンベッダーによってベクトルに変換される
  2. 正規化(Normalize): ベクトルが L2 正規化される(コサイン類似度のため)
  3. インデキシング(Index): ベクトルが設定されたインデックス構造(Flat、HNSW、または IVF)に挿入される
  4. コミット(Commit): commit() の呼び出し時に、インデックスが永続ストレージにフラッシュされる

インデックスタイプ

Laurus は 3 種類のベクトルインデックスタイプをサポートしており、それぞれ異なるパフォーマンス特性を持ちます。

比較

特性FlatHNSWIVF
精度100%(厳密)約 95-99%(近似)約 90-98%(近似)
検索速度O(n) 線形スキャンO(log n) グラフ走査O(n/k) クラスタスキャン
メモリ使用量高(グラフエッジ)中程度(セントロイド)
インデックス構築時間高速中程度低速(クラスタリング)
最適な用途1 万ベクトル未満1 万 - 1,000 万ベクトル100 万ベクトル以上

Flat インデックス

最もシンプルなインデックスです。クエリベクトルを格納されたすべてのベクトルと比較します(総当たり)。

#![allow(unused)]
fn main() {
use laurus::vector::FlatOption;
use laurus::vector::core::distance::DistanceMetric;

let opt = FlatOption {
    dimension: 384,
    distance: DistanceMetric::Cosine,
    ..Default::default()
};
}
  • 利点: 100% の再現率(厳密な結果)、シンプル、低メモリ
  • 欠点: 大規模データセットでは低速(線形スキャン)
  • 使用場面: ベクトル数が約 1 万未満の場合、または厳密な結果が必要な場合

HNSW インデックス

Hierarchical Navigable Small World グラフ。デフォルトで最も一般的に使用されるインデックスタイプです。

graph TB
    subgraph "Layer 2 (sparse)"
        A2["A"] --- C2["C"]
    end

    subgraph "Layer 1 (medium)"
        A1["A"] --- B1["B"]
        A1 --- C1["C"]
        B1 --- D1["D"]
        C1 --- D1
    end

    subgraph "Layer 0 (dense - all vectors)"
        A0["A"] --- B0["B"]
        A0 --- C0["C"]
        B0 --- D0["D"]
        B0 --- E0["E"]
        C0 --- D0
        C0 --- F0["F"]
        D0 --- E0
        E0 --- F0
    end

    A2 -.->|"entry point"| A1
    A1 -.-> A0
    C2 -.-> C1
    C1 -.-> C0
    B1 -.-> B0
    D1 -.-> D0

HNSW アルゴリズムは、上位の疎なレイヤーから下位の密なレイヤーへと検索し、各レベルで検索空間を絞り込みます。

#![allow(unused)]
fn main() {
use laurus::vector::HnswOption;
use laurus::vector::core::distance::DistanceMetric;

let opt = HnswOption {
    dimension: 384,
    distance: DistanceMetric::Cosine,
    m: 16,                  // max connections per node per layer
    ef_construction: 200,   // search width during index building
    ..Default::default()
};
}

HNSW パラメータ

パラメータデフォルト説明影響
m16レイヤーごとのノードあたりの最大双方向接続数大きいほど再現率が向上するが、メモリ消費が増加
ef_construction200インデックス構築時の探索幅大きいほど再現率が向上するが、構築が低速に
dimension128ベクトルの次元数エンベッダーの出力と一致させる必要あり
distanceCosine距離メトリクス下記の距離メトリクスを参照

チューニングのヒント:

  • 再現率を向上させるには m を増やす(例: 32 または 64)。ただしメモリ消費が増加する
  • インデックス品質を向上させるには ef_construction を増やす(例: 400)。ただし構築時間が増加する
  • 検索時には、検索リクエストで設定する ef_search パラメータが探索幅を制御する

IVF インデックス

Inverted File Index。ベクトルをクラスタに分割し、関連するクラスタのみを検索します。

graph TB
    Q["Query Vector"]
    Q --> C1["Cluster 1\n(centroid)"]
    Q --> C2["Cluster 2\n(centroid)"]

    C1 --> V1["vec_3"]
    C1 --> V2["vec_7"]
    C1 --> V3["vec_12"]

    C2 --> V4["vec_1"]
    C2 --> V5["vec_9"]
    C2 --> V6["vec_15"]

    style C1 fill:#f9f,stroke:#333
    style C2 fill:#f9f,stroke:#333
#![allow(unused)]
fn main() {
use laurus::vector::IvfOption;
use laurus::vector::core::distance::DistanceMetric;

let opt = IvfOption {
    dimension: 384,
    distance: DistanceMetric::Cosine,
    n_clusters: 100,   // number of clusters
    n_probe: 10,       // clusters to search at query time
    ..Default::default()
};
}

IVF パラメータ

パラメータデフォルト説明影響
n_clusters100ボロノイセル(Voronoi Cell)の数クラスタ数が多いほど検索は高速になるが、再現率は低下
n_probe1クエリ時に検索するクラスタ数大きいほど再現率が向上するが、検索が低速に
dimension(必須)ベクトルの次元数エンベッダーの出力と一致させる必要あり
distanceCosine距離メトリクス下記の距離メトリクスを参照

チューニングのヒント:

  • n_clusters はベクトル数 n に対して sqrt(n) 程度に設定する
  • 再現率と速度のバランスを取るため、n_proben_clusters の 5-20% に設定する
  • IVF はトレーニングフェーズが必要なため、初回のインデキシングが遅くなる場合がある

距離メトリクス(Distance Metrics)

メトリクス説明値の範囲最適な用途
Cosine1 - コサイン類似度[0, 2]テキストエンベディング(最も一般的)
EuclideanL2 距離[0, +inf)空間データ
ManhattanL1 距離[0, +inf)特徴ベクトル
DotProduct負の内積(-inf, +inf)事前正規化済みベクトル
Angular角度距離[0, pi]方向の類似性
#![allow(unused)]
fn main() {
use laurus::vector::core::distance::DistanceMetric;

let metric = DistanceMetric::Cosine;      // Default for text
let metric = DistanceMetric::Euclidean;    // For spatial data
let metric = DistanceMetric::Manhattan;    // L1 distance
let metric = DistanceMetric::DotProduct;   // For pre-normalized vectors
let metric = DistanceMetric::Angular;      // Angular distance
}

注意: コサイン類似度の場合、ベクトルはインデキシング前に自動的に L2 正規化されます。距離が小さいほど、より類似していることを示します。

量子化(Quantization)

量子化は、精度をある程度犠牲にしてベクトルを圧縮し、メモリ使用量を削減します。

方式Enum バリアント説明メモリ削減率
スカラー 8 ビットScalar8Bit8 ビット整数へのスカラー量子化約 4 倍
プロダクト量子化ProductQuantization { subvector_count }ベクトルをサブベクトルに分割して各々を量子化約 16-64 倍
#![allow(unused)]
fn main() {
use laurus::vector::HnswOption;
use laurus::vector::core::quantization::QuantizationMethod;

let opt = HnswOption {
    dimension: 384,
    quantizer: Some(QuantizationMethod::Scalar8Bit),
    ..Default::default()
};
}

VectorQuantizer

VectorQuantizer は量子化のライフサイクルを管理します。

メソッド説明
new(method, dimension)未トレーニングの量子化器を作成
train(vectors)代表的なベクトルでトレーニング(Scalar8Bit の場合、次元ごとの最小/最大値を計算)
quantize(vector)トレーニング済みパラメータを使用してベクトルを圧縮
dequantize(quantized)量子化されたベクトルをフル精度に復元

Scalar8Bit の場合、トレーニングで各次元の最小値と最大値が計算されます。各成分は [0, 255] の範囲にマッピングされます。逆量子化ではこのマッピングが逆変換されますが、多少の精度損失が生じます。

注意: ProductQuantization は API 上定義されていますが、現在未実装です。使用するとエラーが返されます。

セグメントファイル

各ベクトルインデックスタイプは、データを単一のセグメントファイルに格納します。

インデックスタイプファイル拡張子内容
HNSW.hnswグラフ構造、ベクトル、メタデータ
Flat.flat生ベクトルとメタデータ
IVF.ivfクラスタセントロイド、割り当て済みベクトル、メタデータ

コード例

use std::sync::Arc;
use laurus::{Document, Engine, Schema};
use laurus::lexical::TextOption;
use laurus::vector::HnswOption;
use laurus::vector::core::distance::DistanceMetric;
use laurus::storage::memory::MemoryStorage;

#[tokio::main]
async fn main() -> laurus::Result<()> {
    let storage = Arc::new(MemoryStorage::new(Default::default()));
    let schema = Schema::builder()
        .add_text_field("title", TextOption::default())
        .add_hnsw_field("embedding", HnswOption {
            dimension: 384,
            distance: DistanceMetric::Cosine,
            m: 16,
            ef_construction: 200,
            ..Default::default()
        })
        .build();

    // With an embedder, text in vector fields is automatically embedded
    let engine = Engine::builder(storage, schema)
        .embedder(my_embedder)
        .build()
        .await?;

    // Add text to the vector field — it will be embedded automatically
    engine.add_document("doc-1", Document::builder()
        .add_text("title", "Rust Programming")
        .add_text("embedding", "Rust is a systems programming language.")
        .build()
    ).await?;

    engine.commit().await?;

    Ok(())
}

次のステップ

検索(Search)

このセクションでは、インデキシングされたデータに対するクエリの実行方法を説明します。Laurus は 3 つの検索モードをサポートしており、それぞれ独立して使用することも、組み合わせて使用することもできます。

トピック

Lexical 検索

転置インデックスを使用したキーワードベースの検索について説明します。

  • すべてのクエリタイプ: Term、Phrase、Boolean、Fuzzy、Wildcard、Range、Geo、Span
  • BM25 スコアリングとフィールドブースト
  • テキストベースのクエリのための Query DSL の使用方法

Vector 検索

ベクトルエンベディングを使用した意味的類似性検索について説明します。

  • VectorSearchRequestBuilder API
  • マルチフィールド Vector 検索とスコアモード
  • フィルター付き Vector 検索

ハイブリッド検索

Lexical 検索と Vector 検索を組み合わせた、両方の長所を活かす検索について説明します。

  • SearchRequestBuilder API
  • フュージョンアルゴリズム(RRF、WeightedSum)
  • フィルター付きハイブリッド検索
  • offset/limit によるページネーション

スペル修正については、ライブラリセクションの Spelling Correction を参照してください。

Lexical 検索

Lexical 検索は、転置インデックスに対してキーワードをマッチングすることでドキュメントを検索します。Laurus は、完全一致、フレーズ一致、あいまい一致など、豊富なクエリタイプを提供します。

基本的な使い方

#![allow(unused)]
fn main() {
use laurus::{SearchRequestBuilder, LexicalSearchRequest};
use laurus::lexical::TermQuery;

let request = SearchRequestBuilder::new()
    .lexical_search_request(
        LexicalSearchRequest::new(
            Box::new(TermQuery::new("body", "rust"))
        )
    )
    .limit(10)
    .build();

let results = engine.search(request).await?;
}

クエリタイプ

TermQuery

特定のフィールドに完全一致するタームを含むドキュメントをマッチングします。

#![allow(unused)]
fn main() {
use laurus::lexical::TermQuery;

// Find documents where "body" contains the term "rust"
let query = TermQuery::new("body", "rust");
}

注意: タームは解析後にマッチングされます。フィールドが StandardAnalyzer を使用している場合、インデキシングされたテキストとクエリタームの両方が小文字化されるため、TermQuery::new("body", "rust") は元テキスト内の “Rust” にもマッチします。

PhraseQuery

正確なタームの並びを含むドキュメントをマッチングします。

#![allow(unused)]
fn main() {
use laurus::lexical::query::phrase::PhraseQuery;

// Find documents containing the exact phrase "machine learning"
let query = PhraseQuery::new("body", vec!["machine".to_string(), "learning".to_string()]);

// Or use the convenience method from a phrase string:
let query = PhraseQuery::from_phrase("body", "machine learning");
}

フレーズクエリは、ターム位置情報が格納されている必要があります(TextOption のデフォルト設定)。

BooleanQuery

複数のクエリをブーリアン論理で結合します。

#![allow(unused)]
fn main() {
use laurus::lexical::query::boolean::{BooleanQuery, BooleanQueryBuilder, Occur};

let query = BooleanQueryBuilder::new()
    .must(Box::new(TermQuery::new("body", "rust")))       // AND
    .must(Box::new(TermQuery::new("body", "programming"))) // AND
    .must_not(Box::new(TermQuery::new("body", "python")))  // NOT
    .build();
}
Occur意味DSL での表現
Mustドキュメントが必ずマッチしなければならない+term または AND
Shouldドキュメントがマッチすべき(スコアをブースト)term または OR
MustNotドキュメントがマッチしてはならない-term または NOT
Filter必ずマッチする必要があるが、スコアには影響しない(DSL に相当するものなし)

FuzzyQuery

指定された編集距離(レーベンシュタイン距離)内のタームをマッチングします。

#![allow(unused)]
fn main() {
use laurus::lexical::query::fuzzy::FuzzyQuery;

// Find documents matching "programing" within edit distance 2
// This will match "programming", "programing", etc.
let query = FuzzyQuery::new("body", "programing");  // default max_edits = 2
}

WildcardQuery

ワイルドカードパターンを使用してタームをマッチングします。

#![allow(unused)]
fn main() {
use laurus::lexical::query::wildcard::WildcardQuery;

// '?' matches exactly one character, '*' matches zero or more
let query = WildcardQuery::new("filename", "*.pdf")?;
let query = WildcardQuery::new("body", "pro*")?;
let query = WildcardQuery::new("body", "col?r")?;  // matches "color" and "colour"
}

PrefixQuery

特定のプレフィックスで始まるタームを含むドキュメントをマッチングします。

#![allow(unused)]
fn main() {
use laurus::lexical::query::prefix::PrefixQuery;

// Find documents where "body" contains terms starting with "pro"
// This matches "programming", "program", "production", etc.
let query = PrefixQuery::new("body", "pro");
}

RegexpQuery

正規表現パターンにマッチするタームを含むドキュメントをマッチングします。

#![allow(unused)]
fn main() {
use laurus::lexical::query::regexp::RegexpQuery;

// Find documents where "body" contains terms matching the regex
let query = RegexpQuery::new("body", "^pro.*ing$")?;

// Match version-like patterns
let query = RegexpQuery::new("version", r"^v\d+\.\d+")?;
}

注意: RegexpQuery::new()Result を返します。正規表現パターンは構築時にバリデーションされ、無効なパターンの場合はエラーが返されます。

NumericRangeQuery

数値フィールドの値が指定された範囲内にあるドキュメントをマッチングします。

#![allow(unused)]
fn main() {
use laurus::lexical::NumericRangeQuery;
use laurus::lexical::core::field::NumericType;

// Find documents where "price" is between 10.0 and 100.0 (inclusive)
let query = NumericRangeQuery::new(
    "price",
    NumericType::Float,
    Some(10.0),   // min
    Some(100.0),  // max
    true,         // include min
    true,         // include max
);

// Open-ended range: price >= 50
let query = NumericRangeQuery::new(
    "price",
    NumericType::Float,
    Some(50.0),
    None,     // no upper bound
    true,
    false,
);
}

GeoQuery

地理的な位置に基づいてドキュメントをマッチングします。

#![allow(unused)]
fn main() {
use laurus::lexical::query::geo::GeoQuery;

// Find documents within 10km of Tokyo Station (35.6812, 139.7671)
let query = GeoQuery::within_radius("location", 35.6812, 139.7671, 10.0)?; // radius in kilometers

// Find documents within a bounding box (min_lat, min_lon, max_lat, max_lon)
let query = GeoQuery::within_bounding_box(
    "location",
    35.0, 139.0,  // min (lat, lon)
    36.0, 140.0,  // max (lat, lon)
)?;
}

SpanQuery

ドキュメント内のタームの近接度に基づいてマッチングします。SpanTermQuerySpanNearQuery を使用して近接クエリを構築します。

#![allow(unused)]
fn main() {
use laurus::lexical::query::span::{SpanQuery, SpanTermQuery, SpanNearQuery};

// Find documents where "quick" appears near "fox" (within 3 positions)
let query = SpanNearQuery::new(
    "body",
    vec![
        Box::new(SpanTermQuery::new("body", "quick")) as Box<dyn SpanQuery>,
        Box::new(SpanTermQuery::new("body", "fox")) as Box<dyn SpanQuery>,
    ],
    3,    // slop (max distance between terms)
    true, // in_order (terms must appear in order)
);
}

スコアリング

Lexical 検索結果は BM25 を使用してスコアリングされます。スコアは、ドキュメントがクエリに対してどの程度関連性があるかを反映します。

  • ドキュメント内のターム頻度が高いほどスコアが上昇する
  • インデックス全体でタームが希少なほどスコアが上昇する
  • 短いドキュメントは長いドキュメントに対してブーストされる

フィールドブースト

特定のフィールドをブーストして関連性に影響を与えることができます。

#![allow(unused)]
fn main() {
use laurus::LexicalSearchRequest;

let mut request = LexicalSearchRequest::new(Box::new(query));
request.field_boosts.insert("title".to_string(), 2.0);  // title matches count double
request.field_boosts.insert("body".to_string(), 1.0);
}

LexicalSearchRequest のオプション

オプションデフォルト説明
query(必須)実行するクエリ
limit10結果の最大件数
load_documentstrueドキュメントの全内容をロードするかどうか
min_score0.0最小スコア閾値
timeout_msNone検索タイムアウト(ミリ秒)
parallelfalseセグメント間の並列検索を有効にする
sort_byScore関連性スコアでソート、またはフィールドでソート(asc / desc
field_boostsフィールドごとのスコア倍率

ビルダーメソッド

LexicalSearchRequest はオプション設定のためのビルダースタイルの API をサポートします。

#![allow(unused)]
fn main() {
use laurus::LexicalSearchRequest;
use laurus::lexical::TermQuery;

let request = LexicalSearchRequest::new(Box::new(TermQuery::new("body", "rust")))
    .limit(20)
    .min_score(0.5)
    .timeout_ms(5000)
    .parallel(true)
    .sort_by_field_desc("date")
    .with_field_boost("title", 2.0)
    .with_field_boost("body", 1.0);
}

Query DSL の使用

プログラマティックにクエリを構築する代わりに、テキストベースの Query DSL を使用できます。

#![allow(unused)]
fn main() {
use laurus::lexical::QueryParser;
use laurus::analysis::analyzer::standard::StandardAnalyzer;
use std::sync::Arc;

let analyzer = Arc::new(StandardAnalyzer::default());
let parser = QueryParser::new(analyzer).with_default_field("body");

// Simple term
let query = parser.parse("rust")?;

// Boolean
let query = parser.parse("rust AND programming")?;

// Phrase
let query = parser.parse("\"machine learning\"")?;

// Field-specific
let query = parser.parse("title:rust AND body:programming")?;

// Fuzzy
let query = parser.parse("programing~2")?;

// Range
let query = parser.parse("year:[2020 TO 2024]")?;
}

完全な構文リファレンスは Query DSL を参照してください。

次のステップ

Vector 検索

Vector 検索は、意味的類似性によってドキュメントを検索します。キーワードのマッチングではなく、ベクトル空間におけるクエリの意味とドキュメントエンベディングを比較します。

基本的な使い方

Builder API

#![allow(unused)]
fn main() {
use laurus::SearchRequestBuilder;
use laurus::vector::VectorSearchRequestBuilder;

let request = SearchRequestBuilder::new()
    .vector_search_request(
        VectorSearchRequestBuilder::new()
            .add_text("embedding", "systems programming language")
            .limit(10)
            .build()
    )
    .build();

let results = engine.search(request).await?;
}

add_text() メソッドはテキストをクエリペイロードとして格納します。検索時に、エンジンが設定されたエンベッダーを使用してテキストをエンベディングし、ベクトルインデックスを検索します。

Query DSL

#![allow(unused)]
fn main() {
use laurus::vector::VectorQueryParser;

let parser = VectorQueryParser::new(embedder.clone())
    .with_default_field("embedding");

let request = parser.parse(r#"embedding:~"systems programming""#).await?;
}

VectorSearchRequestBuilder

Builder API により、きめ細かな制御が可能です。

#![allow(unused)]
fn main() {
use laurus::vector::VectorSearchRequestBuilder;
use laurus::vector::store::request::QueryVector;

let request = VectorSearchRequestBuilder::new()
    // Text query (will be embedded at search time)
    .add_text("text_vec", "machine learning")

    // Or use a pre-computed vector directly
    .add_vector("embedding", vec![0.1, 0.2, 0.3, /* ... */])

    // Search parameters
    .limit(20)

    .build();
}

メソッド

メソッド説明
add_text(field, text)特定のフィールドに対するテキストクエリを追加(検索時にエンベディング)
add_vector(field, vector)特定のフィールドに対する事前計算済みクエリベクトルを追加
add_vector_with_weight(field, vector, weight)明示的なウェイトを持つ事前計算済みベクトルを追加
add_payload(field, payload)エンベディング対象の汎用 DataValue ペイロードを追加
add_bytes(field, bytes, mime)バイナリペイロードを追加(例: マルチモーダル用の画像バイト)
field(name)検索を特定のフィールドに制限
fields(names)検索を複数のフィールドに制限
limit(n)結果の最大件数(デフォルト: 10)
score_mode(VectorScoreMode)スコア結合モード(WeightedSumMaxSimLateInteraction
min_score(f32)最小スコア閾値(デフォルト: 0.0)
overfetch(f32)結果品質向上のためのオーバーフェッチ係数(デフォルト: 1.0)
build()VectorSearchRequest を構築

マルチフィールド Vector 検索

単一のリクエストで複数のベクトルフィールドを横断して検索できます。

#![allow(unused)]
fn main() {
let request = VectorSearchRequestBuilder::new()
    .add_text("text_vec", "cute kitten")
    .add_text("image_vec", "fluffy cat")
    .build();
}

各クエリ句はベクトルを生成し、対応するフィールドに対して検索されます。結果は設定されたスコアモードで結合されます。

スコアモード

モード説明
WeightedSum(デフォルト)すべてのクエリ句にわたる(類似度 * ウェイト)の合計
MaxSimクエリ句間の最大類似度スコア
LateInteractionColBERT スタイルの Late Interaction スコアリング

ウェイト

DSL では ^ ブースト構文を使用するか、QueryVectorweight で各フィールドの寄与度を調整します。

text_vec:~"cute kitten"^1.0 image_vec:~"fluffy cat"^0.5

これは、テキストの類似度が画像の類似度の 2 倍の重みを持つことを意味します。

フィルター付き Vector 検索

Lexical フィルターを適用して Vector 検索の結果を絞り込むことができます。

#![allow(unused)]
fn main() {
use laurus::{SearchRequestBuilder, LexicalSearchRequest};
use laurus::lexical::TermQuery;
use laurus::vector::VectorSearchRequestBuilder;

// Vector search with a category filter
let request = SearchRequestBuilder::new()
    .vector_search_request(
        VectorSearchRequestBuilder::new()
            .add_text("embedding", "machine learning")
            .build()
    )
    .filter_query(Box::new(TermQuery::new("category", "tutorial")))
    .limit(10)
    .build();

let results = engine.search(request).await?;
}

フィルタークエリはまず Lexical インデックス上で実行されて許可されるドキュメント ID のセットを特定し、その後 Vector 検索がそれらの ID に制限されます。

数値範囲によるフィルター

#![allow(unused)]
fn main() {
use laurus::lexical::NumericRangeQuery;
use laurus::lexical::core::field::NumericType;

let request = SearchRequestBuilder::new()
    .vector_search_request(
        VectorSearchRequestBuilder::new()
            .add_text("embedding", "type systems")
            .build()
    )
    .filter_query(Box::new(NumericRangeQuery::new(
        "year", NumericType::Integer,
        Some(2020.0), Some(2024.0), true, true
    )))
    .limit(10)
    .build();
}

距離メトリクス(Distance Metrics)

距離メトリクスはスキーマでフィールドごとに設定されます(Vector インデキシング を参照)。

メトリクス説明小さい値 = より類似
Cosine1 - コサイン類似度はい
EuclideanL2 距離はい
ManhattanL1 距離はい
DotProduct負の内積はい
Angular角度距離はい

コード例: 完全な Vector 検索

use std::sync::Arc;
use laurus::{Document, Engine, Schema, SearchRequestBuilder, PerFieldEmbedder};
use laurus::lexical::TextOption;
use laurus::vector::HnswOption;
use laurus::vector::VectorSearchRequestBuilder;
use laurus::storage::memory::MemoryStorage;

#[tokio::main]
async fn main() -> laurus::Result<()> {
    let storage = Arc::new(MemoryStorage::new(Default::default()));

    let schema = Schema::builder()
        .add_text_field("title", TextOption::default())
        .add_hnsw_field("text_vec", HnswOption {
            dimension: 384,
            ..Default::default()
        })
        .build();

    // Set up per-field embedder
    let embedder = Arc::new(my_embedder);
    let pfe = PerFieldEmbedder::new(embedder.clone());
    pfe.add_embedder("text_vec", embedder.clone());

    let engine = Engine::builder(storage, schema)
        .embedder(Arc::new(pfe))
        .build()
        .await?;

    // Index documents (text in vector field is auto-embedded)
    engine.add_document("doc-1", Document::builder()
        .add_text("title", "Rust Programming")
        .add_text("text_vec", "Rust is a systems programming language.")
        .build()
    ).await?;
    engine.commit().await?;

    // Search by semantic similarity
    let results = engine.search(
        SearchRequestBuilder::new()
            .vector_search_request(
                VectorSearchRequestBuilder::new()
                    .add_text("text_vec", "systems language")
                    .build()
            )
            .limit(5)
            .build()
    ).await?;

    for r in &results {
        println!("{}: score={:.4}", r.id, r.score);
    }

    Ok(())
}

次のステップ

ハイブリッド検索(Hybrid Search)

ハイブリッド検索は、Lexical 検索(キーワードマッチング)と Vector 検索(意味的類似性)を組み合わせることで、精度と意味的な関連性の両方を兼ね備えた結果を提供します。これは Laurus の最も強力な検索モードです。

なぜハイブリッド検索なのか

検索タイプ強み弱み
Lexical のみ正確なキーワードマッチング、希少なタームに強い同義語や言い換えを見逃す
Vector のみ意味を理解し、同義語に対応正確なキーワードを見逃す場合がある、精度が低い
ハイブリッド両方の長所を活用設定がやや複雑

仕組み

sequenceDiagram
    participant User
    participant Engine
    participant Lexical as LexicalStore
    participant Vector as VectorStore
    participant Fusion

    User->>Engine: SearchRequest\n(lexical + vector)

    par Execute in parallel
        Engine->>Lexical: BM25 keyword search
        Lexical-->>Engine: Ranked hits (by relevance)
    and
        Engine->>Vector: ANN similarity search
        Vector-->>Engine: Ranked hits (by distance)
    end

    Engine->>Fusion: Merge two result sets
    Note over Fusion: RRF or WeightedSum
    Fusion-->>Engine: Unified ranked list
    Engine-->>User: Vec of SearchResult

基本的な使い方

Builder API

#![allow(unused)]
fn main() {
use laurus::{SearchRequestBuilder, LexicalSearchRequest, FusionAlgorithm};
use laurus::lexical::TermQuery;
use laurus::vector::VectorSearchRequestBuilder;

let request = SearchRequestBuilder::new()
    // Lexical component
    .lexical_search_request(
        LexicalSearchRequest::new(
            Box::new(TermQuery::new("body", "rust"))
        )
    )
    // Vector component
    .vector_search_request(
        VectorSearchRequestBuilder::new()
            .add_text("text_vec", "systems programming")
            .build()
    )
    // Fusion algorithm
    .fusion_algorithm(FusionAlgorithm::RRF { k: 60.0 })
    .limit(10)
    .build();

let results = engine.search(request).await?;
}

Query DSL

単一のクエリ文字列内で Lexical 句と Vector 句を混在させることができます。

#![allow(unused)]
fn main() {
use laurus::UnifiedQueryParser;
use laurus::lexical::QueryParser;
use laurus::vector::VectorQueryParser;

let unified = UnifiedQueryParser::new(
    QueryParser::new(analyzer).with_default_field("body"),
    VectorQueryParser::new(embedder),
);

// Lexical + vector in one query
let request = unified.parse(r#"body:rust text_vec:~"systems programming""#).await?;
let results = engine.search(request).await?;
}

~"..." 構文は Vector 句を識別します。それ以外はすべて Lexical として解析されます。

フュージョンアルゴリズム(Fusion Algorithms)

Lexical と Vector の両方の結果が存在する場合、それらを単一のランキングリストにマージする必要があります。Laurus は 2 つのフュージョンアルゴリズムをサポートしています。

RRF(Reciprocal Rank Fusion)

デフォルトのアルゴリズムです。生のスコアではなく、ランク位置に基づいて結果を結合します。

score(doc) = sum( 1 / (k + rank_i) )

rank_i は各結果リストにおけるドキュメントの位置、k はスムージングパラメータ(デフォルト 60)です。

#![allow(unused)]
fn main() {
use laurus::FusionAlgorithm;

let fusion = FusionAlgorithm::RRF { k: 60.0 };
}

利点:

  • Lexical と Vector の結果間のスコア分布の違いに対してロバスト
  • ウェイトのチューニングが不要
  • すぐに使える(out of the box)

WeightedSum

正規化された Lexical スコアと Vector スコアを線形結合します。

score(doc) = lexical_weight * lexical_score + vector_weight * vector_score
#![allow(unused)]
fn main() {
use laurus::FusionAlgorithm;

let fusion = FusionAlgorithm::WeightedSum {
    lexical_weight: 0.3,
    vector_weight: 0.7,
};
}

使用場面:

  • Lexical と Vector の関連性のバランスを明示的に制御したい場合
  • 一方のシグナルが他方よりも重要であることがわかっている場合

SearchRequest のオプション

オプションデフォルト説明
lexical_search_requestNoneLexical クエリコンポーネント
vector_search_requestNoneVector クエリコンポーネント
filter_queryNoneLexical クエリによるプレフィルター(Lexical と Vector の両方の結果を制限)
fusion_algorithmNone(両方の結果が存在する場合 RRF { k: 60.0 } を使用)Lexical と Vector の結果をマージする方法
limit10返される結果の最大件数
offset0スキップする結果の数(ページネーション用)

SearchResult

各結果には以下が含まれます。

フィールド説明
idString外部ドキュメント ID
scoref32フュージョン後の関連性スコア
documentOption<Document>ドキュメントの全内容(ロードされた場合)

フィルター付きハイブリッド検索

フィルターを適用して、Lexical と Vector の両方の結果を制限できます。

#![allow(unused)]
fn main() {
let request = SearchRequestBuilder::new()
    .lexical_search_request(
        LexicalSearchRequest::new(Box::new(TermQuery::new("body", "rust")))
    )
    .vector_search_request(
        VectorSearchRequestBuilder::new()
            .add_text("text_vec", "systems programming")
            .build()
    )
    // Only search within "tutorial" category
    .filter_query(Box::new(TermQuery::new("category", "tutorial")))
    .fusion_algorithm(FusionAlgorithm::RRF { k: 60.0 })
    .limit(10)
    .build();
}

フィルタリングの仕組み

  1. フィルタークエリが Lexical インデックス上で実行され、許可されるドキュメント ID のセットが生成される
  2. Lexical 検索: フィルターがユーザークエリとブーリアン AND で結合される
  3. Vector 検索: 許可された ID が ANN 検索の制限として渡される

ページネーション

offsetlimit を使用してページネーションを実現します。

#![allow(unused)]
fn main() {
// Page 1: results 0-9
let page1 = SearchRequestBuilder::new()
    .lexical_search_request(/* ... */)
    .vector_search_request(/* ... */)
    .offset(0)
    .limit(10)
    .build();

// Page 2: results 10-19
let page2 = SearchRequestBuilder::new()
    .lexical_search_request(/* ... */)
    .vector_search_request(/* ... */)
    .offset(10)
    .limit(10)
    .build();
}

完全な例

use std::sync::Arc;
use laurus::{
    Document, Engine, Schema, SearchRequestBuilder,
    LexicalSearchRequest, FusionAlgorithm, PerFieldEmbedder,
};
use laurus::lexical::{TextOption, TermQuery};
use laurus::lexical::core::field::IntegerOption;
use laurus::vector::{HnswOption, VectorSearchRequestBuilder};
use laurus::storage::memory::MemoryStorage;

#[tokio::main]
async fn main() -> laurus::Result<()> {
    let storage = Arc::new(MemoryStorage::new(Default::default()));

    // Schema with both lexical and vector fields
    let schema = Schema::builder()
        .add_text_field("title", TextOption::default())
        .add_text_field("body", TextOption::default())
        .add_text_field("category", TextOption::default())
        .add_integer_field("year", IntegerOption::default())
        .add_hnsw_field("body_vec", HnswOption {
            dimension: 384,
            ..Default::default()
        })
        .build();

    // Configure analyzer and embedder (see Text Analysis and Embeddings docs)
    // let analyzer = Arc::new(StandardAnalyzer::new()?);
    // let embedder = Arc::new(CandleBertEmbedder::new("sentence-transformers/all-MiniLM-L6-v2")?);
    let engine = Engine::builder(storage, schema)
        // .analyzer(analyzer)
        // .embedder(embedder)
        .build()
        .await?;

    // Index documents with both text and vector fields
    engine.add_document("doc-1", Document::builder()
        .add_text("title", "Rust Programming Guide")
        .add_text("body", "Rust is a systems programming language.")
        .add_text("category", "programming")
        .add_integer("year", 2024)
        .add_text("body_vec", "Rust is a systems programming language.")
        .build()
    ).await?;
    engine.commit().await?;

    // Hybrid search: keyword "rust" + semantic "systems language"
    let results = engine.search(
        SearchRequestBuilder::new()
            .lexical_search_request(
                LexicalSearchRequest::new(Box::new(TermQuery::new("body", "rust")))
            )
            .vector_search_request(
                VectorSearchRequestBuilder::new()
                    .add_text("body_vec", "systems language")
                    .build()
            )
            .fusion_algorithm(FusionAlgorithm::RRF { k: 60.0 })
            .limit(10)
            .build()
    ).await?;

    for r in &results {
        println!("{}: score={:.4}", r.id, r.score);
    }

    Ok(())
}

次のステップ

Query DSL

Laurus は統合 Query DSL(Domain Specific Language)を提供しており、Lexical(キーワード)検索と Vector(意味的)検索を単一のクエリ文字列で記述できます。UnifiedQueryParser は入力を Lexical 部分と Vector 部分に分割し、適切なサブパーサーに委譲します。

概要

title:hello AND content:~"cute kitten"^0.8
|--- lexical --|    |--- vector --------|

~" パターンが Vector 句を Lexical 句と区別します。それ以外はすべて Lexical クエリとして扱われます。

Lexical クエリ構文

Lexical クエリは、完全一致または近似のキーワードマッチングを使用して転置インデックスを検索します。

Term クエリ

フィールド(またはデフォルトフィールド)に対して単一のタームをマッチングします。

hello
title:hello

ブーリアン演算子

ANDOR(大文字小文字を区別しない)で句を結合します。

title:hello AND body:world
title:hello OR title:goodbye

明示的な演算子なしでスペース区切りされた句は、暗黙的なブーリアン(スコアリング付きの OR として動作)を使用します。

必須 / 禁止句

+(必ずマッチ)と -(マッチ禁止)を使用します。

+title:hello -title:goodbye

フレーズクエリ

ダブルクォートを使用して正確なフレーズをマッチングします。オプションの近接度(~N)で、ターム間に N 語を許可します。

"hello world"
"hello world"~2

ファジークエリ

編集距離を使用した近似マッチング。~ に続けてオプションで最大編集距離を指定します。

roam~
roam~2

ワイルドカードクエリ

?(1 文字)と *(0 文字以上)を使用します。

te?t
test*

範囲クエリ

包含的な [] または排他的な {} の範囲指定。数値フィールドや日付フィールドに有用です。

price:[100 TO 500]
date:{2024-01-01 TO 2024-12-31}
price:[* TO 100]

ブースト

^ で句のウェイトを増加させます。

title:hello^2
"important phrase"^1.5

グルーピング

括弧でサブ式を囲みます。

(title:hello OR title:hi) AND body:world

Lexical PEG 文法

完全な Lexical 文法(parser.pest):

query          = { SOI ~ boolean_query ~ EOI }
boolean_query  = { clause ~ (boolean_op ~ clause | clause)* }
clause         = { required_clause | prohibited_clause | sub_clause }
required_clause   = { "+" ~ sub_clause }
prohibited_clause = { "-" ~ sub_clause }
sub_clause     = { grouped_query | field_query | term_query }
grouped_query  = { "(" ~ boolean_query ~ ")" ~ boost? }
boolean_op     = { ^"AND" | ^"OR" }
field_query    = { field ~ ":" ~ field_value }
field_value    = { range_query | phrase_query | fuzzy_term
                 | wildcard_term | simple_term }
phrase_query   = { "\"" ~ phrase_content ~ "\"" ~ proximity? ~ boost? }
proximity      = { "~" ~ number }
fuzzy_term     = { term ~ "~" ~ fuzziness? ~ boost? }
wildcard_term  = { wildcard_pattern ~ boost? }
simple_term    = { term ~ boost? }
boost          = { "^" ~ boost_value }

Vector クエリ構文

Vector クエリは、解析時にテキストをベクトルにエンベディングし、類似性検索を実行します。

基本構文

field:~"text"
field:~"text"^weight
要素必須説明
field:いいえ対象のベクトルフィールド名content:
~はいVector クエリマーカー
"text"はいエンベディングするテキスト"cute kitten"
^weightいいえスコアウェイト(デフォルト: 1.0)^0.8

Vector クエリの例

# Single field
content:~"cute kitten"

# With boost weight
content:~"cute kitten"^0.8

# Default field (when configured)
~"cute kitten"

# Multiple clauses
content:~"cats" image:~"dogs"^0.5

# Nested field name (dot notation)
metadata.embedding:~"text"

複数句

複数の Vector 句はスペースで区切ります。すべての句が実行され、スコアは score_mode(デフォルト: WeightedSum)を使用して結合されます。

content:~"cats" image:~"dogs"^0.5

この場合のスコア計算:

score = similarity("cats", content) * 1.0
      + similarity("dogs", image)   * 0.5

Vector DSL には AND/OR 演算子はありません。Vector 検索は本質的にランキング操作であり、ウェイト(^)が各句の寄与度を制御します。

スコアモード

モード説明
WeightedSum(デフォルト)すべてのクエリ句にわたる(類似度 * ウェイト)の合計
MaxSimクエリ句間の最大類似度スコア
LateInteractionLate Interaction スコアリング

スコアモードは DSL 構文からは設定できません。Rust API を使用してオーバーライドします。

#![allow(unused)]
fn main() {
let mut request = parser.parse(r#"content:~"cats" image:~"dogs""#).await?;
request.score_mode = VectorScoreMode::MaxSim;
}

Vector PEG 文法

完全な Vector 文法(parser.pest):

query          = { SOI ~ vector_clause+ ~ EOI }
vector_clause  = { field_prefix? ~ "~" ~ quoted_text ~ boost? }
field_prefix   = { field_name ~ ":" }
field_name     = @{ (ASCII_ALPHA | "_") ~ (ASCII_ALPHANUMERIC | "_" | ".")* }
quoted_text    = ${ "\"" ~ inner_text ~ "\"" }
inner_text     = @{ (!("\"") ~ ANY)* }
boost          = { "^" ~ float_value }
float_value    = @{ ASCII_DIGIT+ ~ ("." ~ ASCII_DIGIT+)? }

統合(ハイブリッド)クエリ構文

UnifiedQueryParser を使用すると、単一のクエリ文字列内で Lexical 句と Vector 句を自由に混在させることができます。

title:hello content:~"cute kitten"^0.8

仕組み

  1. 分割(Split): Vector 句(field:~"text"^boost パターンに一致)が正規表現で抽出される
  2. 委譲(Delegate): Vector 部分は VectorQueryParser に、残りは Lexical の QueryParser に渡される
  3. フュージョン(Fuse): Lexical と Vector の両方の結果が存在する場合、フュージョンアルゴリズムで結合される

曖昧性の解消

~" パターンは Vector 句を明確に識別します。Lexical 構文では、~ はターム或いはフレーズのにのみ出現し(例: roam~2"hello world"~10)、クォートの前には出現しないためです。

フュージョンアルゴリズム

クエリに Lexical 句と Vector 句の両方が含まれる場合、結果はフュージョンされます。

アルゴリズム計算式説明
RRF(デフォルト)score = sum(1 / (k + rank))Reciprocal Rank Fusion。異なるスコア分布に対してロバスト。デフォルト k=60。
WeightedSumscore = lexical * a + vector * b設定可能なウェイトによる線形結合。

注意: フュージョンアルゴリズムは DSL 構文では指定できません。UnifiedQueryParser の構築時に .with_fusion() で設定します。デフォルトは RRF(k=60)です。コード例はカスタムフュージョンを参照してください。

統合クエリの例

# Lexical only — no fusion
title:hello AND body:world

# Vector only — no fusion
content:~"cute kitten"

# Hybrid — fusion applied automatically
title:hello content:~"cute kitten"

# Hybrid with boolean operators
title:hello AND category:animal content:~"cute kitten"^0.8

# Multiple vector clauses + lexical
category:animal content:~"cats" image:~"dogs"^0.5

# Default fields (when configured)
hello ~"cats"

コード例

DSL による Lexical 検索

#![allow(unused)]
fn main() {
use std::sync::Arc;
use laurus::analysis::analyzer::standard::StandardAnalyzer;
use laurus::lexical::query::QueryParser;

let analyzer = Arc::new(StandardAnalyzer::new()?);
let parser = QueryParser::new(analyzer)
    .with_default_field("title");

let query = parser.parse("title:hello AND body:world")?;
}

DSL による Vector 検索

#![allow(unused)]
fn main() {
use std::sync::Arc;
use laurus::vector::query::VectorQueryParser;

let parser = VectorQueryParser::new(embedder)
    .with_default_field("content");

let request = parser.parse(r#"content:~"cute kitten"^0.8"#).await?;
}

統合 DSL によるハイブリッド検索

#![allow(unused)]
fn main() {
use laurus::engine::query::UnifiedQueryParser;

let unified = UnifiedQueryParser::new(lexical_parser, vector_parser);

let request = unified.parse(
    r#"title:hello content:~"cute kitten"^0.8"#
).await?;
// request.lexical_search_request  -> Some(...)  — lexical query
// request.vector_search_request   -> Some(...)  — vector query
// request.fusion_algorithm        -> Some(RRF)  — fusion algorithm
}

カスタムフュージョン

#![allow(unused)]
fn main() {
use laurus::engine::search::FusionAlgorithm;

let unified = UnifiedQueryParser::new(lexical_parser, vector_parser)
    .with_fusion(FusionAlgorithm::WeightedSum {
        lexical_weight: 0.3,
        vector_weight: 0.7,
    });
}

ライブラリ概要

laurus クレートは検索エンジンのコアライブラリです。Lexical検索(転置インデックス(Inverted Index)によるキーワードマッチング)、Vector検索(Embeddingによるセマンティック類似度検索)、およびハイブリッド検索(両者の組み合わせ)を統一的なAPIで提供します。

モジュール構成

graph TB
    LIB["laurus (lib.rs)"]

    LIB --> engine["engine\nEngine, EngineBuilder\nSearchRequest, FusionAlgorithm"]
    LIB --> analysis["analysis\nAnalyzer, Tokenizer\nToken Filters, Char Filters"]
    LIB --> lexical["lexical\nInverted Index, BM25\nQuery Types, Faceting, Highlighting"]
    LIB --> vector["vector\nFlat, HNSW, IVF\nDistance Metrics, Quantization"]
    LIB --> embedding["embedding\nCandle BERT, OpenAI\nCLIP, Precomputed"]
    LIB --> storage["storage\nMemory, File, Mmap\nColumnStorage"]
    LIB --> store["store\nDocumentLog (WAL)"]
    LIB --> spelling["spelling\nSpelling Correction\nSuggestion Engine"]
    LIB --> data["data\nDataValue, Document"]
    LIB --> error["error\nLaurusError, Result"]

主要な型

モジュール説明
EngineengineLexical検索とVector検索を統合する検索エンジン
EngineBuilderengineEngineの設定・構築を行うBuilderパターン
Schemaengineフィールド定義とルーティング設定
SearchRequestengine統一的な検索リクエスト(Lexical、Vector、またはハイブリッド)
FusionAlgorithmengine結果マージ戦略(RRFまたはWeightedSum)
Documentdata名前付きフィールド値のコレクション
DataValuedataすべてのフィールド型に対応する統一的な値のenum
LaurusErrorerror各サブシステムのバリアントを含む包括的なエラー型

Feature Flag

laurus クレートはデフォルトではFeatureが有効化されていません。必要に応じてEmbeddingサポートを有効にしてください。

Feature説明依存クレート
embeddings-candleHugging Face CandleによるローカルBERT Embeddingcandle-core, candle-nn, candle-transformers, hf-hub, tokenizers
embeddings-openaiOpenAI API Embeddingreqwest
embeddings-multimodalCLIPマルチモーダルEmbedding(テキスト + 画像)image, embeddings-candle
embeddings-allすべてのEmbedding Featureを含む上記すべて
# Lexical検索のみ(Embeddingなし)
[dependencies]
laurus = "0.1.0"

# ローカルBERT Embeddingを使用
[dependencies]
laurus = { version = "0.1.0", features = ["embeddings-candle"] }

# すべてのFeatureを有効化
[dependencies]
laurus = { version = "0.1.0", features = ["embeddings-all"] }

セクション

Engine

Engine はLaurusの中心的な型です。Lexicalインデックス、Vectorインデックス、およびドキュメントログを単一の非同期APIで統合します。

Engine構造体

#![allow(unused)]
fn main() {
pub struct Engine {
    schema: Schema,
    lexical: LexicalStore,
    vector: VectorStore,
    log: Arc<DocumentLog>,
}
}
フィールド説明
schemaSchemaフィールド定義とルーティングルール
lexicalLexicalStoreキーワード検索用の転置インデックス(Inverted Index)
vectorVectorStore類似度検索用のベクトルインデックス
logArc<DocumentLog>クラッシュリカバリとドキュメント保存のためのWrite-Ahead Log

EngineBuilder

EngineBuilder を使用してEngineを設定・構築します。

#![allow(unused)]
fn main() {
use std::sync::Arc;
use laurus::{Engine, Schema};
use laurus::lexical::TextOption;
use laurus::storage::memory::MemoryStorage;

let storage = Arc::new(MemoryStorage::new(Default::default()));
let schema = Schema::builder()
    .add_text_field("title", TextOption::default())
    .add_text_field("body", TextOption::default())
    .add_default_field("body")
    .build();

let engine = Engine::builder(storage, schema)
    .analyzer(my_analyzer)    // オプション: カスタムテキストAnalyzer
    .embedder(my_embedder)    // オプション: ベクトルEmbedder
    .build()
    .await?;
}

Builderメソッド

メソッドパラメータデフォルト説明
analyzer()Arc<dyn Analyzer>StandardAnalyzerLexicalフィールド用のテキスト解析パイプライン
embedder()Arc<dyn Embedder>NoneVectorフィールド用のEmbeddingモデル
build()Engineを構築(非同期)

Buildライフサイクル

build() が呼び出されると、以下の処理が実行されます。

sequenceDiagram
    participant User
    participant EngineBuilder
    participant Engine

    User->>EngineBuilder: .build().await
    EngineBuilder->>EngineBuilder: split_schema()
    Note over EngineBuilder: Separate fields into<br/>LexicalIndexConfig<br/>+ VectorIndexConfig
    EngineBuilder->>Engine: Create PrefixedStorage (lexical/, vector/, documents/)
    EngineBuilder->>Engine: Create LexicalStore
    EngineBuilder->>Engine: Create VectorStore
    EngineBuilder->>Engine: Create DocumentLog
    EngineBuilder->>Engine: Recover from WAL
    EngineBuilder-->>User: Engine ready
  1. スキーマの分割 – Lexicalフィールド(Text、Integer、Floatなど)は LexicalIndexConfig に、Vectorフィールド(HNSW、Flat、IVF)は VectorIndexConfig に分割されます
  2. プレフィックス付きストレージの作成 – 各コンポーネントに独立した名前空間が割り当てられます(lexical/vector/documents/
  3. ストアの初期化LexicalStoreVectorStore がそれぞれの設定で作成されます
  4. WALからのリカバリ – 前回のセッションでコミットされなかった操作がリプレイされます

スキーマの分割

Schemaにはlexicalフィールドとvectorフィールドの両方が含まれています。ビルド時に split_schema() がこれらを分離します。

graph LR
    S["Schema<br/>title: Text<br/>body: Text<br/>page: Integer<br/>content_vec: HNSW"]

    S --> LC["LexicalIndexConfig<br/>title: TextOption<br/>body: TextOption<br/>page: IntegerOption<br/>_id: KeywordAnalyzer"]

    S --> VC["VectorIndexConfig<br/>content_vec: HnswOption<br/>(dim=384, m=16, ef=200)"]

予約フィールド _id は、完全一致検索のために KeywordAnalyzer を用いて常にLexical設定に追加されます。

フィールドごとのディスパッチ

PerFieldAnalyzer

PerFieldAnalyzer が指定された場合、テキスト解析はフィールドごとのAnalyzerにディスパッチされます。

graph LR
    PFA["PerFieldAnalyzer"]
    PFA -->|"title"| KA["KeywordAnalyzer"]
    PFA -->|"body"| SA["StandardAnalyzer"]
    PFA -->|"description"| JA["JapaneseAnalyzer"]
    PFA -->|"_id"| KA2["KeywordAnalyzer<br/>(always)"]
    PFA -->|other fields| DEF["Default Analyzer"]

PerFieldEmbedder

同様に、PerFieldEmbedder はフィールドごとのEmbedderにEmbedding処理をルーティングします。

graph LR
    PFE["PerFieldEmbedder"]
    PFE -->|"text_vec"| BERT["CandleBertEmbedder<br/>(384 dim)"]
    PFE -->|"image_vec"| CLIP["CandleClipEmbedder<br/>(512 dim)"]
    PFE -->|other fields| DEF["Default Embedder"]

Engineメソッド

ドキュメント操作

メソッド説明
put_document(id, doc)Upsert – 同じIDのドキュメントが既存の場合は置き換え
add_document(id, doc)追加 – 新しいチャンクとして追加(複数のチャンクが同一IDを共有可能)
get_documents(id)外部IDによるすべてのドキュメント/チャンクの取得
delete_documents(id)外部IDによるすべてのドキュメント/チャンクの削除
commit()保留中の変更をストレージにフラッシュ(ドキュメントが検索可能になる)
recover()クラッシュ後にWALをリプレイして未コミット状態を復元
add_field(name, field_option)稼働中のエンジンにフィールドを動的に追加し、更新後の Schema を返す
delete_field(name)稼働中のエンジンからフィールドを動的に削除し、更新後の Schema を返す
schema()現在の Schema への参照を返す

検索

メソッド説明
search(request)統一検索の実行(Lexical、Vector、またはハイブリッド)

search() メソッドは SearchRequest を受け取ります。SearchRequestにはLexicalクエリ、Vectorクエリ、またはその両方を含めることができます。両方が指定された場合、結果は指定された FusionAlgorithm で統合されます。

#![allow(unused)]
fn main() {
use laurus::{SearchRequestBuilder, LexicalSearchRequest, FusionAlgorithm};
use laurus::lexical::TermQuery;

// Lexicalのみの検索
let request = SearchRequestBuilder::new()
    .lexical_search_request(
        LexicalSearchRequest::new(Box::new(TermQuery::new("body", "rust")))
    )
    .limit(10)
    .build();

// RRFフュージョンによるハイブリッド検索
let request = SearchRequestBuilder::new()
    .lexical_search_request(lexical_req)
    .vector_search_request(vector_req)
    .fusion_algorithm(FusionAlgorithm::RRF { k: 60.0 })
    .limit(10)
    .build();

let results = engine.search(request).await?;
}

SearchRequest

フィールドデフォルト説明
lexical_search_requestOption<LexicalSearchRequest>NoneLexicalクエリ
vector_search_requestOption<VectorSearchRequest>NoneVectorクエリ
limitusize10返却する最大結果数
offsetusize0ページネーションのオフセット
fusion_algorithmOption<FusionAlgorithm>RRF (k=60)LexicalとVectorの結果を統合する方法
filter_queryOption<Box<dyn Query>>None両方の検索タイプに適用されるフィルタ

FusionAlgorithm

バリアント説明
RRF { k: f64 }Reciprocal Rank Fusion – ランクベースの結合。スコア = sum(1 / (k + rank))。比較不可能なスコアの大きさを処理します。
WeightedSum { lexical_weight, vector_weight }min-maxスコア正規化を用いた加重結合。重みは[0.0, 1.0]にクランプされます。

関連項目: アーキテクチャ – 高レベルのデータフロー図

スコアリングとランキング

Laurusは、Lexical検索に複数のスコアリングアルゴリズムを提供し、Vector検索には距離ベースの類似度を使用します。このページでは、すべてのスコアリングメカニズムとハイブリッド検索における相互作用について説明します。

Lexicalスコアリング

BM25(デフォルト)

BM25はLexical検索のデフォルトスコアリング関数です。単語頻度とドキュメント長の正規化をバランスさせます。

score = IDF * (tf * (k1 + 1)) / (tf + k1 * (1 - b + b * (doc_len / avg_doc_len)))

各パラメータの意味:

  • tf – ドキュメント内の単語頻度(Term Frequency)
  • IDF – 逆文書頻度(Inverse Document Frequency)。全ドキュメントにおける単語の希少性
  • k1 – 単語頻度の飽和パラメータ
  • b – ドキュメント長の正規化係数
  • doc_len / avg_doc_len – ドキュメント長と平均ドキュメント長の比率

ScoringConfig

ScoringConfig はBM25およびその他のスコアリングパラメータを制御します。

パラメータデフォルト説明
k1f321.2単語頻度の飽和。値が大きいほど単語頻度の重みが増します。
bf320.75フィールド長の正規化。0.0 = 正規化なし、1.0 = 完全な正規化。
tf_idf_boostf321.0TF-IDFのグローバルブースト係数
enable_field_normbooltrueフィールド長の正規化を有効にする
field_boostsHashMap<String, f32>emptyフィールドごとのスコア乗数
enable_coordbooltrueクエリ調整係数(matched_terms / total_query_terms)を有効にする

代替スコアリング関数

関数説明
BM25ScoringFunction設定可能なk1とbを持つBM25(デフォルト)
TfIdfScoringFunctionフィールド長正規化付きの対数正規化TF-IDF
VectorSpaceScoringFunctionドキュメントの単語ベクトル空間におけるコサイン類似度
CustomScoringFunctionカスタムスコアリングロジック用のユーザー定義クロージャ

ScoringRegistry

ScoringRegistry はスコアリングアルゴリズムの中央レジストリを提供します。

#![allow(unused)]
fn main() {
// 事前登録済みアルゴリズム:
// - "bm25"          -> BM25ScoringFunction
// - "tf_idf"        -> TfIdfScoringFunction
// - "vector_space"  -> VectorSpaceScoringFunction
}

フィールドブースト

フィールドブーストは、特定のフィールドからのスコア寄与に乗数を適用します。一部のフィールドが他よりも重要な場合に有用です。

#![allow(unused)]
fn main() {
use std::collections::HashMap;

let mut field_boosts = HashMap::new();
field_boosts.insert("title".to_string(), 2.0);  // titleのマッチはスコア2倍
field_boosts.insert("body".to_string(), 1.0);   // bodyのマッチはスコア1倍
}

調整係数(Coordination Factor)

enable_coord が true の場合、AdvancedScorer は調整係数を適用します。

coord = matched_query_terms / total_query_terms

これはより多くのクエリ単語にマッチするドキュメントに報酬を与えます。例えば、クエリが3つの単語を含み、ドキュメントがそのうち2つにマッチする場合、調整係数は 2/3 = 0.667 になります。

Vectorスコアリング

Vector検索は距離ベースの類似度で結果をランク付けします。

similarity = 1 / (1 + distance)

距離は設定された距離メトリクスを使用して計算されます。

メトリクス説明最適な用途
Cosine1 - コサイン類似度テキストEmbedding(最も一般的)
EuclideanL2距離空間データ
ManhattanL1距離特徴ベクトル
DotProduct符号反転した内積事前正規化されたベクトル
Angular角度距離方向の類似度

ハイブリッド検索のスコア正規化

LexicalとVectorの結果を結合する場合、スコアを比較可能にする必要があります。

RRF(Reciprocal Rank Fusion)

RRFは生のスコアの代わりにランクを使用することで、スコア正規化を完全に回避します。

rrf_score = sum(1 / (k + rank))

k パラメータ(デフォルト: 60)はスムージングを制御します。値が大きいほど上位ランクの結果の重みが小さくなります。

WeightedSum

WeightedSumは、各検索タイプのスコアをmin-max正規化で独立に正規化した後、結合します。

norm_score = (score - min_score) / (max_score - min_score)
final_score = (norm_lexical * lexical_weight) + (norm_vector * vector_weight)

両方の重みは [0.0, 1.0] にクランプされます。

ファセット

ファセット(Faceting)は、フィールド値によって検索結果をカウント・分類する機能です。検索UIでナビゲーションフィルタを構築するために一般的に使用されます(例: 「エレクトロニクス (42)」「書籍 (18)」)。

概念

FacetPath

FacetPath は階層的なファセット値を表します。例えば、商品カテゴリ「Electronics > Computers > Laptops」は3階層のFacetPathです。

#![allow(unused)]
fn main() {
use laurus::lexical::search::features::facet::FacetPath;

// 単一レベルのファセット
let facet = FacetPath::from_value("category", "Electronics");

// コンポーネントからの階層的ファセット
let facet = FacetPath::new("category", vec![
    "Electronics".to_string(),
    "Computers".to_string(),
    "Laptops".to_string(),
]);

// 区切り文字付き文字列から
let facet = FacetPath::from_delimited("category", "Electronics/Computers/Laptops", "/");
}

FacetPathメソッド

メソッド説明
new(field, path)フィールド名とパスコンポーネントからFacetPathを作成
from_value(field, value)単一レベルのファセットを作成
from_delimited(field, path_str, delimiter)区切り文字付きのパス文字列をパース
depth()パスの階層数
is_parent_of(other)このパスが他のパスの親であるか確認
parent()親パスを取得(1階層上)
child(component)コンポーネントを追加して子パスを作成
to_string_with_delimiter(delimiter)区切り文字付き文字列に変換

FacetCount

FacetCount はファセット集計の結果を表します。

#![allow(unused)]
fn main() {
pub struct FacetCount {
    pub path: FacetPath,
    pub count: u64,
    pub children: Vec<FacetCount>,
}
}
フィールド説明
pathFacetPathファセット値
countu64マッチするドキュメント数
childrenVec<FacetCount>階層的なドリルダウン用の子ファセット

例: 階層的ファセット

Category
├── Electronics (42)
│   ├── Computers (18)
│   │   ├── Laptops (12)
│   │   └── Desktops (6)
│   └── Phones (24)
└── Books (35)
    ├── Fiction (20)
    └── Non-Fiction (15)

このツリーの各ノードは、ドリルダウンナビゲーション用に children が設定された FacetCount に対応します。

ユースケース

  • EC(電子商取引): カテゴリ、ブランド、価格帯、評価によるフィルタリング
  • ドキュメント検索: 著者、部門、日付範囲、ドキュメントタイプによるフィルタリング
  • コンテンツ管理: タグ、トピック、コンテンツステータスによるフィルタリング

ハイライト

ハイライト(Highlighting)は検索結果内のマッチした単語をマークアップし、ドキュメントがクエリにマッチした理由をユーザーに視覚的に提示します。Laurusは設定可能なHTMLタグでハイライトされたテキストフラグメントを生成します。

HighlightConfig

HighlightConfig はハイライトの生成方法を制御します。

#![allow(unused)]
fn main() {
use laurus::lexical::search::features::highlight::HighlightConfig;

let config = HighlightConfig::default()
    .tag("mark")
    .css_class("highlight")
    .max_fragments(3)
    .fragment_size(200);
}

設定オプション

オプションデフォルト説明
tagString"mark"ハイライトに使用するHTMLタグ
css_classOption<String>Noneタグに追加するオプションのCSSクラス
max_fragmentsusize5返却するフラグメントの最大数
fragment_sizeusize150フラグメントの目標文字数
fragment_overlapusize20隣接するフラグメント間のオーバーラップ文字数
fragment_separatorString" ... "フラグメント間の区切り文字
return_entire_field_if_no_highlightboolfalseマッチがない場合にフィールド全体の値を返却する
max_analyzed_charsusize1,000,000ハイライト解析対象の最大文字数

Builderメソッド

メソッド説明
tag(tag)HTMLタグを設定(例: "em""strong""mark"
css_class(class)タグのCSSクラスを設定
max_fragments(count)フラグメントの最大数を設定
fragment_size(size)フラグメントの目標文字数を設定
opening_tag()開始HTMLタグ文字列を取得(例: <mark class="highlight">
closing_tag()終了HTMLタグ文字列を取得(例: </mark>

HighlightFragment

各ハイライト結果は HighlightFragment です。

#![allow(unused)]
fn main() {
pub struct HighlightFragment {
    pub text: String,
}
}

text フィールドには、マッチした単語が設定されたHTMLタグで囲まれたフラグメントが含まれます。

出力例

body = "Rust is a systems programming language focused on safety and performance." というドキュメントに対して “rust programming” で検索した場合:

<mark>Rust</mark> is a systems <mark>programming</mark> language focused on safety and performance.

css_class("highlight") を指定した場合:

<mark class="highlight">Rust</mark> is a systems <mark class="highlight">programming</mark> language focused on safety and performance.

フラグメント選択

フィールドが長い場合、Laurusは最も関連性の高いフラグメントを選択します。

  1. テキストが fragment_size 文字のオーバーラップするウィンドウに分割されます
  2. 各フラグメントは含まれるクエリ単語の数でスコアリングされます
  3. 上位 max_fragments 個のフラグメントが fragment_separator で結合されて返却されます

マッチを含むフラグメントがなく、return_entire_field_if_no_highlight が true の場合、フィールド全体の値が代わりに返却されます。

スペル修正

Laurusにはスペル修正システムが組み込まれており、誤入力されたクエリ単語の修正候補を提案し、「もしかして?(Did you mean?)」機能を提供します。

概要

スペル修正器は、編集距離(Levenshtein距離(Levenshtein Distance))と単語頻度データを組み合わせて修正候補を提案します。以下の機能をサポートしています。

  • 単語レベルの候補提案 – 個々の誤入力単語を修正
  • 自動修正 – 高信頼度の修正を自動的に適用
  • 「もしかして?」 – ユーザーに代替クエリを提案
  • クエリ学習 – ユーザーのクエリから学習して候補を改善
  • カスタム辞書 – 独自の単語リストを使用

基本的な使い方

SpellingCorrector

#![allow(unused)]
fn main() {
use laurus::spelling::corrector::SpellingCorrector;

// 組み込みの英語辞書で修正器を作成
let mut corrector = SpellingCorrector::new();

// クエリを修正
let result = corrector.correct("programing langauge");

// 候補が利用可能か確認
if result.has_suggestions() {
    for (word, suggestions) in &result.word_suggestions {
        println!("'{}' -> {:?}", word, suggestions);
    }
}

// 最良の修正済みクエリを取得
if let Some(corrected) = result.query() {
    println!("Corrected: {}", corrected);
}
}

「もしかして?」

DidYouMean ラッパーは検索UIに適した高レベルのインターフェースを提供します。

#![allow(unused)]
fn main() {
use laurus::spelling::corrector::{SpellingCorrector, DidYouMean};

let corrector = SpellingCorrector::new();
let mut did_you_mean = DidYouMean::new(corrector);

if let Some(suggestion) = did_you_mean.suggest("programing") {
    println!("Did you mean: {}?", suggestion);
}
}

設定

CorrectorConfig を使用して動作をカスタマイズできます。

#![allow(unused)]
fn main() {
use laurus::spelling::corrector::{CorrectorConfig, SpellingCorrector};

let config = CorrectorConfig {
    max_distance: 2,              // 最大編集距離(デフォルト: 2)
    max_suggestions: 5,           // 単語あたりの最大候補数(デフォルト: 5)
    min_frequency: 1,             // 最小単語頻度しきい値(デフォルト: 1)
    auto_correct: false,          // 自動修正を有効化(デフォルト: false)
    auto_correct_threshold: 0.8,  // 自動修正の信頼度しきい値(デフォルト: 0.8)
    use_index_terms: true,        // インデックスの単語を辞書として使用(デフォルト: true)
    learn_from_queries: true,     // ユーザーのクエリから学習(デフォルト: true)
};
}

設定オプション

オプションデフォルト説明
max_distanceusize2候補提案のための最大Levenshtein編集距離
max_suggestionsusize5単語あたりの最大候補数
min_frequencyu321候補として提案されるために必要な辞書内の最小頻度
auto_correctboolfalsetrueの場合、しきい値を超える修正を自動的に適用
auto_correct_thresholdf640.8自動修正に必要な信頼度スコア(0.0–1.0)
use_index_termsbooltrue検索インデックスの単語を辞書として使用
learn_from_queriesbooltrueユーザーの検索クエリから新しい単語を学習

CorrectionResult

correct() メソッドは詳細な情報を含む CorrectionResult を返します。

フィールド説明
originalString元のクエリ文字列
correctedOption<String>修正済みクエリ(自動修正が適用された場合)
word_suggestionsHashMap<String, Vec<Suggestion>>誤入力単語ごとにグループ化された候補
confidencef64全体の信頼度スコア(0.0–1.0)
auto_correctedbool自動修正が適用されたかどうか

ヘルパーメソッド

メソッド戻り値説明
has_suggestions()boolいずれかの単語に候補がある場合true
best_suggestion()Option<&Suggestion>最もスコアの高い単一の候補
query()Option<String>修正が行われた場合の修正済みクエリ文字列
should_show_did_you_mean()bool「もしかして?」プロンプトを表示すべきかどうか

カスタム辞書

組み込みの英語辞書の代わりに独自の辞書を提供できます。

#![allow(unused)]
fn main() {
use laurus::spelling::corrector::SpellingCorrector;
use laurus::spelling::dictionary::SpellingDictionary;

// カスタム辞書を構築
let mut dictionary = SpellingDictionary::new();
dictionary.add_word("elasticsearch", 100);
dictionary.add_word("lucene", 80);
dictionary.add_word("laurus", 90);

let corrector = SpellingCorrector::with_dictionary(dictionary);
}

インデックス単語からの学習

use_index_terms が有効な場合、修正器は検索インデックスの単語から学習できます。

#![allow(unused)]
fn main() {
let mut corrector = SpellingCorrector::new();

// インデックスの単語を修正器に提供
let index_terms = vec!["rust", "programming", "search", "engine"];
corrector.learn_from_terms(&index_terms);
}

これにより、ドメイン固有の語彙が組み込まれ、候補の品質が向上します。

統計情報

stats() で修正器の状態を監視できます。

#![allow(unused)]
fn main() {
let stats = corrector.stats();
println!("Dictionary words: {}", stats.dictionary_words);
println!("Total frequency: {}", stats.dictionary_total_frequency);
println!("Learned queries: {}", stats.queries_learned);
}

次のステップ

  • Lexical検索 – クエリタイプを使用した全文検索
  • Query DSL – 人間が読みやすいクエリ構文

ID管理

Laurusは、分散環境における効率的なドキュメントの検索、更新、集約を実現するために、二層構造のID管理戦略を採用しています。

1. 外部ID(String)

外部ID(External ID)は、ユーザーやアプリケーションがドキュメントを一意に識別するための論理的な識別子です。

  • : String
  • 役割: UUID、URL、データベースの主キーなど、任意の一意な値を使用できます。
  • 保存: Lexicalインデックス内の予約システムフィールド名 _id として透過的に永続化されます。
  • 一意性: システム全体でユニークであることが期待されます。
  • 更新: 既存の external_id でドキュメントをインデックスすると、自動的に「削除してから挿入(Delete-then-Insert)」(Upsert)操作がトリガーされ、古いバージョンが最新のものに置き換わります。

2. 内部ID(u64 / Stable ID)

内部ID(Internal ID)は、Laurusのエンジン(LexicalおよびVector)が高性能な操作のために内部的に使用する物理的なハンドルです。

  • : 符号なし64ビット整数(u64
  • 役割: ビットマップ操作、ポイント参照、分散ノード間のルーティングに使用されます。
  • 不変性(Stable): 一度割り当てられた内部IDは、インデックスのマージ(セグメントコンパクション)や再起動によって変更されることはありません。これにより、削除ログやキャッシュの不整合を防止します。

ID構造(Shard-Prefixed)

Laurusはマルチノード分散環境向けに設計されたShard-Prefixed Stable ID方式を採用しています。

ビット範囲名称説明
48-63ビットShard IDノードまたはパーティションを識別するプレフィックス(最大65,535シャード)
0-47ビットLocal IDシャード内で単調増加するドキュメント番号(最大約281兆ドキュメント)

この構造を採用する理由

  1. ゼロコスト集約: u64 IDがグローバルにユニークであるため、アグリゲータはノード間のID衝突を気にせずに高速なソートと重複排除を実行できます。
  2. 高速ルーティング: アグリゲータは上位ビットを見るだけで、ドキュメントの担当物理ノードを即座に特定でき、コストの高いハッシュ検索を回避できます。
  3. 高性能フェッチ: 内部IDは物理データ構造に直接マッピングされます。これにより、Laurusは検索時に「外部IDから内部IDへの変換」ステップをスキップし、O(1) のアクセス速度を実現します。

IDライフサイクル

  1. 登録(engine.put_document() / engine.add_document(): ユーザーが外部IDを持つドキュメントを提供します。
  2. ID割り当て: Engine が現在の shard_id と新しいLocal IDを組み合わせて、Shard-Prefixed内部IDを発行します。
  3. マッピング: エンジンが外部IDと新しい内部IDの対応関係を維持します。
  4. 検索: 検索結果は内部IDから解決された外部ID(String)を返します。
  5. 取得/削除: ユーザー向けAPIは利便性のために外部IDを受け付けますが、エンジンは内部的に内部IDに変換してほぼ即座に処理を行います。

永続化とWAL

Laurusはデータの耐久性を確保するために**Write-Ahead Log(WAL)**を使用します。すべての書き込み操作はインメモリ構造を変更する前にWALに永続化され、プロセスがクラッシュした場合でもデータが失われないことを保証します。

書き込みパス

sequenceDiagram
    participant App as Application
    participant Engine
    participant WAL as DocumentLog (WAL)
    participant Mem as In-Memory Buffers
    participant Disk as Storage (segments)

    App->>Engine: add_document() / delete_documents()
    Engine->>WAL: 1. Append operation to WAL
    Engine->>Mem: 2. Update in-memory buffers

    Note over Mem: Document is buffered but\nNOT yet searchable

    App->>Engine: commit()
    Engine->>Disk: 3. Flush segments to storage
    Engine->>WAL: 4. Truncate WAL
    Note over Disk: Documents are now\nsearchable and durable

主要な原則

  1. WALファースト: すべての書き込み(追加または削除)はインメモリ構造を更新する前にWALに追記されます
  2. バッファリング書き込み: インメモリバッファが commit() が呼ばれるまで変更を蓄積します
  3. アトミックコミット: commit() はすべてのバッファリングされた変更をセグメントファイルにフラッシュし、WALを切り捨てます
  4. クラッシュセーフティ: 書き込みとコミットの間にプロセスがクラッシュした場合、次回起動時にWALがリプレイされます

Write-Ahead Log(WAL)

WALは DocumentLog コンポーネントによって管理され、ストレージバックエンドのルートレベル(engine.wal)に保存されます。

WALエントリタイプ

エントリタイプ説明
Upsertドキュメント内容 + 外部ID + 割り当てられた内部ID
Delete削除するドキュメントの外部ID

WALファイル

WALファイル(engine.wal)は追記専用のバイナリログです。各エントリは以下を含む自己完結型です。

  • 操作タイプ(add/delete)
  • シーケンス番号
  • ペイロード(ドキュメントデータまたはID)

リカバリ

エンジンがビルドされる際(Engine::builder(...).build().await)、残っているWALエントリが自動的にチェックされ、リプレイされます(WALはコミット時に切り捨てられるため、残っているエントリはクラッシュしたセッションのものです)。

graph TD
    Start["Engine::build()"] --> Check["Check WAL for\nuncommitted entries"]
    Check -->|"Entries found"| Replay["Replay operations\ninto in-memory buffers"]
    Replay --> Ready["Engine ready"]
    Check -->|"No entries"| Ready

リカバリは透過的に行われるため、手動で処理する必要はありません。

コミットライフサイクル

#![allow(unused)]
fn main() {
// 1. ドキュメントを追加(バッファリングされ、まだ検索不可)
engine.add_document("doc-1", doc1).await?;
engine.add_document("doc-2", doc2).await?;

// 2. コミット — 永続ストレージにフラッシュ
engine.commit().await?;
// ドキュメントが検索可能に

// 3. さらにドキュメントを追加
engine.add_document("doc-3", doc3).await?;

// 4. ここでプロセスがクラッシュした場合、doc-3はWAL内にあり
//    次回起動時にリカバリされます
}

コミットのタイミング

戦略説明ユースケース
ドキュメントごと最大の耐久性、最小の検索遅延書き込みが少ないリアルタイム検索
バッチごとスループットと遅延の良いバランスバルクインデキシング
定期的最大の書き込みスループット大量データの取り込み

ヒント: コミットはセグメントをストレージにフラッシュするため比較的コストが高い操作です。バルクインデキシングでは、commit() を呼び出す前に多数のドキュメントをバッチ処理してください。

ストレージレイアウト

エンジンは PrefixedStorage を使用してデータを整理します。

<storage root>/
├── lexical/          # 転置インデックスセグメント
│   ├── seg-000/
│   │   ├── terms.dict
│   │   ├── postings.post
│   │   └── ...
│   └── metadata.json
├── vector/           # ベクトルインデックスセグメント
│   ├── seg-000/
│   │   ├── graph.hnsw
│   │   ├── vectors.vecs
│   │   └── ...
│   └── metadata.json
├── documents/        # ドキュメントストレージ
│   └── ...
└── engine.wal        # Write-Ahead Log

次のステップ

削除とコンパクション

Laurusは二段階の削除戦略を採用しています。高速な**論理削除(Logical Deletion)と、それに続く定期的な物理コンパクション(Physical Compaction)**です。

ドキュメントの削除

#![allow(unused)]
fn main() {
// 外部IDでドキュメントを削除
engine.delete_documents("doc-1").await?;
engine.commit().await?;
}

論理削除

ドキュメントが削除された場合、インデックスファイルから即座に削除されるわけではありません。代わりに以下の処理が行われます。

graph LR
    Del["delete_documents('doc-1')"] --> Bitmap["Add internal ID\nto Deletion Bitmap"]
    Bitmap --> Search["Search skips\ndeleted IDs"]
  1. ドキュメントの内部IDが**削除ビットマップ(Deletion Bitmap)**に追加されます
  2. 検索時にビットマップがチェックされ、削除されたドキュメントが結果からフィルタリングされます
  3. 元のデータはセグメントファイルに残ったままです

論理削除を採用する理由

メリット説明
速度O(1) – ビットの反転は即座に完了
不変セグメントセグメントファイルはインプレースで変更されないため、並行性の管理が簡素化
安全なリカバリクラッシュが発生しても、削除ビットマップはWALから再構築可能

Upsert(更新 = 削除 + 挿入)

既存の外部IDでドキュメントをインデックスすると、Laurusは自動的にUpsertを実行します。

  1. 古いドキュメントが論理削除されます(そのIDが削除ビットマップに追加)
  2. 新しい内部IDで新しいドキュメントが挿入されます
  3. 外部IDから内部IDへのマッピングが更新されます
#![allow(unused)]
fn main() {
// 最初の挿入
engine.put_document("doc-1", doc_v1).await?;
engine.commit().await?;

// 更新: 古いバージョンが論理削除され、新しいバージョンが挿入される
engine.put_document("doc-1", doc_v2).await?;
engine.commit().await?;
}

物理コンパクション

時間の経過とともに、論理削除されたドキュメントが蓄積されスペースを浪費します。コンパクションは、削除済みエントリを含まないセグメントファイルを再書き込みすることでスペースを回収します。

graph LR
    subgraph "Before Compaction"
        S1["Segment 0\ndoc-1 (deleted)\ndoc-2\ndoc-3 (deleted)"]
        S2["Segment 1\ndoc-4\ndoc-5"]
    end

    Compact["Compaction"]

    subgraph "After Compaction"
        S3["Segment 0\ndoc-2\ndoc-4\ndoc-5"]
    end

    S1 --> Compact
    S2 --> Compact
    Compact --> S3

コンパクションの処理内容

  1. 既存セグメントからすべての生存(未削除)ドキュメントを読み取ります
  2. 削除済みエントリを含まない転置インデックスやベクトルインデックスを再構築します
  3. 新しいクリーンなセグメントファイルを書き込みます
  4. 古いセグメントファイルを削除します
  5. 削除ビットマップをリセットします

コストと頻度

側面詳細
CPUコスト高い – インデックス構造をゼロから再構築
I/Oコスト高い – すべてのデータを読み取り、新しいセグメントを書き込み
ブロッキングコンパクション中も検索は継続可能(新しいセグメントが準備できるまで古いセグメントが参照される)
頻度削除済みドキュメントがしきい値を超えた場合に実行(例: 全体の10-20%)

コンパクションのタイミング

  • 書き込みが少ないワークロード: 定期的にコンパクション(例: 毎日または毎週)
  • 書き込みが多いワークロード: 削除率がしきい値を超えた場合にコンパクション
  • バルク更新後: 大量のUpsertバッチの後にコンパクション

削除ビットマップ

削除ビットマップは、どの内部IDが削除されたかを追跡します。

  • 保存: 削除済みドキュメントIDのHashSet(AHashSet<u64>
  • 検索: O(1) – ハッシュセットによる検索

ビットマップはインデックスセグメントと一緒に永続化され、リカバリ時にWALから再構築されます。

次のステップ

エラーハンドリング

Laurusはすべての操作に統一的なエラー型を使用します。エラーシステムを理解することで、障害を適切に処理する堅牢なアプリケーションを作成できます。

LaurusError

Laurusのすべての操作は Result<T> を返します。これは std::result::Result<T, LaurusError> のエイリアスです。

LaurusError は、各カテゴリの障害に対応するバリアントを持つenumです。

バリアント説明一般的な原因
IoI/Oエラーファイルが見つからない、権限拒否、ディスク容量不足
Indexインデックス操作エラーインデックスの破損、セグメント読み取り失敗
Schemaスキーマ関連のエラー不明なフィールド名、型の不一致
Analysisテキスト解析エラートークナイザーの失敗、無効なフィルタ設定
Queryクエリの解析/実行エラー不正なQuery DSL、クエリ内の不明なフィールド
Storageストレージバックエンドエラーストレージのオープン失敗、書き込み失敗
Fieldフィールド定義エラー無効なフィールドオプション、重複するフィールド名
JsonJSONシリアライズエラー不正なドキュメントJSON
InvalidOperation無効な操作コミット前の検索、二重クローズ
ResourceExhaustedリソース制限超過メモリ不足、オープンファイル数超過
SerializationErrorバイナリシリアライズエラーディスク上のデータ破損
OperationCancelled操作がキャンセルされたタイムアウト、ユーザーによるキャンセル
NotImplemented機能が利用不可未実装の操作
Other汎用エラータイムアウト、無効な設定、無効な引数

基本的なエラーハンドリング

? 演算子の使用

最もシンプルなアプローチ – エラーを呼び出し元に伝播します。

#![allow(unused)]
fn main() {
use laurus::{Engine, Result};

async fn index_documents(engine: &Engine) -> Result<()> {
    let doc = laurus::Document::builder()
        .add_text("title", "Rust Programming")
        .build();

    engine.put_document("doc1", doc).await?;
    engine.commit().await?;
    Ok(())
}
}

エラーバリアントのマッチング

エラータイプごとに異なる動作が必要な場合:

#![allow(unused)]
fn main() {
use laurus::{Engine, LaurusError};

async fn safe_search(engine: &Engine, query: &str) {
    match engine.search(/* request */).await {
        Ok(results) => {
            for result in results {
                println!("{}: {}", result.id, result.score);
            }
        }
        Err(LaurusError::Query(msg)) => {
            eprintln!("Invalid query syntax: {}", msg);
        }
        Err(LaurusError::Io(e)) => {
            eprintln!("Storage I/O error: {}", e);
        }
        Err(e) => {
            eprintln!("Unexpected error: {}", e);
        }
    }
}
}

downcast によるエラータイプの確認

LaurusErrorstd::error::Error を実装しているため、標準的なエラーハンドリングパターンを使用できます。

#![allow(unused)]
fn main() {
use laurus::LaurusError;

fn is_retriable(error: &LaurusError) -> bool {
    matches!(error, LaurusError::Io(_) | LaurusError::ResourceExhausted(_))
}
}

よくあるエラーシナリオ

スキーマの不一致

スキーマに一致しないフィールドを持つドキュメントの追加:

#![allow(unused)]
fn main() {
// スキーマには "title"(Text)と "year"(Integer)がある
let doc = Document::builder()
    .add_text("title", "Hello")
    .add_text("unknown_field", "this field is not in schema")
    .build();

// スキーマにないフィールドはインデキシング時に黙って無視されます。
// エラーは発生しません -- スキーマで定義されたフィールドのみが処理されます。
}

クエリ解析エラー

無効なQuery DSL構文は Query エラーを返します。

#![allow(unused)]
fn main() {
use laurus::engine::query::UnifiedQueryParser;

let parser = UnifiedQueryParser::new();
match parser.parse("title:\"unclosed phrase") {
    Ok(request) => { /* ... */ }
    Err(LaurusError::Query(msg)) => {
        // msgには解析失敗の詳細が含まれます
        eprintln!("Bad query: {}", msg);
    }
    Err(e) => { /* その他のエラー */ }
}
}

ストレージI/Oエラー

ファイルベースのストレージではI/Oエラーが発生する可能性があります。

#![allow(unused)]
fn main() {
use laurus::storage::{StorageConfig, StorageFactory};

match StorageFactory::open(StorageConfig::File {
    path: "/nonexistent/path".into(),
    loading_mode: Default::default(),
}) {
    Ok(storage) => { /* ... */ }
    Err(LaurusError::Io(e)) => {
        eprintln!("Cannot open storage: {}", e);
    }
    Err(e) => { /* その他のエラー */ }
}
}

便利なコンストラクタ

LaurusError はカスタム実装でエラーを作成するためのファクトリメソッドを提供しています。

メソッド作成されるバリアント
LaurusError::index(msg)Index バリアント
LaurusError::schema(msg)Schema バリアント
LaurusError::analysis(msg)Analysis バリアント
LaurusError::query(msg)Query バリアント
LaurusError::storage(msg)Storage バリアント
LaurusError::field(msg)Field バリアント
LaurusError::other(msg)Other バリアント
LaurusError::cancelled(msg)OperationCancelled バリアント
LaurusError::invalid_argument(msg)“Invalid argument” プレフィックス付き Other
LaurusError::invalid_config(msg)“Invalid configuration” プレフィックス付き Other
LaurusError::not_found(msg)“Not found” プレフィックス付き Other
LaurusError::timeout(msg)“Timeout” プレフィックス付き Other

これらはカスタム Analyzer、Embedder、またはStorage トレイトを実装する際に有用です。

#![allow(unused)]
fn main() {
use laurus::{LaurusError, Result};

fn validate_dimension(dim: usize) -> Result<()> {
    if dim == 0 {
        return Err(LaurusError::invalid_argument("dimension must be > 0"));
    }
    Ok(())
}
}

自動変換

LaurusError は一般的なエラー型に対して From を実装しているため、? で自動変換されます。

ソース型ターゲットバリアント
std::io::ErrorLaurusError::Io
serde_json::ErrorLaurusError::Json
anyhow::ErrorLaurusError::Anyhow

次のステップ

  • 拡張性 – 適切なエラーハンドリングでカスタムトレイトを実装
  • APIリファレンス – 完全なメソッドシグネチャと戻り値の型

拡張性

Laurusはコアコンポーネントにトレイトベースの抽象化を採用しています。これらのトレイトを実装することで、カスタムAnalyzer、Embedder、およびStorageバックエンドを提供できます。

カスタムAnalyzer

Analyzer トレイトを実装して、カスタムテキスト解析パイプラインを作成します。

#![allow(unused)]
fn main() {
use laurus::analysis::analyzer::analyzer::Analyzer;
use laurus::analysis::token::{Token, TokenStream};
use laurus::Result;

#[derive(Debug)]
struct ReverseAnalyzer;

impl Analyzer for ReverseAnalyzer {
    fn analyze(&self, text: &str) -> Result<TokenStream> {
        let tokens: Vec<Token> = text
            .split_whitespace()
            .enumerate()
            .map(|(i, word)| Token {
                text: word.chars().rev().collect(),
                position: i,
                ..Default::default()
            })
            .collect();
        Ok(Box::new(tokens.into_iter()))
    }

    fn name(&self) -> &str {
        "reverse"
    }

    fn as_any(&self) -> &dyn std::any::Any {
        self
    }
}
}

必須メソッド

メソッド説明
analyze(&self, text: &str) -> Result<TokenStream>テキストをトークンストリームに変換
name(&self) -> &strこのAnalyzerの一意な識別子を返す
as_any(&self) -> &dyn Any具象型へのダウンキャストを可能にする

カスタムAnalyzerの使用

Analyzerを EngineBuilder に渡します。

#![allow(unused)]
fn main() {
use std::sync::Arc;

let analyzer = Arc::new(ReverseAnalyzer);
let engine = Engine::builder(storage, schema)
    .analyzer(analyzer)
    .build()
    .await?;
}

フィールドごとのAnalyzerには PerFieldAnalyzer でラップします。

#![allow(unused)]
fn main() {
use laurus::analysis::analyzer::per_field::PerFieldAnalyzer;
use laurus::analysis::analyzer::standard::StandardAnalyzer;

let per_field = PerFieldAnalyzer::new(Arc::new(StandardAnalyzer::new()?));
per_field.add_analyzer("custom_field", Arc::new(ReverseAnalyzer));

let engine = Engine::builder(storage, schema)
    .analyzer(Arc::new(per_field))
    .build()
    .await?;
}

カスタムEmbedder

Embedder トレイトを実装して、独自のベクトルEmbeddingモデルを統合します。

#![allow(unused)]
fn main() {
use async_trait::async_trait;
use laurus::embedding::embedder::{Embedder, EmbedInput, EmbedInputType};
use laurus::vector::core::vector::Vector;
use laurus::{LaurusError, Result};

#[derive(Debug)]
struct MyEmbedder {
    dimension: usize,
}

#[async_trait]
impl Embedder for MyEmbedder {
    async fn embed(&self, input: &EmbedInput<'_>) -> Result<Vector> {
        match input {
            EmbedInput::Text(text) => {
                // Embeddingロジックをここに記述
                let vector = vec![0.0f32; self.dimension];
                Ok(Vector::new(vector))
            }
            _ => Err(LaurusError::invalid_argument(
                "this embedder only supports text input",
            )),
        }
    }

    fn supported_input_types(&self) -> Vec<EmbedInputType> {
        vec![EmbedInputType::Text]
    }

    fn name(&self) -> &str {
        "my-embedder"
    }

    fn as_any(&self) -> &dyn std::any::Any {
        self
    }
}
}

必須メソッド

メソッド説明
async embed(&self, input: &EmbedInput) -> Result<Vector>指定された入力に対するEmbeddingベクトルを生成
supported_input_types(&self) -> Vec<EmbedInputType>サポートする入力タイプを宣言(TextImage
as_any(&self) -> &dyn Anyダウンキャストを可能にする

オプションメソッド

メソッドデフォルト説明
async embed_batch(&self, inputs) -> Result<Vec<Vector>>embed への逐次呼び出しバッチ最適化のためにオーバーライド
name(&self) -> &str"unknown"ログ出力用の識別子
supports(&self, input_type) -> boolsupported_input_types をチェック入力タイプのサポート確認
supports_text() -> boolText を確認テキストサポートの簡略確認
supports_image() -> boolImage を確認画像サポートの簡略確認
is_multimodal() -> boolテキストと画像の両方マルチモーダル確認

カスタムEmbedderの使用

#![allow(unused)]
fn main() {
let embedder = Arc::new(MyEmbedder { dimension: 384 });
let engine = Engine::builder(storage, schema)
    .embedder(embedder)
    .build()
    .await?;
}

フィールドごとのEmbedderには PerFieldEmbedder でラップします。

#![allow(unused)]
fn main() {
use laurus::embedding::per_field::PerFieldEmbedder;

let per_field = PerFieldEmbedder::new(Arc::new(MyEmbedder { dimension: 384 }));
per_field.add_embedder("image_vec", Arc::new(ClipEmbedder::new()?));

let engine = Engine::builder(storage, schema)
    .embedder(Arc::new(per_field))
    .build()
    .await?;
}

カスタムStorage

Storage トレイトを実装して、新しいストレージバックエンドを追加します。

#![allow(unused)]
fn main() {
use laurus::storage::{Storage, StorageInput, StorageOutput, LoadingMode, FileMetadata};
use laurus::Result;

#[derive(Debug)]
struct S3Storage {
    bucket: String,
    prefix: String,
}

impl Storage for S3Storage {
    fn loading_mode(&self) -> LoadingMode {
        LoadingMode::Eager  // S3は完全なダウンロードが必要
    }

    fn open_input(&self, name: &str) -> Result<Box<dyn StorageInput>> {
        // S3からダウンロードしてリーダーを返す
        todo!()
    }

    fn create_output(&self, name: &str) -> Result<Box<dyn StorageOutput>> {
        // S3へのアップロードストリームを作成
        todo!()
    }

    fn create_output_append(&self, name: &str) -> Result<Box<dyn StorageOutput>> {
        todo!()
    }

    fn file_exists(&self, name: &str) -> bool {
        todo!()
    }

    fn delete_file(&self, name: &str) -> Result<()> {
        todo!()
    }

    fn list_files(&self) -> Result<Vec<String>> {
        todo!()
    }

    fn file_size(&self, name: &str) -> Result<u64> {
        todo!()
    }

    fn metadata(&self, name: &str) -> Result<FileMetadata> {
        todo!()
    }

    fn rename_file(&self, old_name: &str, new_name: &str) -> Result<()> {
        todo!()
    }

    fn create_temp_output(&self, prefix: &str) -> Result<(String, Box<dyn StorageOutput>)> {
        todo!()
    }

    fn sync(&self) -> Result<()> {
        todo!()
    }

    fn close(&mut self) -> Result<()> {
        todo!()
    }
}
}

必須メソッド

メソッド説明
open_input(name) -> Result<Box<dyn StorageInput>>ファイルを読み取り用にオープン
create_output(name) -> Result<Box<dyn StorageOutput>>ファイルを書き込み用に作成
create_output_append(name) -> Result<Box<dyn StorageOutput>>ファイルを追記用にオープン
file_exists(name) -> boolファイルの存在を確認
delete_file(name) -> Result<()>ファイルを削除
list_files() -> Result<Vec<String>>すべてのファイルを一覧表示
file_size(name) -> Result<u64>ファイルサイズをバイト単位で取得
metadata(name) -> Result<FileMetadata>ファイルのメタデータを取得
rename_file(old, new) -> Result<()>ファイル名を変更
create_temp_output(prefix) -> Result<(String, Box<dyn StorageOutput>)>一時ファイルを作成
sync() -> Result<()>保留中の書き込みをすべてフラッシュ
close(&mut self) -> Result<()>ストレージを閉じてリソースを解放

オプションメソッド

メソッドデフォルト説明
loading_mode() -> LoadingModeLoadingMode::Eager推奨されるデータロードモード

スレッドセーフティ

3つのトレイトすべてが Send + Sync を要求します。つまり、実装はスレッド間で安全に共有できる必要があります。共有可能な可変状態には Arc<Mutex<_>> またはロックフリーデータ構造を使用してください。

次のステップ

APIリファレンス

このページでは、Laurusの最も重要な型とメソッドのクイックリファレンスを提供します。完全な詳細については、Rustdocを生成してください。

cargo doc --open

Engine

すべてのインデキシングと検索操作を統合する中心的なコーディネーターです。

メソッド説明
Engine::builder(storage, schema)EngineBuilder を作成
engine.put_document(id, doc).await?ドキュメントのUpsert(IDが存在する場合は置き換え)
engine.add_document(id, doc).await?ドキュメントをチャンクとして追加(複数のチャンクが同一IDを共有可能)
engine.delete_documents(id).await?外部IDによるすべてのドキュメント/チャンクの削除
engine.get_documents(id).await?外部IDによるすべてのドキュメント/チャンクの取得
engine.search(request).await?検索リクエストの実行
engine.commit().await?保留中のすべての変更をストレージにフラッシュ
engine.add_field(name, field_option).await?稼働中のエンジンにフィールドを動的に追加
engine.schema()現在のスキーマへの参照を取得
engine.stats()?インデックス統計の取得

put_documentadd_document の違い: put_document はUpsertを実行します。同じ外部IDのドキュメントが既に存在する場合、削除して置き換えます。add_document は常に追加し、複数のドキュメントチャンクが同じ外部IDを共有できます。詳細は Schema & Fields – ドキュメントのインデキシング を参照してください。

EngineBuilder

メソッド説明
EngineBuilder::new(storage, schema)StorageとSchemaでBuilderを作成
.analyzer(Arc<dyn Analyzer>)テキストAnalyzerを設定(デフォルト: StandardAnalyzer
.embedder(Arc<dyn Embedder>)ベクトルEmbedderを設定(オプション)
.build().await?Engine を構築

Schema

ドキュメント構造を定義します。

メソッド説明
Schema::builder()SchemaBuilder を作成

SchemaBuilder

メソッド説明
.add_text_field(name, TextOption)全文検索フィールドを追加
.add_integer_field(name, IntegerOption)整数フィールドを追加
.add_float_field(name, FloatOption)浮動小数点フィールドを追加
.add_boolean_field(name, BooleanOption)真偽値フィールドを追加
.add_datetime_field(name, DateTimeOption)日時フィールドを追加
.add_geo_field(name, GeoOption)地理フィールドを追加
.add_bytes_field(name, BytesOption)バイナリフィールドを追加
.add_hnsw_field(name, HnswOption)HNSWベクトルフィールドを追加
.add_flat_field(name, FlatOption)Flatベクトルフィールドを追加
.add_ivf_field(name, IvfOption)IVFベクトルフィールドを追加
.add_default_field(name)デフォルト検索フィールドを設定
.build()Schema を構築

Document

名前付きフィールド値のコレクションです。

メソッド説明
Document::builder()DocumentBuilder を作成
doc.get(name)名前でフィールド値を取得
doc.has_field(name)フィールドが存在するか確認
doc.field_names()すべてのフィールド名を取得

DocumentBuilder

メソッド説明
.add_text(name, value)テキストフィールドを追加
.add_integer(name, value)整数フィールドを追加
.add_float(name, value)浮動小数点フィールドを追加
.add_boolean(name, value)真偽値フィールドを追加
.add_datetime(name, value)日時フィールドを追加
.add_vector(name, vec)事前計算済みベクトルを追加
.add_geo(name, lat, lon)地理ポイントを追加
.add_bytes(name, data)バイナリデータを追加
.build()Document を構築

SearchRequestBuilder

メソッド説明
SearchRequestBuilder::new()新しいBuilderを作成
.lexical_search_request(req)Lexical検索コンポーネントを設定
.vector_search_request(req)Vector検索コンポーネントを設定
.filter_query(query)プレフィルタクエリを設定
.fusion_algorithm(algo)フュージョンアルゴリズムを設定(デフォルト: RRF)
.limit(n)最大結果数(デフォルト: 10)
.offset(n)N件スキップ(デフォルト: 0)
.build()SearchRequest を構築

LexicalSearchRequest

メソッド説明
LexicalSearchRequest::new(query)クエリで作成
LexicalSearchRequest::from_dsl(query_str)DSLクエリ文字列から作成
.limit(n)最大結果数
.load_documents(bool)ドキュメント内容をロードするかどうか
.min_score(f32)最小スコアしきい値
.timeout_ms(u64)検索タイムアウト(ミリ秒)
.parallel(bool)並列検索を有効化
.sort_by_field_asc(field)フィールドで昇順ソート
.sort_by_field_desc(field)フィールドで降順ソート
.sort_by_score()関連度スコアでソート(デフォルト)
.with_field_boost(field, boost)フィールドレベルのブーストを追加

VectorSearchRequestBuilder

メソッド説明
VectorSearchRequestBuilder::new()新しいBuilderを作成
.add_text(field, text)フィールドのテキストクエリを追加
.add_vector(field, vector)事前計算済みクエリベクトルを追加
.add_bytes(field, bytes, mime)バイナリペイロードを追加(マルチモーダル用)
.limit(n)最大結果数
.score_mode(VectorScoreMode)スコア結合モード(WeightedSum、MaxSim)
.min_score(f32)最小スコアしきい値
.field(name)検索を特定のフィールドに制限
.build()リクエストを構築

SearchResult

フィールド説明
idString外部ドキュメントID
scoref32関連度スコア
documentOption<Document>ドキュメント内容(ロードされた場合)

FusionAlgorithm

バリアント説明
RRF { k: f64 }Reciprocal Rank Fusion(デフォルト k=60.0)
WeightedSum { lexical_weight, vector_weight }スコアの線形結合

クエリタイプ(Lexical)

クエリ説明
TermQuery::new(field, term)完全一致TermQuery::new("body", "rust")
PhraseQuery::new(field, terms)フレーズ一致PhraseQuery::new("body", vec!["machine".into(), "learning".into()])
BooleanQueryBuilder::new()ブール結合.must(q1).should(q2).must_not(q3).build()
FuzzyQuery::new(field, term)あいまい一致(デフォルト max_edits=2)FuzzyQuery::new("body", "programing").max_edits(1)
WildcardQuery::new(field, pattern)ワイルドカードWildcardQuery::new("file", "*.pdf")
NumericRangeQuery::new(...)数値範囲Lexical Search を参照
GeoQuery::within_radius(...)地理半径Lexical Search を参照
SpanNearQuery::new(...)近接Lexical Search を参照
PrefixQuery::new(field, prefix)前方一致PrefixQuery::new("body", "pro")
RegexpQuery::new(field, pattern)?正規表現一致RegexpQuery::new("body", "^pro.*ing$")?

クエリパーサー

パーサー説明
QueryParser::new(analyzer)Lexical DSLクエリをパース
VectorQueryParser::new(embedder)Vector DSLクエリをパース
UnifiedQueryParser::new(lexical, vector)ハイブリッドDSLクエリをパース

Analyzer

説明
StandardAnalyzerRegexTokenizer + 小文字化 + ストップワード
SimpleAnalyzerトークン化のみ(フィルタリングなし)
EnglishAnalyzerRegexTokenizer + 小文字化 + 英語ストップワード
JapaneseAnalyzer日本語形態素解析
KeywordAnalyzerトークン化なし(完全一致)
PipelineAnalyzerカスタムTokenizer + フィルタチェーン
PerFieldAnalyzerフィールドごとのAnalyzerディスパッチ

Embedder

Feature Flag説明
CandleBertEmbedderembeddings-candleローカルBERTモデル
OpenAIEmbedderembeddings-openaiOpenAI API
CandleClipEmbedderembeddings-multimodalローカルCLIPモデル
PrecomputedEmbedder(デフォルト)事前計算済みベクトル
PerFieldEmbedder(デフォルト)フィールドごとのEmbedderディスパッチ

Storage

説明
MemoryStorageインメモリ(非永続)
FileStorageファイルシステムベース(メモリマップドI/O用の use_mmap をサポート)
StorageFactory::create(config)設定から作成

DataValue

バリアントRust型
DataValue::Null
DataValue::Bool(bool)bool
DataValue::Int64(i64)i64
DataValue::Float64(f64)f64
DataValue::Text(String)String
DataValue::Bytes(Vec<u8>, Option<String>)(data, mime_type)
DataValue::Vector(Vec<f32>)Vec<f32>
DataValue::DateTime(DateTime<Utc>)chrono::DateTime<Utc>
DataValue::Geo(f64, f64)(latitude, longitude)

CLI 概要

Laurus はコマンドラインツール laurus を提供しており、コードを書かずにインデックスの作成、ドキュメントの管理、検索クエリの実行が可能です。

機能

  • インデックス管理 – TOML スキーマファイルからインデックスを作成・検査。対話式スキーマジェネレーター付き
  • ドキュメント CRUD – JSON によるドキュメントの追加、取得、削除
  • 検索Query DSL を使用したクエリ実行
  • デュアル出力 – 人間が読みやすいテーブル形式または機械処理向け JSON 形式
  • 対話型 REPL – ライブセッションでインデックスを操作
  • gRPC サーバーlaurus servegRPC サーバーを起動

はじめに

# インストール
cargo install laurus-cli

# スキーマを対話的に生成
laurus create schema

# スキーマからインデックスを作成
laurus --index-dir ./my_index create index --schema schema.toml

# ドキュメントを追加
laurus --index-dir ./my_index add doc --id doc1 --data '{"title":"Hello","body":"World"}'

# 変更をコミット
laurus --index-dir ./my_index commit

# 検索
laurus --index-dir ./my_index search "body:world"

詳細はサブセクションを参照してください:

インストール

crates.io からインストール

cargo install laurus-cli

これにより laurus バイナリが ~/.cargo/bin/ にインストールされます。

ソースからインストール

git clone https://github.com/mosuka/laurus.git
cd laurus
cargo install --path laurus-cli

確認

laurus --version

ハンズオンチュートリアル

このチュートリアルでは、laurus CLI を使った一連のワークフローを体験します。スキーマの作成、インデックスの構築、ドキュメントの登録、検索、更新、削除、そしてインタラクティブ REPL の使い方を順を追って説明します。

前提条件

Step 1: スキーマの作成

まず、インデックスの構造を定義するスキーマファイルを作成します。対話形式で生成することもできます:

laurus create schema

対話ウィザードがフィールドの定義をガイドします。このチュートリアルでは、手動でスキーマファイルを作成します:

cat > schema.toml << 'EOF'
default_fields = ["title", "body"]

[fields.title.Text]
indexed = true
stored = true
term_vectors = false

[fields.body.Text]
indexed = true
stored = true
term_vectors = false

[fields.category.Text]
indexed = true
stored = true
term_vectors = false
EOF

3 つのテキストフィールドを定義しています。default_fields を設定することで、フィールド指定なしのクエリは titlebody の両方を検索します。

Step 2: インデックスの作成

スキーマを使ってインデックスを作成します:

laurus --index-dir ./tutorial_data create index --schema schema.toml

インデックスが作成されたことを確認します:

laurus --index-dir ./tutorial_data get stats

ドキュメント数が 0 であることが表示されます。

Step 3: ドキュメントの登録

ドキュメントをインデックスに追加します。各ドキュメントには ID と JSON 形式のフィールド値が必要です:

laurus --index-dir ./tutorial_data add doc \
  --id doc001 \
  --data '{"title":"Introduction to Rust Programming","body":"Rust is a modern systems programming language that focuses on safety, speed, and concurrency.","category":"programming"}'
laurus --index-dir ./tutorial_data add doc \
  --id doc002 \
  --data '{"title":"Web Development with Rust","body":"Building web applications with Rust has become increasingly popular. Frameworks like Actix and Rocket make it easy to create fast and secure web services.","category":"web-development"}'
laurus --index-dir ./tutorial_data add doc \
  --id doc003 \
  --data '{"title":"Python for Data Science","body":"Python is the most popular language for data science and machine learning. Libraries like NumPy and Pandas provide powerful tools for data analysis.","category":"data-science"}'

Step 4: 変更のコミット

ドキュメントはコミットするまで検索対象になりません:

laurus --index-dir ./tutorial_data commit

Step 5: ドキュメントの検索

基本的な検索

“rust” を含むドキュメントを検索します:

laurus --index-dir ./tutorial_data search "rust"

デフォルトフィールド(titlebody)が検索されます。doc001doc002 が返されます。

フィールド指定検索

title フィールドのみを検索します:

laurus --index-dir ./tutorial_data search "title:python"

doc003 のみが返されます。

カテゴリ検索

laurus --index-dir ./tutorial_data search "category:programming"

doc001 のみが返されます。

ブーリアンクエリ

+(必須)と -(除外)で条件を組み合わせます:

laurus --index-dir ./tutorial_data search "+body:rust -body:web"

“rust” を含み “web” を含まない doc001 のみが返されます。

フレーズ検索

完全一致するフレーズを検索します:

laurus --index-dir ./tutorial_data search 'body:"data science"'

doc003 のみが返されます。

あいまい検索

~ を使ってタイプミスに対応した検索を行います:

laurus --index-dir ./tutorial_data search "body:programing~1"

タイプミスがあっても “programming” にマッチします。

JSON 出力

プログラムでの利用に向けて JSON 形式で結果を取得します:

laurus --index-dir ./tutorial_data --format json search "rust"

Step 6: ドキュメントの取得

ID を指定して特定のドキュメントを取得します:

laurus --index-dir ./tutorial_data get doc --id doc001

Step 7: ドキュメントの削除

ドキュメントを削除してコミットします:

laurus --index-dir ./tutorial_data delete doc --id doc003
laurus --index-dir ./tutorial_data commit

削除されたことを確認します:

laurus --index-dir ./tutorial_data search "python"

結果は返されません。

Step 8: REPL を使う

REPL はインデックスを対話的に操作するためのインタラクティブセッションです:

laurus --index-dir ./tutorial_data repl

REPL で以下のコマンドを試してみてください:

> stats
> search rust
> doc add doc004 {"title":"Go Programming","body":"Go is a statically typed language designed for simplicity and efficiency.","category":"programming"}
> commit
> search programming
> doc get doc004
> doc delete doc004
> commit
> quit

REPL はコマンド履歴(上下キー)や行編集に対応しています。

Step 9: クリーンアップ

チュートリアルで作成したデータを削除します:

rm -rf ./tutorial_data schema.toml

次のステップ

コマンドリファレンス

グローバルオプション

すべてのコマンドで以下のオプションが使用できます:

オプション環境変数デフォルト説明
--index-dir <PATH>LAURUS_INDEX_DIR./laurus_indexインデックスデータディレクトリのパス
--format <FORMAT>table出力形式: table または json
# 例: カスタムデータディレクトリで JSON 出力を使用
laurus --index-dir /var/data/my_index --format json search "title:rust"

create — リソースの作成

create index

スキーマ TOML ファイルから新しいインデックスを作成します。

laurus create index --schema <FILE>

引数:

フラグ必須説明
--schema <FILE>はいインデックススキーマを定義する TOML ファイルのパス

スキーマファイルの形式:

スキーマファイルは Laurus ライブラリの Schema 型と同じ構造に従います。詳細はスキーマフォーマットリファレンスを参照してください。例:

default_fields = ["title", "body"]

[fields.title.Text]
stored = true
indexed = true

[fields.body.Text]
stored = true
indexed = true

[fields.category.Text]
stored = true
indexed = true

例:

laurus --index-dir ./my_index create index --schema schema.toml
# Index created at ./my_index.

注意: インデックスが既に存在する場合はエラーが返されます。再作成するにはデータディレクトリを削除してください。

create schema

対話式ウィザードを通じてスキーマ TOML ファイルを生成します。

laurus create schema [--output <FILE>]

引数:

フラグ必須デフォルト説明
--output <FILE>いいえschema.toml生成されるスキーマの出力ファイルパス

ウィザードは以下の手順で進みます:

  1. フィールド定義 — フィールド名を入力し、型を選択し、型固有のオプションを設定
  2. 繰り返し — 必要な数だけフィールドを追加
  3. デフォルトフィールド — デフォルトの検索対象とする Lexical フィールドを選択
  4. プレビュー — 保存前に生成された TOML を確認
  5. 保存 — スキーマファイルを書き出し

サポートされるフィールド型:

カテゴリオプション
TextLexicalindexed, stored, term_vectors
IntegerLexicalindexed, stored
FloatLexicalindexed, stored
BooleanLexicalindexed, stored
DateTimeLexicalindexed, stored
GeoLexicalindexed, stored
BytesLexicalstored
HnswVectordimension, distance, m, ef_construction
FlatVectordimension, distance
IvfVectordimension, distance, n_clusters, n_probe

例:

# schema.toml を対話的に生成
laurus create schema

# 出力パスを指定
laurus create schema --output my_schema.toml

# 生成されたスキーマからインデックスを作成
laurus create index --schema schema.toml

get — リソースの取得

get stats

インデックスの統計情報を表示します。

laurus get stats

テーブル出力の例:

Document count: 42

Vector fields:
╭──────────┬─────────┬───────────╮
│ Field    │ Vectors │ Dimension │
├──────────┼─────────┼───────────┤
│ text_vec │ 42      │ 384       │
╰──────────┴─────────┴───────────╯

JSON 出力の例:

laurus --format json get stats
{
  "document_count": 42,
  "fields": {
    "text_vec": {
      "vector_count": 42,
      "dimension": 384
    }
  }
}

get schema

現在のインデックスのスキーマを JSON 形式で表示します。

laurus get schema

例:

laurus get schema
# {
#   "fields": { ... },
#   "default_fields": ["title", "body"],
#   ...
# }

get doc

外部 ID でドキュメント(およびすべてのチャンク)を取得します。

laurus get doc --id <ID>

テーブル出力の例:

╭──────┬─────────────────────────────────────────╮
│ ID   │ Fields                                  │
├──────┼─────────────────────────────────────────┤
│ doc1 │ body: This is a test, title: Hello World │
╰──────┴─────────────────────────────────────────╯

JSON 出力の例:

laurus --format json get doc --id doc1
[
  {
    "id": "doc1",
    "document": {
      "title": "Hello World",
      "body": "This is a test document."
    }
  }
]

add — リソースの追加

add doc

インデックスにドキュメントを追加します。ドキュメントは commit を実行するまで検索対象になりません。

laurus add doc --id <ID> --data <JSON>

引数:

フラグ必須説明
--id <ID>はい外部ドキュメント ID(文字列)
--data <JSON>はいJSON 文字列としてのドキュメントフィールド

JSON フォーマットはフィールド名と値を対応付けたフラットなオブジェクトです:

{
  "title": "Introduction to Rust",
  "body": "Rust is a systems programming language.",
  "category": "programming"
}

例:

laurus add doc --id doc1 --data '{"title":"Hello World","body":"This is a test document."}'
# Document 'doc1' added. Run 'commit' to persist changes.

ヒント: 複数のドキュメントが同じ外部 ID を共有できます(チャンキングパターン)。各チャンクに対して add doc を使用してください。

add field

既存のインデックスにフィールドを動的に追加します。

laurus add field --index-dir ./data \
    --name category \
    --field-option '{"Text": {"indexed": true, "stored": true}}'

--field-option 引数はスキーマファイルと同じ外部タグ付き JSON 形式を受け付けます。 フィールド追加後、スキーマは自動的に永続化されます。


delete — リソースの削除

delete field

スキーマからフィールドを動的に削除します。既にインデックスされたデータは残りますが、削除されたフィールドにはアクセスできなくなります。

laurus delete field --name <FIELD_NAME>

例:

laurus delete field --name category
# Field 'category' deleted.

delete doc

外部 ID でドキュメント(およびすべてのチャンク)を削除します。

laurus delete doc --id <ID>

例:

laurus delete doc --id doc1
# Document 'doc1' deleted. Run 'commit' to persist changes.

commit

保留中の変更(追加と削除)をインデックスにコミットします。コミットするまで、変更は検索に反映されません。

laurus commit

例:

laurus --index-dir ./my_index commit
# Changes committed successfully.

search

Query DSL を使用して検索クエリを実行します。

laurus search <QUERY> [--limit <N>] [--offset <N>]

引数:

引数 / フラグ必須デフォルト説明
<QUERY>はいLaurus Query DSL によるクエリ文字列
--limit <N>いいえ10最大結果件数
--offset <N>いいえ0スキップする結果件数

クエリ構文の例:

# Term クエリ
laurus search "body:rust"

# Phrase クエリ
laurus search 'body:"machine learning"'

# Boolean クエリ
laurus search "+body:programming -body:python"

# Fuzzy クエリ(タイポ許容)
laurus search "body:programing~2"

# Wildcard クエリ
laurus search "title:intro*"

# Range クエリ
laurus search "price:[10 TO 50]"

テーブル出力の例:

╭──────┬────────┬─────────────────────────────────────────╮
│ ID   │ Score  │ Fields                                  │
├──────┼────────┼─────────────────────────────────────────┤
│ doc1 │ 0.8532 │ body: Rust is a systems..., title: Intr │
│ doc3 │ 0.4210 │ body: JavaScript powers..., title: Web  │
╰──────┴────────┴─────────────────────────────────────────╯

JSON 出力の例:

laurus --format json search "body:rust" --limit 5
[
  {
    "id": "doc1",
    "score": 0.8532,
    "document": {
      "title": "Introduction to Rust",
      "body": "Rust is a systems programming language."
    }
  }
]

repl

対話型 REPL セッションを開始します。詳細は REPL を参照してください。

laurus repl

serve

gRPC サーバー(およびオプションで HTTP Gateway)を起動します。

laurus serve [OPTIONS]

起動オプション、設定、使用例については laurus-server のドキュメントを参照してください:

スキーマフォーマットリファレンス

スキーマファイルはインデックスの構造を定義します。どのフィールドが存在するか、その型、およびインデックスの方法を指定します。Laurus はスキーマファイルに TOML 形式を使用します。

概要

スキーマは2つのトップレベル要素で構成されます:

# クエリでフィールドが指定されていない場合にデフォルトで検索するフィールド。
default_fields = ["title", "body"]

# フィールド定義。各フィールドには名前と型付き設定があります。
[fields.<field_name>.<FieldType>]
# ... 型固有のオプション
  • default_fieldsQuery DSL でデフォルトの検索対象として使用されるフィールド名のリストです。Lexical フィールド(Text、Integer、Float など)のみデフォルトフィールドに指定できます。このキーはオプションで、デフォルトは空のリストです。
  • fields — フィールド名とその型付き設定のマップです。各フィールドにはフィールド型を1つだけ指定する必要があります。

フィールド命名規則

  • フィールド名は任意の文字列です(例: titlebody_veccreated_at)。
  • _id フィールドは Laurus が内部ドキュメント ID 管理用に予約しています。使用しないでください。
  • フィールド名はスキーマ内で一意である必要があります。

フィールド型

フィールドは Lexical(キーワード/全文検索用)と Vector(類似検索用)の2つのカテゴリに分類されます。1つのフィールドが両方を兼ねることはできません。

Lexical フィールド

Text

全文検索可能なフィールドです。テキストは解析パイプライン(トークン化、正規化、ステミングなど)によって処理されます。

[fields.title.Text]
indexed = true       # このフィールドを検索用にインデックスするかどうか
stored = true        # 取得用に元の値を保存するかどうか
term_vectors = false # タームの位置を保存するかどうか(フレーズクエリ、ハイライト用)
オプションデフォルト説明
indexedbooltrueこのフィールドの検索を有効にする
storedbooltrue結果に返せるよう元の値を保存する
term_vectorsbooltrueフレーズクエリ、ハイライト、More-Like-This 用にタームの位置を保存する

Integer

64ビット符号付き整数フィールド。範囲クエリと完全一致をサポートします。

[fields.year.Integer]
indexed = true
stored = true
オプションデフォルト説明
indexedbooltrue範囲クエリおよび完全一致クエリを有効にする
storedbooltrue元の値を保存する

Float

64ビット浮動小数点フィールド。範囲クエリをサポートします。

[fields.rating.Float]
indexed = true
stored = true
オプションデフォルト説明
indexedbooltrue範囲クエリを有効にする
storedbooltrue元の値を保存する

Boolean

ブーリアンフィールド(true / false)。

[fields.published.Boolean]
indexed = true
stored = true
オプションデフォルト説明
indexedbooltrueブーリアン値によるフィルタリングを有効にする
storedbooltrue元の値を保存する

DateTime

UTC タイムスタンプフィールド。範囲クエリをサポートします。

[fields.created_at.DateTime]
indexed = true
stored = true
オプションデフォルト説明
indexedbooltrue日時の範囲クエリを有効にする
storedbooltrue元の値を保存する

Geo

地理座標フィールド(緯度/経度)。半径クエリおよびバウンディングボックスクエリをサポートします。

[fields.location.Geo]
indexed = true
stored = true
オプションデフォルト説明
indexedbooltrueGeo クエリ(半径、バウンディングボックス)を有効にする
storedbooltrue元の値を保存する

Bytes

生バイナリデータフィールド。インデックスされず、保存のみです。

[fields.thumbnail.Bytes]
stored = true
オプションデフォルト説明
storedbooltrueバイナリデータを保存する

Vector フィールド

Vector フィールドは近似最近傍探索(ANN: Approximate Nearest Neighbor)用にインデックスされます。dimension(各ベクトルの長さ)と distance メトリクスの指定が必要です。

Hnsw

HNSW(Hierarchical Navigable Small World)グラフインデックス。ほとんどのユースケースに最適で、速度と再現率(Recall)のバランスに優れています。

[fields.body_vec.Hnsw]
dimension = 384
distance = "Cosine"
m = 16
ef_construction = 200
base_weight = 1.0
オプションデフォルト説明
dimensioninteger128ベクトルの次元数(Embedding モデルの出力と一致させる必要あり)
distancestring"Cosine"距離メトリクス(距離メトリクスを参照)
minteger16ノードあたりの最大双方向接続数。大きいほど再現率が向上するがメモリ使用量が増加
ef_constructioninteger200インデックス構築時の探索幅。大きいほど品質が向上するが構築が遅くなる
base_weightfloat1.0ハイブリッド検索のスコア融合における重み
quantizerobjectなしオプションの量子化方式(量子化を参照)

チューニングガイドライン:

  • m: 12〜48 が一般的です。高次元ベクトルには大きい値を使用してください。
  • ef_construction: 100〜500。大きい値ほどグラフの品質が向上しますが、構築時間が増加します。
  • dimension: Embedding モデルの出力次元と正確に一致させる必要があります(例: all-MiniLM-L6-v2 は 384、BERT-base は 768、text-embedding-3-small は 1536)。

Flat

ブルートフォース線形スキャンインデックス。近似を行わず正確な結果を返します。小規模データセット(10,000 ベクトル未満)に最適です。

[fields.embedding.Flat]
dimension = 384
distance = "Cosine"
base_weight = 1.0
オプションデフォルト説明
dimensioninteger128ベクトルの次元数
distancestring"Cosine"距離メトリクス(距離メトリクスを参照)
base_weightfloat1.0ハイブリッド検索のスコア融合における重み
quantizerobjectなしオプションの量子化方式(量子化を参照)

Ivf

IVF(Inverted File Index)。ベクトルをクラスタリングし、クラスタのサブセットのみを検索します。大規模データセットに適しています。

[fields.embedding.Ivf]
dimension = 384
distance = "Cosine"
n_clusters = 100
n_probe = 1
base_weight = 1.0
オプションデフォルト説明
dimensioninteger(必須)ベクトルの次元数
distancestring"Cosine"距離メトリクス(距離メトリクスを参照)
n_clustersinteger100クラスタ数。多いほど細かい分割が可能
n_probeinteger1クエリ時に検索するクラスタ数。大きいほど再現率が向上するが遅くなる
base_weightfloat1.0ハイブリッド検索のスコア融合における重み
quantizerobjectなしオプションの量子化方式(量子化を参照)

注意: Hnsw および Flat とは異なり、Ivf の dimension フィールドは必須であり、デフォルト値はありません。

チューニングガイドライン:

  • n_clusters: 一般的な経験則は sqrt(N)(N はベクトルの総数)です。
  • n_probe: 1 から始めて、再現率が許容範囲になるまで増やしてください。一般的な範囲は 1〜20 です。

距離メトリクス

Vector フィールドの distance オプションは以下の値を受け付けます:

説明使用場面
"Cosine"コサイン距離(1 - コサイン類似度)。デフォルト。正規化されたテキスト/画像 Embedding
"Euclidean"L2(ユークリッド)距離空間データ、正規化されていないベクトル
"Manhattan"L1(マンハッタン)距離スパースな特徴ベクトル
"DotProduct"内積(大きいほど類似度が高い)大きさが重要な正規化済みベクトル
"Angular"角度距離コサインに似ているが角度に基づく

ほとんどの Embedding モデル(BERT、Sentence Transformers、OpenAI など)では "Cosine" が適切な選択です。

量子化

Vector フィールドはオプションで量子化(Quantization)をサポートしており、精度を若干犠牲にしてメモリ使用量を削減できます。quantizer オプションを TOML テーブルとして指定します。

なし(デフォルト)

量子化なし — 32ビット浮動小数点のフル精度。

[fields.embedding.Hnsw]
dimension = 384
distance = "Cosine"
# quantizer を省略(量子化なし)

Scalar 8-bit

各 float32 コンポーネントを uint8 に圧縮します(約4倍のメモリ削減)。

[fields.embedding.Hnsw]
dimension = 384
distance = "Cosine"
quantizer = "Scalar8Bit"

Product Quantization

ベクトルをサブベクトルに分割し、それぞれを独立に量子化します。

[fields.embedding.Hnsw]
dimension = 384
distance = "Cosine"

[fields.embedding.Hnsw.quantizer.ProductQuantization]
subvector_count = 48
オプション説明
subvector_countintegerサブベクトルの数。dimension を均等に割り切れる必要があります。

完全な例

全文検索のみ

Lexical 検索のみのシンプルなブログ記事インデックス:

default_fields = ["title", "body"]

[fields.title.Text]
indexed = true
stored = true
term_vectors = false

[fields.body.Text]
indexed = true
stored = true
term_vectors = false

[fields.category.Text]
indexed = true
stored = true
term_vectors = false

[fields.published_at.DateTime]
indexed = true
stored = true

Vector 検索のみ

セマンティック類似検索用の Vector のみのインデックス:

[fields.embedding.Hnsw]
dimension = 768
distance = "Cosine"
m = 16
ef_construction = 200

ハイブリッド検索(Lexical + Vector)

Lexical 検索と Vector 検索を組み合わせた両方の長所を活かす検索:

default_fields = ["title", "body"]

[fields.title.Text]
indexed = true
stored = true
term_vectors = false

[fields.body.Text]
indexed = true
stored = true
term_vectors = true

[fields.category.Text]
indexed = true
stored = true
term_vectors = false

[fields.body_vec.Hnsw]
dimension = 384
distance = "Cosine"
m = 16
ef_construction = 200

ヒント: 1つのフィールドが Lexical と Vector の両方を兼ねることはできません。別々のフィールド(例: テキスト用の body、Embedding 用の body_vec)を使用し、どちらも同じソースコンテンツにマッピングしてください。

E コマースの商品インデックス

複数のフィールド型を組み合わせたより複雑なスキーマ:

default_fields = ["name", "description"]

[fields.name.Text]
indexed = true
stored = true
term_vectors = false

[fields.description.Text]
indexed = true
stored = true
term_vectors = true

[fields.price.Float]
indexed = true
stored = true

[fields.in_stock.Boolean]
indexed = true
stored = true

[fields.created_at.DateTime]
indexed = true
stored = true

[fields.location.Geo]
indexed = true
stored = true

[fields.description_vec.Hnsw]
dimension = 384
distance = "Cosine"

スキーマの生成

CLI を使用して対話的にスキーマ TOML ファイルを生成できます:

laurus create schema
laurus create schema --output my_schema.toml

詳細は create schema を参照してください。

スキーマの使用

スキーマファイルが用意できたら、そこからインデックスを作成します:

laurus create index --schema schema.toml

または Rust でプログラム的に読み込みます:

#![allow(unused)]
fn main() {
use laurus::Schema;

let toml_str = std::fs::read_to_string("schema.toml")?;
let schema: Schema = toml::from_str(&toml_str)?;
}

REPL(対話モード)

REPL は、毎回 laurus コマンドをフルで入力することなく、インデックスを操作できる対話型セッションを提供します。

REPL の起動

laurus --index-dir ./my_index repl
Laurus REPL (type 'help' for commands, 'quit' to exit)
laurus>

REPL は起動時にインデックスを開き、セッション中ロードされた状態を維持します。

利用可能なコマンド

コマンドは CLI と同じ <操作> <リソース> の順序に従います。

コマンド説明
search <query>インデックスを検索
add field <name> <json>スキーマにフィールドを追加
add doc <id> <json>ドキュメントを追加
get statsインデックスの統計情報を表示
get schema現在のスキーマを表示
get doc <id>ID でドキュメントを取得
delete field <name>スキーマからフィールドを削除
delete doc <id>ID でドキュメントを削除
commit保留中の変更をコミット
help利用可能なコマンドを表示
quit / exitREPL を終了

使用例

検索

laurus> search body:rust
╭──────┬────────┬────────────────────────────────────╮
│ ID   │ Score  │ Fields                             │
├──────┼────────┼────────────────────────────────────┤
│ doc1 │ 0.8532 │ body: Rust is a systems..., title… │
╰──────┴────────┴────────────────────────────────────╯

フィールドの管理

laurus> add field category {"Text": {"indexed": true, "stored": true}}
Field 'category' added.
laurus> delete field category
Field 'category' deleted.

ドキュメントの追加とコミット

laurus> add doc doc4 {"title":"New Document","body":"Some content here."}
Document 'doc4' added.
laurus> commit
Changes committed.

情報の取得

laurus> get stats
Document count: 3

laurus> get schema
{
  "fields": { ... },
  "default_fields": ["title", "body"]
}

laurus> get doc doc4
╭──────┬───────────────────────────────────────────────╮
│ ID   │ Fields                                        │
├──────┼───────────────────────────────────────────────┤
│ doc4 │ body: Some content here., title: New Document │
╰──────┴───────────────────────────────────────────────╯

ドキュメントの削除

laurus> delete doc doc4
Document 'doc4' deleted.
laurus> commit
Changes committed.

機能

  • 行編集 — 矢印キー、Home/End キー、および標準的な readline ショートカット
  • 履歴 — 上下矢印キーで以前のコマンドを呼び出し
  • Ctrl+C / Ctrl+D — REPL を正常に終了

サーバー概要

laurus-server クレートは、Laurus 検索エンジン用の gRPC サーバーとオプションの HTTP/JSON ゲートウェイを提供します。エンジンをメモリに常駐させることで、コマンド実行ごとの起動オーバーヘッドを排除します。

機能

  • 永続エンジン – インデックスはリクエスト間で開いたまま維持され、呼び出しごとの WAL リプレイが不要
  • フル gRPC API – インデックス管理、ドキュメント CRUD、コミット、検索(単発 + ストリーミング)
  • HTTP ゲートウェイ – gRPC と併用可能なオプションの HTTP/JSON ゲートウェイで REST スタイルのアクセスを提供
  • ヘルスチェック – ロードバランサーやオーケストレーター向けの標準ヘルスチェックエンドポイント
  • グレースフルシャットダウン – Ctrl+C / SIGINT で保留中の変更を自動的にコミット
  • TOML 設定 – オプションの設定ファイルと CLI・環境変数によるオーバーライド

アーキテクチャ

graph LR
    subgraph "laurus-server"
        GW["HTTP Gateway\n(axum)"]
        GRPC["gRPC Server\n(tonic)"]
        ENG["Engine\n(Arc&lt;RwLock&gt;)"]
    end

    Client1["HTTP Client"] --> GW
    Client2["gRPC Client"] --> GRPC
    GW --> GRPC
    GRPC --> ENG

gRPC サーバーは常に起動します。HTTP ゲートウェイはオプションで、HTTP/JSON リクエストを内部的に gRPC サーバーへプロキシします。

クイックスタート

# デフォルト設定で起動(gRPC ポート 50051)
laurus serve

# HTTP ゲートウェイ付きで起動
laurus serve --http-port 8080

# 設定ファイルを指定して起動
laurus serve --config config.toml

セクション

gRPC サーバーをはじめる

サーバーの起動

gRPC サーバーは laurus CLI の serve サブコマンドで起動します。

laurus serve [OPTIONS]

オプション

オプション短縮形環境変数デフォルト説明
--config <PATH>-cLAURUS_CONFIGTOML 設定ファイルのパス
--host <HOST>-HLAURUS_HOST0.0.0.0リッスンアドレス
--port <PORT>-pLAURUS_PORT50051リッスンポート
--http-port <PORT>LAURUS_HTTP_PORTHTTP ゲートウェイポート(設定すると HTTP ゲートウェイが有効化)

ログの詳細度は標準の RUST_LOG 環境変数で制御します(デフォルト: info)。 RUST_LOG=laurus=debug,tonic=warn のようなフィルタディレクティブの詳細は env_logger の構文を参照してください。

グローバルオプション --index-dir(環境変数: LAURUS_INDEX_DIR)でインデックスデータのディレクトリを指定します。

# CLI 引数を使用
laurus --index-dir ./my_index serve --port 8080

# 環境変数を使用
export LAURUS_INDEX_DIR=./my_index
export LAURUS_PORT=8080
export RUST_LOG=debug
laurus serve

起動時の動作

起動時、サーバーは設定されたデータディレクトリにある既存のインデックスを開こうとします。インデックスが存在しない場合、サーバーはインデックスなしで起動します。後から CreateIndex RPC でインデックスを作成できます。

設定

コマンドラインオプションの代わりに(または併用して)TOML 設定ファイルを使用できます。詳細は設定を参照してください。

laurus serve --config config.toml

HTTP ゲートウェイ

--http-port を設定すると、gRPC サーバーと並行して HTTP/JSON ゲートウェイが起動します。エンドポイントの詳細と使用例は HTTP ゲートウェイを参照してください。

laurus serve --http-port 8080

グレースフルシャットダウン

サーバーがシャットダウンシグナル(Ctrl+C / SIGINT)を受信すると、自動的に以下を実行します。

  1. 新しい接続の受け付けを停止
  2. インデックスへの保留中の変更をコミット
  3. 正常に終了

gRPC での接続

任意の gRPC クライアントでサーバーに接続できます。簡易テストには grpcurl が便利です。

# ヘルスチェック
grpcurl -plaintext localhost:50051 laurus.v1.HealthService/Check

# インデックスの作成
grpcurl -plaintext -d '{
  "schema": {
    "fields": {
      "title": {"text": {"indexed": true, "stored": true, "term_vectors": true}},
      "body": {"text": {"indexed": true, "stored": true, "term_vectors": true}}
    },
    "default_fields": ["title", "body"]
  }
}' localhost:50051 laurus.v1.IndexService/CreateIndex

# ドキュメントの追加
grpcurl -plaintext -d '{
  "id": "doc1",
  "document": {
    "fields": {
      "title": {"text_value": "Hello World"},
      "body": {"text_value": "This is a test document."}
    }
  }
}' localhost:50051 laurus.v1.DocumentService/AddDocument

# コミット
grpcurl -plaintext localhost:50051 laurus.v1.DocumentService/Commit

# 検索
grpcurl -plaintext -d '{"query": "body:test", "limit": 10}' \
  localhost:50051 laurus.v1.SearchService/Search

詳細は gRPC API リファレンスを参照してください。HTTP Gateway を使ったステップバイステップの操作ガイドはハンズオンチュートリアルを参照してください。

ハンズオンチュートリアル

このチュートリアルでは、laurus-server を使った一連のワークフローを体験します。サーバーの起動、インデックスの作成、ドキュメントの登録、検索、更新、削除を順を追って説明します。すべての操作は HTTP Gateway 経由の curl コマンドで行います。

前提条件

  • laurus CLI がインストール済み(インストール を参照)
  • curl が利用可能

Step 1: サーバーの起動

HTTP Gateway を有効にして laurus-server を起動します:

laurus --index-dir /tmp/laurus/tutorial serve --port 50051 --http-port 8080

gRPC サーバー(ポート 50051)と HTTP Gateway(ポート 8080)が起動したことを示すログが表示されます。

サーバーが正常に動作しているか確認します:

curl http://localhost:8080/v1/health

期待されるレスポンス:

{"status":"SERVING_STATUS_SERVING"}

Step 2: インデックスの作成

Lexical 検索用のテキストフィールドと Vector 検索用のベクトルフィールドを含むスキーマでインデックスを作成します。この例ではカスタムアナライザーエンベッダー定義、フィールドごとの設定を示しています:

curl -X POST http://localhost:8080/v1/index \
  -H 'Content-Type: application/json' \
  -d '{
    "schema": {
      "analyzers": {
        "body_analyzer": {
          "char_filters": [{"type": "unicode_normalization", "form": "nfkc"}],
          "tokenizer": {"type": "regex"},
          "token_filters": [
            {"type": "lowercase"},
            {"type": "stop", "words": ["the", "a", "an", "is", "it"]}
          ]
        }
      },
      "embedders": {
        "my_embedder": {"type": "precomputed"}
      },
      "fields": {
        "title": {"text": {"indexed": true, "stored": true, "term_vectors": false, "analyzer": "standard"}},
        "body": {"text": {"indexed": true, "stored": true, "term_vectors": false, "analyzer": "body_analyzer"}},
        "category": {"text": {"indexed": true, "stored": true, "term_vectors": false, "analyzer": "keyword"}},
        "embedding": {"hnsw": {"dimension": 4, "distance": "DISTANCE_METRIC_COSINE", "m": 16, "ef_construction": 200, "embedder": "my_embedder"}}
      },
      "default_fields": ["title", "body"]
    }
  }'

3 つのテキストフィールドと 1 つのベクトルフィールドを持つインデックスが作成されます:

  • title — 組み込みの standard アナライザー(トークン化+小文字化)を使用。
  • bodyanalyzers セクションで定義したカスタム body_analyzer(NFKC 正規化+正規表現トークナイザー+小文字化+カスタムストップワード)を使用。
  • categorykeyword アナライザー(値全体を単一トークンとして扱い、完全一致用)を使用。
  • embedding — HNSW ベクトルインデックス、4 次元、コサイン距離。embedders で定義した my_embedder を使用。このチュートリアルでは precomputed(外部で事前計算したベクトル)を使用。本番環境では、使用する埋め込みモデルに合わせた次元数(例: 384 や 768)を指定してください。

default_fields を設定することで、フィールド指定なしのクエリは titlebody の両方を検索します。

組み込みアナライザー

standard, keyword, english, japanese, simple, noop。省略時はエンジンのデフォルト(standard)が使用されます。

カスタムアナライザーのコンポーネント

以下のコンポーネントを組み合わせてカスタムアナライザーを構成できます:

  • トークナイザー: whitespace, unicode_word, regex, ngram, lindera, whole
  • 文字フィルター: unicode_normalization, pattern_replace, mapping, japanese_iteration_mark
  • トークンフィルター: lowercase, stop, stem, boost, limit, strip, remove_empty, flatten_graph

エンベッダー

embedders セクションでベクトルの生成方法を定義します。各ベクトルフィールドは embedder オプションでエンベッダーを名前で参照できます。利用可能なタイプ:

  • precomputed — ベクトルは外部で事前計算して供給(自動埋め込みなし)。
  • candle_bert — Candle によるローカル BERT モデル。パラメータ: model(HuggingFace モデルID)。embeddings-candle フィーチャが必要。
  • candle_clip — ローカル CLIP マルチモーダルモデル。パラメータ: model(HuggingFace モデルID)。embeddings-multimodal フィーチャが必要。
  • openai — OpenAI API。パラメータ: model(例: "text-embedding-3-small")。embeddings-openai フィーチャと OPENAI_API_KEY 環境変数が必要。

BERT エンベッダーの例(embeddings-candle フィーチャが必要):

{
  "embedders": {
    "bert": {"type": "candle_bert", "model": "sentence-transformers/all-MiniLM-L6-v2"}
  },
  "fields": {
    "embedding": {"hnsw": {"dimension": 384, "embedder": "bert"}}
  }
}

インデックスが作成されたことを確認します:

curl http://localhost:8080/v1/index

期待されるレスポンス:

{"document_count":0,"vector_fields":{}}

Step 3: ドキュメントの登録

ドキュメントをインデックスに追加します。PUT を使って ID 指定でドキュメントを登録します。各ドキュメントにはテキストフィールドと embedding ベクトルが含まれます(本番環境では、これらのベクトルは埋め込みモデルから生成されます):

curl -X PUT http://localhost:8080/v1/documents/doc001 \
  -H 'Content-Type: application/json' \
  -d '{
    "document": {
      "fields": {
        "title": "Introduction to Rust Programming",
        "body": "Rust is a modern systems programming language that focuses on safety, speed, and concurrency.",
        "category": "programming",
        "embedding": [0.9, 0.1, 0.2, 0.0]
      }
    }
  }'
curl -X PUT http://localhost:8080/v1/documents/doc002 \
  -H 'Content-Type: application/json' \
  -d '{
    "document": {
      "fields": {
        "title": "Web Development with Rust",
        "body": "Building web applications with Rust has become increasingly popular. Frameworks like Actix and Rocket make it easy to create fast and secure web services.",
        "category": "web-development",
        "embedding": [0.7, 0.3, 0.5, 0.1]
      }
    }
  }'
curl -X PUT http://localhost:8080/v1/documents/doc003 \
  -H 'Content-Type: application/json' \
  -d '{
    "document": {
      "fields": {
        "title": "Python for Data Science",
        "body": "Python is the most popular language for data science and machine learning. Libraries like NumPy and Pandas provide powerful tools for data analysis.",
        "category": "data-science",
        "embedding": [0.1, 0.8, 0.1, 0.9]
      }
    }
  }'

ベクトルフィールドは数値の JSON 配列として指定します。配列の長さはスキーマで設定した dimension(このチュートリアルでは 4)と一致する必要があります。

Step 4: 変更のコミット

ドキュメントはコミットするまで検索対象になりません。変更をコミットします:

curl -X POST http://localhost:8080/v1/commit

Step 5: ドキュメントの検索

基本的な検索

“rust” を含むドキュメントを検索します:

curl -X POST http://localhost:8080/v1/search \
  -H 'Content-Type: application/json' \
  -d '{"query": "rust", "limit": 10}'

デフォルトフィールド(titlebody)が検索されます。doc001doc002 が返されます。

フィールド指定検索

title フィールドのみを検索します:

curl -X POST http://localhost:8080/v1/search \
  -H 'Content-Type: application/json' \
  -d '{"query": "title:python", "limit": 10}'

doc003 のみが返されます。

カテゴリ検索

curl -X POST http://localhost:8080/v1/search \
  -H 'Content-Type: application/json' \
  -d '{"query": "category:programming", "limit": 10}'

doc001 のみが返されます。

ブーリアンクエリ

ANDORNOT で条件を組み合わせます:

curl -X POST http://localhost:8080/v1/search \
  -H 'Content-Type: application/json' \
  -d '{"query": "rust AND web", "limit": 10}'

“rust” と “web” の両方を含む doc002 のみが返されます。

フィールドブースト

title フィールドのスコアを引き上げて、タイトルの一致を優先します:

curl -X POST http://localhost:8080/v1/search \
  -H 'Content-Type: application/json' \
  -d '{
    "query": "rust",
    "limit": 10,
    "field_boosts": {"title": 2.0}
  }'

ベクトル検索

ベクトルの類似度で検索します。query_vectors にクエリベクトルを指定し、検索対象のフィールドを指定します:

curl -X POST http://localhost:8080/v1/search \
  -H 'Content-Type: application/json' \
  -d '{
    "query_vectors": [
      {
        "vector": [0.85, 0.15, 0.2, 0.05],
        "fields": ["embedding"]
      }
    ],
    "limit": 10
  }'

embedding ベクトルがクエリベクトルに最も近いドキュメントが返されます。doc001 が最上位にランクされます(最も類似したベクトル)。

ハイブリッド検索

Lexical 検索と Vector 検索を組み合わせて、より良い結果を得ます。fusion パラメータで両方のスコアの統合方法を制御します:

curl -X POST http://localhost:8080/v1/search \
  -H 'Content-Type: application/json' \
  -d '{
    "query": "rust",
    "query_vectors": [
      {
        "vector": [0.85, 0.15, 0.2, 0.05],
        "fields": ["embedding"]
      }
    ],
    "fusion": {"rrf": {"k": 60.0}},
    "limit": 10
  }'

Reciprocal Rank Fusion(RRF)を使って Lexical 検索と Vector 検索の結果を統合します。重み付き和による統合も可能です:

curl -X POST http://localhost:8080/v1/search \
  -H 'Content-Type: application/json' \
  -d '{
    "query": "programming",
    "query_vectors": [
      {
        "vector": [0.85, 0.15, 0.2, 0.05],
        "fields": ["embedding"]
      }
    ],
    "fusion": {"weighted_sum": {"lexical_weight": 0.3, "vector_weight": 0.7}},
    "limit": 10
  }'

Step 6: ドキュメントの取得

ID を指定して特定のドキュメントを取得します:

curl http://localhost:8080/v1/documents/doc001

期待されるレスポンス(ベクトルフィールドも含まれます):

{
  "documents": [
    {
      "fields": {
        "title": "Introduction to Rust Programming",
        "body": "Rust is a modern systems programming language that focuses on safety, speed, and concurrency.",
        "category": "programming",
        "embedding": [0.9, 0.1, 0.2, 0.0]
      }
    }
  ]
}

Step 7: ドキュメントの更新

同じ ID で PUT を実行するとドキュメント全体が置き換わります:

curl -X PUT http://localhost:8080/v1/documents/doc001 \
  -H 'Content-Type: application/json' \
  -d '{
    "document": {
      "fields": {
        "title": "Introduction to Rust Programming",
        "body": "Rust is a modern systems programming language that focuses on safety, speed, and concurrency. It provides memory safety without garbage collection.",
        "category": "programming",
        "embedding": [0.9, 0.1, 0.2, 0.0]
      }
    }
  }'

コミットして確認します:

curl -X POST http://localhost:8080/v1/commit
curl http://localhost:8080/v1/documents/doc001

更新された body の内容が反映されています。

Step 8: ドキュメントの削除

ID を指定してドキュメントを削除します:

curl -X DELETE http://localhost:8080/v1/documents/doc003

コミットして反映させます:

curl -X POST http://localhost:8080/v1/commit

ドキュメントが削除されたことを確認します:

curl http://localhost:8080/v1/documents/doc003

期待されるレスポンス:

{"documents":[]}

検索結果にも表示されなくなります:

curl -X POST http://localhost:8080/v1/search \
  -H 'Content-Type: application/json' \
  -d '{"query": "python", "limit": 10}'

結果は返されません。

期待されるレスポンス:

{"results":[]}

Step 9: インデックス統計の確認

現在のインデックス統計を確認します:

curl http://localhost:8080/v1/index

document_count は削除後の残りのドキュメント数を反映しています。

Step 10: クリーンアップ

Ctrl+C でサーバーを停止します。サーバーはグレースフルシャットダウンを行い、保留中の変更をコミットしてから終了します。

チュートリアルで作成したデータを削除します:

rm -rf /tmp/laurus/tutorial

さらに進んだ使い方: 実際の埋め込みモデルの利用

上記のチュートリアルでは簡略化のために precomputed ベクトルを使用しました。本番環境では、埋め込みモデルを使ってテキストを自動的にベクトルに変換するのが一般的です。ここでは BERT ベースのエンベッダーの設定方法を示します。

前提条件

embeddings-candle フィーチャを有効にして laurus をビルドします:

cargo build --release --features embeddings-candle

BERT エンベッダーを使ったスキーマ

インデックスを作成します:

curl -X POST http://localhost:8080/v1/index \
  -H 'Content-Type: application/json' \
  -d '{
    "schema": {
      "embedders": {
        "bert": {
          "type": "candle_bert",
          "model": "sentence-transformers/all-MiniLM-L6-v2"
        }
      },
      "fields": {
        "title": {"text": {"indexed": true, "stored": true, "analyzer": "standard"}},
        "body": {"text": {"indexed": true, "stored": true, "analyzer": "standard"}},
        "embedding": {"hnsw": {"dimension": 384, "distance": "DISTANCE_METRIC_COSINE", "m": 16, "ef_construction": 200, "embedder": "bert"}}
      },
      "default_fields": ["title", "body"]
    }
  }'

モデルは初回使用時に HuggingFace Hub から自動ダウンロードされます。dimension(384)はモデルの出力次元数と一致させる必要があります。

ドキュメントを追加します。embedding フィールドにはテキストを渡すだけで、エンベッダーが自動的にベクトルに変換します:

curl -X PUT http://localhost:8080/v1/documents/doc001 \
  -H 'Content-Type: application/json' \
  -d '{
    "document": {
      "fields": {
        "title": "Introduction to Rust Programming",
        "body": "Rust is a modern systems programming language.",
        "embedding": "Rust is a modern systems programming language."
      }
    }
  }'
curl -X PUT http://localhost:8080/v1/documents/doc002 \
  -H 'Content-Type: application/json' \
  -d '{
    "document": {
      "fields": {
        "title": "Web Development with Rust",
        "body": "Building web applications with Rust using Actix and Rocket.",
        "embedding": "Building web applications with Rust using Actix and Rocket."
      }
    }
  }'

コミットします:

curl -X POST http://localhost:8080/v1/commit

テキストクエリで lexical 検索、ベクトルクエリでセマンティック検索を同時に行います。検索時もテキストからベクトルへの変換が自動的に行われます:

curl -X POST http://localhost:8080/v1/search \
  -H 'Content-Type: application/json' \
  -d '{
    "query": "systems programming",
    "query_vectors": [
      {
        "vector": "systems programming language",
        "fields": ["embedding"]
      }
    ],
    "fusion": {"rrf": {"k": 60.0}},
    "limit": 10
  }'

precomputed エンベッダーではベクトルを直接渡す必要がありますが、candle_bert のようなテキスト対応エンベッダーを使うと、インデックス時も検索時もテキストを直接渡せます。

OpenAI Embeddings の利用

OpenAI の Embedding API を使う場合は、OPENAI_API_KEY 環境変数を設定し、embeddings-openai フィーチャでビルドします:

cargo build --release --features embeddings-openai
export OPENAI_API_KEY="sk-..."

インデックスを作成します:

curl -X POST http://localhost:8080/v1/index \
  -H 'Content-Type: application/json' \
  -d '{
    "schema": {
      "embedders": {
        "openai": {
          "type": "openai",
          "model": "text-embedding-3-small"
        }
      },
      "fields": {
        "title": {"text": {"indexed": true, "stored": true}},
        "embedding": {"hnsw": {"dimension": 1536, "distance": "DISTANCE_METRIC_COSINE", "embedder": "openai"}}
      },
      "default_fields": ["title"]
    }
  }'

text-embedding-3-small モデルは 1536 次元のベクトルを出力します。

利用可能な埋め込みモデル

タイプフィーチャフラグモデル例次元数
candle_bertembeddings-candlesentence-transformers/all-MiniLM-L6-v2384
candle_clipembeddings-multimodalopenai/clip-vit-base-patch32512
openaiembeddings-openaitext-embedding-3-small1536

次のステップ

設定

laurus-server は CLI 引数、環境変数、TOML 設定ファイルで設定できます。

設定の優先順位

サーバーとインデックスの設定は以下の順序で解決されます(優先度が高い順)。

CLI 引数 > 環境変数 > 設定ファイル > デフォルト値

ログの詳細度は RUST_LOG 環境変数でのみ制御します(デフォルト: info)。

例:

# CLI 引数が環境変数と設定ファイルより優先される
LAURUS_PORT=4567 laurus serve --config config.toml --port 1234
# -> ポート 1234 でリッスン

# 環境変数が設定ファイルより優先される
LAURUS_PORT=4567 laurus serve --config config.toml
# -> ポート 4567 でリッスン

# CLI 引数も環境変数も未設定の場合、設定ファイルの値が使用される
laurus serve --config config.toml
# -> config.toml のポートを使用(未設定の場合はデフォルト 50051)

TOML 設定ファイル

フォーマット

[server]
host = "0.0.0.0"
port = 50051
http_port = 8080  # オプション: HTTP ゲートウェイを有効化

[index]
data_dir = "./laurus_index"

ログの詳細度は設定ファイルではなく、RUST_LOG 環境変数で制御します(デフォルト: info)。

フィールドリファレンス

[server] セクション

フィールドデフォルト説明
hostString"0.0.0.0"gRPC サーバーのリッスンアドレス
portInteger50051gRPC サーバーのリッスンポート
http_portIntegerHTTP ゲートウェイポート。設定すると gRPC と並行して HTTP/JSON ゲートウェイが起動

[index] セクション

フィールドデフォルト説明
data_dirString"./laurus_index"インデックスデータディレクトリのパス

環境変数

変数対応する設定説明
LAURUS_HOSTserver.hostリッスンアドレス
LAURUS_PORTserver.portgRPC リッスンポート
LAURUS_HTTP_PORTserver.http_portHTTP ゲートウェイポート
LAURUS_INDEX_DIRindex.data_dirインデックスデータディレクトリ
RUST_LOGログフィルタディレクティブ(例: info, debug, laurus=debug,tonic=warn
LAURUS_CONFIGTOML 設定ファイルのパス

CLI 引数

オプション短縮形デフォルト説明
--config <PATH>-cTOML 設定ファイルのパス
--host <HOST>-H0.0.0.0リッスンアドレス
--port <PORT>-p50051gRPC リッスンポート
--http-port <PORT>HTTP ゲートウェイポート
--index-dir <PATH>./laurus_indexインデックスデータディレクトリ(グローバルオプション)

よくある設定例

開発環境(gRPC のみ)

[server]
host = "127.0.0.1"
port = 50051

[index]
data_dir = "./dev_data"
RUST_LOG=debug laurus serve --config config.toml

本番環境(gRPC + HTTP ゲートウェイ)

[server]
host = "0.0.0.0"
port = 50051
http_port = 8080

[index]
data_dir = "/var/lib/laurus/data"

最小構成(環境変数のみ)

export LAURUS_INDEX_DIR=/var/lib/laurus/data
export LAURUS_PORT=50051
export LAURUS_HTTP_PORT=8080
export RUST_LOG=info
laurus serve

gRPC API リファレンス

すべてのサービスは laurus.v1 protobuf パッケージで定義されています。

サービス一覧

サービスRPC説明
HealthServiceCheckヘルスチェック
IndexServiceCreateIndex, GetIndex, GetSchema, AddField, DeleteFieldインデックスのライフサイクルとスキーマ
DocumentServicePutDocument, AddDocument, GetDocuments, DeleteDocuments, Commitドキュメント CRUD とコミット
SearchServiceSearch, SearchStream単発検索とストリーミング検索

HealthService

Check

サーバーの現在のサービング状態を返します。

rpc Check(HealthCheckRequest) returns (HealthCheckResponse);

レスポンスフィールド:

フィールド説明
statusServingStatusサーバーの準備が完了している場合は SERVING_STATUS_SERVING

IndexService

CreateIndex

指定されたスキーマで新しいインデックスを作成します。インデックスが既に開いている場合は ALREADY_EXISTS エラーを返します。

rpc CreateIndex(CreateIndexRequest) returns (CreateIndexResponse);

リクエストフィールド:

フィールド必須説明
schemaSchemaはいインデックスのスキーマ定義

Schema 構造:

message Schema {
  map<string, FieldOption> fields = 1;
  repeated string default_fields = 2;
  map<string, AnalyzerDefinition> analyzers = 3;
  map<string, EmbedderConfig> embedders = 4;
}
  • fields — フィールド名をキーとしたフィールド定義。
  • default_fields — クエリでフィールドを指定しない場合のデフォルト検索対象フィールド名。
  • analyzers — 名前をキーとしたカスタムアナライザーパイプライン。TextOption.analyzer で参照。
  • embedders — 名前をキーとしたエンベッダー設定。ベクトルフィールドオプション(HnswOption.embedder など)で参照。

AnalyzerDefinition:

message AnalyzerDefinition {
  repeated ComponentConfig char_filters = 1;
  ComponentConfig tokenizer = 2;
  repeated ComponentConfig token_filters = 3;
}

ComponentConfig(文字フィルター、トークナイザー、トークンフィルターに使用):

フィールド説明
typestringコンポーネントタイプ名(例: "whitespace", "lowercase", "unicode_normalization"
paramsmap<string, string>タイプ固有のパラメータ(文字列のキーと値のペア)

EmbedderConfig:

フィールド説明
typestringエンベッダータイプ名(例: "precomputed", "candle_bert", "openai"
paramsmap<string, string>タイプ固有のパラメータ(例: "model""sentence-transformers/all-MiniLM-L6-v2"

FieldOption は以下のフィールドタイプのいずれかを持つ oneof です。

Lexical フィールドVector フィールド
TextOption (indexed, stored, term_vectors, analyzer)HnswOption (dimension, distance, m, ef_construction, base_weight, quantizer, embedder)
IntegerOption (indexed, stored)FlatOption (dimension, distance, base_weight, quantizer, embedder)
FloatOption (indexed, stored)IvfOption (dimension, distance, n_clusters, n_probe, base_weight, quantizer, embedder)
BooleanOption (indexed, stored)
DateTimeOption (indexed, stored)
GeoOption (indexed, stored)
BytesOption (stored)

ベクトルフィールドオプションの embedder フィールドには、Schema.embedders で定義したエンベッダー名を指定します。設定すると、インデックス時にドキュメントのテキストフィールドからベクトルを自動生成します。事前計算済みのベクトルを直接供給する場合は空のままにします。

距離メトリクス: COSINE, EUCLIDEAN, MANHATTAN, DOT_PRODUCT, ANGULAR

量子化手法: NONE, SCALAR_8BIT, PRODUCT_QUANTIZATION

QuantizationConfig 構造:

フィールド説明
methodQuantizationMethod量子化手法(QUANTIZATION_METHOD_NONE, QUANTIZATION_METHOD_SCALAR_8BIT, または QUANTIZATION_METHOD_PRODUCT_QUANTIZATION
subvector_countuint32サブベクトルの数(methodPRODUCT_QUANTIZATION の場合のみ使用。dimension を均等に割り切れる値を指定)。

例:

{
  "schema": {
    "fields": {
      "title": {"text": {"indexed": true, "stored": true, "term_vectors": true}},
      "embedding": {"hnsw": {"dimension": 384, "distance": "DISTANCE_METRIC_COSINE", "m": 16, "ef_construction": 200}}
    },
    "default_fields": ["title"]
  }
}

GetIndex

インデックスの統計情報を取得します。

rpc GetIndex(GetIndexRequest) returns (GetIndexResponse);

レスポンスフィールド:

フィールド説明
document_countuint64インデックス内のドキュメント総数
vector_fieldsmap<string, VectorFieldStats>フィールドごとのベクトル統計情報

VectorFieldStats には vector_countdimension が含まれます。

GetSchema

現在のインデックススキーマを取得します。

rpc GetSchema(GetSchemaRequest) returns (GetSchemaResponse);

レスポンスフィールド:

フィールド説明
schemaSchemaインデックスのスキーマ

AddField

稼働中のインデックスにフィールドを動的に追加します。

rpc AddField(AddFieldRequest) returns (AddFieldResponse);
フィールド説明
namestringフィールド名
field_optionFieldOptionフィールド設定

レスポンス: 更新後の Schema を返します。

DeleteField

稼働中のインデックスからフィールドを動的に削除します。既にインデックスされたデータは残りますが、削除されたフィールドにはアクセスできなくなります。

rpc DeleteField(DeleteFieldRequest) returns (DeleteFieldResponse);

message DeleteFieldRequest {
  string name = 1;
}

message DeleteFieldResponse {
  Schema schema = 1;
}

リクエストフィールド:

フィールド必須説明
namestringはい削除するフィールド名

レスポンス: 更新後の Schema を返します。


DocumentService

PutDocument

ID を指定してドキュメントを挿入または置換します。同じ ID のドキュメントが既に存在する場合は置換されます。

rpc PutDocument(PutDocumentRequest) returns (PutDocumentResponse);

リクエストフィールド:

フィールド必須説明
idstringはい外部ドキュメント ID
documentDocumentはいドキュメントの内容

Document 構造:

message Document {
  map<string, Value> fields = 1;
}

Value は以下の型のいずれかを持つ oneof です。

Proto フィールド説明
Nullnull_valueNull 値
Booleanbool_valueブール値
Integerint64_value64 ビット整数
Floatfloat64_value64 ビット浮動小数点数
Texttext_valueUTF-8 文字列
Bytesbytes_valueバイト列
Vectorvector_valueVectorValue(浮動小数点数のリスト)
DateTimedatetime_valueUnix マイクロ秒(UTC)
Geogeo_valueGeoPoint(緯度、経度)

AddDocument

ドキュメントを追加します。PutDocument と異なり、同じ ID の既存ドキュメントを置換しません。複数のドキュメントが同じ ID を共有できます(チャンキングパターン)。

rpc AddDocument(AddDocumentRequest) returns (AddDocumentResponse);

リクエストフィールドは PutDocument と同じです。

GetDocuments

指定された外部 ID に一致するすべてのドキュメントを取得します。

rpc GetDocuments(GetDocumentsRequest) returns (GetDocumentsResponse);

リクエストフィールド:

フィールド必須説明
idstringはい外部ドキュメント ID

レスポンスフィールド:

フィールド説明
documentsrepeated Document一致するドキュメント

DeleteDocuments

指定された外部 ID に一致するすべてのドキュメントを削除します。

rpc DeleteDocuments(DeleteDocumentsRequest) returns (DeleteDocumentsResponse);

Commit

保留中の変更(追加および削除)をインデックスにコミットします。コミットされるまで、変更は検索に反映されません。

rpc Commit(CommitRequest) returns (CommitResponse);

SearchService

Search

検索クエリを実行し、結果を単一のレスポンスとして返します。

rpc Search(SearchRequest) returns (SearchResponse);

レスポンスフィールド:

フィールド説明
resultsrepeated SearchResult関連度順の検索結果
total_hitsuint64マッチするドキュメントの総数(limit/offset 適用前)

SearchStream

検索クエリを実行し、結果を 1 件ずつストリーミングで返します。

rpc SearchStream(SearchRequest) returns (stream SearchResult);

SearchRequest フィールド

フィールド必須説明
querystringいいえQuery DSL による Lexical 検索クエリ
query_vectorsrepeated QueryVectorいいえベクトル検索クエリ
limituint32いいえ最大結果件数(デフォルト: エンジンのデフォルト値)
offsetuint32いいえスキップする結果件数
fusionFusionAlgorithmいいえハイブリッド検索の Fusion アルゴリズム
lexical_paramsLexicalParamsいいえLexical 検索パラメータ
vector_paramsVectorParamsいいえベクトル検索パラメータ
field_boostsmap<string, float>いいえフィールドごとのスコアブースト

query または query_vectors のいずれか 1 つ以上を指定する必要があります。

QueryVector

フィールド説明
vectorrepeated floatクエリベクトル
weightfloatこのベクトルの重み(デフォルト: 1.0)
fieldsrepeated string対象のベクトルフィールド(空の場合は全フィールド)

FusionAlgorithm

以下の 2 つのオプションを持つ oneof です。

  • RRF (Reciprocal Rank Fusion): k パラメータ(デフォルト: 60)
  • WeightedSum: lexical_weightvector_weight

LexicalParams

フィールド説明
min_scorefloat最小スコア閾値
timeout_msuint64検索タイムアウト(ミリ秒)
parallelbool並列検索を有効化
sort_bySortSpecスコアの代わりにフィールドでソート

SortSpec

フィールド説明
fieldstringソート対象のフィールド名。空文字列はスコアでソートすることを意味する
orderSortOrderSORT_ORDER_ASC(昇順)または SORT_ORDER_DESC(降順)

VectorParams

フィールド説明
fieldsrepeated string対象のベクトルフィールド
score_modeVectorScoreModeWEIGHTED_SUM, MAX_SIM, または LATE_INTERACTION
overfetchfloatオーバーフェッチ係数(デフォルト: 2.0)
min_scorefloat最小スコア閾値

SearchResult

フィールド説明
idstring外部ドキュメント ID
scorefloat関連度スコア
documentDocumentドキュメントの内容

{
  "query": "body:rust",
  "query_vectors": [
    {"vector": [0.1, 0.2, 0.3], "weight": 1.0}
  ],
  "limit": 10,
  "fusion": {
    "rrf": {"k": 60}
  },
  "field_boosts": {
    "title": 2.0
  }
}

エラーハンドリング

gRPC エラーは標準の Status コードとして返されます。

Laurus エラーgRPC ステータス発生条件
Schema / Query / Field / JSONINVALID_ARGUMENT不正なリクエストまたはスキーマ
インデックス未オープンFAILED_PRECONDITIONCreateIndex の前に RPC が呼び出された場合
インデックスが既に存在ALREADY_EXISTSCreateIndex が 2 回呼び出された場合
未実装UNIMPLEMENTEDまだサポートされていない機能
内部エラーINTERNALI/O、ストレージ、または予期しないエラー

HTTP ゲートウェイ

HTTP ゲートウェイは Laurus 検索エンジンへの RESTful HTTP/JSON インターフェースを提供します。gRPC サーバーと並行して動作し、リクエストを内部的にプロキシします。

Client (HTTP/JSON) --> HTTP Gateway (axum) --> gRPC Server (tonic) --> Engine

HTTP ゲートウェイの有効化

http_port を設定するとゲートウェイが起動します。

# CLI 引数で指定
laurus serve --http-port 8080

# 環境変数で指定
LAURUS_HTTP_PORT=8080 laurus serve

# 設定ファイルで指定
laurus serve --config config.toml
# ([server] セクションで http_port を設定)

http_port が未設定の場合、gRPC サーバーのみが起動します。

エンドポイント

メソッドパスgRPC メソッド説明
GET/v1/healthHealthService/Checkヘルスチェック
POST/v1/indexIndexService/CreateIndex新しいインデックスを作成
GET/v1/indexIndexService/GetIndexインデックスの統計情報を取得
GET/v1/schemaIndexService/GetSchemaインデックスのスキーマを取得
PUT/v1/documents/:idDocumentService/PutDocumentドキュメントの Upsert
POST/v1/documents/:idDocumentService/AddDocumentドキュメントの追加(チャンク)
GET/v1/documents/:idDocumentService/GetDocumentsID でドキュメントを取得
DELETE/v1/documents/:idDocumentService/DeleteDocumentsID でドキュメントを削除
POST/v1/commitDocumentService/Commit保留中の変更をコミット
POST/v1/schema/fieldsIndexService/AddFieldフィールドの追加
DELETE/v1/schema/fields/:nameIndexService/DeleteFieldフィールドの削除
POST/v1/searchSearchService/Search検索(単発)
POST/v1/search/streamSearchService/SearchStream検索(Server-Sent Events)

API の使用例

ヘルスチェック

curl http://localhost:8080/v1/health

インデックスの作成

curl -X POST http://localhost:8080/v1/index \
  -H 'Content-Type: application/json' \
  -d '{
    "schema": {
      "fields": {
        "title": {"text": {"indexed": true, "stored": true, "term_vectors": true}},
        "body": {"text": {"indexed": true, "stored": true, "term_vectors": true}}
      },
      "default_fields": ["title", "body"]
    }
  }'

インデックス統計情報の取得

curl http://localhost:8080/v1/index

スキーマの取得

curl http://localhost:8080/v1/schema

ドキュメントの Upsert(PUT)

ドキュメントが既に存在する場合は置換します。

curl -X PUT http://localhost:8080/v1/documents/doc1 \
  -H 'Content-Type: application/json' \
  -d '{
    "document": {
      "fields": {
        "title": "Hello World",
        "body": "This is a test document."
      }
    }
  }'

ドキュメントの追加(POST)

同じ ID の既存ドキュメントを置換せずに新しいチャンクを追加します。

curl -X POST http://localhost:8080/v1/documents/doc1 \
  -H 'Content-Type: application/json' \
  -d '{
    "document": {
      "fields": {
        "title": "Hello World",
        "body": "This is a test document."
      }
    }
  }'

ドキュメントの取得

curl http://localhost:8080/v1/documents/doc1

ドキュメントの削除

curl -X DELETE http://localhost:8080/v1/documents/doc1

コミット

curl -X POST http://localhost:8080/v1/commit

検索

curl -X POST http://localhost:8080/v1/search \
  -H 'Content-Type: application/json' \
  -d '{"query": "body:test", "limit": 10}'

フィールドブースト付き検索

curl -X POST http://localhost:8080/v1/search \
  -H 'Content-Type: application/json' \
  -d '{
    "query": "rust programming",
    "limit": 10,
    "field_boosts": {"title": 2.0}
  }'

ハイブリッド検索

curl -X POST http://localhost:8080/v1/search \
  -H 'Content-Type: application/json' \
  -d '{
    "query": "body:rust",
    "query_vectors": [{"vector": [0.1, 0.2, 0.3], "weight": 1.0}],
    "limit": 10,
    "fusion": {"rrf": {"k": 60}}
  }'

ストリーミング検索(SSE)

/v1/search/stream エンドポイントは Server-Sent Events(SSE)として結果を返します。各結果は個別のイベントとして送信されます。

curl -N -X POST http://localhost:8080/v1/search/stream \
  -H 'Content-Type: application/json' \
  -d '{"query": "body:test", "limit": 10}'

レスポンスは SSE イベントのストリームです。

data: {"id":"doc1","score":0.8532,"document":{...}}

data: {"id":"doc2","score":0.4210,"document":{...}}

リクエスト/レスポンス形式

すべてのリクエストおよびレスポンスボディは JSON を使用します。JSON の構造は gRPC の protobuf メッセージに対応しています。メッセージ定義の詳細は gRPC API リファレンスを参照してください。

MCP サーバー概要

laurus-mcp クレートは、Laurus 検索エンジン用の Model Context Protocol (MCP) サーバーを提供します。実行中の laurus-server インスタンスへの gRPC クライアントとして動作し、Claude などの AI アシスタントが標準 MCP stdio トランスポートを通じてドキュメントのインデックス登録や検索を行えるようにします。

機能

  • MCP stdio トランスポート — サブプロセスとして起動し、stdin/stdout 経由で AI クライアントと通信
  • gRPC クライアント — すべてのツール呼び出しを実行中の laurus-server インスタンスにプロキシ
  • 全 laurus 検索モード — Lexical(BM25)、Vector(HNSW/Flat/IVF)、ハイブリッド検索
  • 動的接続connect ツールで任意の laurus-server エンドポイントに接続可能
  • ドキュメントライフサイクル — MCP ツールを通じてドキュメントの追加・更新・削除・取得が可能

アーキテクチャ

graph LR
    subgraph "laurus-mcp"
        MCP["MCP Server\n(stdio)"]
    end

    AI["AI クライアント\n(Claude など)"] -->|"stdio (JSON-RPC)"| MCP
    MCP -->|"gRPC"| SRV["laurus-server\n(常駐)"]
    SRV --> Disk["ディスク上のインデックス"]

MCP サーバーは AI クライアントによって起動される子プロセスとして動作します。すべてのツール呼び出しを gRPC 経由で laurus-server インスタンスにプロキシします。laurus-server は MCP サーバーとは別途、事前に起動しておく必要があります。

クイックスタート

# ステップ 1: laurus-server を起動
laurus serve --grpc-port 50051

# ステップ 2: Claude Code で MCP サーバーを設定
claude mcp add laurus laurus mcp --endpoint http://localhost:50051

または手動で設定ファイルを編集:

{
  "mcpServers": {
    "laurus": {
      "command": "laurus",
      "args": ["mcp", "--endpoint", "http://localhost:50051"]
    }
  }
}

セクション

laurus-mcp をはじめる

前提条件

  • laurus CLI バイナリがインストール済み(cargo install laurus-cli
  • 実行中の laurus-server インスタンス(laurus-server はじめにを参照)
  • MCP をサポートする AI クライアント(Claude Desktop、Claude Code など)

設定

ステップ 1: laurus-server を起動

laurus serve --grpc-port 50051

ステップ 2: MCP クライアントの設定

Claude Code

CLI コマンドで追加する方法(推奨):

claude mcp add laurus laurus mcp --endpoint http://localhost:50051

または ~/.claude/settings.json を直接編集:

{
  "mcpServers": {
    "laurus": {
      "command": "laurus",
      "args": ["mcp", "--endpoint", "http://localhost:50051"]
    }
  }
}

Claude Desktop

macOS の場合は ~/Library/Application Support/Claude/claude_desktop_config.json、 Windows の場合は %APPDATA%\Claude\claude_desktop_config.json を編集:

{
  "mcpServers": {
    "laurus": {
      "command": "laurus",
      "args": ["mcp", "--endpoint", "http://localhost:50051"]
    }
  }
}

使用ワークフロー

ワークフロー 1: 既存のインデックスを使用する

CLI でインデックスを事前に作成してから MCP サーバーで検索します:

# ステップ 1: スキーマファイルを作成
cat > schema.toml << 'EOF'
[fields.title]
Text = { indexed = true, stored = true }

[fields.body]
Text = { indexed = true, stored = true }
EOF

# ステップ 2: サーバーを起動してインデックスを作成
laurus serve --grpc-port 50051 &
laurus create index --schema schema.toml

# ステップ 3: MCP サーバーを起動(Claude が自動的に起動)
laurus mcp --endpoint http://localhost:50051

ワークフロー 2: AI 主導のインデックス作成

インデックスを事前に作成せず MCP サーバーを起動し、AI にインデックスを作成させます:

# laurus-server を起動(インデックス不要)
laurus serve --grpc-port 50051

次に Claude に依頼します:

「ブログ記事用の検索インデックスを作成してください。タイトルと本文テキストで検索できるようにして、著者と公開日も保存したいです。」

Claude は connect ツールを呼び出して接続し、スキーマを設計して create_index を自動的に呼び出します。

ワークフロー 3: 実行時に接続する

エンドポイントを指定せずに MCP サーバーを起動します:

laurus mcp

次に Claude に接続を依頼します:

http://localhost:50051 の laurus サーバーに接続してください」

Claude は他のツールを使用する前に connect を呼び出します。

ライフサイクル

laurus-server 起動(別プロセス)
  └─ gRPC ポート 50051 でリッスン

Claude 起動
  └─ 起動: laurus mcp --endpoint http://localhost:50051
       └─ stdio イベントループに入る
            ├─ stdin 経由でツール呼び出しを受信
            ├─ gRPC 経由で laurus-server にプロキシ
            └─ stdout 経由で結果を送信
Claude 終了
  └─ laurus-mcp プロセスが終了
  └─ laurus-server は継続して動作

MCP ツールリファレンス

laurus MCP サーバーは以下のツールを公開しています。

connect

実行中の laurus-server gRPC エンドポイントに接続します。--endpoint フラグなしでサーバーを起動した場合や、実行時に別の laurus-server に切り替える場合に、他のツールを使用する前にこのツールを呼び出してください。

パラメーター

名前必須説明
endpointstringはいgRPC エンドポイント URL(例: http://localhost:50051

Tool: connect
endpoint: "http://localhost:50051"

結果: Connected to laurus-server at http://localhost:50051.


create_index

指定されたスキーマで新しい検索インデックスを作成します。

パラメーター

名前必須説明
schema_jsonstringはいJSON 文字列としてのスキーマ定義

スキーマ JSON フォーマット

FieldOption は serde の externally-tagged 表現を使用します(バリアント名がキーになります):

{
  "fields": {
    "title":     { "Text":    { "indexed": true, "stored": true } },
    "body":      { "Text":    {} },
    "score":     { "Float":   {} },
    "count":     { "Integer": {} },
    "active":    { "Boolean": {} },
    "created":   { "DateTime": {} },
    "embedding": { "Hnsw":    { "dimension": 384 } }
  }
}

Tool: create_index
schema_json: {"fields": {"title": {"Text": {}}, "body": {"Text": {}}}}

結果: Index created successfully at /path/to/index.


get_index

現在の検索インデックスの統計情報を取得します。

パラメーター

なし。

結果

{
  "document_count": 42,
  "vector_fields": ["embedding"]
}

add_document

インデックスにドキュメントを追加またはアップサートします。ドキュメントを追加した後は commit を呼び出してください。

パラメーター

名前必須説明
idstringはい外部ドキュメント識別子
documentobjectはいJSON オブジェクトとしてのドキュメントフィールド
modestringいいえ"put"(デフォルト、アップサート)または "add"(チャンク追加)

モード

  • put(デフォルト): 同じ id を持つ既存のドキュメントを削除してから新しいものをインデックスします。
  • add: 新しいチャンクとして追加します。複数のチャンクが同じ id を持てます(大きなドキュメントの分割に便利)。

Tool: add_document
id: "doc-1"
document: {"title": "Hello World", "body": "これはテストドキュメントです。"}

結果: Document 'doc-1' added. Call commit to persist changes.


get_document

外部 ID でドキュメントを取得します。

パラメーター

名前必須説明
idstringはい外部ドキュメント識別子

結果

{
  "id": "doc-1",
  "documents": [
    { "title": "Hello World", "body": "これはテストドキュメントです。" }
  ]
}

delete_document

外部 ID でドキュメントを削除します。削除後は commit を呼び出してください。

パラメーター

名前必須説明
idstringはい外部ドキュメント識別子

結果: Document 'doc-1' deleted. Call commit to persist changes.


commit

保留中の変更をディスクにコミットします。変更を検索可能かつ永続的にするため、add_document または delete_document の後に必ず呼び出してください。

パラメーター

なし。

結果: Changes committed successfully.


add_field

インデックスにフィールドを追加します。

パラメーター

名前必須説明
namestringはいフィールド名
field_option_jsonstringはいJSON 形式のフィールド設定

{
  "name": "category",
  "field_option_json": "{\"Text\": {\"indexed\": true, \"stored\": true}}"
}

delete_field

インデックスからフィールドを削除します。既にインデックスされたデータは残りますが、削除されたフィールドにはアクセスできなくなります。

パラメーター

名前必須説明
namestringはい削除するフィールド名

Tool: delete_field
name: "category"

結果: Field 'category' deleted.


search

laurus クエリ DSL を使用してドキュメントを検索します。

パラメーター

名前必須説明
querystringはいlaurus クエリ DSL による検索クエリ
limitintegerいいえ最大結果数(デフォルト: 10)
offsetintegerいいえページネーション用スキップ数(デフォルト: 0)

クエリ DSL の例

クエリ説明
helloデフォルトフィールド全体のターム検索
title:helloフィールド指定のターム検索
title:hello AND body:worldブール AND
"exact phrase"フレーズ検索
roam~2ファジー検索(編集距離 2)
count:[1 TO 10]範囲検索
title:helo~1フィールド指定のファジー検索

結果

{
  "total": 2,
  "results": [
    {
      "id": "doc-1",
      "score": 3.14,
      "document": { "title": "Hello World", "body": "..." }
    },
    {
      "id": "doc-2",
      "score": 1.57,
      "document": { "title": "Hello Again", "body": "..." }
    }
  ]
}

典型的なワークフロー

1. connect         → 実行中の laurus-server に接続
2. create_index    → スキーマを定義(インデックスが存在しない場合)
3. add_field       → フィールドを追加(必要に応じて)
4. add_document    → ドキュメントをインデックス(必要に応じて繰り返し)
5. commit          → 変更をディスクに永続化
6. search          → インデックスを検索
7. add_document    → ドキュメントを更新
8. delete_document → ドキュメントを削除
9. delete_field    → 不要なフィールドを削除(必要に応じて)
10. commit         → 変更を永続化

ビルドとテスト

前提条件

  • Rust 1.85 以降(edition 2024)
  • Cargo(Rust に付属)
  • protobuf コンパイラprotoc)– laurus-server のビルドに必要

ビルド

# すべてのクレートをビルド
cargo build

# 特定の Feature を指定してビルド
cargo build --features embeddings-candle

# リリースモードでビルド
cargo build --release

テスト

# すべてのテストを実行
cargo test

# 名前を指定して特定のテストを実行
cargo test <test_name>

# 特定のクレートのテストを実行
cargo test -p laurus
cargo test -p laurus-cli
cargo test -p laurus-server

Lint

# clippy を警告エラー扱いで実行
cargo clippy -- -D warnings

フォーマット

# フォーマットチェック
cargo fmt --check

# フォーマットを適用
cargo fmt

ドキュメント

API ドキュメント

# Rust API ドキュメントを生成して開く
cargo doc --no-deps --open

mdBook ドキュメント

# ドキュメントサイトをビルド
mdbook build docs

# ローカルプレビューサーバーを起動 (http://localhost:3000)
mdbook serve docs

# Markdown ファイルを Lint
markdownlint-cli2 "docs/src/**/*.md"

Feature Flags

laurus クレートはデフォルトでは Feature が無効の状態で提供されます。必要に応じて Embedding サポートを有効にしてください。

利用可能な Feature

Feature説明主な依存クレート
embeddings-candleHugging Face Candle によるローカル BERT Embeddingcandle-core, candle-nn, candle-transformers, hf-hub, tokenizers
embeddings-openaiOpenAI API Embeddingreqwest
embeddings-multimodalCLIP マルチモーダル Embedding(テキスト + 画像)image, embeddings-candle
embeddings-allすべての Embedding Feature を統合上記すべて

各 Feature の詳細

embeddings-candle

CandleBertEmbedder を有効にし、CPU 上でローカルに BERT モデルを実行できるようにします。モデルは初回使用時に Hugging Face Hub からダウンロードされます。

[dependencies]
laurus = { version = "0.1.0", features = ["embeddings-candle"] }

embeddings-openai

OpenAIEmbedder を有効にし、OpenAI Embeddings API を呼び出せるようにします。実行時に OPENAI_API_KEY 環境変数が必要です。

[dependencies]
laurus = { version = "0.1.0", features = ["embeddings-openai"] }

embeddings-multimodal

CandleClipEmbedder を有効にし、CLIP ベースのテキストおよび画像 Embedding を使用できるようにします。embeddings-candle を暗黙的に有効にします。

[dependencies]
laurus = { version = "0.1.0", features = ["embeddings-multimodal"] }

embeddings-all

すべての Embedding Feature を有効にする便利な Feature です。

[dependencies]
laurus = { version = "0.1.0", features = ["embeddings-all"] }

Feature Flag がバイナリサイズに与える影響

Embedding Feature を有効にすると、コンパイル時間とバイナリサイズが増加する依存クレートが追加されます。

構成おおよその影響
Feature なし(Lexical のみ)ベースライン
embeddings-candle+ Candle ML フレームワーク
embeddings-openai+ reqwest HTTP クライアント
embeddings-multimodal+ 画像処理 + Candle
embeddings-all上記すべて

Lexical(キーワード)検索のみが必要な場合は、Feature を有効にせずに Laurus を使用することで、最小のバイナリサイズと最速のコンパイル時間を実現できます。

プロジェクト構成

Laurus は 3 つのクレートを持つ Cargo ワークスペースとして構成されています。

ワークスペースレイアウト

laurus/                          # リポジトリルート
├── Cargo.toml                   # ワークスペース定義
├── laurus/                      # コア検索エンジンライブラリ
│   ├── Cargo.toml
│   ├── src/
│   │   ├── lib.rs               # パブリック API とモジュール宣言
│   │   ├── engine.rs            # Engine, EngineBuilder, SearchRequest
│   │   ├── analysis/            # テキスト解析パイプライン
│   │   ├── lexical/             # 転置インデックス(Inverted Index)と Lexical 検索
│   │   ├── vector/              # ベクトルインデックス(Flat, HNSW, IVF)
│   │   ├── embedding/           # Embedder 実装
│   │   ├── storage/             # ストレージバックエンド(memory, file, mmap)
│   │   ├── store/               # ドキュメントログ(WAL)
│   │   ├── spelling/            # スペル修正
│   │   ├── data/                # DataValue, Document 型
│   │   └── error.rs             # LaurusError 型
│   └── examples/                # 実行可能なサンプル
├── laurus-cli/                  # コマンドラインインターフェース
│   ├── Cargo.toml
│   └── src/
│       └── main.rs              # CLI エントリーポイント(clap)
├── laurus-server/               # gRPC サーバー + HTTP ゲートウェイ
│   ├── Cargo.toml
│   ├── proto/                   # Protobuf サービス定義
│   └── src/
│       ├── lib.rs               # サーバーライブラリ
│       ├── config.rs            # TOML 設定
│       ├── grpc/                # gRPC サービス実装
│       └── gateway/             # HTTP/JSON ゲートウェイ(axum)
└── docs/                        # mdBook ドキュメント
    ├── book.toml
    └── src/
        └── SUMMARY.md           # 目次

クレートの役割

クレート種類説明
laurusライブラリLexical 検索、ベクトル検索、ハイブリッド検索を備えたコア検索エンジン
laurus-cliバイナリインデックス管理、ドキュメント CRUD、検索、REPL のための CLI ツール
laurus-serverライブラリ + バイナリオプションの HTTP/JSON ゲートウェイ付き gRPC サーバー

laurus-clilaurus-server はどちらも laurus ライブラリクレートに依存しています。

設計規約

  • モジュールスタイル: ファイルベースのモジュール(Rust 2018 edition スタイル)、mod.rs は使用しない
    • src/tokenizer.rs + src/tokenizer/dictionary.rs
    • 不可: src/tokenizer/mod.rs
  • エラーハンドリング: ライブラリのエラー型には thiserroranyhow はバイナリクレートのみ
  • unwrap() / expect() 禁止: 本番コードでは使用不可(テストでは使用可)
  • 非同期: すべてのパブリック API は Tokio ランタイムで async/await を使用
  • Unsafe: すべての unsafe ブロックに // SAFETY: ... コメントが必須
  • ドキュメント: すべてのパブリックな型、関数、列挙型にドキュメントコメント(///)が必須
  • ライセンス: 依存クレートは MIT または Apache-2.0 互換であること