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_query(
            laurus::lexical::search::searcher::LexicalSearchQuery::Obj(
                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 として 5 つのクレートで構成されています。

graph TB
    CLI["laurus-cli\n(Binary Crate)\nCLI + REPL"]
    SRV["laurus-server\n(Library + Binary)\ngRPC Server + HTTP Gateway"]
    MCP["laurus-mcp\n(Library + Binary)\nMCP Server"]
    PY["laurus-python\n(cdylib)\nPython Bindings"]
    LIB["laurus\n(Library Crate)\nCore Search Engine"]

    CLI --> LIB
    CLI --> SRV
    CLI --> MCP
    SRV --> LIB
    MCP --> SRV
    MCP --> LIB
    PY --> LIB
クレート種類説明
laurusLibraryコア検索エンジン – Lexical 検索、Vector 検索、ハイブリッド検索
laurus-cliBinaryインデックス管理と検索のためのコマンドラインインターフェース
laurus-serverLibrary + Binaryオプションの HTTP/JSON ゲートウェイ付き gRPC サーバー
laurus-mcpLibrary + BinaryMCP(Model Context Protocol)サーバー
laurus-pythoncdylibPyO3 による Python バインディング

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

全体概要

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;
use laurus::lexical::TermQuery;
use laurus::lexical::search::searcher::LexicalSearchQuery;

// Search for "rust" in the "body" field
let request = SearchRequestBuilder::new()
    .lexical_query(
        LexicalSearchQuery::Obj(
            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, Result, Schema, SearchRequestBuilder,
};
use laurus::lexical::{TextOption, TermQuery};
use laurus::lexical::search::searcher::LexicalSearchQuery;
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_query(
            LexicalSearchQuery::Obj(
                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

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

すべての 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。

Earth-Centered Earth-Fixed (ECEF) 座標系を用いた 3D 地理検索のサンプルです。6 つのランドマーク(東京タワー、東京スカイツリー、富士山頂、自由の女神像、シドニー・オペラハウス、ISS サンプル点)を wgs84_to_ecef で変換してインデックスします。

cargo run --example geo3d_search

デモ内容: Geo3dDistanceQuery(球)、Geo3dBoundingBoxQuery(3D AABB)、Geo3dNearestQuery(k-NN)、および wgs84_to_ecef 変換ユーティリティ。バウンディングボックスクエリは ISS サンプルを意図的に除外し、高度が第三の軸として機能することを示します。

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 --> G3["Geo3d"]
    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()緯度/経度のペア。半径検索とバウンディングボックスクエリをサポート
Geo3dGeo3dOptionadd_geo3d_field()3D ECEF 直交座標ポイント(x, y, z、メートル)。3D 距離検索・バウンディングボックス・k-NN クエリをサポート。詳細は 3D 地理検索 を参照
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)2D 地理座標フィールドを追加(WGS84)
add_geo_ecef(name, x, y, z)(f64, f64, f64)3D ECEF 直交座標ポイントを追加(メートル)
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(GeoPoint),                   // 2D WGS84 ポイント (latitude, longitude)
    GeoEcef(GeoEcefPoint),           // 3D ECEF 直交座標ポイント (x, y, z)、メートル
    Int64Array(Vec<i64>),            // 多値整数フィールド
    Float64Array(Vec<f64>),          // 多値浮動小数点フィールド
}
}

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 システムフィールドです。

_id — 外部ドキュメント ID

put_document / add_document に渡された外部ドキュメント ID を格納します。 KeywordAnalyzer(完全一致)でインデクシングされ、自動的に挿入されるため スキーマに追加する必要はありません。

動的スキーマ

Laurus はスキーマに宣言されていないフィールドを含むドキュメントも受け付けます。 挙動は Schema に設定する DynamicFieldPolicy で制御します:

ポリシー未宣言フィールドに対する挙動
Strictわかりやすいエラーメッセージで投入を拒否する
Dynamic(デフォルト)値から型を推論してスキーマへ自動追加する
Ignore未宣言フィールドを静かに破棄し、他のフィールドはインデックスする

Builder でポリシーを設定します:

#![allow(unused)]
fn main() {
use laurus::{DynamicFieldPolicy, Schema};

let schema = Schema::builder()
    .dynamic_field_policy(DynamicFieldPolicy::Dynamic)
    .build();
}

型推論ルール(Dynamic ポリシー)

投入される値推論されるフィールド型
stringText(転置インデックス、BM25)
integerInteger(BKD tree)
floatFloat(BKD tree)
boolBoolean
整数の配列(例: [1, 2, 3]Integermulti_valued = true
浮動小数点を含む数値配列(例: [1.5, 2.0, 3]Floatmulti_valued = true
緯度キー(lat または latitude)と経度キー(lonlnglongitude のいずれか)を持ち、値が範囲内の objectGeo
数値の xyz の 3 キーをすべて持つ object(有限値、ECEF メートル単位)Geo3d

ベクトルフィールド(Hnsw / Flat / Ivf)と Bytes自動推論の 対象外です。スキーマへ明示的に宣言してください。なお、同一 object 内で 2D 用キー(lat / lon)と 3D 用キー(x / y / z)を混在させた 場合は曖昧と判定してエラーとなります。いずれか一方の形式のみ使用して ください。

多値数値フィールド

IntegerFloat フィールドは multi_valued = true を指定することで、 1 ドキュメントに複数の値を保持できます。範囲クエリはいずれかの値が条件を満たせばマッチ する Lucene 流の挙動で、スコアは constant(マッチ件数による加点なし)です。

多値フィールドに単一値を送った場合は要素 1 個の配列に自動ラップされます。 逆に単一値フィールドに配列を送ると、暗黙の切り捨てではなくエラーになります。

型衝突

既に宣言されているフィールドに別の型の値が到着した場合、Laurus は 宣言された型への変換を試みます。変換ルールは以下の通りです:

宣言型受け取った値結果
IntegerInt64そのまま格納
IntegerFloat64(3.14)3 へ切り捨て(情報損失あり — 下の警告を参照)
IntegerText("42")42 としてパース
IntegerText("abc")エラー
FloatInt64f64 に拡張
FloatText("3.14")パース
BooleanInt64(0) / Int64(1)false / true
BooleanText("true"/"false")大文字小文字を無視してパース
Text任意のスカラー値文字列化
Geo / Geo3d / Bytes / ベクトル対応 variant 以外エラー

変換エラーの扱いはポリシーに依存します:

  • Strict: ただちにエラーを返す
  • Dynamic: エラーを返す(安全とみなせる変換はこの層ですべて試し切っている)
  • Ignore: 該当フィールドのみ破棄し、他のフィールドはインデックスする

⚠️ 警告: 静かな情報損失が発生しうる

いくつかの変換は、エラーを返さずに情報を失います:

  • Integer フィールドは受け取った Float 値を切り捨てます3.143-3.9-3)。投入は成功します
  • Float フィールドは f64 仮数部に収まらない巨大な整数で精度を失う可能性があります
  • Text フィールドはスカラーを文字列化して受け入れます(元の型情報は消えます)
  • Ignore は非互換なフィールドを静かに捨てます

データの正確性を優先したい場合は、DynamicFieldPolicy::Strict を使う(あるいは必要なフィールドをすべて事前に宣言する)ことを推奨します。 Dynamic ポリシーは「ドキュメントを投入できる」ことを「入力データを 1 ビットも失わない」ことより優先します。

Query DSL と未宣言フィールド

スキーマが確定した後、クエリパーサは field:value 句で参照されるフィールドが すべてスキーマに存在することを検証します。titl:hellotitle:hello の打ち間違い) のような typo は、結果が無言で空になるのではなく、明確なパースエラーとして返ります。

動的フィールド管理

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

フィールドの追加

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(日本語ストップワード)

JapaneseAnalyzer::newLinderaTokenizer::new と同じ引数(segmentation mode、Lindera 辞書ディレクトリのパス、任意のユーザー辞書パス)を受け取ります。laurus は Lindera の embed-* features をデフォルトで有効化しないため、IPADIC 等の辞書を実ファイルパスとして必ず指定する必要があります。

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

// Lindera 辞書を展開済みのパスを指定する。
let analyzer = JapaneseAnalyzer::new(
    "normal",
    "/var/lib/lindera/ipadic",
    None,
)?;
// "東京都に住んでいる" → ["東京", "都", "住ん", "いる"]
}

Schema 経由で参照する場合は構造化された AnalyzerSpec 形式でパラメータを渡します(後述の PerFieldAnalyzer を参照)。

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 で解析されます。

Schema からの per-field analyzer 設定

実装で直接 PerFieldAnalyzer を組み立てる代わりに、スキーマ宣言で analyzer を割り当てる場合がほとんどです。テキストフィールドの analyzer 設定は次の 2 つの形式を受け付けます。

// 1. パラメータ不要の組込 analyzer、または schema.analyzers に登録した名前。
{ "analyzer": "standard" }
{ "analyzer": "english" }
{ "analyzer": "my_custom_pipeline" }

// 2. パラメータ付きの組込プリセット。現状は Japanese プリセットのみで、Lindera 辞書のパスが必須。
{
  "analyzer": {
    "language": "japanese",
    "mode": "normal",
    "dict": "/var/lib/lindera/ipadic"
  }
}

文字列単独の "japanese" は辞書パスを伴わないためエラーとなります。既存スキーマで "analyzer": "japanese" を保存していた場合は、上記の構造化形式に移行してください。

プリセットに収まらないパイプラインを使いたい場合は、schema.analyzersAnalyzerDefinition として登録し、フィールドからは名前で参照します。

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)

地理フィールドには 2 種類あり、いずれも同じ多次元 BKD-Tree プリミティブにバックアップされています:

フィールド型次元数座標サポートされるクエリ
Geo2WGS84 緯度・経度(度)半径検索、バウンディングボックス
Geo3d3ECEF 直交座標 (x, y, z)(メートル)3D 距離検索(球)、3D バウンディングボックス、k-NN

Geo3d は高度が一級の次元になる用途(ドローン・衛星・屋内 3D 測位など、 2D の Geo フィールドでは情報が失われたり極で歪んだりするケース)で 適しています。座標系・WGS84 変換ヘルパー・DSL 構文については 3D 地理検索 (ECEF) を参照してください。

セグメント(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数値・日付・Geo(2D)・Geo3d(3D ECEF)フィールドの 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;
use laurus::lexical::TermQuery;
use laurus::lexical::search::searcher::LexicalSearchQuery;

let request = SearchRequestBuilder::new()
    .lexical_query(
        LexicalSearchQuery::Obj(
            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

2D 地理座標(WGS84 緯度・経度)に基づいてドキュメントをマッチングします。

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

// Find documents within 10 km (= 10 000 m) of Tokyo Station (35.6812, 139.7671)
let query = GeoQuery::within_radius("location", 35.6812, 139.7671, 10_000.0)?; // distance in metres

// 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)
)?;
}

Geo3dDistanceQuery / Geo3dBoundingBoxQuery / Geo3dNearestQuery

3 種類のクエリが、ECEF 直交座標(メートル)にバックアップされた 3D の Geo3d フィールドを対象とします。高度が意味を持つ用途や、2D の Geo フィールドでは極特異点が問題になるケースで使ってください。座標系・WGS84 変換ヘルパー・実例については 3D 地理検索 を参照してください。

#![allow(unused)]
fn main() {
use laurus::GeoEcefPoint;
use laurus::lexical::query::geo3d::{
    Geo3dDistanceQuery, Geo3dBoundingBoxQuery, Geo3dNearestQuery,
};

let centre = GeoEcefPoint::new(-3_955_182.0, 3_350_553.0, 3_700_276.0);

// 球: `centre` から 5 km 以内のドキュメント
let q = Geo3dDistanceQuery::new("position", centre, 5_000.0);

// 軸並行 3D バウンディングボックス(コンストラクタが軸ごとに min ≤ max を検証)
let min = GeoEcefPoint::new(-4_000_000.0, 3_300_000.0, 3_650_000.0);
let max = GeoEcefPoint::new(-3_900_000.0, 3_400_000.0, 3_750_000.0);
let q = Geo3dBoundingBoxQuery::new("position", min, max)?;

// k-NN: 最も近い 10 件、半径スケジュールをカスタマイズ
let q = Geo3dNearestQuery::new("position", centre, 10)
    .with_initial_radius(500.0)
    .with_max_radius(1_000_000.0);
}
クエリスコア
Geo3dDistanceQuery1 - distance / radius[0, 1] にクランプ
Geo3dBoundingBoxQueryマッチした全ドキュメントで定数 1.0
Geo3dNearestQuery最も近いヒットが 1.0、返却された集合内で最も遠いヒットが 0.0 となるよう正規化

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::SearchRequestBuilder;
use laurus::lexical::search::searcher::LexicalSearchQuery;

let request = SearchRequestBuilder::new()
    .lexical_query(LexicalSearchQuery::Obj(Box::new(query)))
    .add_field_boost("title", 2.0)  // title matches count double
    .add_field_boost("body", 1.0)
    .build();
}

Lexical 検索オプション(SearchRequestBuilder 経由)

Lexical 検索の動作パラメータは SearchRequestBuilder のメソッドで設定します。これらは SearchRequestlexical_options フィールドに格納されます。

オプションデフォルト説明
field_boostsフィールドごとのスコア倍率
min_score0.0最小スコア閾値
timeout_msNone検索タイムアウト(ミリ秒)
parallelfalseセグメント間の並列検索を有効にする
sort_byScore関連性スコアでソート、またはフィールドでソート(asc / desc

ビルダーメソッド

SearchRequestBuilder を使用してクエリとオプションを設定します。

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

let request = SearchRequestBuilder::new()
    .lexical_query(LexicalSearchQuery::Obj(Box::new(TermQuery::new("body", "rust"))))
    .limit(20)
    .lexical_min_score(0.5)
    .lexical_timeout_ms(5000)
    .lexical_parallel(true)
    .sort_by(SortField::FieldDesc("date".to_string()))
    .add_field_boost("title", 2.0)
    .add_field_boost("body", 1.0)
    .build();
}

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_query(
        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;
use laurus::lexical::TermQuery;
use laurus::vector::VectorSearchRequestBuilder;

// Vector search with a category filter
let request = SearchRequestBuilder::new()
    .vector_query(
        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_query(
        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_query(
                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, FusionAlgorithm};
use laurus::lexical::TermQuery;
use laurus::lexical::search::searcher::LexicalSearchQuery;
use laurus::vector::VectorSearchRequestBuilder;

let request = SearchRequestBuilder::new()
    // Lexical component
    .lexical_query(
        LexicalSearchQuery::Obj(
            Box::new(TermQuery::new("body", "rust"))
        )
    )
    // Vector component
    .vector_query(
        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 句の識別はスキーマのフィールド型に基づいて行われます。ベクトルフィールドとして定義されたフィールド名を持つ句は 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 のフィールド

フィールドデフォルト説明
querySearchQueryDsl("")検索クエリ仕様(Dsl / Lexical / Vector / Hybrid)
limitusize10返される結果の最大件数
offsetusize0スキップする結果の数(ページネーション用)
fusion_algorithmOption<FusionAlgorithm>None(ハイブリッド時は RRF { k: 60.0 } を使用)Lexical と Vector の結果をマージする方法
filter_queryOption<Box<dyn Query>>NoneLexical クエリによるプレフィルター(Lexical と Vector の両方の結果を制限)
lexical_optionsLexicalSearchOptionsデフォルトLexical 検索の動作パラメータ
vector_optionsVectorSearchOptionsデフォルトVector 検索の動作パラメータ

SearchResult

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

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

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

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

#![allow(unused)]
fn main() {
let request = SearchRequestBuilder::new()
    .lexical_query(
        LexicalSearchQuery::Obj(Box::new(TermQuery::new("body", "rust")))
    )
    .vector_query(
        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_query(/* ... */)
    .vector_query(/* ... */)
    .offset(0)
    .limit(10)
    .build();

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

完全な例

use std::sync::Arc;
use laurus::{
    Document, Engine, Schema, SearchRequestBuilder,
    FusionAlgorithm, PerFieldEmbedder,
};
use laurus::lexical::{TextOption, TermQuery};
use laurus::lexical::core::field::IntegerOption;
use laurus::lexical::search::searcher::LexicalSearchQuery;
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_query(
                LexicalSearchQuery::Obj(Box::new(TermQuery::new("body", "rust")))
            )
            .vector_query(
                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 句の区別は、フィールド名に基づいて行われます。スキーマ上でベクトルフィールドとして定義されたフィールド名が指定された場合、その句は Vector クエリとして扱われます。

フィールド検証

クエリ中の field:value 句は、パース時にスキーマと照合されます。スキーマに宣言されていないフィールドを参照したクエリは、結果が黙って空になるのではなく、エラーとして返されます。これにより typo(例: title:hello のつもりで titl:hello)を早期に検出できます。

未知のフィールドを含むドキュメントを受け付けたい場合は、スキーマの dynamic_field_policy を設定して、投入時にフィールドを追加する動作を有効にしてください。一度フィールドがスキーマに登録されれば、それを参照するクエリは正常に動作します。

Lexical クエリ構文

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

Term クエリ

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

hello
title:hello

ブーリアン演算子

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

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

AND は対称的に動作します。両側の句を必須(Must)にマークするため、title:hello AND body:world両方 の句にマッチするドキュメントだけを返します。連鎖(a AND b AND c)でも 3 つすべてが必須となります。ただし +(必須)または -(禁止)が明示されている句は元の意図が維持され、AND で上書きされることはありません。

明示的な演算子なしでスペース区切りされた句は、暗黙的なブーリアン(スコアリング付きの OR として動作)を使用します。例えば a b AND c は「a は任意、bc の両方が必須」と解釈されます。

必須 / 禁止句

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

+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]

2D 地理クエリ(geo_*)

2 種類の関数形式を Geo(2D 緯度 / 経度)フィールドに対して使えます。 緯度・経度は度単位、距離はメートル単位の符号付き浮動小数点数:

location:geo_distance(lat, lon, distance_m)
location:geo_bbox(min_lat, min_lon, max_lat, max_lon)
形式動作
geo_distance(lat, lon, distance_m)中心 (lat, lon) から distance_m メートル以内の格納済み座標を返す
geo_bbox(min_lat, min_lon, max_lat, max_lon)軸並行緯度・経度範囲に含まれる格納済み座標を返す

例:

# 東京から 10 km(= 10 000 m)以内(35.6895, 139.6917)
location:geo_distance(35.6895, 139.6917, 10000)

# 軸並行緯度・経度範囲
location:geo_bbox(35.0, 139.0, 36.0, 140.0)

クエリ対象フィールドはスキーマで Geo フィールドとして宣言されている必要が あります。緯度は [-90, 90]、経度は [-180, 180] の範囲外を拒否します。

3D 地理クエリ(geo3d_*

3 種類の関数形式を Geo3d(3D ECEF 直交座標)フィールドに対して使えます。 k 以外の引数はすべてメートル単位の符号付き浮動小数点数、k のみ符号なし整数:

position:geo3d_distance(x, y, z, distance_m)
position:geo3d_bbox(min_x, min_y, min_z, max_x, max_y, max_z)
position:geo3d_nearest(x, y, z, k)
形式動作
geo3d_distance(x, y, z, distance_m)(x, y, z) から distance_m メートル以内の格納済みポイントを返す
geo3d_bbox(min_x, min_y, min_z, max_x, max_y, max_z)軸並行 3D ボックスに含まれる格納済みポイントを返す
geo3d_nearest(x, y, z, k)(x, y, z) に最も近い k 件をユークリッド距離順で返す

例:

# 東京タワーから 5 km 以内(ECEF 座標)
position:geo3d_distance(-3955182, 3350553, 3700276, 5000)

# 軸並行 ECEF バウンディングボックス内
position:geo3d_bbox(-4000000, 3300000, 3650000, -3900000, 3400000, 3750000)

# 最も近い 10 件
position:geo3d_nearest(-3955182, 3350553, 3700276, 10)

クエリ対象フィールドはスキーマで Geo3d として宣言されている必要があります。 座標系・WGS84 変換ヘルパー・詳細な意味論については 3D 地理検索 を参照してください。

ブースト

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

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    = { geo3d_query | geo_query | range_query | phrase_query
                 | fuzzy_term | wildcard_term | simple_term }
geo3d_query    = { geo3d_distance | geo3d_bbox | geo3d_nearest }
geo3d_distance = { ^"geo3d_distance" ~ "(" ~ signed_float ~ "," ~ signed_float
                 ~ "," ~ signed_float ~ "," ~ signed_float ~ ")" }
geo3d_bbox     = { ^"geo3d_bbox" ~ "(" ~ signed_float ~ "," ~ signed_float
                 ~ "," ~ signed_float ~ "," ~ signed_float ~ ","
                 ~ signed_float ~ "," ~ signed_float ~ ")" }
geo3d_nearest  = { ^"geo3d_nearest" ~ "(" ~ signed_float ~ "," ~ signed_float
                 ~ "," ~ signed_float ~ "," ~ unsigned_int ~ ")" }
geo_query      = { geo_distance | geo_bbox }
geo_distance   = { ^"geo_distance" ~ "(" ~ signed_float ~ "," ~ signed_float
                 ~ "," ~ signed_float ~ ")" }
geo_bbox       = { ^"geo_bbox" ~ "(" ~ signed_float ~ "," ~ signed_float
                 ~ "," ~ signed_float ~ "," ~ signed_float ~ ")" }
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
field:"text"^weight
要素必須説明
field:はい対象のベクトルフィールド名(スキーマでベクトルフィールドとして定義されている必要があります)content:
"text" または textはいエンベディングするテキスト(クォート付きまたはクォートなし)"cute kitten"python
^weightいいえスコアウェイト(デフォルト: 1.0)^0.8

Vector クエリの例

# Single field (quoted text)
content:"cute kitten"

# Single field (unquoted text)
content:python

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

# 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 | unquoted_text) ~ boost? }
field_prefix   = { field_name ~ ":" }
field_name     = @{ (ASCII_ALPHA | "_") ~ (ASCII_ALPHANUMERIC | "_" | ".")* }
quoted_text    = ${ "\"" ~ inner_text ~ "\"" }
unquoted_text  = @{ (!(WHITE_SPACE | "^" | "\"") ~ ANY)+ }
inner_text     = @{ (!("\"") ~ ANY)* }
boost          = { "^" ~ float_value }
float_value    = @{ ASCII_DIGIT+ ~ ("." ~ ASCII_DIGIT+)? }

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

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

title:hello content:"cute kitten"^0.8

仕組み

  1. 分割(Split): スキーマのフィールド型に基づいて、各句が Lexical か Vector かを判定する。ベクトルフィールドとして定義されたフィールド名を持つ句は Vector 句として抽出される
  2. 委譲(Delegate): Vector 部分は VectorQueryParser に、残りは Lexical の QueryParser に渡される
  3. フュージョン(Fuse): Lexical と Vector の両方の結果が存在する場合、フュージョンアルゴリズムで結合される

曖昧性の解消

Vector 句と Lexical 句の区別は、スキーマのフィールド型に基づいて行われます。フィールド名がスキーマ上でベクトルフィールド(HNSW、Flat、IVF など)として定義されている場合、その句は 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)です。コード例はカスタムフュージョンを参照してください。

ハイブリッド AND/OR セマンティクス(+ プレフィックス)

デフォルトでは、ハイブリッドクエリは union(OR) を使用します。Lexical 結果または Vector 結果のいずれかに含まれるドキュメントが返されます。Vector 句に + プレフィックスを付けると intersection(AND) に切り替わり、両方の結果セットに存在するドキュメントのみが返されます。

構文モード動作
title:Rust content:"system process"OR(union)Lexical クエリまたは Vector クエリにマッチするドキュメントが返される
title:Rust +content:"system process"AND(intersection)Lexical と Vector の両方にマッチするドキュメントのみが返される
+title:Rust +content:"system process"AND(intersection)両方の句が必須。Lexical フィールドの + は既存の required clause として処理される

ルール:

  • Vector 句に + プレフィックスがない場合、フュージョンは Lexical と Vector の結果を union(OR) で結合します
  • 1 つでも Vector 句に + プレフィックスがある場合、フュージョンは intersection(AND) に切り替わり、Lexical と Vector の両方の結果セットに存在するドキュメントのみが返されます
  • Lexical フィールドの +(例: +title:Rust)は、Lexical クエリパーサーによって required clause(必須句)として解釈されます。これは既存の Tantivy/Lucene スタイルの動作であり、それ自体ではハイブリッドフュージョンの intersection モードをトリガーしません

統合クエリの例

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

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

# Vector only — unquoted text
content:python

# Hybrid — fusion applied automatically (OR / union)
title:hello content:"cute kitten"

# Hybrid with AND / intersection — both result sets required
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

コード例

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.query  -> SearchQuery::Hybrid { lexical: ..., vector: ... }
// 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,
    });
}

BKD-Tree

Laurus は数値・日時・地理ポイントなどのデータを BKD-Tree (Block KD-Tree) に格納する。BKD-Tree はディスク常駐の多次元インデックスで、レンジ・バウンディング ボックス・距離・k 近傍 (k-NN) の各クエリを単一のファイル形式で扱える。

「空間的な形」を持つあらゆるフィールド型はこの BKD プリミティブを共有する:

フィールド型次元数座標空間
Integer / Float(単一値・多値)1スカラー
DateTime1Unix マイクロ秒(UTC)
Geo2緯度・経度(度)
Geo3d3ECEF 直交座標(メートル)

新しい空間フィールド型を追加する作業は、次元数を選んでクエリ側の IntersectVisitor を書くだけに帰着する。 ライタ・リーダ・オンディスクレイアウトはそのまま再利用される。

ファイルフォーマット (Version 2)

.bkd セグメントファイルは自己完結型のバイナリで、3 つの領域から成る:

+----------------------------------------+
| File Header                            |   固定長・バージョンタグ付き
+----------------------------------------+
| Leaf Blocks                            |   row-major points + doc_ids
|   leaf 0                               |
|   leaf 1                               |
|   ...                                  |
+----------------------------------------+
| Index Nodes                            |   内部ナビゲーションノード
|   node N-1                             |
|   ...                                  |
|   node 0  (root, written last)         |
+----------------------------------------+

ヘッダ (BKDFileHeader) は magicversion(現在は 2)、num_dimsbytes_per_dim、総ポイント数、リーフブロック数、全体の軸ごと min/max、 インデックス領域とルートノードへのオフセットを保持する。

Leaf Block レイアウト

各リーフブロックは、その部分木に属するポイントを格納する:

count               u32                       — リーフ内のポイント数
leaf_min            [f64; num_dims]           — リーフレベルの AABB 下端
leaf_max            [f64; num_dims]           — リーフレベルの AABB 上端
point_values        [f64; count * num_dims]   — row-major のポイント座標
doc_ids             [u64; count]              — ドキュメント ID の並列配列

リーフごとに AABB を持たせることで、クエリ領域がリーフの外側にある場合 (Outside) や全内包される場合 (Inside) には、ポイントを 1 つも読まずに リーフ全体を判定できる。

内部 Index Node レイアウト

内部ノードは分割情報に加え、子ごとの AABB も保持する:

split_dim           u32                       — 分割する軸
split_value         f64                       — 分割しきい値
left_min            [f64; num_dims]           — 左部分木の AABB 下端
left_max            [f64; num_dims]           — 左部分木の AABB 上端
right_min           [f64; num_dims]           — 右部分木の AABB 下端
right_max           [f64; num_dims]           — 右部分木の AABB 上端
left_offset         u64                       — 左子ノードのファイルオフセット
right_offset        u64                       — 右子ノードのファイルオフセット

ノードごとの AABB(v2 で追加)は、分割値だけを持っていた v1 レイアウトを 置き換える。これにより Inside / Outside の枝刈りが、再帰的な 探索ではなく定数時間の矩形判定で済むようになった。

ビルドアルゴリズム

BKDWriter::write は、平坦な row-major のポイントバッファと並列の doc_ids バッファからツリーを構築する。構築は 最も広い軸で分割する (widest-axis split) ヒューリスティクスで進む:

  1. 入力部分集合の AABB を計算する。
  2. (max - min) レンジが最も広い軸を選ぶ(同点時は次元番号の小さい方を 採用して決定的にする)。
  3. インデックスの並びをその軸でソートし、中央値で分割する。
  4. 部分木が block_size(既定 512)以下のポイント数になるまで再帰し、 そうなったらリーフとして書き出す。
  5. 子が flush された後、各親の left_offset / right_offset を後埋めする。

ビルダはポイント/doc_id バッファ自体ではなくインデックスの並び (permutation)をソートするため、ポイント数に関わらずポイント単位の ヒープ確保を一切おこなわない。

数値ロバスト性

座標は全順序で比較できる必要がある。BKDWriter::writeNaN を明示的に 拒否する。NaN には順序が定義されておらず、分割判定とノードごとの AABB 不変条件を破壊するためである。±INFINITY は両方とも受理され、クエリでは 「無限大」を表す自然なセンチネルとして機能する。

IntersectVisitor プロトコル

BKD インデックスへのクエリは IntersectVisitor の実装として表現する。リーダはツリーを辿りながら、ビジタに 3 種類の 情報を尋ねる:

#![allow(unused)]
fn main() {
pub enum CellRelation {
    Inside,   // 部分木全体がヒット — ポイント単位の照合不要
    Outside,  // 部分木全体をスキップできる
    Crosses,  // 再帰、もしくはリーフをポイント単位で照合
}

pub trait IntersectVisitor {
    fn compare(&self, cell: &AABB) -> CellRelation;
    fn visit_inside(&mut self, doc_id: u64);
    fn visit(&mut self, doc_id: u64, point: &[f64]);
}
}

リーダのトラバーサルは次のように進む:

graph TD
    A["compare(node.aabb)"]
    A -->|Inside| B["部分木の各 doc_id を visit_inside(doc_id) で報告<br/>(座標は読まない)"]
    A -->|Outside| C["部分木をスキップ"]
    A -->|Crosses, 内部ノード| D["子ノードへ再帰"]
    A -->|Crosses, リーフ| E["各ポイントについて visit(doc_id, point)<br/>ヒット判定はビジタに委ねる"]

この 3 値分類こそが枝刈りを実現する鍵である。常に Crosses を返すビジタを 書いても結果は正しい — 単にリーフ全件走査に退化するだけだ。

レンジクエリ

レガシーの BKDTree::range_search API は intersect の薄いラッパに なっている。RangeQueryVisitor を半開区間/閉区間のパラメータから組み立て、 無限大の None スロットを ±INFINITY に変換する。境界の包含・排他は ビジタ自身が処理する。

3D 地理クエリ

3 つのビジタが laurus::lexical::query::geo3d に存在し、 Geo3d (3D ECEF) フィールドをターゲットにする:

クエリcompare の判定領域visit の点ごとの判定
Geo3dDistanceQuery(centre, radius) と AABBユークリッド距離 ≤ radius
Geo3dBoundingBoxQueryクエリ AABB と セル AABB点がクエリ AABB に内包
Geo3dNearestQuery (k-NN)クエリ点を中心に拡大していく球距離 ≤ 現在の k 番目最良値

同じプリミティブで将来のあらゆる空間クエリ(ポリゴンクエリや 2D Geo の大円距離クエリなど)を新しいビジタとして実装できる。

リーダの内部実装

BKDReader::intersect は 1 クエリにつき 1 つのスクラッチバッファ (IntersectScratch) を使う。バッファは出会った最大のリーフサイズまで 拡大されたあと、後続のリーフでも再利用される。結果として、何枚のリーフを 辿っても、1 クエリの間にアロケータに触れる回数はごく少数で済む。

単一リーフだけのツリー(非常に小さなフィールド)は特別扱いされる: 「ルートオフセット」がそのまま唯一のリーフを指すため、内部ノードの 降下処理は完全にスキップされる。

関連項目

  • 3D 地理検索 (ECEF) — ECEF 距離・バウンディング ボックス・k-NN を実装した具体的な BKD ベースのビジタ。
  • Lexical インデクシング.bkd セグメントファイルがセグメント全体のレイアウト内のどこに位置するか。
  • Lexical 検索NumericRangeQueryGeoQueryGeo3dDistanceQuery の Rust API エントリポイント。

3D 地理検索 (ECEF)

Laurus は用途の異なる 2 種類の地理フィールド型を提供する:

フィールド型バックエンド構造座標系こんなときに
Geo2D BKD-TreeWGS84 緯度・経度(度)データが地表のみで、高度を考慮しなくてよい場合
Geo3d3D BKD-TreeECEF 直交座標 (x, y, z)(メートル)高度が一級の次元になる用途(ドローン・衛星・屋内測位・複数フロア建物)

両者は同じ BKD-Tree プリミティブを共有しており、Geo3d は単に第 3 の次元と異なるクエリ語彙を加えただけである。

なぜ ECEF なのか

(緯度, 経度, 高度) の組は人間にとっては便利だが、ユークリッド距離が そのままでは使えない:経度 1 度は赤道で約 111 km だが極では 0 km、 そして「高度」は地球表面に沿って曲がっている。

ECEF (Earth-Centered Earth-Fixed) はこれを直交座標系にフラット化する:

  • 原点は地球の質量中心。
  • +X 軸は赤道上の経度 0° を貫く。
  • +Y 軸は赤道上の経度 90° E を貫く。
  • +Z 軸は地理北極を貫く。
  • 3 軸とも単位はメートル

この座標系では 2 点間の直線距離が単なるユークリッドノルムになる — 球面三角法も極特異点も経度のラップアラウンドも要らない。これこそが、 Geo3d クエリを 3D BKD-Tree 上で(特殊な空間コードなしで)使える 理由である。

Geo3d フィールドの定義

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

let schema = Schema::builder()
    .add_geo3d_field("position", Geo3dOption::default())
    .build();
}

Geo3dOption は他の lexical フィールドオプションと同じく indexed / stored を提供する。インデックス済みの値はフィールドの 3D BKD-Tree に格納され、stored な値は get_documents で返ってくる。

TOML 形式(laurus-cli で使用):

[fields.position.Geo3d]
indexed = true
stored = true

3D ポイントのインデクシング

DocumentBuilder::add_geo_ecef を使って、メートル単位の生の直交座標で ポイントを追加する:

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

// (lat=35.6586°, lon=139.7454°, height=250m) のポイント
// 既に ECEF へ変換済み(後述「座標変換」参照)。
let doc = Document::builder()
    .add_geo_ecef("position", -3_955_182.0, 3_350_553.0, 3_700_276.0)
    .build();

engine.put_document("tokyo-tower", doc).await?;
engine.commit().await?;
}

値が既に GeoEcefPoint として手元にある場合は、統一 DataValue API を 使う:

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

let p = GeoEcefPoint::new(-3_955_182.0, 3_350_553.0, 3_700_276.0);
let doc = Document::builder()
    .add_field("position", DataValue::GeoEcef(p))
    .build();
}

座標変換 (WGS84 ↔ ECEF)

入力は通常 (緯度°, 経度°, 高度 m) でやって来る。Laurus は laurus::util::ecef で標準的な相互変換を提供する:

#![allow(unused)]
fn main() {
use laurus::util::ecef::{wgs84_to_ecef, ecef_to_wgs84};

// 順変換: lat/lon/height → ECEF
let p = wgs84_to_ecef(35.6586, 139.7454, 250.0);

// 逆変換: ECEF → lat/lon/height
let (lat, lon, height) = ecef_to_wgs84(&p);
}
関数方向アルゴリズム
wgs84_to_ecef(lat°, lon°, h_m)地理 → 直交卯酉線(prime vertical)の閉形式公式
ecef_to_wgs84(&GeoEcefPoint)直交 → 地理Bowring 1985 の閉形式を初期値にした Newton-Raphson 3 反復

逆変換は地表下から LEO 軌道高度をはるかに超える領域まで、各軸で サブ µm の精度で往復する。極付近では、cos(lat) → 0 で発散する 標準形 h = p / cos(lat) - N の代わりに、子午線形 h = z / sin(lat) - N(1 - e²) に切り替える。

内部で使う WGS84 楕円体定数は pub const として公開されている (WGS84_A, WGS84_F, WGS84_B, WGS84_E2, WGS84_E_PRIME_SQ)ので、 外部コードからも laurus が使っているのと同じ値を参照できる。

クエリ種別

Geo3d フィールドを対象とするクエリは 3 種類ある。いずれも 3D BKD-Tree を共有する IntersectVisitor の実装である。

球(半径) — Geo3dDistanceQuery

中心点から distance_m メートル以内に格納済みポイントが入っているドキュメントを すべて返す。スコアは 1 - distance / radius[0, 1] にクランプした 値。

#![allow(unused)]
fn main() {
use laurus::lexical::query::geo3d::Geo3dDistanceQuery;
use laurus::GeoEcefPoint;

let centre = GeoEcefPoint::new(-3_955_182.0, 3_350_553.0, 3_700_276.0);
let query = Geo3dDistanceQuery::new("position", centre, 5_000.0); // 5 km 半径
}

ビジタは Outside 判定に AABB::min_distance_sq_to_pointInside 判定に AABB::max_distance_sq_to_point を使う。どちらも 2 乗距離空間で比較するため、sqrt を回避できる。

バウンディングボックス — Geo3dBoundingBoxQuery

(min_x, min_y, min_z)(max_x, max_y, max_z) で定義された軸並行 3D ボックスにポイントが入っているドキュメントを返す。

#![allow(unused)]
fn main() {
use laurus::lexical::query::geo3d::Geo3dBoundingBoxQuery;
use laurus::GeoEcefPoint;

let min = GeoEcefPoint::new(-4_000_000.0, 3_300_000.0, 3_650_000.0);
let max = GeoEcefPoint::new(-3_900_000.0, 3_400_000.0, 3_750_000.0);
let query = Geo3dBoundingBoxQuery::new("position", min, max)?;
}

コンストラクタが Result を返すのは、すべての軸で min[i] ≤ max[i] を 満たす必要があるため。境界がおかしい場合は構築時点で拒否され、 「黙って空結果」を防げる。

注意点。 クエリボックスは ECEF 直交座標で軸並行になっている。 (lat, lon, height) の 2 つの隅をそれぞれ ECEF に変換して作った ボックスは、ユーザーが直感的に思い描く球殻領域とは違う形をしている。 地表上の領域クエリを書くなら、Geo3dDistanceQuery(真の 3D 球)の方が 多くの場合ユーザー直感に近い。

k 近傍 — Geo3dNearestQuery

クエリ点に最も近い k 件のドキュメントを返す。クエリは小さなプローブ球 (既定 1 km)から始め、以下のいずれかを満たすまで半径を倍加していく:

  1. 重複なしで k 件以上の候補を集めた、
  2. 倍加しても何も新しいヒットが見つからなくなった、
  3. 半径が max_radius_m(既定 1.0e10 m=10⁷ km)に達した。
#![allow(unused)]
fn main() {
use laurus::lexical::query::geo3d::Geo3dNearestQuery;
use laurus::GeoEcefPoint;

let centre = GeoEcefPoint::new(-3_955_182.0, 3_350_553.0, 3_700_276.0);
let query = Geo3dNearestQuery::new("position", centre, 10) // 上位 10 件
    .with_initial_radius(500.0)         // 500 m から開始
    .with_max_radius(1_000_000.0);      // 1 000 km で打ち切り
}

結果が k 件未満になるのは、単に max_radius_m 以内に k 件のポイントが 存在しないことを意味する。スコアは「最も近いヒットが 1.0、返却された 集合内で最も遠いヒットが 0.0」となるよう正規化される。

k-NN ビジタは compare から CellRelation::Inside を返さない — OutsideCrosses のみ — ので、すべての候補がその座標と一緒に visit へ届けられ、k-NN の順序付けに真の距離を使える。

Query DSL

Geo3d クエリは統一 Query DSL からも使える(Query DSL → Geo3d 関数 を参照):

position:geo3d_distance(-3955182, 3350553, 3700276, 5000)
position:geo3d_bbox(-4000000, 3300000, 3650000, -3900000, 3400000, 3750000)
position:geo3d_nearest(-3955182, 3350553, 3700276, 10)
形式引数
geo3d_distance(x, y, z, distance_m)中心 (x, y, z) と最大距離(m)
geo3d_bbox(min_x, min_y, min_z, max_x, max_y, max_z)AABB の対角の 2 隅
geo3d_nearest(x, y, z, k)中心 (x, y, z) と整数 k

数値引数はすべて符号付き浮動小数。geo3d_nearestk のみ符号なし整数。

ワイヤーフォーマット

gRPC

Protocol Buffers では、3D 地理を専用の Value バリアント、Geo3dPoint メッセージ、Geo3dOption スキーマオプションとして公開している:

message Geo3dPoint {
  double x = 1;
  double y = 2;
  double z = 3;
}

message Value {
  oneof kind {
    // ...
    Geo3dPoint geo3d_value = 12;
  }
}

message Geo3dOption {
  bool indexed = 1;
  bool stored = 2;
}

完全な定義は common.protoindex.proto を参照。

HTTP gateway / MCP

JSON ドキュメントでは Geo3d 値を { "x": …, "y": …, "z": … } の オブジェクトとして受け付ける:

{
  "position": { "x": -3955182.0, "y": 3350553.0, "z": 3700276.0 }
}

HTTP gateway はこれをエンジンへ転送する前に DataValue::GeoEcef に変換する。 MCP サーバーも同じ規約に従う。

Geo3d を使うべきでないケース

  • 純粋な 2D 地図クエリ(地表上の座標からの半径検索、タイル地図上の 単純なバウンディングボックスなど) — 引き続き Geo を使うこと。 2D BKD はひとつ次元が軽く、WGS84 入力もユーザー直感に近い。
  • 学習済みロケーション埋め込みの近似的な意味類似度 — それはベクトル フィールド(Hnsw, Flat, Ivf)の領分であって、Geo3d ではない。

関連項目

  • BKD-TreeInteger, Float, DateTime, 2D Geo も含めて支える、共通の多次元インデックス。
  • スキーマとフィールドGeo3d を含む フィールド型の一覧。
  • Query DSL — geo3d DSL 文法の詳細。
  • Lexical 検索 — Lexical クエリ全般の Rust API エントリポイント。
  • laurus-wasm/examples/geo3d/ — ブラウザ向けデモ。CesiumJS の 3D 地球儀上にリアルタイム航空機位置を プロットし、geo3d_bboxgeo3d_nearest を実演します。

ライブラリ概要

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, FusionAlgorithm};
use laurus::lexical::TermQuery;
use laurus::lexical::search::searcher::LexicalSearchQuery;

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

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

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

SearchRequest

フィールドデフォルト説明
querySearchQueryDsl("")検索クエリ仕様(Dsl / Lexical / Vector / Hybrid)
limitusize10返却する最大結果数
offsetusize0ページネーションのオフセット
fusion_algorithmOption<FusionAlgorithm>None(ハイブリッド時はRRF k=60)LexicalとVectorの結果を統合する方法
filter_queryOption<Box<dyn Query>>None両方の検索タイプに適用されるフィルタ
lexical_optionsLexicalSearchOptionsデフォルトLexical検索の動作パラメータ(ブースト、タイムアウト等)
vector_optionsVectorSearchOptionsデフォルトVector検索の動作パラメータ(スコアモード等)

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
}

BM25Plan(事前計算済みクエリ)

同じクエリに対して多数のドキュメントをスコアリングする場合、BM25Plan はクエリビルド時に単語ごとの IDF、フィールド長正規化の不変項、k1 + 1 を一度だけ算出し、各ドキュメントを軽量な数値ループでスコアリングします。スコアリングメソッドは 2 つあります。

  • BM25Plan::score(&doc_stats)BM25ScoringFunction::score のドロップイン高速版。DocumentStatsHashMap<String, u64> 経由で単語頻度を引きます。
  • BM25Plan::score_packed(&term_freqs, doc_length)query_terms での位置でインデックスされた &[u64] をパック済み単語頻度として受け取ります。ドキュメントごとの文字列キー HashMap 引きを完全に回避できるため、呼び出し側でドキュメントごとに頻度を 1 度だけパックできる場合に使ってください。
#![allow(unused)]
fn main() {
use laurus::lexical::search::scoring::bm25::{BM25Plan, ScoringConfig};

let plan = BM25Plan::new(&query_terms, &collection_stats, &ScoringConfig::default());

// HashMap パス(ドロップイン高速版):
let score = plan.score(&doc_stats);

// Packed パス(ドキュメントごとの HashMap 引きを回避):
let term_freqs: Vec<u64> = query_terms
    .iter()
    .map(|t| *doc_stats.term_frequencies.get(t).unwrap_or(&0))
    .collect();
let score = plan.score_packed(&term_freqs, doc_stats.doc_length);
}

BM25ScoringFunction::score は 1 ドキュメントずつスコアリングする呼び出し側のために維持されています。

フィールドブースト

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

#![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フィールド定義エラー無効なフィールドオプション、重複するフィールド名
BenchmarkFailedベンチマークエラーベンチマーク実行失敗
ThreadJoinErrorスレッド join エラーワーカースレッドでのパニック
JsonJSONシリアライズエラー不正なドキュメントJSON
Anyhowanyhow ラップエラーanyhow 経由のサードパーティクレートエラー
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を作成
.query_dsl(dsl)統合DSLクエリ文字列を設定
.lexical_query(query)Lexical検索クエリを設定(LexicalSearchQuery
.vector_query(query)Vector検索クエリを設定(VectorSearchQuery
.filter_query(query)プレフィルタクエリを設定
.fusion_algorithm(algo)フュージョンアルゴリズムを設定(デフォルト: RRF)
.limit(n)最大結果数(デフォルト: 10)
.offset(n)N件スキップ(デフォルト: 0)
.add_field_boost(field, boost)Lexical検索のフィールドブーストを追加
.lexical_min_score(f32)Lexical検索の最小スコアしきい値
.lexical_timeout_ms(u64)Lexical検索のタイムアウト(ミリ秒)
.lexical_parallel(bool)Lexical検索の並列実行を有効化
.sort_by(SortField)Lexical検索のソート順を設定
.vector_score_mode(VectorScoreMode)Vector検索のスコア結合モードを設定
.vector_min_score(f32)Vector検索の最小スコアしきい値
.build()SearchRequest を構築

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 docs --id doc001

Step 7: ドキュメントの削除

ドキュメントを削除してコミットします:

laurus --index-dir ./tutorial_data delete docs --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 で以下のコマンドを試してみてください:

> get stats
> search rust
> add doc doc004 {"title":"Go Programming","body":"Go is a statically typed language designed for simplicity and efficiency.","category":"programming"}
> commit
> search programming
> get docs doc004
> delete docs 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

新しいインデックスを作成します。--schema が指定された場合はその TOML ファイルを使用し、省略された場合は対話型スキーマウィザードが起動します。

laurus create index [--schema <FILE>]

引数:

フラグ必須説明
--schema <FILE>いいえインデックススキーマを定義する TOML ファイルのパス。省略時はインデックスディレクトリに既存の schema.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.

# 対話型ウィザード(--schema フラグなし)
laurus --index-dir ./my_index create index
# === Laurus Schema Generator ===
# Field name: title
# ...
# Index created at ./my_index.

注意: schema.tomlstore/ の両方が存在する場合はエラーが返されます。再作成するにはインデックスディレクトリを削除してください。schema.toml のみ存在する場合(作成が中断された場合など)は、--schema なしで create 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
Geo3dLexicalindexed, 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 docs

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

laurus get docs --id <ID>

テーブル出力の例:

╭──────┬─────────────────────────────────────────╮
│ ID   │ Fields                                  │
├──────┼─────────────────────────────────────────┤
│ doc1 │ body: This is a test, title: Hello World │
╰──────┴─────────────────────────────────────────╯

JSON 出力の例:

laurus --format json get docs --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 を使用してください。


put — リソースの上書き(Upsert)

put doc

インデックスにドキュメントを上書き(upsert)します。同じ ID のドキュメントが既に存在する場合、全チャンクが削除されてから新しいドキュメントがインデックスされます。ドキュメントは commit を実行するまで検索対象になりません。

laurus put doc --id <ID> --data <JSON>

引数:

フラグ必須説明
--id <ID>はい外部ドキュメント ID(文字列)
--data <JSON>はいJSON 文字列としてのドキュメントフィールド

例:

laurus put doc --id doc1 --data '{"title":"Updated Title","body":"This replaces the existing document."}'
# Document 'doc1' put (upserted). Run 'commit' to persist changes.

注意: add doc とは異なり、put doc は指定 ID の既存チャンクをすべて置き換えます。チャンクを追記したい場合は add doc を、ドキュメント全体を置き換えたい場合は put 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 docs

外部 ID で全ドキュメント(チャンクを含む)を削除します。

laurus delete docs --id <ID>

例:

laurus delete docs --id doc1
# Documents '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]"

# 3D 地理クエリ(球 / バウンディングボックス / k-NN)
laurus search "position:geo3d_distance(-3955182, 3350553, 3700276, 5000)"
laurus search "position:geo3d_bbox(-4000000, 3300000, 3650000, -3900000, 3400000, 3750000)"
laurus search "position:geo3d_nearest(-3955182, 3350553, 3700276, 10)"

テーブル出力の例:

╭──────┬────────┬─────────────────────────────────────────╮
│ 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 形式を使用します。

概要

スキーマは 3 つのトップレベル要素で構成されます:

# スキーマに宣言されていないフィールドの扱い。省略時は "dynamic"。
dynamic_field_policy = "dynamic"

# クエリでフィールドが指定されていない場合にデフォルトで検索するフィールド。
default_fields = ["title", "body"]

# フィールド定義。各フィールドには名前と型付き設定があります。
[fields.<field_name>.<FieldType>]
# ... 型固有のオプション
  • dynamic_field_policy — スキーマに宣言されていないフィールドがドキュメントに含まれる場合の挙動を制御します。値は "strict" / "dynamic" / "ignore"。デフォルトは "dynamic"。詳細および「dynamic では情報損失が起きうる」という警告は 動的スキーマ を参照してください。
  • default_fieldsQuery DSL でデフォルトの検索対象として使用されるフィールド名のリストです。Lexical フィールド(Text、Integer、Float など)のみデフォルトフィールドに指定できます。このキーはオプションで、デフォルトは空のリストです。
  • fields — フィールド名とその型付き設定のマップです。各フィールドにはフィールド型を1つだけ指定する必要があります。

フィールド命名規則

  • フィールド名は任意の文字列です(例: titlebody_veccreated_at)。
  • アンダースコア(_)で始まるフィールド名はエンジンの予約領域です。例外として _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
multi_valued = false
オプションデフォルト説明
indexedbooltrue範囲クエリおよび完全一致クエリを有効にする
storedbooltrue元の値を保存する
multi_valuedboolfalse整数の配列を受け付け、範囲クエリはいずれかの値が条件を満たせばマッチ(Lucene 流の “any match”、constant スコア)

Float

64ビット浮動小数点フィールド。範囲クエリをサポートします。

[fields.rating.Float]
indexed = true
stored = true
multi_valued = false
オプションデフォルト説明
indexedbooltrue範囲クエリを有効にする
storedbooltrue元の値を保存する
multi_valuedboolfalse浮動小数点の配列を受け付け、範囲クエリはいずれかの値が条件を満たせばマッチ(Lucene 流の “any match”、constant スコア)

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元の値を保存する

Geo3d

3D Earth-Centered Earth-Fixed (ECEF) 直交座標系の点フィールド(x / y / z はメートル単位)。geo3d_distance(球)、geo3d_bbox(3D AABB)、geo3d_nearest(k-NN)クエリをサポートします。座標系および wgs84_to_ecef / ecef_to_wgs84 の変換ユーティリティについては 3D 地理検索 (ECEF) を参照してください。

[fields.position.Geo3d]
indexed = true
stored = true
オプションデフォルト説明
indexedbooltrue3D 地理クエリ(geo3d_distancegeo3d_bboxgeo3d_nearest)を有効にする
storedbooltrue元の (x, y, z) 値を保存する

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 が起動し、作成を案内します:

Laurus REPL — no index found at ./my_index.
Use 'create index <schema_path>' to create one, or 'help' for commands.
laurus>

利用可能なコマンド

コマンドは CLI と同じ <操作> <リソース> の順序に従います。

コマンド説明
create index [schema_path]インデックスを作成(パス省略時は対話型ウィザード)
create schema <output_path>対話型スキーマ生成ウィザード
search <query>インデックスを検索
add field <name> <json>スキーマにフィールドを追加
add doc <id> <json>ドキュメントを追加(追記、同一 ID で複数チャンク可)
put doc <id> <json>ドキュメントを上書き(同一 ID の既存チャンクを置換)
get statsインデックスの統計情報を表示
get schema現在のスキーマを表示
get docs <id>ID で全ドキュメント(チャンクを含む)を取得
delete field <name>スキーマからフィールドを削除
delete docs <id>ID で全ドキュメント(チャンクを含む)を削除
commit保留中の変更をコミット
help利用可能なコマンドを表示
quit / exitREPL を終了

注意: createhelpquit 以外のコマンドはインデックスがロードされている必要があります。インデックスがロードされていない場合、まず create index を実行するようメッセージが表示されます。

使用例

インデックスの作成

laurus> create index ./schema.toml
Index created at ./my_index.
laurus> add doc doc1 {"title":"Hello","body":"World"}
Document 'doc1' added.

検索

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 docs doc4
╭──────┬───────────────────────────────────────────────╮
│ ID   │ Fields                                        │
├──────┼───────────────────────────────────────────────┤
│ doc4 │ body: Some content here., title: New Document │
╰──────┴───────────────────────────────────────────────╯

ドキュメントの削除

laurus> delete docs doc4
Documents '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;
  DynamicFieldPolicy dynamic_field_policy = 5;
}

enum DynamicFieldPolicy {
  DYNAMIC_FIELD_POLICY_UNSPECIFIED = 0;
  DYNAMIC_FIELD_POLICY_STRICT = 1;
  DYNAMIC_FIELD_POLICY_DYNAMIC = 2;
  DYNAMIC_FIELD_POLICY_IGNORE = 3;
}
  • fields — フィールド名をキーとしたフィールド定義。
  • default_fields — クエリでフィールドを指定しない場合のデフォルト検索対象フィールド名。
  • analyzers — 名前をキーとしたカスタムアナライザーパイプライン。TextOption.analyzer で参照。
  • embedders — 名前をキーとしたエンベッダー設定。ベクトルフィールドオプション(HnswOption.embedder など)で参照。
  • dynamic_field_policy — 投入されたドキュメントに含まれるが fields宣言されていないフィールドの扱い。UNSPECIFIED(値 0)は後方互換のため DYNAMIC として解釈されます。挙動マトリクスおよび DYNAMIC での情報損失警告は スキーマとフィールド を参照してください。

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, multi_valued)FlatOption (dimension, distance, base_weight, quantizer, embedder)
FloatOption (indexed, stored, multi_valued)IvfOption (dimension, distance, n_clusters, n_probe, base_weight, quantizer, embedder)
BooleanOption (indexed, stored)
DateTimeOption (indexed, stored)
GeoOption (indexed, stored)
Geo3dOption (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(緯度、経度)
Geo3dgeo3d_valueGeo3dPoint(x, y, z メートル単位、ECEF 直交座標系)

Geo3dPoint:

フィールド説明
xdoubleX 座標(メートル単位、ECEF: 赤道面、+X 方向は経度 0°)
ydoubleY 座標(メートル単位、ECEF: 赤道面、+Y 方向は東経 90°)
zdoubleZ 座標(メートル単位、ECEF: +Z 方向は北極)

座標系の詳細および wgs84_to_ecef / ecef_to_wgs84 の変換ユーティリティについては 3D 地理検索 (ECEF) を参照してください。

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 つ以上を指定する必要があります。

3D 地理クエリ

3D ECEF の地理クエリは SearchRequest.query に渡す Lexical DSL 文字列で表現します。専用のメッセージ型はなく、コアライブラリで使用される DSL 形式がそのまま gRPC 経由でも動作します。3 種類の形式があります(構文の詳細は Query DSL → 3D 地理クエリ を参照):

  • position:geo3d_distance(x, y, z, distance_m)(x, y, z) を中心とした最大距離(メートル単位)の球
  • position:geo3d_bbox(min_x, min_y, min_z, max_x, max_y, max_z) — 3D 軸並行バウンディングボックス
  • position:geo3d_nearest(x, y, z, k)(x, y, z) に最も近い k 個の近傍点

position はフィールド名で、スキーマで宣言した実際の Geo3d 型フィールドに置き換えてください。すべての数値引数は符号付きの double 値で、k は符号なし整数です。

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": {
      "dynamic_field_policy": "dynamic",
      "fields": {
        "title": {"text": {"indexed": true, "stored": true, "term_vectors": true}},
        "body": {"text": {"indexed": true, "stored": true, "term_vectors": true}}
      },
      "default_fields": ["title", "body"]
    }
  }'

dynamic_field_policy は省略可能なキーで、スキーマに宣言されていないフィールドの扱いを制御します。指定できる値は "strict" / "dynamic"(デフォルト)/ "ignore" の 3 種類です。詳細および "dynamic" での情報損失に関する警告は スキーマとフィールド を参照してください。

インデックス統計情報の取得

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 フィールド値の型推論

ドキュメント投入リクエスト(PUT /v1/documents/:id または POST /v1/documents/:id)のボディに含まれる document.fields の各値は、 スキーマレス取り込みと同じ推論ルールでエンジンの DataValue に変換されます。これにより HTTP 経路と gRPC 経路で挙動が一致します。

JSON 値推論されるフィールド型備考
null(スキップ)NullValue として送出され、取り込み時に破棄されます。
true / falseboolean
整数(i64 に収まる)integer
浮動小数点 / 巨大整数float
"text"text
[1, 2, 3](全要素 integer)integermulti_valued: true多値数値フィールド。
[1.0, 2.5](非整数を含む数値配列)floatmulti_valued: true
[](空配列)(スキップ)要素型を決定できないためフィールドはスキップされます。
{"latitude": ..., "longitude": ...}geo
{"lat": ..., "lon": ...} / {"lat": ..., "lng": ...}geolatitude / longitude の短縮別名を受け付けます。
{"x": ..., "y": ..., "z": ...}geo3d3 キーすべて必須、有限な数値、ECEF メートル単位。lat/lon キーとの混在は拒否されます。

以下の場合、ゲートウェイは HTTP 400(Bad Request)を返します:

  • 配列が混在型もしくは非数値要素を含む(例: [1, "x"]
  • オブジェクトが有効な地理座標ではない(2D は latitude/longitude キーが、 3D は x/y/z のいずれかが欠けている)
  • 緯度が [-90, 90] の範囲外、または経度が [-180, 180] の範囲外
  • 3D ECEF の座標が有限値でない(NaN / Inf
  • 同一オブジェクトに 2D(lat/lon)と 3D(x/y/z)のキーが混在

ベクトルおよびバイト列フィールドは JSON だけからは推論できないため、 スキーマで明示的に宣言する必要があります。宣言済みのベクトルフィールドに 数値配列が送られた場合は自動的に f32 ベクトルへキャストされるので、 REST クライアントは埋め込みベクトルを通常の JSON 配列として送信できます。

3D 地理クエリ

3D ECEF クエリは query に渡す Lexical DSL 文字列をそのまま再利用します。ゲートウェイは文字列を変更せずエンジンへ転送するため、gRPC 経由と同じ DSL 形式が HTTP 経由でも動作します:

curl -X POST http://localhost:8080/v1/search \
  -H 'Content-Type: application/json' \
  -d '{
    "query": "position:geo3d_distance(-3955182, 3350553, 3700276, 5000)",
    "limit": 10
  }'

geo3d_bbox および geo3d_nearest の構文は Query DSL → 3D 地理クエリ を参照してください。

リクエスト/レスポンス形式

すべてのリクエストおよびレスポンスボディは 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 --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 --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 --port 50051 &
laurus create index --schema schema.toml

# ステップ 3: MCP サーバーを Claude Code に登録
claude mcp add laurus -- laurus mcp --endpoint http://localhost:50051

ワークフロー 2: AI 主導のインデックス作成

laurus-server を起動してから MCP サーバーを登録し、AI にインデックスを作成させます:

# ステップ 1: laurus-server を起動(インデックス不要)
laurus serve --port 50051

# ステップ 2: MCP サーバーを Claude Code に登録
claude mcp add laurus -- laurus mcp --endpoint http://localhost:50051

次に Claude に依頼します:

「ブログ記事用の検索インデックスを作成してください。タイトルと本文テキストで検索できるようにして、著者と公開日も保存したいです。」

Claude はスキーマを設計して create_index を自動的に呼び出します。

ワークフロー 3: 実行時に接続する

エンドポイントを指定せずに MCP サーバーを登録します:

claude mcp add laurus -- laurus mcp

または設定ファイルを直接編集:

{
  "mcpServers": {
    "laurus": {
      "command": "laurus",
      "args": ["mcp"]
    }
  }
}

次に Claude に接続を依頼します:

http://localhost:50051 の laurus サーバーに接続してください」

Claude は他のツールを使用する前に connect を呼び出します。

MCP サーバーの削除

Claude Code から登録済みの MCP サーバーを削除するには:

claude mcp remove laurus

Claude Desktop の場合は、設定ファイルから laurus エントリを削除してアプリケーションを再起動してください。

ライフサイクル

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 表現を使用します(バリアント名がキーになります):

{
  "dynamic_field_policy": "Dynamic",
  "fields": {
    "title":     { "Text":    { "indexed": true, "stored": true } },
    "body":      { "Text":    {} },
    "score":     { "Float":   {} },
    "count":     { "Integer": {} },
    "active":    { "Boolean": {} },
    "created":   { "DateTime": {} },
    "embedding": { "Hnsw":    { "dimension": 384 } }
  }
}

オプションの dynamic_field_policy キーは、スキーマに宣言されていないフィールドが投入ドキュメントに含まれる場合の挙動を制御します。指定可能な値は "Strict" / "Dynamic"(デフォルト)/ "Ignore"警告: "Dynamic" では integer フィールドに入ってきた float 値が静かに切り捨てられます(3.143)。厳密さが必要なら "Strict" を使用してください。詳細な挙動マトリクスは スキーマとフィールド を参照してください。

Tool: create_index
schema_json: {"fields": {"title": {"Text": {}}, "body": {"Text": {}}}}

結果: Index created successfully at /path/to/index.

3D ECEF 座標を扱う Geo3d フィールドを含むスキーマ:

{
  "fields": {
    "title":    { "Text":  { "indexed": true, "stored": true } },
    "position": { "Geo3d": { "indexed": true, "stored": true } }
  }
}

座標系については 3D 地理検索 (ECEF) を参照してください。Geo3d フィールドは geo3d_distance / geo3d_bbox / geo3d_nearest の DSL 形式で検索できます(後述の search ツールを参照)。


get_stats

現在の検索インデックスの統計情報(ドキュメント数、ベクトルフィールド情報など)を取得します。

パラメーター

なし。

結果

{
  "document_count": 42,
  "vector_fields": ["embedding"]
}

get_schema

現在のインデックスのスキーマ(全フィールド定義と設定)を取得します。

パラメーター

なし。

結果

{
  "fields": {
    "title": { "Text": { "indexed": true, "stored": true } },
    "body": { "Text": {} },
    "embedding": { "Hnsw": { "dimension": 384 } }
  },
  "default_fields": ["title", "body"]
}

put_document

インデックスにドキュメントを上書き(upsert)します。同じ ID のドキュメントが既に存在する場合、全チャンクが削除されてから新しいドキュメントがインデックスされます。ドキュメント追加後は commit を呼び出してください。

パラメーター

名前必須説明
idstringはい外部ドキュメント識別子
documentobjectはいJSON オブジェクトとしてのドキュメントフィールド

Tool: put_document
id: "doc-1"
document: {"title": "Hello World", "body": "これはテストドキュメントです。"}

結果: Document 'doc-1' put (upserted). Call commit to persist changes.

Geo3d 値を含む例:

Tool: put_document
id: "drone-1"
document: {"title": "東京上空のドローン", "position": {"x": -3955182.0, "y": 3350553.0, "z": 3700276.0}}

MCP サーバーは 3D ECEF 点を xyz キーを持つ JSON オブジェクト(メートル単位)として受け付けます。これは HTTP ゲートウェイの挙動とは異なり、HTTP ゲートウェイでは現在 JSON から Geo3d を推論しません。MCP では書き込み・読み出しともに完全対応しています。


add_document

インデックスにドキュメントを新しいチャンクとして追加します。put_document とは異なり、同じ ID の既存ドキュメントを削除せずに追記します。大きなドキュメントをチャンクに分割する際に便利です。ドキュメント追加後は commit を呼び出してください。

パラメーター

名前必須説明
idstringはい外部ドキュメント識別子
documentobjectはいJSON オブジェクトとしてのドキュメントフィールド

Tool: add_document
id: "doc-1"
document: {"title": "Hello World - Part 2", "body": "これは続きです。"}

結果: Document 'doc-1' added as chunk. Call commit to persist changes.


get_documents

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

パラメーター

名前必須説明
idstringはい外部ドキュメント識別子

結果

{
  "id": "doc-1",
  "documents": [
    { "title": "Hello World", "body": "これはテストドキュメントです。" }
  ]
}

delete_documents

外部 ID で全ドキュメント(チャンクを含む)を削除します。削除後は commit を呼び出してください。

パラメーター

名前必須説明
idstringはい外部ドキュメント識別子

結果: Documents 'doc-1' deleted. Call commit to persist changes.


commit

保留中の変更をディスクにコミットします。変更を検索可能かつ永続的にするため、put_documentadd_document、または delete_documents の後に必ず呼び出してください。

パラメーター

なし。

結果: 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 を使用してドキュメントを検索します。Lexical 検索、Vector 検索、ハイブリッド検索を単一のクエリ文字列でサポートします。

パラメーター

名前必須説明
querystringはいlaurus 統一クエリ DSL による検索クエリ
limitintegerいいえ最大結果数(デフォルト: 10)
offsetintegerいいえページネーション用スキップ数(デフォルト: 0)
fusionstringいいえハイブリッド検索用の融合アルゴリズム(JSON)
field_boostsstringいいえフィールド毎のブースト係数(JSON)

クエリ DSL の例

Lexical 検索

クエリ説明
helloデフォルトフィールド全体のターム検索
title:helloフィールド指定のターム検索
title:hello AND body:worldブール AND
"exact phrase"フレーズ検索
roam~2ファジー検索(編集距離 2)
count:[1 TO 10]範囲検索
title:helo~1フィールド指定のファジー検索

3D 地理検索

クエリ説明
position:geo3d_distance(x, y, z, distance_m)(x, y, z) を中心とした最大距離(メートル単位)の球
position:geo3d_bbox(min_x, min_y, min_z, max_x, max_y, max_z)3D 軸並行バウンディングボックス
position:geo3d_nearest(x, y, z, k)(x, y, z) に最も近い k 個の近傍点

position はフィールド名で、スキーマで宣言した実際の Geo3d 型フィールドに置き換えてください。完全な DSL 構文は Query DSL → 3D 地理クエリ を参照してください。

Vector 検索

クエリ説明
content:"cute kitten"特定フィールドでの Vector 検索(クォート付き)
content:python特定フィールドでの Vector 検索(クォートなし)
content:"cute kitten"^0.8重み付き Vector 検索
a:"cats" b:"dogs"^0.5複数の Vector クエリ

ハイブリッド検索

クエリ説明
title:hello content:"cute kitten"Lexical + Vector(OR/union — いずれかの結果を返す)
title:hello +content:"cute kitten"Lexical + Vector(AND/intersection — 両方にマッチした結果のみ)
+title:hello +content:"cute kitten"両方必須(AND)。Lexical フィールドの + は required clause
title:hello AND body:world content:"cats"^0.8ブール Lexical + 重み付き Vector

融合アルゴリズムの例

{"rrf": {"k": 60.0}}
{"weighted_sum": {"lexical_weight": 0.7, "vector_weight": 0.3}}

フィールドブーストの例

{"title": 2.0, "body": 1.0}

結果

{
  "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        → フィールドを追加(必要に応じて)
   delete_field     → フィールドを削除(必要に応じて)
4. put_document     → ドキュメントを上書き(必要に応じて繰り返し)
   add_document     → ドキュメントチャンクを追記(必要に応じて)
5. commit           → 変更をディスクに永続化
6. search           → インデックスを検索
7. get_documents    → ID でドキュメントを取得
8. delete_documents → ドキュメントを削除
9. commit           → 変更を永続化

Python バインディング概要

laurus-python パッケージは Laurus 検索エンジンの Python バインディングです。PyO3Maturin を使ってネイティブ Rust 拡張としてビルドされており、Python プログラムからネイティブに近いパフォーマンスで Laurus の Lexical 検索、Vector 検索、ハイブリッド検索機能を利用できます。

機能

  • Lexical 検索 – BM25 スコアリングを備えた転置インデックスによる全文検索
  • Vector 検索 – Flat、HNSW、IVF インデックスを使用した近似最近傍(ANN)検索
  • ハイブリッド検索 – フュージョンアルゴリズム(RRF、WeightedSum)で Lexical と Vector の結果を統合
  • 豊富なクエリ DSL – Term、Phrase、Fuzzy、Wildcard、NumericRange、Geo、Boolean、Span クエリ
  • テキスト解析 – トークナイザー、フィルター、ステマー、同義語展開
  • 柔軟なストレージ – インメモリ(一時的)またはファイルベース(永続的)インデックス
  • Python らしい API – 型情報を備えた直感的な Python クラス

アーキテクチャ

graph LR
    subgraph "laurus-python"
        PyIndex["Index\n(Python クラス)"]
        PyQuery["クエリクラス"]
        PySearch["SearchRequest\n/ SearchResult"]
    end

    Python["Python アプリケーション"] -->|"メソッド呼び出し"| PyIndex
    Python -->|"クエリオブジェクト"| PyQuery
    PyIndex -->|"PyO3 FFI"| Engine["laurus::Engine\n(Rust)"]
    PyQuery -->|"PyO3 FFI"| Engine
    Engine --> Storage["ストレージ\n(Memory / File)"]

Python クラスは Rust エンジンの薄いラッパーです。 各呼び出しは PyO3 の FFI 境界を一度だけ越え、その後 Rust エンジンが操作をネイティブコードで実行します。

Rust エンジン内部は非同期 I/O を使用していますが、 Python 側のメソッドはすべて同期関数として公開されています。 これは Python の GIL(Global Interpreter Lock)の制約により、 単一インタプリタ内での真の並行実行ができないためです。 非同期 API にすると asyncio.run() が常に必要になり、 API が煩雑になります。代わりに、各メソッドは内部で tokio::Runtime::block_on() を呼び出し、非同期 Rust を 同期 Python にブリッジしています。

注意: Node.js バインディング(laurus-nodejs)では、 同じ Rust エンジンのメソッドをネイティブな async / Promise API として公開しています。 Node.js のイベントループは非同期をネイティブにサポート しているためです。

クイックスタート

import laurus

# インメモリインデックスを作成
index = laurus.Index()

# ドキュメントをインデックス
index.put_document("doc1", {"title": "Rust 入門", "body": "システムプログラミング言語です。"})
index.put_document("doc2", {"title": "Python データサイエンス", "body": "Python によるデータ解析。"})
index.commit()

# 検索
results = index.search("title:rust", limit=5)
for r in results:
    print(f"[{r.id}] score={r.score:.4f}  {r.document['title']}")

セクション

インストール

PyPI からインストール

pip install laurus

ソースからビルド

ソースからビルドするには Rust ツールチェーン(1.75 以降)と Maturin が必要です。

# Maturin をインストール
pip install maturin

# リポジトリをクローン
git clone https://github.com/mosuka/laurus.git
cd laurus/laurus-python

# 開発モードでビルドとインストール
maturin develop

# またはリリースホイールをビルド
maturin build --release
pip install target/wheels/laurus-*.whl

動作確認

import laurus
index = laurus.Index()
print(index)  # Index()

動作要件

  • Python 3.8 以降
  • コンパイル済みネイティブ拡張以外のランタイム依存関係なし

クイックスタート

1. インデックスを作成する

import laurus

# インメモリインデックス(一時的、プロトタイピングに最適)
index = laurus.Index()

# ファイルベースインデックス(永続的)
schema = laurus.Schema()
schema.add_text_field("title")
schema.add_text_field("body")
index = laurus.Index(path="./myindex", schema=schema)

2. ドキュメントをインデックスする

index.put_document("doc1", {
    "title": "Rust 入門",
    "body": "Rust は安全性とパフォーマンスに重点を置いたシステムプログラミング言語です。",
})
index.put_document("doc2", {
    "title": "Python データサイエンス",
    "body": "Python はデータ解析と機械学習に広く使われています。",
})
index.commit()

3. Lexical 検索

# DSL 文字列
results = index.search("title:rust", limit=5)

# クエリオブジェクト
results = index.search(laurus.TermQuery("body", "python"), limit=5)

# 結果を表示
for r in results:
    print(f"[{r.id}] score={r.score:.4f}  {r.document['title']}")

4. Vector 検索

Vector 検索にはベクトルフィールドを含むスキーマと事前計算済みエンベディングが必要です。

import laurus

schema = laurus.Schema()
schema.add_text_field("title")
schema.add_hnsw_field("embedding", dimension=4)

index = laurus.Index(schema=schema)
index.put_document("doc1", {"title": "Rust", "embedding": [0.1, 0.2, 0.3, 0.4]})
index.put_document("doc2", {"title": "Python", "embedding": [0.9, 0.8, 0.7, 0.6]})
index.commit()

query_vec = [0.1, 0.2, 0.3, 0.4]
results = index.search(laurus.VectorQuery("embedding", query_vec), limit=3)

5. ハイブリッド検索

request = laurus.SearchRequest(
    lexical_query=laurus.TermQuery("title", "rust"),
    vector_query=laurus.VectorQuery("embedding", query_vec),
    fusion=laurus.RRF(k=60.0),
    limit=5,
)
results = index.search(request)

6. 更新と削除

# 更新: put_document は同じ ID の全バージョンを置換する
index.put_document("doc1", {"title": "更新されたタイトル", "body": "新しいコンテンツ。"})
index.commit()

# 既存バージョンを削除せずに新しいバージョンを追記(RAG チャンキングパターン)
index.add_document("doc1", {"title": "チャンク 2", "body": "追加のチャンク。"})
index.commit()

# 全バージョンを取得
docs = index.get_documents("doc1")

# 削除
index.delete_documents("doc1")
index.commit()

7. スキーマ管理

schema = laurus.Schema()
schema.add_text_field("title")
schema.add_text_field("body")
schema.add_integer_field("year")
schema.add_float_field("score")
schema.add_boolean_field("published")
schema.add_bytes_field("thumbnail")
schema.add_geo_field("location")
schema.add_datetime_field("created_at")
schema.add_hnsw_field("embedding", dimension=384)
schema.add_flat_field("small_vec", dimension=64)
schema.add_ivf_field("ivf_vec", dimension=128, n_clusters=100)

8. インデックス統計

stats = index.stats()
print(stats["document_count"])
print(stats["vector_fields"])

API リファレンス

Index

Laurus 検索エンジンをラップするメインクラスです。

class Index:
    def __init__(self, path: str | None = None, schema: Schema | None = None) -> None: ...

コンストラクタ

パラメータデフォルト説明
pathstr | NoneNone永続ストレージのディレクトリパス。None の場合はインメモリインデックスを作成します。
schemaSchema | NoneNoneスキーマ定義。省略時は空のスキーマが使用されます。

メソッド

メソッド説明
put_document(id, doc)ドキュメントをアップサート(upsert)します。同じ ID の既存バージョンをすべて置換します。
add_document(id, doc)既存バージョンを削除せずにドキュメントチャンクを追記します。
get_documents(id) -> list[dict]指定 ID の全保存バージョンを返します。
delete_documents(id)指定 ID の全バージョンを削除します。
commit()バッファリングされた書き込みをフラッシュし、すべての保留中の変更を検索可能にします。
search(query, *, limit=10, offset=0) -> list[SearchResult]検索クエリを実行します。
stats() -> dictインデックス統計(document_countvector_fields)を返します。

search の query 引数

query パラメータは以下のいずれかを受け付けます:

  • DSL 文字列(例: "title:hello""content:\"memory safety\"")
  • Lexical クエリオブジェクトTermQueryPhraseQueryBooleanQuery など)
  • Vector クエリオブジェクトVectorQueryVectorTextQuery
  • SearchRequest(完全な制御が必要な場合)

Schema

Index のフィールドとインデックスタイプを定義します。

class Schema:
    def __init__(self) -> None: ...

フィールドメソッド

メソッド説明
add_text_field(name, *, stored=True, indexed=True, term_vectors=False, analyzer=None)全文フィールド(転置インデックス、BM25)。analyzer には組込名("standard" / "english" / "keyword" / "simple" / "noop"、または add_analyzer で登録したカスタム名)か、{"language": "japanese", "mode": "normal", "dict": "/var/lib/lindera/ipadic"} のようなパラメータ付きプリセットの dict を渡せます。文字列単独の "japanese" は Lindera 辞書パスが必須なため拒否されます。
add_integer_field(name, *, stored=True, indexed=True, multi_valued=False)64 ビット整数フィールド。multi_valued=True で整数配列を受け付け(範囲クエリは “any match”)。
add_float_field(name, *, stored=True, indexed=True, multi_valued=False)64 ビット浮動小数点フィールド。multi_valued=True で浮動小数点配列を受け付け(範囲クエリは “any match”)。
add_boolean_field(name, *, stored=True, indexed=True)ブールフィールド。
add_bytes_field(name, *, stored=True)生バイトフィールド。
add_geo_field(name, *, stored=True, indexed=True)地理座標フィールド(緯度/経度)。
add_geo3d_field(name, *, stored=True, indexed=True)3D ECEF カルテシアン座標フィールド(x, y, z はメートル)。詳細は Geo3d の概念
add_datetime_field(name, *, stored=True, indexed=True)UTC 日時フィールド。
add_hnsw_field(name, dimension, *, distance="cosine", m=16, ef_construction=200, embedder=None)HNSW 近似最近傍ベクトルフィールド。
add_flat_field(name, dimension, *, distance="cosine", embedder=None)Flat(総当たり)ベクトルフィールド。
add_ivf_field(name, dimension, *, distance="cosine", n_clusters=100, n_probe=1, embedder=None)IVF 近似最近傍ベクトルフィールド。

その他のメソッド

メソッド説明
add_embedder(name, config)名前付きエンベダー定義を登録します。config"type" キーを持つ辞書です(下記参照)。
set_default_fields(fields)デフォルト検索フィールドを設定(文字列のリスト)。
set_dynamic_field_policy(policy)未宣言フィールドの扱いを設定。policy"strict" / "dynamic"(デフォルト)/ "ignore"。詳細は下記を参照。
dynamic_field_policy()現在のポリシーを小文字の文字列で返す。
field_names()全フィールド名を返す。

Dynamic field policy(動的フィールドポリシー)

ドキュメントに含まれるがスキーマに宣言されていないフィールドの扱いを制御します:

  • "strict" — ドキュメントを拒否
  • "dynamic"(デフォルト)— 各未宣言フィールドの型を推論してスキーマに追加。警告: integer フィールドに入ってきた float 値は静かに切り捨てられます(3.143)。厳密さが必要なら "strict" を使用してください
  • "ignore" — 未宣言フィールドを静かに破棄

詳細な挙動マトリクスは スキーマとフィールド を参照してください。

エンベダータイプ

"type"必須キーFeature Flag
"precomputed"(常に利用可能)
"candle_bert""model"embeddings-candle
"candle_clip""model"embeddings-multimodal
"openai""model"embeddings-openai

距離メトリクス

説明
"cosine"コサイン類似度(デフォルト)
"euclidean"ユークリッド距離
"dot_product"内積
"manhattan"マンハッタン距離
"angular"角度距離

クエリクラス

TermQuery

TermQuery(field: str, term: str)

指定フィールドに完全一致する語句を含むドキュメントを検索します。

PhraseQuery

PhraseQuery(field: str, terms: list[str])

指定した語句が順序どおりに含まれるドキュメントを検索します。

FuzzyQuery

FuzzyQuery(field: str, term: str, *, max_edits: int = 2)

編集距離が max_edits 以内の近似一致を検索します。max_edits はキーワード専用引数です。

WildcardQuery

WildcardQuery(field: str, pattern: str)

ワイルドカードパターン検索。* は任意の文字列、? は任意の1文字に一致します。

NumericRangeQuery

NumericRangeQuery(field: str, *, min: int | float | None = None, max: int | float | None = None)

[min, max] の範囲内の数値を検索します。開いた境界には None を指定する (または省略する)と開放されます。minmax はキーワード専用引数です。 数値型(整数または浮動小数点)は min/max の Python 型から推論されます。

GeoDistanceQuery

GeoDistanceQuery.within_radius(
    field: str, lat: float, lon: float, distance_m: float,
)

地理的距離検索(半径指定)。指定した地点から distance_m メートル以内の (lat, lon) 座標を持つドキュメントを返します。

GeoBoundingBoxQuery

GeoBoundingBoxQuery.within_bounding_box(
    field: str,
    min_lat: float, min_lon: float,
    max_lat: float, max_lon: float,
)

地理的範囲(バウンディングボックス)検索。軸並行 [min_lat, max_lat] × [min_lon, max_lon] 内の (lat, lon) 座標を持つドキュメントを返します。

Geo3dDistanceQuery

Geo3dDistanceQuery.within_sphere(
    field: str, x: float, y: float, z: float, distance_m: float,
)

3D ECEF 座標フィールドへの球距離検索。中心 (x, y, z) から distance_m メートル以内 の座標を持つドキュメントを返します。ECEF の理論については Geo3d の概念 を参照。

Geo3dBoundingBoxQuery

Geo3dBoundingBoxQuery.within_box(
    field: str,
    min_x: float, min_y: float, min_z: float,
    max_x: float, max_y: float, max_z: float,
)

軸並行 3D 範囲(AABB)検索。[min_x, max_x] × [min_y, max_y] × [min_z, max_z] 内 にある ECEF 座標を持つドキュメントを返します。

Geo3dNearestQuery

Geo3dNearestQuery.k_nearest(
    field: str,
    x: float, y: float, z: float,
    k: int,
    *,
    initial_radius_m: float | None = None,
    max_radius_m: float | None = None,
)

3D ECEF 座標フィールドへの k 最近傍検索。(x, y, z) から最も近い k 件のドキュ メントを返します。initial_radius_m / max_radius_m は反復拡張サーチの探索コーン を調整します。

BooleanQuery

bq = BooleanQuery()
bq.must(query)
bq.should(query)
bq.must_not(query)

複合ブールクエリ。引数なしでコンストラクタを呼び出し、must / should / must_not メソッドで節を一つずつ追加します。各メソッドは任意のクエリ オブジェクト(ネストされた BooleanQuery も含む)を受け付けます。

must 節はすべて一致する必要があり、must_not 節は一致してはなりません。 should 節はスコアリングに寄与し、must 節が無い場合は少なくとも1つが 一致する必要があります。

SpanQuery

# 単一語句
SpanQuery.term(field: str, term: str)

# Near: slop 位置以内の語句
SpanQuery.near(field: str, terms: list[str], *, slop: int = 0, ordered: bool = True)

# ネストされた SpanQuery 句を使った Near
SpanQuery.near_spans(field: str, clauses: list[SpanQuery], *, slop: int = 0, ordered: bool = True)

# Containing: big スパンが little スパンを含む
SpanQuery.containing(field: str, big: SpanQuery, little: SpanQuery)

# Within: 最大距離での include スパンと exclude スパン
SpanQuery.within(field: str, include: SpanQuery, exclude: SpanQuery, distance: int)

位置・近接スパンクエリ。静的ファクトリメソッドで構築します。near は語句 文字列のリストを受け取り、near_spans はネスト式のために SpanQuery オブジェクトのリストを受け取ります。slopordered はキーワード専用 引数です。

VectorQuery

VectorQuery(field: str, vector: list[float])

事前計算済みエンベディングベクトルを使った近似最近傍検索を行います。

VectorTextQuery

VectorTextQuery(field: str, text: str)

クエリ時に text をエンベディングに変換してベクトル検索を行います。インデックスにエンベダーの設定が必要です。


SearchRequest

高度な制御が必要な場合の完全なリクエストクラスです。

class SearchRequest:
    def __init__(
        self,
        *,
        query=None,
        lexical_query=None,
        vector_query=None,
        filter_query=None,
        fusion=None,
        limit: int = 10,
        offset: int = 0,
    ) -> None: ...
パラメータ説明
queryDSL 文字列または単一クエリオブジェクト。lexical_query / vector_query と排他的。
lexical_query明示的なハイブリッド検索の Lexical コンポーネント。
vector_query明示的なハイブリッド検索の Vector コンポーネント。
filter_queryスコアリング後に適用する Lexical フィルター。
fusionフュージョンアルゴリズム(RRF または WeightedSum)。両コンポーネント指定時のデフォルトは RRF(k=60)
limit最大結果件数(デフォルト 10)。
offsetページネーションオフセット(デフォルト 0)。

SearchResult

Index.search() が返すクラスです。

class SearchResult:
    id: str          # 外部ドキュメント識別子
    score: float     # 関連性スコア
    document: dict | None  # 取得されたフィールド値。stored=False の場合は None

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

RRF

RRF(k: float = 60.0)

逆順位フュージョン(Reciprocal Rank Fusion)。Lexical と Vector の結果リストを順位位置によってマージします。k は平滑化定数で、値が大きいほど上位ランクの影響が小さくなります。

WeightedSum

WeightedSum(lexical_weight: float = 0.5, vector_weight: float = 0.5)

両スコアリストをそれぞれ正規化した後、lexical_weight * lexical_score + vector_weight * vector_score として結合します。


テキスト解析

SynonymDictionary

class SynonymDictionary:
    def __init__(self) -> None: ...
    def add_synonym_group(self, synonyms: list[str]) -> None: ...

WhitespaceTokenizer

class WhitespaceTokenizer:
    def __init__(self) -> None: ...
    def tokenize(self, text: str) -> list[Token]: ...

SynonymGraphFilter

class SynonymGraphFilter:
    def __init__(
        self,
        dictionary: SynonymDictionary,
        keep_original: bool = True,
        boost: float = 1.0,
    ) -> None: ...
    def apply(self, tokens: list[Token]) -> list[Token]: ...

Token

class Token:
    text: str
    position: int
    start_offset: int
    end_offset: int
    boost: float
    stopped: bool
    position_increment: int
    position_length: int

フィールド値の型マッピング

Python の値は自動的に Laurus の DataValue 型に変換されます:

Python 型Laurus 型備考
NoneNull
boolBoolint より先にチェック
intInt64
floatFloat64
strText
bytesBytes
list[float]Vector要素は f32 に変換
(lat, lon) タプルGeo2 つの float
datetime.datetimeDateTimeisoformat() 経由で変換

Node.js バインディング概要

laurus-nodejs パッケージは、Laurus 検索エンジンの Node.js/TypeScript バインディングです。 napi-rs を使用したネイティブアドオンとして ビルドされており、Node.js プログラムから Laurus の Lexical 検索、 Vector 検索、ハイブリッド検索機能にネイティブに近い性能で アクセスできます。

特徴

  • Lexical 検索 – BM25 スコアリングによる転置インデックスベースの全文検索
  • Vector 検索 – Flat、HNSW、IVF インデックスによる近似最近傍(ANN)検索
  • ハイブリッド検索 – RRF、WeightedSum による Lexical と Vector の結果融合
  • 豊富なクエリ DSL – Term、Phrase、Fuzzy、Wildcard、 NumericRange、Geo、Boolean、Span クエリ
  • テキスト解析 – トークナイザー、フィルター、ステマー、同義語展開
  • 柔軟なストレージ – インメモリ(揮発性)またはファイルベース(永続)
  • TypeScript 型定義.d.ts ファイルの自動生成
  • 非同期 API – 全 I/O 操作が Promise を返す

アーキテクチャ

graph LR
    subgraph "laurus-nodejs"
        JsIndex["Index\n(JS クラス)"]
        JsQuery["Query クラス群"]
        JsSearch["SearchRequest\n/ SearchResult"]
    end

    Node["Node.js アプリケーション"] -->|"メソッド呼び出し"| JsIndex
    Node -->|"クエリオブジェクト"| JsQuery
    JsIndex -->|"napi-rs FFI"| Engine["laurus::Engine\n(Rust)"]
    JsQuery -->|"napi-rs FFI"| Engine
    Engine --> Storage["Storage\n(Memory / File)"]

JavaScript クラスは Rust エンジンの薄いラッパーです。 各呼び出しは napi-rs の FFI 境界を一度だけ越え、 Rust エンジンが完全にネイティブコードで処理を実行します。

全 I/O メソッド(searchcommitputDocument 等)は async で Promise を返します。napi-rs 内蔵の tokio ランタイムで実行され、Node.js のイベントループをブロック しません。Schema 構築、Query 作成、stats() は I/O を 伴わないため同期メソッドです。

注意: Python バインディング(laurus-python)では、 同じ Rust エンジンのメソッドを同期関数として公開 しています。Python の GIL(Global Interpreter Lock)の 制約により非同期 API が煩雑になるためです。Node.js には この制約がないため、非同期 Rust エンジンを直接 Promise として公開しています。

クイックスタート

import { Index, Schema } from "laurus-nodejs";

// インメモリインデックスを作成
const schema = new Schema();
schema.addTextField("name");
schema.addTextField("description");
schema.setDefaultFields(["name", "description"]);

const index = await Index.create(null, schema);

// ドキュメントをインデックス
await index.putDocument("express", {
  name: "Express",
  description: "Fast minimalist web framework for Node.js.",
});
await index.putDocument("fastify", {
  name: "Fastify",
  description: "Fast and low overhead web framework.",
});
await index.commit();

// 検索
const results = await index.search("framework", 5);
for (const r of results) {
  console.log(`[${r.id}] score=${r.score.toFixed(4)}  ${r.document.name}`);
}

セクション

インストール

npm から

npm install laurus-nodejs

ソースから

ソースからビルドするには Rust ツールチェーン(1.85 以降)と Node.js 18 以上が必要です。

# リポジトリをクローン
git clone https://github.com/mosuka/laurus.git
cd laurus/laurus-nodejs

# 依存パッケージのインストール
npm install

# ネイティブモジュールのビルド(リリース)
npm run build

# デバッグモード(ビルドが速い)
npm run build:debug

確認

import { Index } from "laurus-nodejs";
const index = await Index.create();
console.log(index.stats());
// { documentCount: 0, vectorFields: {} }

要件

  • Node.js 18 以上
  • コンパイル済みネイティブアドオン以外のランタイム依存なし

クイックスタート

1. インデックスの作成

import { Index, Schema } from "laurus-nodejs";

// インメモリインデックス(揮発性、プロトタイピング向け)
const index = await Index.create();

// ファイルベースインデックス(永続化)
const schema = new Schema();
schema.addTextField("name");
schema.addTextField("description");
const persistentIndex = await Index.create("./myindex", schema);

2. ドキュメントのインデックス

await index.putDocument("express", {
  name: "Express",
  description: "Fast minimalist web framework for Node.js.",
});
await index.putDocument("fastify", {
  name: "Fastify",
  description: "Fast and low overhead web framework.",
});
await index.commit();

3. Lexical 検索

// DSL 文字列
const results = await index.search("name:express", 5);

// Term クエリ
const results2 = await index.searchTerm(
  "description", "framework", 5,
);

// 結果の表示
for (const r of results) {
  console.log(`[${r.id}] score=${r.score.toFixed(4)}  ${r.document.name}`);
}

4. Vector 検索

Vector 検索にはベクトルフィールドを持つスキーマと 事前計算済みの埋め込みベクトルが必要です。

import { Index, Schema } from "laurus-nodejs";

const schema = new Schema();
schema.addTextField("name");
schema.addHnswField("embedding", 4);

const index = await Index.create(null, schema);
await index.putDocument("express", {
  name: "Express",
  embedding: [0.1, 0.2, 0.3, 0.4],
});
await index.putDocument("pg", {
  name: "pg",
  embedding: [0.9, 0.8, 0.7, 0.6],
});
await index.commit();

const results = await index.searchVector(
  "embedding", [0.1, 0.2, 0.3, 0.4], 3,
);

5. ハイブリッド検索

import {
  Index,
  RRF,
  SearchRequest,
  TermQuery,
  VectorQuery,
} from "laurus-nodejs";

const req = new SearchRequest({ limit: 5 });
req.setLexicalTerm(new TermQuery("name", "express"));
req.setVectorQuery(new VectorQuery("embedding", [0.1, 0.2, 0.3, 0.4]));
req.setRrfFusion(new RRF(60.0));

const results = await index.searchWithRequest(req);

6. 更新と削除

// 更新: putDocument は既存バージョンをすべて置換
await index.putDocument("express", {
  name: "Express v5",
  description: "Updated content.",
});
await index.commit();

// バージョン追記(RAG チャンキングパターン)
await index.addDocument("express", {
  name: "Express chunk 2",
  description: "Additional chunk.",
});
await index.commit();

// 全バージョンの取得
const docs = await index.getDocuments("express");

// 削除
await index.deleteDocuments("express");
await index.commit();

7. スキーマ管理

const schema = new Schema();
schema.addTextField("name");
schema.addTextField("description");
schema.addIntegerField("stars");
schema.addFloatField("score");
schema.addBooleanField("published");
schema.addBytesField("thumbnail");
schema.addGeoField("location");
schema.addDatetimeField("createdAt");
schema.addHnswField("embedding", 384);
schema.addFlatField("smallVec", 64);
schema.addIvfField("ivfVec", 128, "cosine", 100, 1);

8. インデックス統計

const stats = index.stats();
console.log(stats.documentCount);
console.log(stats.vectorFields);

API リファレンス

Index

主要なエントリポイント。Laurus 検索エンジンをラップします。

class Index {
  static create(
    path?: string | null,
    schema?: Schema,
  ): Promise<Index>;
}

ファクトリメソッド

パラメータデフォルト説明
pathstring | nullnull永続化ストレージのディレクトリ。null でインメモリ。
schemaSchemaスキーマ定義。

メソッド

メソッド説明
putDocument(id, doc)ドキュメントを上書き保存。
addDocument(id, doc)既存バージョンを残してチャンクを追記。
getDocuments(id)指定 ID の全バージョンを取得。
deleteDocuments(id)指定 ID の全バージョンを削除。
commit()書き込みをフラッシュし変更を検索可能にする。
search(query, limit?, offset?)DSL 文字列で検索。
searchTerm(field, term, limit?, offset?)完全一致 Term 検索。
searchVector(field, vector, limit?, offset?)事前計算ベクトルで検索。
searchVectorText(field, text, limit?, offset?)テキストを自動埋め込みして検索。
searchWithRequest(request)SearchRequest で検索。
stats()インデックス統計を返す。

ドキュメント操作と検索メソッドはすべて非同期で Promise を返します。 stats() は同期メソッドです。


Schema

Index のフィールドとインデックス型を定義します。

class Schema {
  constructor();
}

フィールドメソッド

メソッド説明
addTextField(name, stored?, indexed?, termVectors?, analyzer?)全文検索フィールド(転置インデックス、BM25)。analyzer にはパラメータ不要の組込名("standard" / "english" / "keyword" / "simple" / "noop"、または addAnalyzer で登録したカスタム名)を指定します。Lindera 辞書パスが必要な Japanese プリセットを使う場合は、lindera tokenizer を含むカスタム analyzer を登録して、その名前を参照してください。
addIntegerField(name, stored?, indexed?, multiValued?)64 ビット整数フィールド。multiValued: true で整数配列を受け付け(範囲クエリは “any match”)。
addFloatField(name, stored?, indexed?, multiValued?)64 ビット浮動小数点フィールド。multiValued: true で浮動小数点配列を受け付け(範囲クエリは “any match”)。
addBooleanField(name, stored?, indexed?)真偽値フィールド。
addBytesField(name, stored?)バイナリデータフィールド。
addGeoField(name, stored?, indexed?)地理座標フィールド。
addGeo3dField(name, stored?, indexed?)3D ECEF カルテシアン座標フィールド(x, y, z はメートル)。詳細は Geo3d の概念
addDatetimeField(name, stored?, indexed?)UTC 日時フィールド。
addHnswField(name, dimension, distance?, m?, efConstruction?, embedder?)HNSW ベクトルフィールド。
addFlatField(name, dimension, distance?, embedder?)Flat(全探索)ベクトルフィールド。
addIvfField(name, dimension, distance?, nClusters?, nProbe?, embedder?)IVF ベクトルフィールド。
addEmbedder(name, config)名前付き Embedder を登録。
setDefaultFields(fields)デフォルト検索フィールドを設定。
setDynamicFieldPolicy(policy)未宣言フィールドの扱いを設定。policy"strict" / "dynamic"(デフォルト)/ "ignore"。詳細は下記を参照。
dynamicFieldPolicy()現在のポリシーを小文字の文字列で返す。
fieldNames()全フィールド名を返す。
toString()スキーマの文字列表現("Schema(fields=[...])" 形式)を返す。

Dynamic field policy(動的フィールドポリシー)

ドキュメントに含まれるがスキーマに宣言されていないフィールドの扱いを制御します:

  • "strict" — ドキュメントを拒否
  • "dynamic"(デフォルト)— 各未宣言フィールドの型を推論してスキーマに追加。警告: integer フィールドに入ってきた float 値は静かに切り捨てられます(3.143)。厳密さが必要なら "strict" を使用してください
  • "ignore" — 未宣言フィールドを静かに破棄

詳細な挙動マトリクスは スキーマとフィールド を参照してください。

距離指標

説明
"cosine"コサイン類似度(デフォルト)
"euclidean"ユークリッド距離
"dot_product"内積
"manhattan"マンハッタン距離
"angular"角度距離

クエリクラス

TermQuery

new TermQuery(field: string, term: string)

指定フィールドで完全一致する Term を含むドキュメントにマッチ。

PhraseQuery

new PhraseQuery(field: string, terms: string[])

指定順序で Term を含むドキュメントにマッチ。

FuzzyQuery

new FuzzyQuery(field: string, term: string, maxEdits?: number)

最大 maxEdits 編集距離までの近似マッチ(デフォルト 2)。

WildcardQuery

new WildcardQuery(field: string, pattern: string)

パターンマッチ。* は任意の文字列、? は任意の1文字。

NumericRangeQuery

new NumericRangeQuery(
  field: string,
  min?: number | null,
  max?: number | null,
  numericType?: "integer" | "float",
)

[min, max] 範囲の数値にマッチします。null(または省略)で開放端。 numericType は内部の範囲型を選択します("integer"(デフォルト)または "float")。それ以外の値は例外をスローします。

GeoDistanceQuery

GeoDistanceQuery.withinRadius(
  field: string, lat: number, lon: number, distanceM: number,
): GeoDistanceQuery

地理的距離検索(半径指定)。

GeoBoundingBoxQuery

GeoBoundingBoxQuery.withinBoundingBox(
  field: string,
  minLat: number, minLon: number,
  maxLat: number, maxLon: number,
): GeoBoundingBoxQuery

地理的バウンディングボックス検索。

Geo3dDistanceQuery

Geo3dDistanceQuery.withinSphere(
  field: string,
  x: number, y: number, z: number,
  distanceM: number,
): Geo3dDistanceQuery

3D ECEF 座標フィールドへの球距離検索。中心から distanceM メートル以内の (x, y, z) 座標を持つドキュメントを返します。ECEF の理論については Geo3d の概念 を参照。

Geo3dBoundingBoxQuery

Geo3dBoundingBoxQuery.withinBox(
  field: string,
  minX: number, minY: number, minZ: number,
  maxX: number, maxY: number, maxZ: number,
): Geo3dBoundingBoxQuery

軸並行 3D 範囲(AABB)検索。

Geo3dNearestQuery

Geo3dNearestQuery.kNearest(
  field: string,
  x: number, y: number, z: number,
  k: number,
  initialRadiusM?: number,
  maxRadiusM?: number,
): Geo3dNearestQuery

3D ECEF 座標フィールドへの k 最近傍検索。initialRadiusM / maxRadiusM は 反復拡張サーチの探索コーンを調整します。

BooleanQuery

class BooleanQuery {
  constructor();
  // 各クエリタイプ X について(X は次のいずれか):
  //   { Term, Phrase, Fuzzy, Wildcard, NumericRange,
  //     GeoDistance, GeoBoundingBox,
  //     Geo3dDistance, Geo3dBoundingBox, Geo3dNearest,
  //     Boolean, Span }
  mustX(query: X): void;
  shouldX(query: X): void;
  mustNotX(query: X): void;
}

MUST / SHOULD / MUST_NOT 句による複合ブーリアンクエリ。各句は対応するクエリ クラスのインスタンスを引数に取ります。例: mustTerm(new TermQuery("body", "rust"))shouldGeo3dNearest(Geo3dNearestQuery.kNearest(...))

Node.js バインディングは多態 must(query) ではなく 36 個の per-type メソッド (12 クエリタイプ × 3 極性)を公開しています。これは js_name を上書きした クラスに対する napi-deriveEither<&T, ...> 引数バリデーションの制限を 回避するためです。

must 節はすべて一致する必要があり、mustNot 節は一致してはなりません。 should 節はスコアリングに寄与し、must 節が無い場合は少なくとも1つが 一致する必要があります。

const bq = new BooleanQuery();
bq.mustTerm(new TermQuery("body", "programming"));
bq.mustNotTerm(new TermQuery("title", "python"));
bq.shouldFuzzy(new FuzzyQuery("body", "data", 1));

SpanQuery

SpanQuery.term(field: string, term: string): SpanQuery
SpanQuery.near(
  field: string, terms: string[],
  slop?: number, ordered?: boolean,
): SpanQuery
SpanQuery.nearSpans(
  field: string, clauses: SpanQuery[],
  slop?: number, ordered?: boolean,
): SpanQuery
SpanQuery.containing(
  field: string, big: SpanQuery, little: SpanQuery,
): SpanQuery
SpanQuery.within(
  field: string,
  include: SpanQuery, exclude: SpanQuery, distance: number,
): SpanQuery

位置・近接ベースのスパンクエリ。

VectorQuery

new VectorQuery(field: string, vector: number[])

事前計算済み埋め込みベクトルによる最近傍検索。

VectorTextQuery

new VectorTextQuery(field: string, text: string)

クエリ時にテキストを埋め込みに変換して検索。 インデックスに Embedder の設定が必要。


SearchRequest

高度な制御のための全機能検索リクエスト。

interface SearchRequestOptions {
  queryDsl?: string;
  limit?: number;   // デフォルト 10
  offset?: number;  // デフォルト 0
}

class SearchRequest {
  constructor(options?: SearchRequestOptions);
}

コンストラクタにはプリミティブな options を渡し、多態クエリ句は下記の per-type セッターで設定します。BooleanQuery 同様、napi-deriveEither<&T, ...> バリデーション制限を回避するため per-type 化されています。

DSL とフュージョンセッター

メソッド説明
setQueryDsl(dsl: string)DSL 文字列クエリを設定。
setRrfFusion(rrf: RRF)RRF フュージョンを使用。
setWeightedSumFusion(ws: WeightedSum)加重和フュージョンを使用。

ベクトルセッター

メソッド説明
setVectorQuery(query: VectorQuery)事前計算ベクトルクエリを設定。
setVectorTextQuery(query: VectorTextQuery)テキストベースのベクトルクエリを設定(登録 Embedder で自動埋め込み)。

Lexical セッター(per-type)

X{ Term, Phrase, Fuzzy, Wildcard, NumericRange, GeoDistance, GeoBoundingBox, Geo3dDistance, Geo3dBoundingBox, Geo3dNearest, Boolean, Span } の各クエリタイプとして、以下のメソッドが公開されています:

メソッド説明
setLexicalX(query: X)明示的なハイブリッドリクエストの Lexical コンポーネントを設定。
setFilterX(query: X)スコアリング後のフィルタコンポーネントを設定。

合計 24 個の per-type セッター(12 lexical + 12 filter)に加え、上記の DSL / ベクトル / フュージョンセッターが利用可能です。

const req = new SearchRequest({ limit: 5 });
req.setLexicalTerm(new TermQuery("title", "rust"));
req.setVectorQuery(new VectorQuery("embedding", [0.1, 0.2, 0.3, 0.4]));
req.setRrfFusion(new RRF(60.0));
const results = await index.searchWithRequest(req);

SearchResult

検索メソッドが配列として返す結果。

interface SearchResult {
  id: string;        // 外部ドキュメント識別子
  score: number;     // 関連度スコア
  document: object | null; // 取得フィールド、stored=false の場合は null
}

融合アルゴリズム

RRF

new RRF(k?: number)  // デフォルト 60.0

Reciprocal Rank Fusion。ランク位置で Lexical と Vector の 結果リストを統合。

WeightedSum

new WeightedSum(
  lexicalWeight?: number,  // デフォルト 0.5
  vectorWeight?: number,   // デフォルト 0.5
)

両スコアリストを個別に正規化し、加重和で結合。


テキスト解析

SynonymDictionary

class SynonymDictionary {
  constructor();
  addSynonymGroup(terms: string[]): void;
}

WhitespaceTokenizer

class WhitespaceTokenizer {
  constructor();
  tokenize(text: string): Token[];
}

SynonymGraphFilter

class SynonymGraphFilter {
  constructor(
    dictionary: SynonymDictionary,
    keepOriginal?: boolean,  // デフォルト true
    boost?: number,          // デフォルト 1.0
  );
  apply(tokens: Token[]): Token[];
}

Token

interface Token {
  text: string;
  position: number;
  startOffset: number;
  endOffset: number;
  boost: number;
  stopped: boolean;
  positionIncrement: number;
  positionLength: number;
}

フィールド値の型

JavaScript の値は自動的に Laurus の DataValue 型に変換されます:

JavaScript 型Laurus 型備考
nullNull
booleanBool
number(整数)Int64
number(浮動小数点)Float64
stringTextISO 8601 文字列は DateTime になる
number[]Vectorf32 に変換
{ lat, lon }Geo2 つの number
{ x, y, z }GeoEcef3 つの number 値(メートル単位、3D ECEF 直交座標)

開発環境のセットアップ

laurus-nodejs バインディングのローカル開発環境の構築、 ビルド、テスト実行について説明します。

前提条件

  • Rust 1.85 以降(Cargo 含む)
  • Node.js 18 以降(npm 含む)
  • リポジトリがローカルにクローン済み
git clone https://github.com/mosuka/laurus.git
cd laurus

ビルド

開発ビルド

デバッグモードで Rust ネイティブアドオンをコンパイルします。 Rust ソースを変更した後は再実行してください。

cd laurus-nodejs
npm install
npm run build:debug

リリースビルド

npm run build

ビルドの確認

node -e "
const { Index } = require('./index.js');
Index.create().then(idx => console.log(idx.stats()));
"
// { documentCount: 0, vectorFields: {} }

テスト

テストには Vitest を使用し、 __tests__/ に配置されています。

# 全テスト実行
npm test

特定のテストを名前で実行:

npx vitest run -t "searches with DSL string"

リントとフォーマット

# Rust リント(Clippy)
cargo clippy -p laurus-nodejs -- -D warnings

# Rust フォーマットチェック
cargo fmt -p laurus-nodejs --check

# フォーマット適用
cargo fmt -p laurus-nodejs

クリーンアップ

# ビルド成果物の削除
rm -f *.node index.js index.d.ts

# node_modules の削除
rm -rf node_modules

プロジェクト構成

laurus-nodejs/
├── Cargo.toml          # Rust クレートマニフェスト
├── build.rs            # napi-build セットアップ
├── package.json        # npm パッケージメタデータ
├── README.md           # 英語 README
├── README_ja.md        # 日本語 README
├── src/                # Rust ソース(napi-rs バインディング)
│   ├── lib.rs          # モジュール登録
│   ├── index.rs        # Index クラス
│   ├── schema.rs       # Schema クラス
│   ├── query.rs        # Query クラス群
│   ├── search.rs       # SearchRequest / SearchResult / Fusion
│   ├── analysis.rs     # Tokenizer / Filter / Token
│   ├── convert.rs      # JS ↔ DataValue 変換
│   └── errors.rs       # エラーマッピング
├── __tests__/          # Vitest 統合テスト
│   └── index.spec.mjs
└── examples/           # 実行可能な Node.js サンプル
    ├── quickstart.mjs
    ├── lexical-search.mjs
    ├── vector-search.mjs
    └── hybrid-search.mjs

WASM バインディング概要

laurus-wasm パッケージは、Laurus 検索エンジンの WebAssembly バインディングです。 サーバーなしで、ブラウザやエッジランタイム(Cloudflare Workers、Vercel Edge Functions、Deno Deploy) 上で直接、レキシカル検索・ベクトル検索・ハイブリッド検索を実行できます。

機能

  • レキシカル検索 – BM25 スコアリングによる転置インデックスベースの全文検索
  • ベクトル検索 – Flat、HNSW、IVF インデックスによる近似最近傍探索
  • ハイブリッド検索 – RRF、WeightedSum による融合アルゴリズム
  • クエリ DSL – Term、Phrase、Fuzzy、Wildcard、NumericRange、Geo、Boolean、Span
  • テキスト分析 – トークナイザー、フィルター、同義語展開
  • インメモリストレージ – 高速な一時インデックス
  • OPFS 永続化 – Origin Private File System によるページリロード後のデータ保持
  • TypeScript 型定義 – 自動生成される .d.ts ファイル
  • 非同期 API – すべての I/O 操作は Promise を返す

アーキテクチャ

graph LR
    subgraph "laurus-wasm"
        WASM[wasm-bindgen API]
    end
    subgraph "laurus(コア)"
        Engine
        MemoryStorage
    end
    subgraph "ブラウザ"
        JS[JavaScript / TypeScript]
        OPFS[Origin Private File System]
    end
    JS --> WASM
    WASM --> Engine
    Engine --> MemoryStorage
    WASM -.->|永続化| OPFS

Embedding 戦略

ネイティブ環境では Laurus に複数の組み込み Embedder(Candle BERT、Candle CLIP、 OpenAI API)が用意されており、ドキュメントのインデックス時や searchVectorText("field", "query text") 実行時にエンジンが自動で呼び出します。 これらネイティブ Embedder は wasm32-unknown-unknown 上で動作しないため、 WASM ビルドでは無効化されています:

EmbedderDependencyWhy it cannot run in WASM
candle_bertcandle (GPU/SIMD)Requires native SIMD intrinsics and file system for models
candle_clipcandleSame as above
openaireqwest (HTTP)Requires a full async HTTP client (tokio + TLS)

(これらは embeddings-candle / embeddings-openai Feature Flags で管理されており、 wasm32-unknown-unknown で無効化される native feature に依存するため WASM ビルドから除外されます。)

laurus-wasm ではその代わりに以下 2 種類の addEmbedder タイプを公開しています:

  • "precomputed" — 呼び出し側が putDocument() / searchVector() 経由で ベクトルを直接渡します。エンジンは埋め込みを行いません。
  • "callback" — JavaScript コールバック embed: (text) => Promise<number[]> を登録し、エンジンがインジェスト時および searchVectorText() から呼び出します。Transformers.js 等のブラウザ内埋め込み ライブラリと組み合わせることでエンジン内自動埋め込みが実現でき、ネイティブ環境と 同じく searchVectorText("field", "query text") を呼び出すだけで利用できます。

Option A — 事前計算済みベクトル

JavaScript 側で埋め込みを計算し、事前計算済みベクトルを putDocument()searchVector() に渡します:

// Transformers.js を使用(all-MiniLM-L6-v2、384次元)
import { pipeline } from '@huggingface/transformers';

const embedder = await pipeline('feature-extraction', 'Xenova/all-MiniLM-L6-v2');

async function embed(text) {
  const output = await embedder(text, { pooling: 'mean', normalize: true });
  return Array.from(output.data);
}

// 事前計算済み埋め込みでインデックス
const vec = await embed("Rust 入門");
await index.putDocument("doc1", { title: "Rust 入門", embedding: vec });
await index.commit();

// 事前計算済みクエリ埋め込みで検索
const queryVec = await embed("安全なシステムプログラミング");
const results = await index.searchVector("embedding", queryVec);

このアプローチにより、ネイティブ環境と同じ Sentence Transformer モデルを使った セマンティック検索がブラウザ内で実現できます。埋め込み計算は candle ではなく Transformers.js(ONNX Runtime Web)が担当します。

Option B — Callback Embedder

Transformers.js の同じパイプラインを "callback" Embedder として登録すれば、 エンジンが自動で呼び出してくれます。登録後は呼び出し側がベクトルを管理することなく、 インジェストおよび searchVectorText() が透過的に動作します:

import { pipeline } from '@huggingface/transformers';

const extractor = await pipeline('feature-extraction', 'Xenova/all-MiniLM-L6-v2');

schema.addEmbedder("transformers", {
  type: "callback",
  embed: async (text) => {
    const output = await extractor(text, { pooling: 'mean', normalize: true });
    return Array.from(output.data);
  },
});
schema.addHnswField("embedding", 384, "cosine", undefined, undefined, "transformers");
const index = await Index.create(schema);

await index.putDocument("doc1", { title: "Rust 入門" });
await index.commit();

const results = await index.searchVectorText("embedding", "安全なシステムプログラミング");

Option A と比べると、Callback アプローチではエンジンがインジェスト時に埋め込みを キャッシュでき、書き込み側と読み出し側で埋め込みコードを重複させずに済みます。 ただし commit() のたびに JS コールバックの解決を待つため、大量バルク投入時には 事前計算ベクトルのほうが有利な場合があります。

laurus-wasm と laurus-nodejs の使い分け

基準laurus-wasmlaurus-nodejs
実行環境ブラウザ、エッジランタイムNode.js サーバー
パフォーマンス良好(シングルスレッド)最高(ネイティブ、マルチスレッド)
ストレージインメモリ + OPFSインメモリ + ファイルシステム
埋め込み事前計算 + JS コールバックCandle、OpenAI、事前計算
パッケージnpm install laurus-wasmnpm install laurus-nodejs
バイナリサイズ約 5-10 MB(WASM)プラットフォームネイティブ

インストール

npm / yarn / pnpm

npm install laurus-wasm
# または
yarn add laurus-wasm
# または
pnpm add laurus-wasm

CDN(ES Module)

<script type="module">
  import init, { Index, Schema } from 'https://unpkg.com/laurus-wasm/laurus_wasm.js';
  await init();
  // ...
</script>

ソースからビルド

前提条件:

git clone https://github.com/mosuka/laurus.git
cd laurus/laurus-wasm

# バンドラー向け(webpack、vite 等)
wasm-pack build --target bundler --release

# ブラウザ直接利用向け(<script type="module">)
wasm-pack build --target web --release

出力は pkg/ ディレクトリに生成されます。

ブラウザ対応状況

laurus-wasm は以下をサポートするブラウザが必要です:

  • WebAssembly(すべてのモダンブラウザ)
  • ES Modules

OPFS 永続化には以下のブラウザが対応しています:

ブラウザ最小バージョン
Chrome102+
Firefox111+
Safari15.2+
Edge102+

クイックスタート

基本的な使い方(インメモリ)

import init, { Index, Schema } from 'laurus-wasm';

// WASM モジュールを初期化
await init();

// スキーマを定義
const schema = new Schema();
schema.addTextField("title");
schema.addTextField("body");
schema.setDefaultFields(["title", "body"]);

// インメモリインデックスを作成
const index = await Index.create(schema);

// ドキュメントを追加
await index.putDocument("doc1", {
  title: "Rust 入門",
  body: "Rust はシステムプログラミング言語です"
});
await index.putDocument("doc2", {
  title: "WebAssembly ガイド",
  body: "WASM はブラウザでネイティブに近いパフォーマンスを実現します"
});
await index.commit();

// 検索
const results = await index.search("rust");
for (const result of results) {
  console.log(`${result.id}: ${result.score}`);
  console.log(result.document);
}

永続化ストレージ(OPFS)

import init, { Index, Schema } from 'laurus-wasm';

await init();

const schema = new Schema();
schema.addTextField("title");
schema.addTextField("body");

// 永続化インデックスを開く(ページリロード後もデータが保持される)
const index = await Index.open("my-index", schema);

// ドキュメントを追加
await index.putDocument("doc1", {
  title: "Hello",
  body: "World"
});

// commit() で自動的に OPFS に永続化される
await index.commit();

// 次のページロード時、Index.open("my-index") でデータが復元される

日本語形態素検索

ブラウザ WASM では Lindera 辞書をファイルシステムパスで指定できないため、 OPFS にロードした IPADIC のバイト列から analyzer を構築します。

import init, { Index, Schema, JapaneseAnalyzer } from 'laurus-wasm';
import {
  downloadDictionary,
  loadDictionaryFiles,
  hasDictionary,
} from 'laurus-wasm/opfs';

await init();

// 1. 初回訪問時に IPADIC アーカイブを OPFS にキャッシュする。zip は
//    アプリと同一オリジンで配信する必要がある(GitHub Releases は CORS
//    でブロックされる)。圧縮 ~16 MB / 展開後 ~58 MB。
if (!(await hasDictionary("ipadic"))) {
  await downloadDictionary("./dict/lindera-ipadic.zip", "ipadic", {
    onProgress: ({ phase, loaded, total }) => console.log(phase, loaded, total),
  });
}

// 2. 8 つのコンポーネントファイルを読み出して analyzer を構築する。
const f = await loadDictionaryFiles("ipadic");
const ja = JapaneseAnalyzer.fromBytes(
  f.metadata, f.dictDa, f.dictVals, f.dictWordsIdx,
  f.dictWords, f.matrixMtx, f.charDef, f.unk,
  "normal",
);

// 3. analyzer をスキーマに登録し、テキストフィールドから名前で参照する。
const schema = new Schema();
schema.addAnalyzer("ja-ipadic", ja);
schema.addTextField("title", undefined, undefined, undefined, "ja-ipadic");
schema.addTextField("body", undefined, undefined, undefined, "ja-ipadic");
schema.setDefaultFields(["title", "body"]);

const index = await Index.create(schema);

await index.putDocument("doc1", {
  title: "形態素解析",
  body: "Lindera は Rust 製の形態素解析ライブラリです。",
});
await index.commit();

const results = await index.search("形態素");
console.log(results[0].document.title); // "形態素解析"

JapaneseAnalyzer.fromBytes の完全なシグネチャと OPFS ヘルパ API は API リファレンス を参照してください。

ベクトル検索

import init, { Index, Schema } from 'laurus-wasm';

await init();

const schema = new Schema();
schema.addTextField("title");
schema.addHnswField("embedding", 3); // 3次元ベクトル

const index = await Index.create(schema);

await index.putDocument("doc1", {
  title: "Rust",
  embedding: [1.0, 0.0, 0.0]
});
await index.putDocument("doc2", {
  title: "Python",
  embedding: [0.0, 1.0, 0.0]
});
await index.commit();

// ベクトル類似度で検索
const results = await index.searchVector("embedding", [0.9, 0.1, 0.0]);
console.log(results[0].document.title); // "Rust"

バンドラーでの利用

Vite

// vite.config.js
import wasm from 'vite-plugin-wasm';

export default {
  plugins: [wasm()]
};

Webpack 5

Webpack 5 は asyncWebAssembly で WASM をネイティブサポートしています:

// webpack.config.js
module.exports = {
  experiments: {
    asyncWebAssembly: true
  }
};

API リファレンス

Index

検索インデックスの作成・クエリを行うメインエントリポイントです。

静的メソッド

Index.create(schema?)

新しいインメモリ(一時)インデックスを作成します。

  • 引数:
    • schema (Schema, 省略可) – スキーマ定義
  • 戻り値: Promise<Index>

Index.open(name, schema?)

OPFS で永続化されたインデックスを開くか、新規作成します。

  • 引数:
    • name (string) – インデックス名(OPFS サブディレクトリ)
    • schema (Schema, 省略可) – スキーマ定義
  • 戻り値: Promise<Index>

インスタンスメソッド

putDocument(id, document)

ドキュメントを置換(upsert)します。

  • 引数:
    • id (string) – ドキュメント識別子
    • document (object) – スキーマフィールドに対応するキーバリューペア
  • 戻り値: Promise<void>

addDocument(id, document)

ドキュメントバージョンを追加します(マルチバージョン RAG パターン)。

  • 引数・戻り値: putDocument と同じ

getDocuments(id)

ドキュメントの全バージョンを取得します。

  • 引数: id (string)
  • 戻り値: Promise<object[]>

deleteDocuments(id)

ドキュメントの全バージョンを削除します。

  • 引数: id (string)
  • 戻り値: Promise<void>

commit()

書き込みをフラッシュし、変更を検索可能にします。 Index.open() で作成したインデックスの場合、OPFS にも自動永続化されます。

  • 戻り値: Promise<void>

search(query, limit?, offset?)

DSL 文字列クエリで検索します。

  • 引数:
    • query (string) – クエリ DSL(例: "title:hello"
    • limit (number, デフォルト 10)
    • offset (number, デフォルト 0)
  • 戻り値: Promise<SearchResult[]>

searchTerm(field, term, limit?, offset?)

完全一致タームで検索します。

  • 引数:
    • field (string) – フィールド名
    • term (string) – 検索ターム
    • limit, offset (number, 省略可)
  • 戻り値: Promise<SearchResult[]>

searchVector(field, vector, limit?, offset?)

ベクトル類似度で検索します。

  • 引数:
    • field (string) – ベクトルフィールド名
    • vector (number[]) – クエリ埋め込みベクトル
    • limit, offset (number, 省略可)
  • 戻り値: Promise<SearchResult[]>

searchVectorText(field, text, limit?, offset?)

テキストで検索します(登録された埋め込み器で変換)。

  • 引数:
    • field (string) – ベクトルフィールド名
    • text (string) – 埋め込み対象テキスト
    • limit, offset (number, 省略可)
  • 戻り値: Promise<SearchResult[]>

searchGeo3dDistance(field, x, y, z, distanceM, limit?, offset?)

3D ECEF 座標フィールドへの球距離検索。中心 (x, y, z) から distanceM メートル以内 の座標を持つドキュメントを返します。ECEF の理論については Geo3d の概念 を参照。

  • 引数:
    • field (string) – Geo3d フィールド名
    • x, y, z (number) – 中心 ECEF 座標(メートル)
    • distanceM (number) – 中心からの最大距離(メートル)
    • limit, offset (number, 省略可)
  • 戻り値: Promise<SearchResult[]>

searchGeo3dBoundingBox(field, minX, minY, minZ, maxX, maxY, maxZ, limit?, offset?)

3D ECEF 座標フィールドへの軸並行範囲(AABB)検索。

  • 引数:
    • field (string) – Geo3d フィールド名
    • minX, minY, minZ, maxX, maxY, maxZ (number) – 範囲境界(メートル)
    • limit, offset (number, 省略可)
  • 戻り値: Promise<SearchResult[]>

searchGeo3dNearest(field, x, y, z, k, limit?, offset?, initialRadiusM?, maxRadiusM?)

3D ECEF 座標フィールドへの k 最近傍検索。(x, y, z) から最も近い k 件のドキュ メントを返します。initialRadiusM / maxRadiusM(オプション)で反復拡張サーチの 探索コーンを調整できます。

  • 引数:
    • field (string) – Geo3d フィールド名
    • x, y, z (number) – 中心 ECEF 座標(メートル)
    • k (number) – 返す近傍件数
    • limit, offset (number, 省略可)
    • initialRadiusM, maxRadiusM (number, 省略可)
  • 戻り値: Promise<SearchResult[]>

stats()

インデックス統計を返します。

  • 戻り値: { documentCount: number, vectorFields: { [name]: { count, dimension } } }

Schema

インデックスフィールドと埋め込み器を定義するビルダーです。

コンストラクタ

new Schema()

空のスキーマを作成します。

メソッド

addTextField(name, stored?, indexed?, termVectors?, analyzer?)

全文検索テキストフィールドを追加します。analyzer にはパラメータ不要の 組込名("standard" / "english" / "keyword" / "simple" / "noop")または addAnalyzer() で登録したランタイム analyzer 名を 指定します。

日本語の形態素解析を行う場合は、まず JapaneseAnalyzer を IPADIC の バイト列から構築し、addAnalyzer() で登録してください。 JapaneseAnalyzer.fromBytesaddAnalyzer を参照。

addIntegerField(name, stored?, indexed?, multiValued?)

64 ビット整数フィールドを追加します。multiValued: true を指定すると整数配列を受け付け、 範囲クエリはいずれかの値が条件を満たせばマッチ(Lucene 流の “any match”、constant スコア)します。

addFloatField(name, stored?, indexed?, multiValued?)

64 ビット浮動小数点フィールドを追加します。multiValued: true を指定すると浮動小数点配列を受け付け、 範囲クエリはいずれかの値が条件を満たせばマッチ(Lucene 流の “any match”、constant スコア)します。

addBooleanField(name, stored?, indexed?)

真偽値フィールドを追加します。

addDatetimeField(name, stored?, indexed?)

日時フィールドを追加します。

addGeoField(name, stored?, indexed?)

地理座標フィールドを追加します。

addGeo3dField(name, stored?, indexed?)

3D ECEF カルテシアン座標フィールド(x, y, z はメートル)を追加します。値は { x, y, z } オブジェクトで投入します。詳細は Geo3d の概念 を参照。

WASM バインディングは Geo3dDistanceQuery / Geo3dBoundingBoxQuery / Geo3dNearestQuery を JS クラスとして公開していません(wasm-bindgen は dyn Query トレイトオブジェクトを公開できないため)。代わりに上記の Index.searchGeo3dDistance / Index.searchGeo3dBoundingBox / Index.searchGeo3dNearest メソッドを使用してください。

addBytesField(name, stored?)

バイナリデータフィールドを追加します。

addHnswField(name, dimension, distance?, m?, efConstruction?, embedder?)

HNSW ベクトルインデックスフィールドを追加します。

  • distance: "cosine"(デフォルト)、"euclidean""dot_product""manhattan""angular"
  • m: 分岐係数(デフォルト 16)
  • efConstruction: 構築時の探索幅(デフォルト 200)

addFlatField(name, dimension, distance?, embedder?)

全探索ベクトルインデックスフィールドを追加します。

addIvfField(name, dimension, distance?, nClusters?, nProbe?, embedder?)

IVF ベクトルインデックスフィールドを追加します。

  • nClusters: パーティショニングクラスタ数(デフォルト 100)
  • nProbe: 検索時にプローブするクラスタ数(デフォルト 1)

addAnalyzer(name, analyzer)

事前に構築した analyzer インスタンスを name で登録します。テキスト フィールドが Named 形式で analyzer を参照するときに、組込名や schema.analyzers 定義よりも先に解決されます。

現状は JapaneseAnalyzer.fromBytes で構築した JapaneseAnalyzer のみ受け付けます。ブラウザ WASM では { "language": "japanese", "dict": ... } プリセットがファイルシステム パスを解決できないため、ランタイムレジストリ経由が日本語 analyzer を 利用する唯一の現実的な経路です。

import { JapaneseAnalyzer, Schema } from "laurus-wasm";
import { downloadDictionary, loadDictionaryFiles } from "laurus-wasm/opfs";

await downloadDictionary("./dict/lindera-ipadic.zip", "ipadic");
const f = await loadDictionaryFiles("ipadic");
const ja = JapaneseAnalyzer.fromBytes(
  f.metadata, f.dictDa, f.dictVals, f.dictWordsIdx,
  f.dictWords, f.matrixMtx, f.charDef, f.unk, "normal",
);

const schema = new Schema();
schema.addAnalyzer("ja-ipadic", ja);
schema.addTextField("body", undefined, undefined, undefined, "ja-ipadic");

addEmbedder(name, config)

名前付き埋め込み器を登録します。WASM では以下の 2 種類の type をサポートします:

  • "precomputed" — 埋め込みは行いません。ベクトルは putDocument() / searchVector() 経由で直接渡します。
  • "callback" — JavaScript コールバック embed: (text) => Promise<number[]> を 登録します。エンジンがインジェスト時および searchVectorText() で呼び出します。 Transformers.js などのブラウザ内埋め込みライブラリと組み合わせることで、 エンジン内自動埋め込みが可能になります。
// Precomputed embedder
schema.addEmbedder("precomputed-embedder", { type: "precomputed" });

// Callback embedder(例: Transformers.js)
schema.addEmbedder("callback-embedder", {
  type: "callback",
  embed: async (text) => {
    const output = await pipeline(text, { pooling: "mean", normalize: true });
    return Array.from(output.data);
  },
});

setDefaultFields(fields)

デフォルト検索フィールドを設定します。

setDynamicFieldPolicy(policy)

ドキュメントに含まれるがスキーマに宣言されていないフィールドの扱いを設定します。policy"strict" / "dynamic"(デフォルト)/ "ignore" のいずれか(大文字小文字を無視)。不正な値を渡すと例外をスローします。

  • "strict" — ドキュメントを拒否
  • "dynamic" — 各未宣言フィールドの型を推論してスキーマに追加。警告: integer フィールドに入ってきた float 値は静かに切り捨てられます(3.143
  • "ignore" — 未宣言フィールドを静かに破棄

詳細な挙動マトリクスは スキーマとフィールド を参照してください。

dynamicFieldPolicy()

現在のポリシーを小文字の文字列で返します。

fieldNames()

定義済みフィールド名の配列を返します。

toString()

スキーマの文字列表現("Schema(fields=[...])" 形式)を返します。

SearchResult

interface SearchResult {
  id: string;
  score: number;
  document: object | null;
}

Analysis

JapaneseAnalyzer

Lindera 辞書のバイト列から構築する日本語形態素解析 analyzer。 ブラウザ WASM には実ファイルシステムが無いため、標準の { "language": "japanese", "dict": "/path/to/ipadic" } プリセットは 利用できません。代わりに Lindera 辞書アーカイブ(典型的には lindera-ipadic-X.Y.Z.zip)を取得して OPFS ヘルパ で OPFS に保存し、8 つのコンポーネントバイト配列を JapaneseAnalyzer.fromBytes に渡してください。

JapaneseAnalyzer.fromBytes(metadata, dictDa, ..., mode?)

IPADIC のバイト列から analyzer を構築する static ファクトリ。

引数(mode 以外はすべて Uint8Array):

引数対応するファイル
metadatametadata.json
dictDadict.da(Double-Array Trie)
dictValsdict.vals
dictWordsIdxdict.wordsidx
dictWordsdict.words
matrixMtxmatrix.mtx
charDefchar_def.bin
unkunk.bin
mode"normal"(デフォルト)/ "search" / "decompose"

いずれかのコンポーネントの deserialization に失敗した場合、または mode 文字列が不正な場合は throw します。

import { JapaneseAnalyzer } from "laurus-wasm";
import { loadDictionaryFiles } from "laurus-wasm/opfs";

const f = await loadDictionaryFiles("ipadic");
const ja = JapaneseAnalyzer.fromBytes(
  f.metadata, f.dictDa, f.dictVals, f.dictWordsIdx,
  f.dictWords, f.matrixMtx, f.charDef, f.unk,
  "normal",
);

パイプラインは NFKC 正規化 → 日本語 iteration mark 正規化 → Lindera 形態素解析 → lowercase → 日本語 stop word フィルタ で、ネイティブ側の japanese プリセットと完全に一致します。

OPFS ヘルパ

laurus-wasm/opfs サブパスは、Lindera 辞書をブラウザの Origin Private File System にダウンロード・保存・読込するヘルパを提供します。 JapaneseAnalyzer.fromBytes と組み合わせて使用します。

import {
  downloadDictionary,
  loadDictionaryFiles,
  hasDictionary,
  listDictionaries,
  removeDictionary,
} from "laurus-wasm/opfs";
関数説明
downloadDictionary(url, name, options?).zip を fetch し、Web の DecompressionStream API で展開して、Lindera 8 ファイルを OPFS の laurus/dictionaries/<name>/ 配下に保存します。options.onProgress({ phase, loaded?, total? }) で進捗通知を受け取れます。
loadDictionaryFiles(name)8 ファイルを { metadata, dictDa, dictVals, dictWordsIdx, dictWords, matrixMtx, charDef, unk } オブジェクトとして読み出し、JapaneseAnalyzer.fromBytes にそのまま渡せる形にします。
hasDictionary(name)辞書ディレクトリが OPFS にあれば true
listDictionaries()保存済み辞書名の配列を返します。
removeDictionary(name)辞書ディレクトリを削除します。

ブラウザ CORS の制約により GitHub Releases から直接 fetch できないため、 zip はアプリと同一オリジンで配信してください(Laurus デモではデプロイ 時に ./dict/lindera-ipadic.zip を WASM と同じパスに同梱します)。

WhitespaceTokenizer

const tokenizer = new WhitespaceTokenizer();
const tokens = tokenizer.tokenize("hello world");
// [{ text, position, startOffset, endOffset, boost, stopped, positionIncrement, positionLength }]

空白を境界としてテキストを分割し、Token オブジェクトの配列を返します。

SynonymDictionary

const dict = new SynonymDictionary();
dict.addSynonymGroup(["ml", "machine learning"]);

同義語グループの辞書。グループ内のすべての語句が互いに同義語として扱われます。

SynonymGraphFilter

new SynonymGraphFilter(dictionary, keepOriginal = true, boost = 1.0)
  • dictionary (SynonymDictionary) — 同義語グループのソース。
  • keepOriginal (boolean, デフォルト true) — 元のトークンを挿入された同義語と 並べて保持します。
  • boost (number, デフォルト 1.0) — 挿入される同義語トークンに適用される スコアブースト。
const filter = new SynonymGraphFilter(dict, true, 0.8);
const expanded = filter.apply(tokens);

SynonymDictionary の同義語でトークンを展開するトークンフィルターです。

開発

前提条件

  • Rust(stable、wasm32-unknown-unknown ターゲット付き)
  • wasm-pack
  • Node.js(テストと npm publish 用)
rustup target add wasm32-unknown-unknown
cargo install wasm-pack

ビルド

cd laurus-wasm

# デバッグビルド(コンパイル高速)
wasm-pack build --target web --dev

# リリースビルド(最適化)
wasm-pack build --target web --release

# バンドラーターゲット(webpack、vite 等)
wasm-pack build --target bundler --release

プロジェクト構成

laurus-wasm/
├── Cargo.toml          # Rust 依存関係(wasm-bindgen、laurus コア)
├── package.json        # npm パッケージメタデータ
├── src/
│   ├── lib.rs          # モジュール宣言
│   ├── index.rs        # Index クラス(CRUD + 検索)
│   ├── schema.rs       # Schema ビルダー
│   ├── search.rs       # SearchRequest / SearchResult
│   ├── query.rs        # クエリ型定義
│   ├── convert.rs      # JsValue ↔ Document 変換
│   ├── analysis.rs     # トークナイザー / フィルターラッパー
│   ├── errors.rs       # LaurusError → JsValue 変換
│   └── storage.rs      # OPFS 永続化レイヤー
└── js/
    └── opfs_bridge.js  # Origin Private File System 用 JS グルーコード

アーキテクチャノート

ストレージ戦略

laurus-wasm は二層ストレージアプローチを採用しています:

  1. MemoryStorage(ランタイム) – すべての読み書き操作は Laurus の インメモリストレージを経由します。これは Storage トレイトの Send + Sync 要件を満たします。

  2. OPFS(永続化) – commit() 時に MemoryStorage の全状態が OPFS ファイルにシリアライズされます。Index.open() 時に OPFS ファイルが MemoryStorage にロードされます。

この設計により、JS ハンドルの Send + Sync 非互換性を回避しつつ、 コアエンジンを変更せずに永続化を実現しています。

Feature Flags

laurus コアは Feature Flags で WASM をサポートしています:

# laurus-wasm はデフォルト機能なしで laurus に依存
laurus = { workspace = true, default-features = false }

これにより、ネイティブ専用の依存関係(tokio/full、rayon、memmap2 等)が 除外され、#[cfg(target_arch = "wasm32")] フォールバックで並列処理が 逐次処理に切り替わります。

テスト

# ビルド確認
cargo build -p laurus-wasm --target wasm32-unknown-unknown

# Clippy
cargo clippy -p laurus-wasm --target wasm32-unknown-unknown -- -D warnings

ブラウザテストは wasm-pack test で実行できます:

wasm-pack test --headless --chrome

Ruby バインディング概要

laurus gem は Laurus 検索エンジンの Ruby バインディングです。Magnusrb_sys を使ってネイティブ Rust 拡張としてビルドされており、Ruby プログラムからネイティブに近いパフォーマンスで Laurus の Lexical 検索、Vector 検索、ハイブリッド検索機能を利用できます。

機能

  • Lexical 検索 – BM25 スコアリングを備えた転置インデックスによる全文検索
  • Vector 検索 – Flat、HNSW、IVF インデックスを使用した近似最近傍(ANN)検索
  • ハイブリッド検索 – フュージョンアルゴリズム(RRF、WeightedSum)で Lexical と Vector の結果を統合
  • 豊富なクエリ DSL – Term、Phrase、Fuzzy、Wildcard、NumericRange、Geo、Boolean、Span クエリ
  • テキスト解析 – トークナイザー、フィルター、ステマー、同義語展開
  • 柔軟なストレージ – インメモリ(一時的)またはファイルベース(永続的)インデックス
  • Ruby らしい APILaurus:: 名前空間の直感的な Ruby クラス

アーキテクチャ

graph LR
    subgraph "laurus-ruby (gem)"
        RbIndex["Index\n(Ruby クラス)"]
        RbQuery["クエリクラス"]
        RbSearch["SearchRequest\n/ SearchResult"]
    end

    Ruby["Ruby アプリケーション"] -->|"メソッド呼び出し"| RbIndex
    Ruby -->|"クエリオブジェクト"| RbQuery
    RbIndex -->|"Magnus FFI"| Engine["laurus::Engine\n(Rust)"]
    RbQuery -->|"Magnus FFI"| Engine
    Engine --> Storage["ストレージ\n(Memory / File)"]

Ruby クラスは Rust エンジンの薄いラッパーです。 各呼び出しは Magnus の FFI 境界を一度だけ越え、その後 Rust エンジンが操作をネイティブコードで実行します。

Rust エンジン内部は非同期 I/O を使用していますが、 Ruby 側のメソッドはすべて同期関数として公開されています。 各メソッドは内部で tokio::Runtime::block_on() を呼び出し、 非同期 Rust を同期 Ruby にブリッジしています。

クイックスタート

require "laurus"

# インメモリインデックスを作成
index = Laurus::Index.new

# ドキュメントをインデックス
index.put_document("doc1", { "title" => "Rust 入門", "body" => "システムプログラミング言語です。" })
index.put_document("doc2", { "title" => "Ruby Web 開発", "body" => "Ruby による Web アプリケーション。" })
index.commit

# 検索
results = index.search("title:rust", limit: 5)
results.each do |r|
  puts "[#{r.id}] score=#{format('%.4f', r.score)}  #{r.document['title']}"
end

セクション

インストール

RubyGems からインストール

gem install laurus

または Gemfile に追加します:

gem "laurus"

その後、以下を実行します:

bundle install

ソースからビルド

ソースからビルドするには Rust ツールチェーン(1.85 以降)と rb_sys が必要です。

# リポジトリをクローン
git clone https://github.com/mosuka/laurus.git
cd laurus/laurus-ruby

# 依存関係をインストール
bundle install

# ネイティブ拡張をコンパイル
bundle exec rake compile

# またはローカルに gem をインストール
gem build laurus.gemspec
gem install laurus-*.gem

動作確認

require "laurus"
index = Laurus::Index.new
puts index  # Index()

動作要件

  • Ruby 3.1 以降
  • Rust ツールチェーン(gem インストール時に rb_sys 経由で自動的に呼び出されます)
  • コンパイル済みネイティブ拡張以外のランタイム依存関係なし

クイックスタート

1. インデックスを作成する

require "laurus"

# インメモリインデックス(一時的、プロトタイピングに最適)
index = Laurus::Index.new

# ファイルベースインデックス(永続的)
schema = Laurus::Schema.new
schema.add_text_field("title")
schema.add_text_field("body")
index = Laurus::Index.new(path: "./myindex", schema: schema)

2. ドキュメントをインデックスする

index.put_document("doc1", {
  "title" => "Rust 入門",
  "body" => "Rust は安全性とパフォーマンスに重点を置いたシステムプログラミング言語です。",
})
index.put_document("doc2", {
  "title" => "Ruby Web 開発",
  "body" => "Ruby は Web アプリケーションと高速プロトタイピングに広く使われています。",
})
index.commit

3. Lexical 検索

# DSL 文字列
results = index.search("title:rust", limit: 5)

# クエリオブジェクト
results = index.search(Laurus::TermQuery.new("body", "ruby"), limit: 5)

# 結果を表示
results.each do |r|
  puts "[#{r.id}] score=#{format('%.4f', r.score)}  #{r.document['title']}"
end

4. Vector 検索

Vector 検索にはベクトルフィールドを含むスキーマと事前計算済みエンベディングが必要です。

require "laurus"

schema = Laurus::Schema.new
schema.add_text_field("title")
schema.add_hnsw_field("embedding", 4)

index = Laurus::Index.new(schema: schema)
index.put_document("doc1", { "title" => "Rust", "embedding" => [0.1, 0.2, 0.3, 0.4] })
index.put_document("doc2", { "title" => "Ruby", "embedding" => [0.9, 0.8, 0.7, 0.6] })
index.commit

query_vec = [0.1, 0.2, 0.3, 0.4]
results = index.search(Laurus::VectorQuery.new("embedding", query_vec), limit: 3)

5. ハイブリッド検索

request = Laurus::SearchRequest.new(
  lexical_query: Laurus::TermQuery.new("title", "rust"),
  vector_query: Laurus::VectorQuery.new("embedding", query_vec),
  fusion: Laurus::RRF.new(k: 60.0),
  limit: 5,
)
results = index.search(request)

6. 更新と削除

# 更新: put_document は同じ ID の全バージョンを置換する
index.put_document("doc1", { "title" => "更新されたタイトル", "body" => "新しいコンテンツ。" })
index.commit

# 既存バージョンを削除せずに新しいバージョンを追記(RAG チャンキングパターン)
index.add_document("doc1", { "title" => "チャンク 2", "body" => "追加のチャンク。" })
index.commit

# 全バージョンを取得
docs = index.get_documents("doc1")

# 削除
index.delete_documents("doc1")
index.commit

7. スキーマ管理

schema = Laurus::Schema.new
schema.add_text_field("title")
schema.add_text_field("body")
schema.add_integer_field("year")
schema.add_float_field("score")
schema.add_boolean_field("published")
schema.add_bytes_field("thumbnail")
schema.add_geo_field("location")
schema.add_datetime_field("created_at")
schema.add_hnsw_field("embedding", 384)
schema.add_flat_field("small_vec", 64)
schema.add_ivf_field("ivf_vec", 128, n_clusters: 100)

8. インデックス統計

stats = index.stats
puts stats["document_count"]
puts stats["vector_fields"]

API リファレンス

Index

Laurus 検索エンジンをラップするメインクラスです。

Laurus::Index.new(path: nil, schema: nil)

コンストラクタ

パラメータデフォルト説明
path:String | nilnil永続ストレージのディレクトリパス。nil の場合はインメモリインデックスを作成します。
schema:Schema | nilnilスキーマ定義。省略時は空のスキーマが使用されます。

メソッド

メソッド説明
put_document(id, doc)ドキュメントをアップサート(upsert)します。同じ ID の既存バージョンをすべて置換します。
add_document(id, doc)既存バージョンを削除せずにドキュメントチャンクを追記します。
get_documents(id) -> Array<Hash>指定 ID の全保存バージョンを返します。
delete_documents(id)指定 ID の全バージョンを削除します。
commitバッファリングされた書き込みをフラッシュし、すべての保留中の変更を検索可能にします。
search(query, limit: 10, offset: 0) -> Array<SearchResult>検索クエリを実行します。
stats -> Hashインデックス統計("document_count""vector_fields")を返します。

search の query 引数

query パラメータは以下のいずれかを受け付けます:

  • DSL 文字列(例: "title:hello""content:\"memory safety\"")
  • Lexical クエリオブジェクトTermQueryPhraseQueryBooleanQuery など)
  • Vector クエリオブジェクトVectorQueryVectorTextQuery
  • SearchRequest(完全な制御が必要な場合)

Schema

Index のフィールドとインデックスタイプを定義します。

Laurus::Schema.new

フィールドメソッド

メソッド説明
add_text_field(name, stored: true, indexed: true, term_vectors: false, analyzer: nil)全文フィールド(転置インデックス、BM25)。analyzer: にはパラメータ不要の組込名("standard" / "english" / "keyword" / "simple" / "noop"、または add_analyzer で登録したカスタム名)を指定します。Lindera 辞書パスが必要な Japanese プリセットは、lindera tokenizer を含むカスタム analyzer として登録し、名前で参照してください。
add_integer_field(name, stored: true, indexed: true, multi_valued: false)64 ビット整数フィールド。multi_valued: true で整数配列を受け付け(範囲クエリは “any match”)。
add_float_field(name, stored: true, indexed: true, multi_valued: false)64 ビット浮動小数点フィールド。multi_valued: true で浮動小数点配列を受け付け(範囲クエリは “any match”)。
add_boolean_field(name, stored: true, indexed: true)ブールフィールド。
add_bytes_field(name, stored: true)生バイトフィールド。
add_geo_field(name, stored: true, indexed: true)地理座標フィールド(緯度/経度)。
add_geo3d_field(name, stored: true, indexed: true)3D ECEF カルテシアン座標フィールド(x, y, z はメートル)。詳細は Geo3d の概念
add_datetime_field(name, stored: true, indexed: true)UTC 日時フィールド。
add_hnsw_field(name, dimension, distance: "cosine", m: 16, ef_construction: 200, embedder: nil)HNSW 近似最近傍ベクトルフィールド。
add_flat_field(name, dimension, distance: "cosine", embedder: nil)Flat(総当たり)ベクトルフィールド。
add_ivf_field(name, dimension, distance: "cosine", n_clusters: 100, n_probe: 1, embedder: nil)IVF 近似最近傍ベクトルフィールド。

その他のメソッド

メソッド説明
add_embedder(name, config)名前付きエンベダー定義を登録します。config"type" キーを持つ Hash です(下記参照)。
set_default_fields(fields)クエリでフィールドが指定されていない場合に使用するデフォルトフィールドを設定します。fields は文字列の配列です。
set_dynamic_field_policy(policy)未宣言フィールドの扱いを設定します。policy"strict" / "dynamic"(デフォルト)/ "ignore"。詳細は下記を参照。
dynamic_field_policy -> String現在のポリシーを小文字の文字列で返します。
field_names -> Array<String>このスキーマに定義されたフィールド名のリストを返します。

Dynamic field policy(動的フィールドポリシー)

ドキュメントに含まれるがスキーマに宣言されていないフィールドの扱いを制御します:

  • "strict" — ドキュメントを拒否
  • "dynamic"(デフォルト)— 各未宣言フィールドの型を推論してスキーマに追加。警告: integer フィールドに入ってきた float 値は静かに切り捨てられます(3.143)。厳密さが必要なら "strict" を使用してください
  • "ignore" — 未宣言フィールドを静かに破棄

詳細な挙動マトリクスは スキーマとフィールド を参照してください。

エンベダータイプ

"type"必須キーFeature Flag
"precomputed"(常に利用可能)
"candle_bert""model"embeddings-candle
"candle_clip""model"embeddings-multimodal
"openai""model"embeddings-openai

距離メトリクス

説明
"cosine"コサイン類似度(デフォルト)
"euclidean"ユークリッド距離
"dot_product"内積
"manhattan"マンハッタン距離
"angular"角度距離

クエリクラス

TermQuery

Laurus::TermQuery.new(field, term)

指定フィールドに完全一致する語句を含むドキュメントを検索します。

PhraseQuery

Laurus::PhraseQuery.new(field, terms)

指定した語句が順序どおりに含まれるドキュメントを検索します。terms は文字列の配列です。

FuzzyQuery

Laurus::FuzzyQuery.new(field, term, max_edits: 2)

編集距離が max_edits 以内の近似一致を検索します。

WildcardQuery

Laurus::WildcardQuery.new(field, pattern)

ワイルドカードパターン検索。* は任意の文字列、? は任意の1文字に一致します。

NumericRangeQuery

Laurus::NumericRangeQuery.new(field, min: nil, max: nil)

[min, max] の範囲内の数値を検索します。開いた境界には nil を指定します。型(整数または浮動小数点)は min/max の Ruby 型から推論されます。

GeoDistanceQuery

Laurus::GeoDistanceQuery.within_radius(field, lat, lon, distance_m)

地理的距離検索(半径指定)。指定した地点から distance_m メートル以内の (lat, lon) 座標を持つドキュメントを返します。

GeoBoundingBoxQuery

Laurus::GeoBoundingBoxQuery.within_bounding_box(
  field, min_lat, min_lon, max_lat, max_lon,
)

地理的範囲(バウンディングボックス)検索。軸並行 [min_lat, max_lat] × [min_lon, max_lon] 内の (lat, lon) 座標を持つドキュメントを返します。

Geo3dDistanceQuery

Laurus::Geo3dDistanceQuery.within_sphere(field, x, y, z, distance_m)

3D ECEF 座標フィールドへの球距離検索。中心 (x, y, z) から distance_m メートル以内 の座標を持つドキュメントを返します。ECEF の理論については Geo3d の概念 を参照。

Geo3dBoundingBoxQuery

Laurus::Geo3dBoundingBoxQuery.within_box(
  field,
  min_x, min_y, min_z,
  max_x, max_y, max_z,
)

軸並行 3D 範囲(AABB)検索。

Geo3dNearestQuery

Laurus::Geo3dNearestQuery.k_nearest(
  field, x, y, z, k,
  initial_radius_m: nil,
  max_radius_m: nil,
)

3D ECEF 座標フィールドへの k 最近傍検索。initial_radius_m: / max_radius_m: キーワード引数(オプション)で反復拡張サーチの探索コーンを調整できます。

BooleanQuery

bq = Laurus::BooleanQuery.new
bq.must(query)
bq.should(query)
bq.must_not(query)

複合ブールクエリ。must 節はすべて一致する必要があり、must_not 節は一致してはなりません。should 節はスコアリングに寄与し、must 節が無い場合は少なくとも1つが一致する必要があります。

SpanQuery

# 単一語句
Laurus::SpanQuery.term(field, term)

# Near: slop 位置以内の語句
Laurus::SpanQuery.near(field, terms, slop: 0, ordered: true)

# ネストされた SpanQuery 句を使った Near
Laurus::SpanQuery.near_spans(field, clauses, slop: 0, ordered: true)

# Containing: big スパンが little スパンを含む
Laurus::SpanQuery.containing(field, big, little)

# Within: 最大距離での include スパンと exclude スパン
Laurus::SpanQuery.within(field, include_span, exclude_span, distance)

位置・近接スパンクエリ。near は語句文字列の配列を受け取り、near_spans はネスト式のために SpanQuery オブジェクトの配列を受け取ります。

VectorQuery

Laurus::VectorQuery.new(field, vector)

事前計算済みエンベディングベクトルを使った近似最近傍検索を行います。vector は Float の配列です。

VectorTextQuery

Laurus::VectorTextQuery.new(field, text)

クエリ時に text をエンベディングに変換してベクトル検索を行います。インデックスにエンベダーの設定が必要です。


SearchRequest

高度な制御が必要な場合の完全なリクエストクラスです。

Laurus::SearchRequest.new(
  query: nil,
  lexical_query: nil,
  vector_query: nil,
  filter_query: nil,
  fusion: nil,
  limit: 10,
  offset: 0,
)
パラメータ説明
query:DSL 文字列または単一クエリオブジェクト。lexical_query: / vector_query: と排他的。
lexical_query:明示的なハイブリッド検索の Lexical コンポーネント。
vector_query:明示的なハイブリッド検索の Vector コンポーネント。
filter_query:スコアリング後に適用する Lexical フィルター。
fusion:フュージョンアルゴリズム(RRF または WeightedSum)。両コンポーネント指定時のデフォルトは RRF(k: 60)
limit:最大結果件数(デフォルト 10)。
offset:ページネーションオフセット(デフォルト 0)。

SearchResult

Index#search が返すクラスです。

result.id        # => String   -- 外部ドキュメント識別子
result.score     # => Float    -- 関連性スコア
result.document  # => Hash|nil -- 取得されたフィールド値。削除済みの場合は nil

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

RRF

Laurus::RRF.new(k: 60.0)

逆順位フュージョン(Reciprocal Rank Fusion)。Lexical と Vector の結果リストを順位位置によってマージします。k は平滑化定数で、値が大きいほど上位ランクの影響が小さくなります。

WeightedSum

Laurus::WeightedSum.new(lexical_weight: 0.5, vector_weight: 0.5)

両スコアリストをそれぞれ正規化した後、lexical_weight * lexical_score + vector_weight * vector_score として結合します。


テキスト解析

SynonymDictionary

dict = Laurus::SynonymDictionary.new
dict.add_synonym_group(["fast", "quick", "rapid"])

同義語グループの辞書です。グループ内のすべての語句は互いの同義語として扱われます。

WhitespaceTokenizer

tokenizer = Laurus::WhitespaceTokenizer.new
tokens = tokenizer.tokenize("hello world")

空白で分割してテキストをトークン化し、Token オブジェクトの配列を返します。

SynonymGraphFilter

filter = Laurus::SynonymGraphFilter.new(dictionary, keep_original: true, boost: 1.0)
expanded = filter.apply(tokens)

SynonymDictionary の同義語でトークンを展開するトークンフィルターです。

Token

token.text                # => String  -- トークンテキスト
token.position            # => Integer -- トークンストリーム内の位置
token.start_offset        # => Integer -- 元テキスト内の文字開始オフセット
token.end_offset          # => Integer -- 元テキスト内の文字終了オフセット
token.boost               # => Float   -- スコアブースト係数(1.0 = 調整なし)
token.stopped             # => Boolean -- ストップフィルターによって除去されたかどうか
token.position_increment  # => Integer -- 前のトークンの位置との差分
token.position_length     # => Integer -- このトークンがカバーする位置数

フィールド値の型マッピング

Ruby の値は自動的に Laurus の DataValue 型に変換されます:

Ruby 型Laurus 型備考
nilNull
true / falseBool
IntegerInt64
FloatFloat64
StringText
Array(数値)Vector要素は f32 に変換
Hash"lat", "lon"Geo2 つの Float
Hash"x", "y", "z"GeoEcef3 つの Float 値(メートル単位、3D ECEF 直交座標)
Time / Stringiso8601 に応答)DateTimeiso8601 経由で変換

開発環境のセットアップ

このページでは laurus-ruby バインディングのローカル開発環境の構築、ビルド、テストスイートの実行方法について説明します。

前提条件

  • Rust 1.85 以降(Cargo 含む)
  • Ruby 3.1 以降(Bundler 含む)
  • リポジトリがローカルにクローンされていること
git clone https://github.com/mosuka/laurus.git
cd laurus

ビルド

開発ビルド

Rust ネイティブ拡張をデバッグモードでコンパイルします。Rust ソースを変更した場合は再実行してください。

cd laurus-ruby
bundle install
bundle exec rake compile

リリースビルド

gem build laurus.gemspec

ビルドの確認

ruby -e "
require 'laurus'
index = Laurus::Index.new
puts index.stats
"
# {"document_count"=>0, "vector_fields"=>{}}

テスト

テストは Minitest を使用しており、test/ ディレクトリにあります。

# 全テスト実行
bundle exec rake test

特定のテストファイルを実行する場合:

bundle exec ruby -Ilib -Itest test/test_index.rb

Lint とフォーマット

# Rust lint(Clippy)
cargo clippy -p laurus-ruby -- -D warnings

# Rust フォーマットチェック
cargo fmt -p laurus-ruby --check

# フォーマット適用
cargo fmt -p laurus-ruby

クリーンアップ

# ビルド成果物を削除
bundle exec rake clean

# インストールされた gem を削除
rm -rf vendor/bundle

プロジェクト構成

laurus-ruby/
├── Cargo.toml          # Rust クレートマニフェスト
├── laurus.gemspec      # Gem 仕様
├── Gemfile             # Bundler 依存関係ファイル
├── Rakefile            # Rake タスク(compile、test、clean)
├── lib/
│   └── laurus.rb       # Ruby エントリポイント(ネイティブ拡張をロード)
├── ext/
│   └── laurus_ruby/    # ネイティブ拡張ビルド設定
│       └── extconf.rb  # rb_sys 拡張設定
├── src/                # Rust ソース(Magnus バインディング)
│   ├── lib.rs          # モジュール登録
│   ├── index.rs        # Index クラス
│   ├── schema.rs       # Schema クラス
│   ├── query.rs        # クエリクラス
│   ├── search.rs       # SearchRequest / SearchResult / Fusion
│   ├── analysis.rs     # Tokenizer / Filter / Token
│   ├── convert.rs      # Ruby ↔ DataValue 変換
│   └── errors.rs       # エラーマッピング
├── test/               # Minitest テスト
│   ├── test_helper.rb
│   └── test_index.rb
└── examples/           # 実行可能な Ruby サンプル

PHP バインディング概要

laurus PHP エクステンションは Laurus 検索エンジンの PHP バインディングです。ext-php-rs を使ってネイティブ Rust 拡張としてビルドされており、PHP プログラムからネイティブに近いパフォーマンスで Laurus の Lexical 検索、Vector 検索、ハイブリッド検索機能を利用できます。

機能

  • Lexical 検索 – BM25 スコアリングを備えた転置インデックスによる全文検索
  • Vector 検索 – Flat、HNSW、IVF インデックスを使用した近似最近傍(ANN)検索
  • ハイブリッド検索 – フュージョンアルゴリズム(RRF、WeightedSum)で Lexical と Vector の結果を統合
  • 豊富なクエリ DSL – Term、Phrase、Fuzzy、Wildcard、NumericRange、Geo、Boolean、Span クエリ
  • テキスト解析 – トークナイザー、フィルター、ステマー、同義語展開
  • 柔軟なストレージ – インメモリ(一時的)またはファイルベース(永続的)インデックス
  • PHP らしい APILaurus\ 名前空間の直感的な PHP クラス

アーキテクチャ

graph LR
    subgraph "laurus-php (extension)"
        PhpIndex["Index\n(PHP クラス)"]
        PhpQuery["クエリクラス"]
        PhpSearch["SearchRequest\n/ SearchResult"]
    end

    PHP["PHP アプリケーション"] -->|"メソッド呼び出し"| PhpIndex
    PHP -->|"クエリオブジェクト"| PhpQuery
    PhpIndex -->|"ext-php-rs FFI"| Engine["laurus::Engine\n(Rust)"]
    PhpQuery -->|"ext-php-rs FFI"| Engine
    Engine --> Storage["ストレージ\n(Memory / File)"]

PHP クラスは Rust エンジンの薄いラッパーです。 各呼び出しは ext-php-rs の FFI 境界を一度だけ越え、その後 Rust エンジンが操作をネイティブコードで実行します。

Rust エンジン内部は非同期 I/O を使用していますが、 PHP 側のメソッドはすべて同期関数として公開されています。 各メソッドは内部で tokio::Runtime::block_on() を呼び出し、 非同期 Rust を同期 PHP にブリッジしています。

クイックスタート

<?php

use Laurus\Index;

// インメモリインデックスを作成
$index = new Index();

// ドキュメントをインデックス
$index->putDocument("doc1", ["title" => "Introduction to Rust", "body" => "Systems programming language."]);
$index->putDocument("doc2", ["title" => "PHP for Web Development", "body" => "Web applications with PHP."]);
$index->commit();

// 検索
$results = $index->search("title:rust", 5);
foreach ($results as $r) {
    printf("[%s] score=%.4f  %s\n", $r->getId(), $r->getScore(), $r->getDocument()["title"]);
}

セクション

インストール

ソースからビルド

ソースからビルドするには Rust ツールチェーン(1.85 以降)と PHP 8.1 以降(開発ヘッダー付き)が必要です。

# リポジトリをクローン
git clone https://github.com/mosuka/laurus.git
cd laurus/laurus-php

# ネイティブ拡張をビルド
cargo build --release

# 共有ライブラリを PHP エクステンションディレクトリにコピー
# (正確なパスは OS と PHP バージョンによって異なります)
cp ../target/release/liblaurus_php.so $(php -r "echo ini_get('extension_dir');")

次に php.ini にエクステンションを追加します:

extension=laurus_php.so

または、コマンドラインでエクステンションをロードすることもできます:

php -d extension=liblaurus_php.so your_script.php

動作確認

<?php

use Laurus\Index;

$index = new Index();
echo $index;  // Index()

動作要件

  • PHP 8.1 以降(開発ヘッダー付き: php-dev / php-devel
  • Rust ツールチェーン 1.85 以降(Cargo 含む)
  • コンパイル済みネイティブ拡張以外のランタイム依存関係なし

クイックスタート

1. インデックスを作成する

<?php

use Laurus\Index;
use Laurus\Schema;

// インメモリインデックス(一時的、プロトタイピングに最適)
$index = new Index();

// ファイルベースインデックス(永続的)
$schema = new Schema();
$schema->addTextField("title");
$schema->addTextField("body");
$index = new Index("./myindex", $schema);

2. ドキュメントをインデックスする

$index->putDocument("doc1", [
    "title" => "Introduction to Rust",
    "body" => "Rust is a systems programming language focused on safety and performance.",
]);
$index->putDocument("doc2", [
    "title" => "PHP for Web Development",
    "body" => "PHP is widely used for web applications and rapid prototyping.",
]);
$index->commit();

3. Lexical 検索

// DSL 文字列
$results = $index->search("title:rust", 5);

// クエリオブジェクト
$results = $index->search(new \Laurus\TermQuery("body", "php"), 5);

// 結果を表示
foreach ($results as $r) {
    printf("[%s] score=%.4f  %s\n", $r->getId(), $r->getScore(), $r->getDocument()["title"]);
}

4. Vector 検索

Vector 検索にはベクトルフィールドを含むスキーマと事前計算済みエンベディングが必要です。

<?php

use Laurus\Index;
use Laurus\Schema;
use Laurus\VectorQuery;

$schema = new Schema();
$schema->addTextField("title");
$schema->addHnswField("embedding", 4);

$index = new Index(null, $schema);
$index->putDocument("doc1", ["title" => "Rust", "embedding" => [0.1, 0.2, 0.3, 0.4]]);
$index->putDocument("doc2", ["title" => "PHP", "embedding" => [0.9, 0.8, 0.7, 0.6]]);
$index->commit();

$queryVec = [0.1, 0.2, 0.3, 0.4];
$results = $index->search(new VectorQuery("embedding", $queryVec), 3);

5. ハイブリッド検索

use Laurus\SearchRequest;
use Laurus\TermQuery;
use Laurus\VectorQuery;
use Laurus\RRF;

$request = new SearchRequest(
    query: null,
    lexicalQuery: new TermQuery("title", "rust"),
    vectorQuery: new VectorQuery("embedding", $queryVec),
    filterQuery: null,
    fusion: new RRF(60.0),
    limit: 5,
);
$results = $index->search($request);

6. 更新と削除

// 更新: putDocument は同じ ID の全バージョンを置換する
$index->putDocument("doc1", ["title" => "Updated Title", "body" => "New content."]);
$index->commit();

// 既存バージョンを削除せずに新しいバージョンを追記(RAG チャンキングパターン)
$index->addDocument("doc1", ["title" => "Chunk 2", "body" => "Additional chunk."]);
$index->commit();

// 全バージョンを取得
$docs = $index->getDocuments("doc1");

// 削除
$index->deleteDocuments("doc1");
$index->commit();

7. スキーマ管理

$schema = new \Laurus\Schema();
$schema->addTextField("title");
$schema->addTextField("body");
$schema->addIntegerField("year");
$schema->addFloatField("score");
$schema->addBooleanField("published");
$schema->addBytesField("thumbnail");
$schema->addGeoField("location");
$schema->addDatetimeField("created_at");
$schema->addHnswField("embedding", 384);
$schema->addFlatField("small_vec", 64);
$schema->addIvfField("ivf_vec", 128, "cosine", 100, 1);

8. インデックス統計

$stats = $index->stats();
echo $stats["documentCount"];
echo $stats["vectorFields"];

API リファレンス

Index

Laurus 検索エンジンをラップするメインクラスです。

new \Laurus\Index(?string $path = null, ?Schema $schema = null)

コンストラクタ

パラメータデフォルト説明
$pathstring|nullnull永続ストレージのディレクトリパス。null の場合はインメモリインデックスを作成します。
$schemaSchema|nullnullスキーマ定義。省略時は空のスキーマが使用されます。

メソッド

メソッド説明
putDocument(string $id, array $doc): voidドキュメントをアップサート(upsert)します。同じ ID の既存バージョンをすべて置換します。
addDocument(string $id, array $doc): void既存バージョンを削除せずにドキュメントチャンクを追記します。
getDocuments(string $id): array指定 ID の全保存バージョンを返します。
deleteDocuments(string $id): void指定 ID の全バージョンを削除します。
commit(): voidバッファリングされた書き込みをフラッシュし、すべての保留中の変更を検索可能にします。
search(mixed $query, int $limit = 10, int $offset = 0): array検索クエリを実行します。SearchResult の配列を返します。
stats(): arrayインデックス統計("documentCount""vectorFields")を返します。

search の query 引数

$query パラメータは以下のいずれかを受け付けます:

  • DSL 文字列(例: "title:hello""embedding:\"memory safety\"")
  • Lexical クエリオブジェクトTermQueryPhraseQueryBooleanQuery など)
  • Vector クエリオブジェクトVectorQueryVectorTextQuery
  • SearchRequest(完全な制御が必要な場合)

Schema

Index のフィールドとインデックスタイプを定義します。

new \Laurus\Schema()

フィールドメソッド

メソッド説明
addTextField(string $name, bool $stored = true, bool $indexed = true, bool $termVectors = false, ?string $analyzer = null): void全文フィールド(転置インデックス、BM25)。$analyzer にはパラメータ不要の組込名("standard" / "english" / "keyword" / "simple" / "noop"、または addAnalyzer で登録したカスタム名)を指定します。Lindera 辞書パスが必要な Japanese プリセットは、lindera tokenizer を含むカスタム analyzer として登録し、名前で参照してください。
addIntegerField(string $name, bool $stored = true, bool $indexed = true, bool $multiValued = false): void64 ビット整数フィールド。$multiValued = true で整数配列を受け付け(範囲クエリは “any match”)。
addFloatField(string $name, bool $stored = true, bool $indexed = true, bool $multiValued = false): void64 ビット浮動小数点フィールド。$multiValued = true で浮動小数点配列を受け付け(範囲クエリは “any match”)。
addBooleanField(string $name, bool $stored = true, bool $indexed = true): voidブールフィールド。
addBytesField(string $name, bool $stored = true): void生バイトフィールド。
addGeoField(string $name, bool $stored = true, bool $indexed = true): void地理座標フィールド(緯度/経度)。
addGeo3dField(string $name, bool $stored = true, bool $indexed = true): void3D ECEF カルテシアン座標フィールド(x, y, z はメートル)。詳細は Geo3d の概念
addDatetimeField(string $name, bool $stored = true, bool $indexed = true): voidUTC 日時フィールド。
addHnswField(string $name, int $dimension, ?string $distance = "cosine", int $m = 16, int $efConstruction = 200, ?string $embedder = null): voidHNSW 近似最近傍ベクトルフィールド。
addFlatField(string $name, int $dimension, ?string $distance = "cosine", ?string $embedder = null): voidFlat(総当たり)ベクトルフィールド。
addIvfField(string $name, int $dimension, ?string $distance = "cosine", int $nClusters = 100, int $nProbe = 1, ?string $embedder = null): voidIVF 近似最近傍ベクトルフィールド。

その他のメソッド

メソッド説明
addEmbedder(string $name, array $config): void名前付きエンベダー定義を登録します。$config"type" キーを持つ連想配列です(下記参照)。
setDefaultFields(array $fieldNames): voidクエリでフィールドが指定されていない場合に使用するデフォルトフィールドを設定します。$fieldNames は文字列の配列です。
setDynamicFieldPolicy(string $policy): void未宣言フィールドの扱いを設定します。$policy"strict" / "dynamic"(デフォルト)/ "ignore"。詳細は下記を参照。
dynamicFieldPolicy(): string現在のポリシーを小文字の文字列で返します。
fieldNames(): arrayこのスキーマに定義されたフィールド名のリストを返します。

Dynamic field policy(動的フィールドポリシー)

ドキュメントに含まれるがスキーマに宣言されていないフィールドの扱いを制御します:

  • "strict" — ドキュメントを拒否
  • "dynamic"(デフォルト)— 各未宣言フィールドの型を推論してスキーマに追加。警告: integer フィールドに入ってきた float 値は静かに切り捨てられます(3.143)。厳密さが必要なら "strict" を使用してください
  • "ignore" — 未宣言フィールドを静かに破棄

詳細な挙動マトリクスは スキーマとフィールド を参照してください。

エンベダータイプ

"type"必須キーFeature Flag
"precomputed"(常に利用可能)
"candle_bert""model"embeddings-candle
"candle_clip""model"embeddings-multimodal
"openai""model"embeddings-openai

距離メトリクス

説明
"cosine"コサイン類似度(デフォルト)
"euclidean"ユークリッド距離
"dot_product"内積
"manhattan"マンハッタン距離
"angular"角度距離

クエリクラス

TermQuery

new \Laurus\TermQuery(string $field, string $term)

指定フィールドに完全一致する語句を含むドキュメントを検索します。

PhraseQuery

new \Laurus\PhraseQuery(string $field, array $terms)

指定した語句が順序どおりに含まれるドキュメントを検索します。$terms は文字列の配列です。

FuzzyQuery

new \Laurus\FuzzyQuery(string $field, string $term, int $maxEdits = 2)

編集距離が $maxEdits 以内の近似一致を検索します。

WildcardQuery

new \Laurus\WildcardQuery(string $field, string $pattern)

ワイルドカードパターン検索。* は任意の文字列、? は任意の1文字に一致します。

NumericRangeQuery

new \Laurus\NumericRangeQuery(string $field, mixed $min, mixed $max, ?string $numericType = "integer")

[$min, $max] の範囲内の数値を検索します。開いた境界には null を指定します。$numericType には "integer" または "float" を設定します。

GeoDistanceQuery

\Laurus\GeoDistanceQuery::withinRadius(
    string $field, float $lat, float $lon, float $distanceM,
): GeoDistanceQuery

地理的距離検索(半径指定)。指定した地点から $distanceM メートル以内の (lat, lon) 座標を持つドキュメントを返します。

GeoBoundingBoxQuery

\Laurus\GeoBoundingBoxQuery::withinBoundingBox(
    string $field,
    float $minLat, float $minLon,
    float $maxLat, float $maxLon,
): GeoBoundingBoxQuery

地理的範囲(バウンディングボックス)検索。軸並行 [$minLat, $maxLat] × [$minLon, $maxLon] 内の (lat, lon) 座標を持つドキュメントを返します。

Geo3dDistanceQuery

\Laurus\Geo3dDistanceQuery::withinSphere(
    string $field,
    float $x, float $y, float $z,
    float $distanceM,
): Geo3dDistanceQuery

3D ECEF 座標フィールドへの球距離検索。中心 (x, y, z) から $distanceM メートル以内 の座標を持つドキュメントを返します。ECEF の理論については Geo3d の概念 を参照。

Geo3dBoundingBoxQuery

\Laurus\Geo3dBoundingBoxQuery::withinBox(
    string $field,
    float $minX, float $minY, float $minZ,
    float $maxX, float $maxY, float $maxZ,
): Geo3dBoundingBoxQuery

軸並行 3D 範囲(AABB)検索。

Geo3dNearestQuery

\Laurus\Geo3dNearestQuery::kNearest(
    string $field,
    float $x, float $y, float $z,
    int $k,
    ?float $initialRadiusM = null,
    ?float $maxRadiusM = null,
): Geo3dNearestQuery

3D ECEF 座標フィールドへの k 最近傍検索。$initialRadiusM / $maxRadiusM (オプション)で反復拡張サーチの探索コーンを調整できます。

BooleanQuery

$bq = new \Laurus\BooleanQuery();
$bq->must($query);
$bq->should($query);
$bq->mustNot($query);

複合ブールクエリ。must 節はすべて一致する必要があり、mustNot 節は一致してはなりません。should 節はスコアリングに寄与し、must 節が無い場合は少なくとも1つが一致する必要があります。

SpanQuery

// 単一語句
\Laurus\SpanQuery::term(string $field, string $term): SpanQuery

// Near: slop 位置以内の語句
\Laurus\SpanQuery::near(string $field, array $terms, int $slop = 0, bool $ordered = true): SpanQuery

// NearSpans: slop 位置以内のネストされた SpanQuery 句
\Laurus\SpanQuery::nearSpans(string $field, array $clauses, int $slop = 0, bool $ordered = true): SpanQuery

// Containing: big スパンが little スパンを含む
\Laurus\SpanQuery::containing(string $field, SpanQuery $big, SpanQuery $little): SpanQuery

// Within: 最大距離での include スパンと exclude スパン
\Laurus\SpanQuery::within(string $field, SpanQuery $include, SpanQuery $exclude, int $distance): SpanQuery

位置・近接スパンクエリ。near は語句文字列の配列を受け取り、nearSpans は ネスト式のために SpanQuery オブジェクトの配列を受け取ります(各句のフィールド は外側の $field に再ルートされます)。

VectorQuery

new \Laurus\VectorQuery(string $field, array $vector)

事前計算済みエンベディングベクトルを使った近似最近傍検索を行います。$vector は Float の配列です。

VectorTextQuery

new \Laurus\VectorTextQuery(string $field, string $text)

クエリ時に $text をエンベディングに変換してベクトル検索を行います。インデックスにエンベダーの設定が必要です。


SearchRequest

高度な制御が必要な場合の完全なリクエストクラスです。

new \Laurus\SearchRequest(
    mixed $query = null,
    mixed $lexicalQuery = null,
    mixed $vectorQuery = null,
    mixed $filterQuery = null,
    mixed $fusion = null,
    int $limit = 10,
    int $offset = 0,
)
パラメータ説明
$queryDSL 文字列または単一クエリオブジェクト。$lexicalQuery / $vectorQuery と排他的。
$lexicalQuery明示的なハイブリッド検索の Lexical コンポーネント。
$vectorQuery明示的なハイブリッド検索の Vector コンポーネント。
$filterQueryスコアリング後に適用する Lexical フィルター。
$fusionフュージョンアルゴリズム(RRF または WeightedSum)。両コンポーネント指定時のデフォルトは RRF(k: 60)
$limit最大結果件数(デフォルト 10)。
$offsetページネーションオフセット(デフォルト 0)。

SearchResult

Index->search() が返すクラスです。

$result->getId()        // string   -- 外部ドキュメント識別子
$result->getScore()     // float    -- 関連性スコア
$result->getDocument()  // array|null -- 取得されたフィールド値。stored=false の場合は null

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

RRF

new \Laurus\RRF(float $k = 60.0)

逆順位フュージョン(Reciprocal Rank Fusion)。Lexical と Vector の結果リストを順位位置によってマージします。$k は平滑化定数で、値が大きいほど上位ランクの影響が小さくなります。

WeightedSum

new \Laurus\WeightedSum(float $lexicalWeight = 0.5, float $vectorWeight = 0.5)

両スコアリストをそれぞれ正規化した後、$lexicalWeight * lexical_score + $vectorWeight * vector_score として結合します。


テキスト解析

SynonymDictionary

$dict = new \Laurus\SynonymDictionary();
$dict->addSynonymGroup(["fast", "quick", "rapid"]);

同義語グループの辞書です。グループ内のすべての語句は互いの同義語として扱われます。

WhitespaceTokenizer

$tokenizer = new \Laurus\WhitespaceTokenizer();
$tokens = $tokenizer->tokenize("hello world");

空白で分割してテキストをトークン化し、Token オブジェクトの配列を返します。

SynonymGraphFilter

new \Laurus\SynonymGraphFilter(SynonymDictionary $dictionary, bool $keepOriginal = true, float $boost = 1.0)
パラメータ説明
$dictionary同義語グループのソース。
$keepOriginaltrue(デフォルト)の場合は元のトークンも同義語と並べて保持します。
$boost挿入される同義語トークンに適用されるスコアブースト(デフォルト 1.0)。
$filter = new \Laurus\SynonymGraphFilter($dictionary, true, 1.0);
$expanded = $filter->apply($tokens);

SynonymDictionary の同義語でトークンを展開するトークンフィルターです。

Token

$token->getText()               // string  -- トークンテキスト
$token->getPosition()           // int     -- トークンストリーム内の位置
$token->getStartOffset()        // int     -- 元テキスト内の文字開始オフセット
$token->getEndOffset()          // int     -- 元テキスト内の文字終了オフセット
$token->getBoost()              // float   -- スコアブースト係数(1.0 = 調整なし)
$token->isStopped()             // bool    -- ストップフィルターによって除去されたかどうか
$token->getPositionIncrement()  // int     -- 前のトークンの位置との差分
$token->getPositionLength()     // int     -- このトークンがカバーする位置数

フィールド値の型マッピング

PHP の値は自動的に Laurus の DataValue 型に変換されます:

PHP 型Laurus 型備考
nullNull
true / falseBool
intInt64
floatFloat64
stringText
array(数値)Vector要素は f32 に変換
array"lat", "lon"Geo2 つの float
array"x", "y", "z"GeoEcef3 つの float 値(メートル単位、3D ECEF 直交座標)
string(ISO 8601)DateTimeISO 8601 形式からパース

開発環境のセットアップ

このページでは laurus-php バインディングのローカル開発環境の構築、ビルド、テストスイートの実行方法について説明します。

前提条件

  • Rust 1.85 以降(Cargo 含む)
  • PHP 8.1 以降(開発ヘッダー付き: php-dev / php-devel
  • Composer(依存関係管理用)
  • リポジトリがローカルにクローンされていること
git clone https://github.com/mosuka/laurus.git
cd laurus

ビルド

開発ビルド

Rust ネイティブ拡張をデバッグモードでコンパイルします。Rust ソースを変更した場合は再実行してください。

cd laurus-php
cargo build

ビルド成果物は ../target/debug/liblaurus_php.so に生成されます。

リリースビルド

cd laurus-php
cargo build --release

ビルド成果物は ../target/release/liblaurus_php.so に生成されます。

ビルドの確認

php -d extension=../target/release/liblaurus_php.so -r "
use Laurus\Index;
\$index = new Index();
print_r(\$index->stats());
"
# Array ( [documentCount] => 0 [vectorFields] => Array ( ) )

テスト

テストは PHPUnit を使用しており、tests/ ディレクトリにあります。

# テスト依存関係をインストール
composer install

# 全テスト実行
php -d extension=../target/release/liblaurus_php.so vendor/bin/phpunit tests/

特定のテストファイルを実行する場合:

php -d extension=../target/release/liblaurus_php.so vendor/bin/phpunit tests/LaurusTest.php

Lint とフォーマット

# Rust lint(Clippy)
cargo clippy -p laurus-php -- -D warnings

# Rust フォーマットチェック
cargo fmt -p laurus-php --check

# フォーマット適用
cargo fmt -p laurus-php

クリーンアップ

# ビルド成果物を削除
cargo clean

# Composer 依存関係を削除
rm -rf vendor/

Workspace 統合と clang-sys パッチ

laurus-phpext-php-rs を使用しており、 ext-php-rs は ext-php-rs-clang-sysclang-sys のフォーク)に依存しています。 一方、laurus-rubymagnusrb-sysbindgenclang-sys(オリジナル)に依存しています。 両方のクレートが links = "clang" を宣言しており、Cargo は同一 workspace 内で同じ links 値を持つ パッケージを 2 つ許可しません。

laurus-phplaurus-ruby を workspace メンバーとして共存させるため、ルートの Cargo.tomlext-php-rs-clang-syslinks 宣言を除去したローカルコピーに patch しています:

# Cargo.toml(workspace ルート)
[patch.crates-io]
ext-php-rs-clang-sys = { path = "patches/ext-php-rs-clang-sys" }

パッチは patches/ext-php-rs-clang-sys/ にあります。上流クレートからの唯一の変更点は Cargo.tomllinks = "clang" の除去です。clang-sysext-php-rs-clang-sys は どちらも libclang をビルド時のみ使用し(bindgen によるヘッダー解析)、最終バイナリにはリンク されないため、この変更は安全です。

パッチが必要な条件

このパッチは laurus-phplaurus-ruby が同一の Cargo workspace のメンバーである場合にのみ 必要です。laurus-ruby を workspace から除外するか、laurus-php[workspace] exclude で 除外すれば、links = "clang" の競合は発生しないため、パッチとルート Cargo.toml[patch.crates-io] セクションを削除できます。

パッチの更新

ext-php-rs をアップグレードして新しいバージョンの ext-php-rs-clang-sys が 使われるようになった場合、パッチを更新してください:

# 1. laurus-php/Cargo.toml で ext-php-rs を更新した後:
cargo update -p ext-php-rs

# 2. 新しい ext-php-rs-clang-sys ソースをコピー
cp -r ~/.cargo/registry/src/index.crates.io-*/ext-php-rs-clang-sys-<NEW_VERSION>/* \
      patches/ext-php-rs-clang-sys/

# 3. links 宣言を除去
sed -i 's/^links = "clang"/# links = "clang"/' patches/ext-php-rs-clang-sys/Cargo.toml

# 4. ビルドを確認
cargo build -p laurus-php -p laurus-ruby

macOS リンカーフラグ (-undefined dynamic_lookup)

PHP 拡張は共有ライブラリ(.so / .dylib)であり、実行時に PHP インタプリタに ロードされます。PHP API シンボル(zend_*, php_* 等)は PHP バイナリ本体に 定義されており、拡張がリンクするライブラリには含まれません。Linux ではリンカーが 共有ライブラリ内の未定義シンボルをデフォルトで許容するため問題ありませんが、 macOS ではリンカーが未定義シンボルをエラーとして扱い、ビルドが失敗します:

ld: symbol(s) not found for architecture arm64

修正方法は -Wl,-undefined,dynamic_lookup をリンカーに渡すことです。これにより シンボル解決がロード時(PHP が拡張を dlopen する時点)まで延期されます。

このフラグは .cargo/config.toml には設定しません。設定すると workspace 内の 全クレートに適用され、PHP 以外のクレートでも未定義シンボルがエラーにならなくなる ためです。代わりに laurus-php のビルド時のみ適用します:

Makefile(ローカル開発):

build-laurus-php:
ifeq ($(shell uname -s),Darwin)
    RUSTFLAGS="-C link-args=-Wl,-undefined,dynamic_lookup" cargo build -p laurus-php --release
else
    cargo build -p laurus-php --release
endif

CI(GitHub Actions):

- name: Build PHP extension
  shell: bash
  run: |
    if [ "$RUNNER_OS" == "macOS" ]; then
      export RUSTFLAGS="-C link-args=-Wl,-undefined,dynamic_lookup"
    fi
    cargo build --release -p laurus-php

macOS でビルドする際は、cargo build -p laurus-php を直接実行するのではなく、 make build-laurus-php または make test-laurus-php を使用してください。

プロジェクト構成

laurus-php/
├── Cargo.toml          # Rust クレートマニフェスト
├── composer.json       # Composer パッケージ定義
├── composer.lock       # ロックされた依存関係バージョン
├── src/                # Rust ソース(ext-php-rs バインディング)
│   ├── lib.rs          # モジュール登録
│   ├── index.rs        # Index クラス
│   ├── schema.rs       # Schema クラス
│   ├── query.rs        # クエリクラス
│   ├── search.rs       # SearchRequest / SearchResult / Fusion
│   ├── analysis.rs     # Tokenizer / Filter / Token
│   ├── convert.rs      # PHP <-> DataValue 変換
│   └── errors.rs       # エラーマッピング
├── tests/              # PHPUnit テスト
│   └── LaurusTest.php
└── examples/           # 実行可能な PHP サンプル

ビルドとテスト

前提条件

  • 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"

ベンチマーク

このガイドでは、laurus のベンチマークの実行方法、ベースライン(baseline)の保存と比較方法、プルリクエストでの結果報告方法について説明します。

ベンチマークスイートは laurus/benches/ 配下にあり、Criterion で構築されています。衛生ルール(決定的シード、ファイル冒頭ドキュメント、sanity assert、sample_size ポリシー)は laurus/benches/common.rs で一元管理されています。

スイート一覧

ファイルスコープ
bkd_bench.rsBKD ツリーの範囲検索(range search)、交差判定(intersect)、構築(1D / 2D / 3D、10k / 100k / 1M ポイント)
distance_bench.rsDistanceMetric::distance の cosine / Euclidean / Manhattan / dot product(現状は単一次元、次元スイープは #424 で対応予定)
lexical_search_bench.rsEngine::search 経由のエンドツーエンド lexical 検索(term / boolean / phrase / fuzzy / DSL)
search_perf.rsPosting iterator の skip_toBM25Scorer::score、SIMD バッチスコアリング、コンパクト posting 変換
spell_correction_bench.rsSpellingCorrector::correct、各イテレーションごとに fresh corrector を渡す cold-state 計測
synonym_bench.rsSynonymDictionary::get_synonyms のルックアップ(100 / 1k / 10k グループ)と構築コスト
text_analysis_bench.rsStandardAnalyzer::analyze のシングルドキュメントとバッチ(100 ドキュメント)解析
vector_search_bench.rsFlat / IVF / HNSW の構築と検索(1k / 5k ベクタ、dim 128、top-10)

各ファイル冒頭の //! ドキュメントコメントにスコープ・シナリオ・フィルタ方法が書かれています。実行前に確認してください。

ベンチマークの実行

単一ベンチファイルを実行:

cargo bench -p laurus --bench distance_bench

criterion id でフィルタ(部分一致):

cargo bench -p laurus --bench distance_bench -- cosine
cargo bench -p laurus --bench vector_search_bench -- "HNSW Search/top10"

コンパイル確認のみ(CI やリファクタリング時に有用):

cargo bench -p laurus --bench distance_bench --no-run

ワークスペースの全ベンチを実行:

cargo bench -p laurus

ベースラインの保存と比較

Criterion は名前付きベースラインをサポートしており、フィーチャーブランチを main(または任意の参照状態)と比較できます。

現在の状態を main という名前のベースラインとして保存:

cargo bench -p laurus --bench distance_bench -- --save-baseline main

その後の実行結果をベースラインと比較:

cargo bench -p laurus --bench distance_bench -- --baseline main

出力には change: 行がベンチマーク単位で表示され、変化率と判定(No change in performance detectedPerformance has improvedPerformance has regressed)が示されます。Criterion はベースラインを target/criterion/<bench-id>/<baseline>/ 配下に保存します。

perf PR の推奨フロー:

  1. main(または変更前の状態)で — cargo bench --bench RELEVANT -- --save-baseline main
  2. ブランチで変更を実装
  3. ブランチで — cargo bench --bench RELEVANT -- --baseline main
  4. change: 行を PR 説明にコピーする

推奨環境

µs / ns 単位のマイクロベンチマークはシステムノイズに敏感です。意味のある数値を得るには:

  • CPU governor: performance に設定(Linux):

    sudo cpupower frequency-set -g performance
    
  • Turbo boost: 周波数スケーリングが結果を歪めないように無効化:

    echo 1 | sudo tee /sys/devices/system/cpu/intel_pstate/no_turbo   # Intel
    

    AMD システムや BIOS レベルの設定は異なるためベンダーのドキュメントを参照してください。

  • バックグラウンド負荷: ブラウザ・IDE・ビルドウォッチャー・Docker を停止する。CPU を共有するものは短時間ベンチを歪めます。

  • コア固定(任意): 利用可能なら固定コアにピン留め:

    taskset -c 2 cargo bench -p laurus --bench distance_bench
    
  • 再実行: 2 回実行して比較する。チューニング済みマシンで ~5 % 以下の差はノイズ。共有ワークステーションでは ~5 % を超える差もノイズの可能性がある。1 回の実行結果を過大解釈しない。

環境を安定化できない場合は、不安定な数値を権威ある数値として提示せず、PR で明示的に断ること(例:「共有ノート PC で測定、~10 % のノイズを想定」)。

Make ターゲット

Makefile から共通エントリポイントを利用できます:

make bench             # cargo bench -p laurus
make bench-baseline    # cargo bench -p laurus -- --save-baseline main
make bench-compare     # cargo bench -p laurus -- --baseline main

単一ベンチを指定する場合は BENCH=name を渡します:

make bench BENCH=distance_bench
make bench-baseline BENCH=distance_bench
make bench-compare BENCH=distance_bench

PR 説明テンプレート

PR で計測可能なパフォーマンス変化を主張する場合、下記のようなテーブルを説明に貼り付けてください:

## Performance

Environment: <CPU モデル>, governor=performance, turbo disabled, dedicated machine.

Baseline: `main` at <commit-sha>. After: this branch at <commit-sha>.

| Bench | Before | After | Δ | Verdict |
| --- | --- | --- | --- | --- |
| `distance_metrics/cosine` | 4.20 µs | 3.10 µs | -26 % | improved |
| `distance_metrics/euclidean` | 2.18 µs | 2.16 µs | -1 % | no change |

Reproduce: `cargo bench -p laurus --bench distance_bench -- --baseline main`

比較が再現可能となるように、ベースラインと変更後の commit SHA を必ず含めてください。チューニング済みマシンで実行した場合でも環境を明示してください。

新しいベンチマークの追加

新規ベンチファイルを追加する際は、laurus/benches/common.rs のスイート全体衛生ルールに従ってください:

  1. common::DEFAULT_SEED(または lcg_* ヘルパ)で決定的シードを使う。rand::rng() は使わない。
  2. ファイル冒頭に //! ドキュメントコメントでスコープ・シナリオ・実行コマンド・フィルタ例を記載する。
  3. タイミング b.iter の外側で 1 度だけ assert! を実行し、空結果を出す regression を黙ってパスさせない。
  4. SAMPLE_SIZE_FAST(デフォルト、50 ms 以下の操作向け)または SAMPLE_SIZE_SLOW(構築パス向け)のいずれかを選ぶ。中間値は使わない。
  5. laurus/Cargo.toml[[bench]] name = "..." harness = false で登録する。クレートは autobenches = false を設定しているため、benches/ 配下のファイルは自動検出されない。

ファイル間でヘルパを共有する必要がある場合は、benches/common.rs を拡張してコード重複を避けてください。

CI 連携

現状、CI ではリグレッション検出のベンチジョブは実行していません。perf 系の PR は、推奨環境下で取得した baseline-vs-after 数値を手動で投稿することが想定されています。

将来的には大きなリグレッションで失敗するスモークセットのベンチジョブを追加する案があり、アンブレラ Issue #429 で追跡されています。

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 互換であること