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 DSL | Lexical、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"]
- スキーマを定義する — フィールドとその型(text、integer、vector など)を宣言します
- Engine を構築する — テキスト用のアナライザと Vector 用のエンベッダを接続します
- ドキュメントをインデックスする — Engine が各フィールドを適切なインデックスに自動的に振り分けます
- 検索する — Lexical、Vector、またはハイブリッドクエリを実行し、ランク付けされた結果を取得します
ドキュメントマップ
| セクション | 学べること |
|---|---|
| はじめに | Laurus をインストールし、数分で最初の検索を実行する |
| アーキテクチャ | Engine のコンポーネントとデータフローを理解する |
| コアコンセプト | スキーマ、テキスト解析、エンベディング、ストレージ |
| インデクシング | 転置インデックスと Vector インデックスの内部動作 |
| 検索 | クエリの種類、Vector 検索、ハイブリッド融合 |
| Query DSL | すべての検索タイプに対応した人間が読みやすいクエリ構文 |
| ライブラリ (laurus) | Engine の内部構造、スコアリング、ファセット、拡張性 |
| CLI (laurus-cli) | インデックス管理と検索のためのコマンドラインツール |
| サーバー (laurus-server) | HTTP Gateway を備えた gRPC サーバー |
| 開発ガイド | Laurus のビルド、テスト、コントリビュート |
クイックサンプル
use std::sync::Arc;
use laurus::{Document, Engine, Schema, SearchRequestBuilder, Result};
use laurus::lexical::{TextOption, TermQuery};
use laurus::storage::memory::MemoryStorage;
#[tokio::main]
async fn main() -> Result<()> {
// 1. Storage
let storage = Arc::new(MemoryStorage::new(Default::default()));
// 2. Schema
let schema = Schema::builder()
.add_text_field("title", TextOption::default())
.add_text_field("body", TextOption::default())
.add_default_field("body")
.build();
// 3. Engine
let engine = Engine::builder(storage, schema).build().await?;
// 4. Index a document
let doc = Document::builder()
.add_text("title", "Hello Laurus")
.add_text("body", "A fast search library for Rust")
.build();
engine.add_document("doc-1", doc).await?;
engine.commit().await?;
// 5. Search
let request = SearchRequestBuilder::new()
.lexical_search_request(
laurus::LexicalSearchRequest::new(
Box::new(TermQuery::new("body", "rust"))
)
)
.limit(10)
.build();
let results = engine.search(request).await?;
for r in &results {
println!("{}: score={:.4}", r.id, r.score);
}
Ok(())
}
ライセンス
Laurus は MIT ライセンス のもとで提供されています。
アーキテクチャ
このページでは、Laurus の内部構造について説明します。アーキテクチャを理解することで、スキーマ設計、Analyzer の選択、検索戦略についてより適切な判断ができるようになります。
プロジェクト構成
Laurus は Cargo workspace として 3 つのクレートで構成されています。
graph TB
CLI["laurus-cli\n(Binary Crate)\nCLI + REPL"]
SRV["laurus-server\n(Library + Binary)\ngRPC Server + HTTP Gateway"]
LIB["laurus\n(Library Crate)\nCore Search Engine"]
CLI --> LIB
SRV --> LIB
| クレート | 種類 | 説明 |
|---|---|---|
| laurus | Library | コア検索エンジン – Lexical 検索、Vector 検索、ハイブリッド検索 |
| laurus-cli | Binary | インデックス管理と検索のためのコマンドラインインターフェース |
| laurus-server | Library + Binary | オプションの HTTP/JSON ゲートウェイ付き gRPC サーバー |
各クレートの詳細については以下を参照してください。
全体概要
Laurus は単一の Engine を中心に構成されており、4 つの内部コンポーネントを統括します。
graph TB
subgraph Engine
SCH["Schema"]
LS["LexicalStore\n(Inverted Index)"]
VS["VectorStore\n(HNSW / Flat / IVF)"]
DL["DocumentLog\n(WAL + Document Storage)"]
end
Storage["Storage (trait)\nMemory / File / File+Mmap"]
LS --- Storage
VS --- Storage
DL --- Storage
| コンポーネント | 役割 |
|---|---|
| Schema | フィールドとその型を宣言し、各フィールドのルーティング先を決定する |
| LexicalStore | キーワード検索のための転置インデックス(Inverted Index)(BM25 スコアリング) |
| VectorStore | 類似度検索のためのベクトルインデックス(Flat、HNSW、または IVF) |
| DocumentLog | 耐久性のための WAL(Write-Ahead Log)+ ドキュメントストレージ |
3 つのストアはすべて単一の Storage バックエンドを共有し、キープレフィックス(lexical/、vector/、documents/)によって分離されています。
Engine のライフサイクル
Engine の構築
EngineBuilder が各パーツから Engine を組み立てます。
#![allow(unused)]
fn main() {
let engine = Engine::builder(storage, schema)
.analyzer(analyzer) // optional: for text fields
.embedder(embedder) // optional: for vector fields
.build()
.await?;
}
sequenceDiagram
participant User
participant EngineBuilder
participant Engine
User->>EngineBuilder: new(storage, schema)
User->>EngineBuilder: .analyzer(analyzer)
User->>EngineBuilder: .embedder(embedder)
User->>EngineBuilder: .build().await
EngineBuilder->>EngineBuilder: split_schema()
Note over EngineBuilder: Separate fields into\nLexicalIndexConfig\n+ VectorIndexConfig
EngineBuilder->>Engine: Create LexicalStore
EngineBuilder->>Engine: Create VectorStore
EngineBuilder->>Engine: Create DocumentLog
EngineBuilder->>Engine: Recover from WAL
EngineBuilder-->>User: Engine ready
build() 時に Engine は以下の処理を行います。
- スキーマの分割 — Lexical フィールドは
LexicalIndexConfigへ、Vector フィールドはVectorIndexConfigへ振り分けられる - プレフィックス付きストレージの作成 — 各コンポーネントが分離された名前空間を取得する(
lexical/、vector/、documents/) - ストアの初期化 —
LexicalStoreとVectorStoreがそれぞれの設定で構築される - 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 つのステージで構成されています。
- フィルタ(オプション) — Lexical インデックスに対してフィルタクエリを実行し、許可されたドキュメント ID のセットを取得する
- 検索 — Lexical クエリと Vector クエリを並列に実行する
- フュージョン — 両方のクエリタイプが存在する場合、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/ プレフィックス |
| フィールドごとのディスパッチ | PerFieldAnalyzer と PerFieldEmbedder がフィールド固有の実装にルーティングする |
次のステップ
- フィールドタイプとスキーマ設計を理解する: スキーマとフィールド
- テキスト解析について学ぶ: テキスト解析
- Embedding について学ぶ: Embedding
はじめに
Laurus へようこそ! このセクションでは、ライブラリのインストールから最初の検索の実行までをガイドします。
作成するもの
このガイドを終えると、以下の機能を持つ検索エンジンが動作するようになります:
- テキストドキュメントのインデックス
- キーワード(Lexical)検索の実行
- セマンティック(Vector)検索の実行
- ハイブリッド検索による両者の統合
前提条件
- Rust 1.85 以降(edition 2024)
- Cargo(Rust に同梱)
- Tokio ランタイム(Laurus は非同期 API を使用します)
ステップ
ワークフロー概要
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.toml に laurus と tokio(非同期ランタイム)を追加します:
[dependencies]
laurus = "0.1.0"
tokio = { version = "1", features = ["full"] }
Feature Flags
Laurus はデフォルトで最小限の機能セットで提供されます。必要に応じて追加の機能を有効にしてください:
| Feature | 説明 | ユースケース |
|---|---|---|
| (default) | コアライブラリ(Lexical 検索、ストレージ、アナライザ — エンベディングなし) | キーワード検索のみ |
embeddings-candle | Hugging Face Candle によるローカル BERT エンベディング | 外部 API 不要の Vector 検索 |
embeddings-openai | OpenAI API エンベディング(text-embedding-3-small 等) | クラウドベースの Vector 検索 |
embeddings-multimodal | Candle による 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_field | Text(全文検索可能) | "Hello world" |
add_integer_field | 64 ビット整数 | 42 |
add_float_field | 64 ビット浮動小数点数 | 3.14 |
add_boolean_field | ブール値 | true / false |
add_datetime_field | UTC 日時 | 2024-01-15T10:30:00Z |
add_hnsw_field | Vector(HNSW インデックス) | [0.1, 0.2, ...] |
add_flat_field | Vector(Flat インデックス) | [0.1, 0.2, ...] |
全一覧はスキーマとフィールドを参照してください。
ステップ 3 — Engine の構築
Engine は Storage、Schema、ランタイムコンポーネントを統合します:
#![allow(unused)]
fn main() {
use laurus::Engine;
let engine = Engine::builder(storage, schema)
.build()
.await?;
}
テキストフィールドのみを使用する場合、デフォルトの StandardAnalyzer が自動的に適用されます。解析のカスタマイズや Vector エンベディングの追加については、アーキテクチャを参照してください。
ステップ 4 — ドキュメントのインデックス
DocumentBuilder でドキュメントを作成し、Engine に追加します:
#![allow(unused)]
fn main() {
use laurus::Document;
// Each document needs a unique external ID (string)
let doc = Document::builder()
.add_text("title", "Introduction to Rust")
.add_text("body", "Rust is a systems programming language focused on safety and performance.")
.build();
engine.add_document("doc-1", doc).await?;
let doc = Document::builder()
.add_text("title", "Python for Data Science")
.add_text("body", "Python is widely used in machine learning and data analysis.")
.build();
engine.add_document("doc-2", doc).await?;
let doc = Document::builder()
.add_text("title", "Web Development with JavaScript")
.add_text("body", "JavaScript powers interactive web applications and server-side code with Node.js.")
.build();
engine.add_document("doc-3", doc).await?;
// Commit to make documents searchable
engine.commit().await?;
}
重要: ドキュメントは
commit()が呼ばれるまで検索可能になりません。
ステップ 5 — 検索
SearchRequestBuilder とクエリを使ってインデックスを検索します:
#![allow(unused)]
fn main() {
use laurus::{SearchRequestBuilder, LexicalSearchRequest};
use laurus::lexical::TermQuery;
// Search for "rust" in the "body" field
let request = SearchRequestBuilder::new()
.lexical_search_request(
LexicalSearchRequest::new(
Box::new(TermQuery::new("body", "rust"))
)
)
.limit(10)
.build();
let results = engine.search(request).await?;
for result in &results {
println!("ID: {}, Score: {:.4}", result.id, result.score);
if let Some(doc) = &result.document {
if let Some(title) = doc.get("title") {
println!(" Title: {:?}", title);
}
}
}
}
完全なサンプル
以下は、コピー・ペーストしてそのまま実行できる完全なプログラムです:
use std::sync::Arc;
use laurus::{
Document, Engine, LexicalSearchRequest,
Result, Schema, SearchRequestBuilder,
};
use laurus::lexical::{TextOption, TermQuery};
use laurus::storage::memory::MemoryStorage;
#[tokio::main]
async fn main() -> Result<()> {
// 1. Storage
let storage = Arc::new(MemoryStorage::new(Default::default()));
// 2. Schema
let schema = Schema::builder()
.add_text_field("title", TextOption::default())
.add_text_field("body", TextOption::default())
.add_default_field("body")
.build();
// 3. Engine
let engine = Engine::builder(storage, schema).build().await?;
// 4. Index documents
for (id, title, body) in [
("doc-1", "Introduction to Rust", "Rust is a systems programming language focused on safety."),
("doc-2", "Python for Data Science", "Python is widely used in machine learning."),
("doc-3", "Web Development", "JavaScript powers interactive web applications."),
] {
let doc = Document::builder()
.add_text("title", title)
.add_text("body", body)
.build();
engine.add_document(id, doc).await?;
}
engine.commit().await?;
// 5. Search
let request = SearchRequestBuilder::new()
.lexical_search_request(
LexicalSearchRequest::new(
Box::new(TermQuery::new("body", "rust"))
)
)
.limit(10)
.build();
let results = engine.search(request).await?;
for r in &results {
println!("{}: score={:.4}", r.id, r.score);
}
Ok(())
}
次のステップ
- Engine の内部動作を学ぶ: アーキテクチャ
- Schema とフィールド型を理解する: スキーマとフィールド
- Vector 検索を追加する: Vector 検索
- Lexical と Vector を統合する: ハイブリッド検索
サンプル
laurus/examples/ ディレクトリには、ライブラリのさまざまな機能を示す実行可能なサンプルが含まれています。
サンプルの実行
# Feature Flags なしでサンプルを実行
cargo run --example <name>
# Feature Flags を指定してサンプルを実行
cargo run --example <name> --features <flag>
利用可能なサンプル
quickstart
基本的なワークフローを示す最小限のサンプルです: Storage の作成、Schema の定義、Engine の構築、ドキュメントのインデックス、検索を行います。
cargo run --example quickstart
デモ内容: インメモリストレージ、TextOption、TermQuery、LexicalSearchRequest
lexical_search
すべての Lexical クエリ型を示す包括的なサンプルです。Builder API と QueryParser DSL の両方を使用します。
cargo run --example lexical_search
デモ内容: TermQuery、PhraseQuery、FuzzyQuery、WildcardQuery、NumericRangeQuery、GeoQuery、BooleanQuery、SpanQuery
vector_search
モックエンベッダを使用した Vector 検索のサンプルです。フィルタ付き Vector 検索や DSL 構文も含みます。
cargo run --example vector_search
デモ内容: PerFieldEmbedder、VectorSearchRequestBuilder、フィルタ付き検索、DSL 構文(field:~"query")
hybrid_search
異なる融合アルゴリズムを用いた Lexical 検索と Vector 検索の統合サンプルです。
cargo run --example hybrid_search
デモ内容: Lexical のみ、Vector のみ、ハイブリッド検索。RRF と WeightedSum の両方の融合アルゴリズム。Builder API と DSL。
search_with_candle
Hugging Face Candle を使用した実際の BERT エンベディングによる Vector 検索です。初回実行時にモデルが自動的にダウンロードされます(約 80 MB)。
cargo run --example search_with_candle --features embeddings-candle
必要条件: embeddings-candle Feature Flag
デモ内容: CandleBertEmbedder(sentence-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 環境変数
デモ内容: OpenAIEmbedder(text-embedding-3-small、1536 次元)
multimodal_search
CLIP モデルを使用したマルチモーダル(テキスト + 画像)検索です。
cargo run --example multimodal_search --features embeddings-multimodal
必要条件: embeddings-multimodal Feature Flag
デモ内容: CandleClipEmbedder、ファイルシステムからの画像インデックス、テキスト→画像クエリおよび画像→画像クエリ
synonym_graph_filter
解析時のトークン展開のための SynonymGraphFilter のデモです。
cargo run --example synonym_graph_filter
デモ内容: シノニム辞書の作成、シノニムによるトークン展開、ブーストの適用、トークンの position および position_length 属性
ヘルパーモジュール: common.rs
common.rs ファイルは、サンプルで使用される共通ユーティリティを提供します:
memory_storage()– インメモリストレージインスタンスの作成per_field_analyzer()– 特定のフィールドにKeywordAnalyzerを設定したPerFieldAnalyzerの作成MockEmbedder– 実際のモデルなしで Vector 検索をテストするためのモックEmbedder実装
スキーマとフィールド
Schema はドキュメントの構造を定義します。どのフィールドが存在し、各フィールドがどのようにインデクシングされるかを指定します。Schema は Engine にとって唯一の情報源です。
CLI で使用される TOML ファイル形式については、スキーマフォーマットリファレンスを参照してください。
Schema
Schema は名前付きフィールドのコレクションです。各フィールドはLexical フィールド(キーワード検索用)または Vector フィールド(類似度検索用)のいずれかです。
#![allow(unused)]
fn main() {
use laurus::Schema;
use laurus::lexical::TextOption;
use laurus::lexical::core::field::IntegerOption;
use laurus::vector::HnswOption;
let schema = Schema::builder()
.add_text_field("title", TextOption::default())
.add_text_field("body", TextOption::default())
.add_integer_field("year", IntegerOption::default())
.add_hnsw_field("embedding", HnswOption::default())
.add_default_field("body")
.build();
}
デフォルトフィールド
add_default_field() は、クエリがフィールド名を明示的に指定しない場合に検索対象となるフィールドを指定します。これは Query DSL パーサーで使用されます。
フィールドタイプ
graph TB
FO["FieldOption"]
FO --> T["Text"]
FO --> I["Integer"]
FO --> FL["Float"]
FO --> B["Boolean"]
FO --> DT["DateTime"]
FO --> G["Geo"]
FO --> BY["Bytes"]
FO --> FLAT["Flat"]
FO --> HNSW["HNSW"]
FO --> IVF["IVF"]
Lexical フィールド
Lexical フィールドは転置インデックス(Inverted Index)を使用してインデクシングされ、キーワードベースのクエリをサポートします。
| タイプ | Rust 型 | SchemaBuilder メソッド | 説明 |
|---|---|---|---|
| Text | TextOption | add_text_field() | 全文検索可能。Analyzer によりトークン化される |
| Integer | IntegerOption | add_integer_field() | 64 ビット符号付き整数。範囲クエリをサポート |
| Float | FloatOption | add_float_field() | 64 ビット浮動小数点数。範囲クエリをサポート |
| Boolean | BooleanOption | add_boolean_field() | true / false |
| DateTime | DateTimeOption | add_datetime_field() | UTC タイムスタンプ。範囲クエリをサポート |
| Geo | GeoOption | add_geo_field() | 緯度/経度のペア。半径検索とバウンディングボックスクエリをサポート |
| Bytes | BytesOption | add_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);
}
| オプション | デフォルト | 説明 |
|---|---|---|
indexed | true | フィールドが検索可能かどうか |
stored | true | 元の値が取得用に保存されるかどうか |
term_vectors | true | ターム位置が保存されるかどうか(フレーズクエリやハイライトに必要) |
Vector フィールド
Vector フィールドは近似最近傍(ANN: Approximate Nearest Neighbor)検索のためのベクトルインデックスを使用してインデクシングされます。
| タイプ | Rust 型 | SchemaBuilder メソッド | 説明 |
|---|---|---|---|
| Flat | FlatOption | add_flat_field() | ブルートフォース線形スキャン。正確な結果 |
| HNSW | HnswOption | add_hnsw_field() | Hierarchical Navigable Small World グラフ。高速な近似検索 |
| IVF | IvfOption | add_ivf_field() | Inverted File Index。クラスタベースの近似検索 |
HNSW フィールドオプション(最も一般的)
#![allow(unused)]
fn main() {
use laurus::vector::HnswOption;
use laurus::vector::core::distance::DistanceMetric;
let opt = HnswOption {
dimension: 384, // vector dimensions
distance: DistanceMetric::Cosine, // distance metric
m: 16, // max connections per layer
ef_construction: 200, // construction search width
base_weight: 1.0, // default scoring weight
quantizer: None, // optional quantization
};
}
パラメータの詳細なガイダンスについては、Vector インデクシングを参照してください。
Document
Document は名前付きフィールド値のコレクションです。DocumentBuilder を使用してドキュメントを構築します。
#![allow(unused)]
fn main() {
use laurus::Document;
let doc = Document::builder()
.add_text("title", "Introduction to Rust")
.add_text("body", "Rust is a systems programming language.")
.add_integer("year", 2024)
.add_float("rating", 4.8)
.add_boolean("published", true)
.build();
}
ドキュメントのインデクシング
Engine はドキュメントを追加するための 2 つのメソッドを提供しており、それぞれ異なるセマンティクスを持ちます。
| メソッド | 動作 | ユースケース |
|---|---|---|
put_document(id, doc) | Upsert — 同じ ID のドキュメントが存在する場合、置き換えられる | 標準的なドキュメントインデクシング |
add_document(id, doc) | Append — 新しいチャンクとしてドキュメントを追加。同じ ID で複数のチャンクを持てる | チャンク分割されたドキュメント(例: 段落に分割された長い記事) |
#![allow(unused)]
fn main() {
// Upsert: replaces any existing document with id "doc1"
engine.put_document("doc1", doc).await?;
// Append: adds another chunk under the same id "doc1"
engine.add_document("doc1", chunk2).await?;
// Always commit after indexing
engine.commit().await?;
}
ドキュメントの取得
get_documents を使用して、外部 ID でドキュメント(チャンクを含む)を取得します。
#![allow(unused)]
fn main() {
let docs = engine.get_documents("doc1").await?;
for doc in &docs {
if let Some(title) = doc.get("title") {
println!("Title: {:?}", title);
}
}
}
ドキュメントの削除
外部 ID を共有するすべてのドキュメントとチャンクを削除します。
#![allow(unused)]
fn main() {
engine.delete_documents("doc1").await?;
engine.commit().await?;
}
ドキュメントのライフサイクル
graph LR
A["Build Document"] --> B["put/add_document()"]
B --> C["WAL"]
C --> D["commit()"]
D --> E["Searchable"]
E --> F["get_documents()"]
E --> G["delete_documents()"]
重要: ドキュメントは
commit()が呼び出されるまで検索可能になりません。
DocumentBuilder メソッド
| メソッド | 値の型 | 説明 |
|---|---|---|
add_text(name, value) | String | テキストフィールドを追加 |
add_integer(name, value) | i64 | 整数フィールドを追加 |
add_float(name, value) | f64 | 浮動小数点数フィールドを追加 |
add_boolean(name, value) | bool | ブールフィールドを追加 |
add_datetime(name, value) | DateTime<Utc> | 日時フィールドを追加 |
add_vector(name, value) | Vec<f32> | 事前計算済みベクトルフィールドを追加 |
add_geo(name, lat, lon) | (f64, f64) | 地理座標フィールドを追加 |
add_bytes(name, data) | Vec<u8> | バイナリデータを追加 |
add_field(name, value) | DataValue | 任意の値型を追加 |
DataValue
DataValue は Laurus におけるフィールド値を表す統合列挙型です。
#![allow(unused)]
fn main() {
pub enum DataValue {
Null,
Bool(bool),
Int64(i64),
Float64(f64),
Text(String),
Bytes(Vec<u8>, Option<String>), // (data, optional MIME type)
Vector(Vec<f32>),
DateTime(DateTime<Utc>),
Geo(f64, f64), // (latitude, longitude)
}
}
DataValue は一般的な型に対して From<T> を実装しているため、.into() 変換が使用できます。
#![allow(unused)]
fn main() {
use laurus::DataValue;
let v: DataValue = "hello".into(); // Text
let v: DataValue = 42i64.into(); // Int64
let v: DataValue = 3.14f64.into(); // Float64
let v: DataValue = true.into(); // Bool
let v: DataValue = vec![0.1f32, 0.2].into(); // Vector
}
予約フィールド
_id フィールドは Laurus の内部使用のために予約されています。外部ドキュメント ID を格納し、常に KeywordAnalyzer(完全一致)でインデクシングされます。スキーマに追加する必要はありません。自動的に管理されます。
動的フィールド管理
稼働中のエンジンに対して、フィールドの追加および削除を動的に行えます。 フィールドの型変更はサポートされていません。
フィールドの追加
Engine::add_field() を使用すると、稼働中のエンジンにフィールドを動的に追加できます。
Lexical フィールドの追加
let updated_schema = engine.add_field(
"category",
FieldOption::Text(TextOption::default()),
).await?;
Vector フィールドの追加
let updated_schema = engine.add_field(
"embedding",
FieldOption::Flat(FlatOption::default().dimension(384)),
).await?;
既存のドキュメントには影響がありません(新しいフィールドの値が存在しないだけです)。
フィールドの削除
Engine::delete_field() を使用すると、稼働中のエンジンからフィールドを動的に削除できます。
let updated_schema = engine.delete_field("category").await?;
フィールド削除時の動作は以下の通りです。
- スキーマからフィールド定義が削除されます。
default_fieldsに含まれている場合、そこからも削除されます。- フィールドに紐づくアナライザーおよびエンベッダーの登録が解除されます。
- 既にインデックスされたデータは物理的に残りますが、スキーマから削除されたフィールドにはアクセスできなくなります。
共通の注意事項
返却された Schema は呼び出し側で永続化する必要があります(例: schema.toml への書き出し)。
スキーマ設計のヒント
-
Lexical フィールドと Vector フィールドを分離する — フィールドは Lexical か Vector のいずれかであり、両方にはなりません。ハイブリッド検索には、別々のフィールドを作成してください(例: テキスト用に
body、ベクトル用にbody_vec)。 -
完全一致フィールドには
KeywordAnalyzerを使用する — カテゴリ、ステータス、タグフィールドはPerFieldAnalyzer経由でKeywordAnalyzerを使用し、トークン化を避けてください。 -
適切なベクトルインデックスを選択する — ほとんどの場合は HNSW、小規模データセットには Flat、非常に大規模なデータセットには IVF を使用してください。詳細は Vector インデクシングを参照。
-
デフォルトフィールドを設定する — Query DSL を使用する場合、デフォルトフィールドを設定することで、ユーザーは
body:helloの代わりにhelloと記述できます。 -
スキーマジェネレータを使用する —
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
解析パイプラインは以下で構成されます。
- Char Filter — トークン化の前に文字レベルで生テキストを正規化する
- Tokenizer — テキストを生トークン(単語、文字、n-gram)に分割する
- 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;
}
}
TokenStream は Box<dyn Iterator<Item = Token> + Send> であり、トークンの遅延イテレータです。
Token には以下のフィールドが含まれます。
| フィールド | 型 | 説明 |
|---|---|---|
text | String | トークンテキスト |
position | usize | 元テキスト内の位置 |
start_offset | usize | 元テキスト内の開始バイトオフセット |
end_offset | usize | 元テキスト内の終了バイトオフセット |
position_increment | usize | 前のトークンからの距離 |
position_length | usize | トークンのスパン(同義語の場合は 1 より大きい) |
boost | f32 | トークンレベルのスコアリング重み |
stopped | bool | ストップワードとしてマークされているかどうか |
metadata | Option<TokenMetadata> | 追加のトークンメタデータ |
組み込み Analyzer
StandardAnalyzer
デフォルトの Analyzer です。ほとんどの西洋言語に適しています。
パイプライン: RegexTokenizer(Unicode 単語境界) → LowercaseFilter → StopFilter(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) → JapaneseIterationMarkCharFilter → LinderaTokenizer → LowercaseFilter → StopFilter(日本語ストップワード)
#![allow(unused)]
fn main() {
use laurus::analysis::analyzer::japanese::JapaneseAnalyzer;
let analyzer = JapaneseAnalyzer::new()?;
// "東京都に住んでいる" → ["東京", "都", "に", "住ん", "で", "いる"]
}
KeywordAnalyzer
入力全体を単一のトークンとして扱います。トークン化や正規化は行いません。
#![allow(unused)]
fn main() {
use laurus::analysis::analyzer::keyword::KeywordAnalyzer;
let analyzer = KeywordAnalyzer::new();
// "Hello World" → ["Hello World"]
}
完全一致が必要なフィールド(カテゴリ、タグ、ステータスコード)に使用してください。
SimpleAnalyzer
フィルタリングなしでテキストをトークン化します。元の大文字小文字とすべてのトークンが保持されます。解析パイプラインを完全に制御したい場合や、Tokenizer を単独でテストしたい場合に便利です。
パイプライン: ユーザー指定の Tokenizer のみ(Char Filter なし、Token Filter なし)
#![allow(unused)]
fn main() {
use laurus::analysis::analyzer::simple::SimpleAnalyzer;
use laurus::analysis::tokenizer::regex::RegexTokenizer;
use std::sync::Arc;
let tokenizer = Arc::new(RegexTokenizer::new()?);
let analyzer = SimpleAnalyzer::new(tokenizer);
// "Hello World" → ["Hello", "World"]
// (no lowercasing, no stop word removal)
}
Tokenizer のテストや、別のステップで手動で Token Filter を適用したい場合に使用してください。
EnglishAnalyzer
英語に特化した Analyzer です。トークン化、小文字化、一般的な英語ストップワードの除去を行います。
パイプライン: RegexTokenizer(Unicode 単語境界) → LowercaseFilter → StopFilter(128 個の一般的な英語ストップワード)
#![allow(unused)]
fn main() {
use laurus::analysis::analyzer::language::english::EnglishAnalyzer;
let analyzer = EnglishAnalyzer::new()?;
// "The Quick Brown Fox" → ["quick", "brown", "fox"]
// ("The" is removed by stop word filtering, remaining tokens are lowercased)
}
PipelineAnalyzer
任意の Char Filter、Tokenizer、Token Filter のシーケンスを組み合わせてカスタムパイプラインを構築します。
#![allow(unused)]
fn main() {
use laurus::analysis::analyzer::pipeline::PipelineAnalyzer;
use laurus::analysis::char_filter::unicode_normalize::{
NormalizationForm, UnicodeNormalizationCharFilter,
};
use laurus::analysis::tokenizer::regex::RegexTokenizer;
use laurus::analysis::token_filter::lowercase::LowercaseFilter;
use laurus::analysis::token_filter::stop::StopFilter;
use laurus::analysis::token_filter::stem::StemFilter;
let analyzer = PipelineAnalyzer::new(Arc::new(RegexTokenizer::new()?))
.add_char_filter(Arc::new(UnicodeNormalizationCharFilter::new(NormalizationForm::NFKC)))
.add_filter(Arc::new(LowercaseFilter::new()))
.add_filter(Arc::new(StopFilter::new()))
.add_filter(Arc::new(StemFilter::new())); // Porter stemmer
}
PerFieldAnalyzer
PerFieldAnalyzer を使用すると、同一 Engine 内で異なるフィールドに異なる Analyzer を割り当てることができます。
graph LR
PFA["PerFieldAnalyzer"]
PFA -->|"title"| KW["KeywordAnalyzer"]
PFA -->|"body"| STD["StandardAnalyzer"]
PFA -->|"description_ja"| JP["JapaneseAnalyzer"]
PFA -->|other fields| DEF["Default\n(StandardAnalyzer)"]
#![allow(unused)]
fn main() {
use std::sync::Arc;
use laurus::analysis::analyzer::standard::StandardAnalyzer;
use laurus::analysis::analyzer::keyword::KeywordAnalyzer;
use laurus::analysis::analyzer::per_field::PerFieldAnalyzer;
// Default analyzer for fields not explicitly configured
let per_field = PerFieldAnalyzer::new(
Arc::new(StandardAnalyzer::default())
);
// Use KeywordAnalyzer for exact-match fields
per_field.add_analyzer("category", Arc::new(KeywordAnalyzer::new()));
per_field.add_analyzer("status", Arc::new(KeywordAnalyzer::new()));
let engine = Engine::builder(storage, schema)
.analyzer(Arc::new(per_field))
.build()
.await?;
}
注意:
_idフィールドは設定に関係なく、常にKeywordAnalyzerで解析されます。
Char Filter
Char Filter は Tokenizer に渡される前の生入力テキストに対して動作します。Unicode 正規化、文字マッピング、パターンベースの置換などの文字レベルの正規化を行います。これにより、Tokenizer がクリーンで正規化されたテキストを受け取ることが保証されます。
すべての Char Filter は CharFilter トレイトを実装します。
#![allow(unused)]
fn main() {
pub trait CharFilter: Send + Sync {
fn filter(&self, input: &str) -> (String, Vec<Transformation>);
fn name(&self) -> &'static str;
}
}
Transformation レコードは文字位置がどのようにシフトしたかを記述し、Engine がトークン位置を元テキストにマッピングできるようにします。
| Char Filter | 説明 |
|---|---|
UnicodeNormalizationCharFilter | Unicode 正規化(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 の使用
PipelineAnalyzer に add_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 | 説明 |
|---|---|
RegexTokenizer | Unicode 単語境界で分割。空白と句読点で区切る |
UnicodeWordTokenizer | Unicode 単語境界で分割 |
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() メソッドは Vector(Vec<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 を有効にする必要があります。
| Embedder | Feature Flag | 依存関係 |
|---|---|---|
CandleBertEmbedder | embeddings-candle | candle-core, candle-nn, candle-transformers, hf-hub, tokenizers |
OpenAIEmbedder | embeddings-openai | reqwest |
CandleClipEmbedder | embeddings-multimodal | image + 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
FileStorage は use_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 が内部的に管理するため、直接操作する必要はありません。
バックエンドの選択
| 要因 | MemoryStorage | FileStorage | FileStorage (mmap) |
|---|---|---|---|
| 耐久性 | なし | 完全 | 完全 |
| 読み取り速度 | 最速 | 中程度 | 高速 |
| 書き込み速度 | 最速 | 中程度 | 中程度 |
| メモリ使用量 | データサイズに比例 | 少ない | OS 管理 |
| 最大データサイズ | RAM による制限 | ディスクによる制限 | ディスク + アドレス空間による制限 |
| 最適な用途 | テスト、小規模データセット | 一般的な利用 | 大規模な読み取り負荷の高いデータセット |
推奨事項
- 開発 / テスト: ファイルクリーンアップなしで高速に反復するために
MemoryStorageを使用 - 本番(一般): 信頼性の高い永続化のために
FileStorageを使用 - 本番(大規模): 大規模なインデックスがあり OS のページキャッシュを活用したい場合は
FileStorageのuse_mmap = trueを使用
次のステップ
- Lexical インデックスの仕組みを学ぶ: Lexical インデクシング
- Vector インデックスの仕組みを学ぶ: Vector インデクシング
インデキシング(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()
ステップごとの流れ
- 解析(Analyze): テキストが設定されたアナライザー(トークナイザー + フィルター)を通過し、正規化されたタームのストリームが生成される
- バッファリング(Buffer): タームはフィールドごとに整理され、インメモリの書き込みバッファに格納される
- コミット(Commit):
commit()の呼び出し時に、バッファがストレージ上の新しいセグメントにフラッシュされる
転置インデックス(Inverted Index)
転置インデックスは、基本的にタームからドキュメントリストへのマップです。
graph LR
subgraph "Term Dictionary"
T1["'brown'"]
T2["'fox'"]
T3["'quick'"]
T4["'rust'"]
end
subgraph "Posting Lists"
P1["doc_1, doc_3"]
P2["doc_1"]
P3["doc_1, doc_2"]
P4["doc_2, doc_3"]
end
T1 --> P1
T2 --> P2
T3 --> P3
T4 --> P4
| コンポーネント | 説明 |
|---|---|
| Term Dictionary | インデックス内のすべてのユニークなタームのソート済みリスト。高速なプレフィックス検索をサポート |
| Posting Lists | 各タームに対する、ドキュメント ID とメタデータ(ターム頻度、位置情報)のリスト |
| Doc Values | 数値フィールドや日付フィールドでのソート/フィルター操作のためのカラム指向ストレージ |
Posting List の内容
Posting List の各エントリには以下の情報が含まれます。
| フィールド | 説明 |
|---|---|
| Document ID | 内部 u64 識別子 |
| Term Frequency | そのドキュメント内でタームが出現する回数 |
| Positions(オプション) | ドキュメント内でタームが出現する位置(フレーズクエリに必要) |
| Weight | このポスティングのスコアウェイト |
数値フィールドと日付フィールド
整数、浮動小数点数、日時フィールドは、BKD ツリー を使用してインデキシングされます。BKD ツリーは範囲クエリに最適化された空間分割データ構造です。
graph TB
Root["BKD Root"]
Root --> L["values < 50"]
Root --> R["values >= 50"]
L --> LL["values < 25"]
L --> LR["25 <= values < 50"]
R --> RL["50 <= values < 75"]
R --> RR["values >= 75"]
BKD ツリーにより、price:[10 TO 100] や date:[2024-01-01 TO 2024-12-31] のような範囲クエリを効率的に評価できます。
地理フィールド(Geo Fields)
地理フィールドは緯度/経度のペアを格納します。以下のクエリをサポートする空間データ構造を使用してインデキシングされます。
- 半径クエリ(Radius queries): 中心点から N キロメートル以内のすべてのポイントを検索
- バウンディングボックスクエリ(Bounding box queries): 矩形領域内のすべてのポイントを検索
セグメント(Segments)
Lexical インデックスはセグメントに分割されて構成されます。各セグメントはイミュータブルで自己完結型のミニインデックスです。
graph TB
LI["Lexical Index"]
LI --> S1["Segment 0"]
LI --> S2["Segment 1"]
LI --> S3["Segment 2"]
S1 --- F1[".dict (terms)"]
S1 --- F2[".post (postings)"]
S1 --- F3[".bkd (numerics)"]
S1 --- F4[".docs (doc store)"]
S1 --- F5[".dv (doc values)"]
S1 --- F6[".meta (metadata)"]
S1 --- F7[".lens (field lengths)"]
| ファイル拡張子 | 内容 |
|---|---|
.dict | Term Dictionary(ソート済みターム + メタデータオフセット) |
.post | Posting Lists(ドキュメント ID、ターム頻度、位置情報) |
.bkd | 数値フィールドおよび日付フィールドの BKD ツリーデータ |
.docs | 格納されたフィールド値(元のドキュメント内容) |
.dv | ソートおよびフィルタリング用の Doc Values |
.meta | セグメントメタデータ(ドキュメント数、ターム数など) |
.lens | フィールド長の正規化値(BM25 スコアリング用) |
セグメントのライフサイクル
- 作成(Create):
commit()が呼び出されるたびに新しいセグメントが作成される - 検索(Search): すべてのセグメントが並列に検索され、結果がマージされる
- マージ(Merge): 定期的に、複数の小さなセグメントがより大きなセグメントにマージされ、クエリパフォーマンスが向上する
- 削除(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.2 と b = 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 インデキシング
- Lexical インデックスに対してクエリを実行する: Lexical 検索
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
ステップごとの流れ
- エンベディング(Embed): テキスト(または画像)が、設定されたエンベッダーによってベクトルに変換される
- 正規化(Normalize): ベクトルが L2 正規化される(コサイン類似度のため)
- インデキシング(Index): ベクトルが設定されたインデックス構造(Flat、HNSW、または IVF)に挿入される
- コミット(Commit):
commit()の呼び出し時に、インデックスが永続ストレージにフラッシュされる
インデックスタイプ
Laurus は 3 種類のベクトルインデックスタイプをサポートしており、それぞれ異なるパフォーマンス特性を持ちます。
比較
| 特性 | Flat | HNSW | IVF |
|---|---|---|---|
| 精度 | 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 パラメータ
| パラメータ | デフォルト | 説明 | 影響 |
|---|---|---|---|
m | 16 | レイヤーごとのノードあたりの最大双方向接続数 | 大きいほど再現率が向上するが、メモリ消費が増加 |
ef_construction | 200 | インデックス構築時の探索幅 | 大きいほど再現率が向上するが、構築が低速に |
dimension | 128 | ベクトルの次元数 | エンベッダーの出力と一致させる必要あり |
distance | Cosine | 距離メトリクス | 下記の距離メトリクスを参照 |
チューニングのヒント:
- 再現率を向上させるには
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_clusters | 100 | ボロノイセル(Voronoi Cell)の数 | クラスタ数が多いほど検索は高速になるが、再現率は低下 |
n_probe | 1 | クエリ時に検索するクラスタ数 | 大きいほど再現率が向上するが、検索が低速に |
dimension | (必須) | ベクトルの次元数 | エンベッダーの出力と一致させる必要あり |
distance | Cosine | 距離メトリクス | 下記の距離メトリクスを参照 |
チューニングのヒント:
n_clustersはベクトル数nに対してsqrt(n)程度に設定する- 再現率と速度のバランスを取るため、
n_probeをn_clustersの 5-20% に設定する - IVF はトレーニングフェーズが必要なため、初回のインデキシングが遅くなる場合がある
距離メトリクス(Distance Metrics)
| メトリクス | 説明 | 値の範囲 | 最適な用途 |
|---|---|---|---|
Cosine | 1 - コサイン類似度 | [0, 2] | テキストエンベディング(最も一般的) |
Euclidean | L2 距離 | [0, +inf) | 空間データ |
Manhattan | L1 距離 | [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 ビット | Scalar8Bit | 8 ビット整数へのスカラー量子化 | 約 4 倍 |
| プロダクト量子化 | ProductQuantization { subvector_count } | ベクトルをサブベクトルに分割して各々を量子化 | 約 16-64 倍 |
#![allow(unused)]
fn main() {
use laurus::vector::HnswOption;
use laurus::vector::core::quantization::QuantizationMethod;
let opt = HnswOption {
dimension: 384,
quantizer: Some(QuantizationMethod::Scalar8Bit),
..Default::default()
};
}
VectorQuantizer
VectorQuantizer は量子化のライフサイクルを管理します。
| メソッド | 説明 |
|---|---|
new(method, dimension) | 未トレーニングの量子化器を作成 |
train(vectors) | 代表的なベクトルでトレーニング(Scalar8Bit の場合、次元ごとの最小/最大値を計算) |
quantize(vector) | トレーニング済みパラメータを使用してベクトルを圧縮 |
dequantize(quantized) | 量子化されたベクトルをフル精度に復元 |
Scalar8Bit の場合、トレーニングで各次元の最小値と最大値が計算されます。各成分は [0, 255] の範囲にマッピングされます。逆量子化ではこのマッピングが逆変換されますが、多少の精度損失が生じます。
注意:
ProductQuantizationは API 上定義されていますが、現在未実装です。使用するとエラーが返されます。
セグメントファイル
各ベクトルインデックスタイプは、データを単一のセグメントファイルに格納します。
| インデックスタイプ | ファイル拡張子 | 内容 |
|---|---|---|
| HNSW | .hnsw | グラフ構造、ベクトル、メタデータ |
| Flat | .flat | 生ベクトルとメタデータ |
| IVF | .ivf | クラスタセントロイド、割り当て済みベクトル、メタデータ |
コード例
use std::sync::Arc;
use laurus::{Document, Engine, Schema};
use laurus::lexical::TextOption;
use laurus::vector::HnswOption;
use laurus::vector::core::distance::DistanceMetric;
use laurus::storage::memory::MemoryStorage;
#[tokio::main]
async fn main() -> laurus::Result<()> {
let storage = Arc::new(MemoryStorage::new(Default::default()));
let schema = Schema::builder()
.add_text_field("title", TextOption::default())
.add_hnsw_field("embedding", HnswOption {
dimension: 384,
distance: DistanceMetric::Cosine,
m: 16,
ef_construction: 200,
..Default::default()
})
.build();
// With an embedder, text in vector fields is automatically embedded
let engine = Engine::builder(storage, schema)
.embedder(my_embedder)
.build()
.await?;
// Add text to the vector field — it will be embedded automatically
engine.add_document("doc-1", Document::builder()
.add_text("title", "Rust Programming")
.add_text("embedding", "Rust is a systems programming language.")
.build()
).await?;
engine.commit().await?;
Ok(())
}
次のステップ
検索(Search)
このセクションでは、インデキシングされたデータに対するクエリの実行方法を説明します。Laurus は 3 つの検索モードをサポートしており、それぞれ独立して使用することも、組み合わせて使用することもできます。
トピック
Lexical 検索
転置インデックスを使用したキーワードベースの検索について説明します。
- すべてのクエリタイプ: Term、Phrase、Boolean、Fuzzy、Wildcard、Range、Geo、Span
- BM25 スコアリングとフィールドブースト
- テキストベースのクエリのための Query DSL の使用方法
Vector 検索
ベクトルエンベディングを使用した意味的類似性検索について説明します。
- VectorSearchRequestBuilder API
- マルチフィールド Vector 検索とスコアモード
- フィルター付き Vector 検索
ハイブリッド検索
Lexical 検索と Vector 検索を組み合わせた、両方の長所を活かす検索について説明します。
- SearchRequestBuilder API
- フュージョンアルゴリズム(RRF、WeightedSum)
- フィルター付きハイブリッド検索
- offset/limit によるページネーション
スペル修正については、ライブラリセクションの Spelling Correction を参照してください。
Lexical 検索
Lexical 検索は、転置インデックスに対してキーワードをマッチングすることでドキュメントを検索します。Laurus は、完全一致、フレーズ一致、あいまい一致など、豊富なクエリタイプを提供します。
基本的な使い方
#![allow(unused)]
fn main() {
use laurus::{SearchRequestBuilder, LexicalSearchRequest};
use laurus::lexical::TermQuery;
let request = SearchRequestBuilder::new()
.lexical_search_request(
LexicalSearchRequest::new(
Box::new(TermQuery::new("body", "rust"))
)
)
.limit(10)
.build();
let results = engine.search(request).await?;
}
クエリタイプ
TermQuery
特定のフィールドに完全一致するタームを含むドキュメントをマッチングします。
#![allow(unused)]
fn main() {
use laurus::lexical::TermQuery;
// Find documents where "body" contains the term "rust"
let query = TermQuery::new("body", "rust");
}
注意: タームは解析後にマッチングされます。フィールドが
StandardAnalyzerを使用している場合、インデキシングされたテキストとクエリタームの両方が小文字化されるため、TermQuery::new("body", "rust")は元テキスト内の “Rust” にもマッチします。
PhraseQuery
正確なタームの並びを含むドキュメントをマッチングします。
#![allow(unused)]
fn main() {
use laurus::lexical::query::phrase::PhraseQuery;
// Find documents containing the exact phrase "machine learning"
let query = PhraseQuery::new("body", vec!["machine".to_string(), "learning".to_string()]);
// Or use the convenience method from a phrase string:
let query = PhraseQuery::from_phrase("body", "machine learning");
}
フレーズクエリは、ターム位置情報が格納されている必要があります(TextOption のデフォルト設定)。
BooleanQuery
複数のクエリをブーリアン論理で結合します。
#![allow(unused)]
fn main() {
use laurus::lexical::query::boolean::{BooleanQuery, BooleanQueryBuilder, Occur};
let query = BooleanQueryBuilder::new()
.must(Box::new(TermQuery::new("body", "rust"))) // AND
.must(Box::new(TermQuery::new("body", "programming"))) // AND
.must_not(Box::new(TermQuery::new("body", "python"))) // NOT
.build();
}
| Occur | 意味 | DSL での表現 |
|---|---|---|
Must | ドキュメントが必ずマッチしなければならない | +term または AND |
Should | ドキュメントがマッチすべき(スコアをブースト) | term または OR |
MustNot | ドキュメントがマッチしてはならない | -term または NOT |
Filter | 必ずマッチする必要があるが、スコアには影響しない | (DSL に相当するものなし) |
FuzzyQuery
指定された編集距離(レーベンシュタイン距離)内のタームをマッチングします。
#![allow(unused)]
fn main() {
use laurus::lexical::query::fuzzy::FuzzyQuery;
// Find documents matching "programing" within edit distance 2
// This will match "programming", "programing", etc.
let query = FuzzyQuery::new("body", "programing"); // default max_edits = 2
}
WildcardQuery
ワイルドカードパターンを使用してタームをマッチングします。
#![allow(unused)]
fn main() {
use laurus::lexical::query::wildcard::WildcardQuery;
// '?' matches exactly one character, '*' matches zero or more
let query = WildcardQuery::new("filename", "*.pdf")?;
let query = WildcardQuery::new("body", "pro*")?;
let query = WildcardQuery::new("body", "col?r")?; // matches "color" and "colour"
}
PrefixQuery
特定のプレフィックスで始まるタームを含むドキュメントをマッチングします。
#![allow(unused)]
fn main() {
use laurus::lexical::query::prefix::PrefixQuery;
// Find documents where "body" contains terms starting with "pro"
// This matches "programming", "program", "production", etc.
let query = PrefixQuery::new("body", "pro");
}
RegexpQuery
正規表現パターンにマッチするタームを含むドキュメントをマッチングします。
#![allow(unused)]
fn main() {
use laurus::lexical::query::regexp::RegexpQuery;
// Find documents where "body" contains terms matching the regex
let query = RegexpQuery::new("body", "^pro.*ing$")?;
// Match version-like patterns
let query = RegexpQuery::new("version", r"^v\d+\.\d+")?;
}
注意:
RegexpQuery::new()はResultを返します。正規表現パターンは構築時にバリデーションされ、無効なパターンの場合はエラーが返されます。
NumericRangeQuery
数値フィールドの値が指定された範囲内にあるドキュメントをマッチングします。
#![allow(unused)]
fn main() {
use laurus::lexical::NumericRangeQuery;
use laurus::lexical::core::field::NumericType;
// Find documents where "price" is between 10.0 and 100.0 (inclusive)
let query = NumericRangeQuery::new(
"price",
NumericType::Float,
Some(10.0), // min
Some(100.0), // max
true, // include min
true, // include max
);
// Open-ended range: price >= 50
let query = NumericRangeQuery::new(
"price",
NumericType::Float,
Some(50.0),
None, // no upper bound
true,
false,
);
}
GeoQuery
地理的な位置に基づいてドキュメントをマッチングします。
#![allow(unused)]
fn main() {
use laurus::lexical::query::geo::GeoQuery;
// Find documents within 10km of Tokyo Station (35.6812, 139.7671)
let query = GeoQuery::within_radius("location", 35.6812, 139.7671, 10.0)?; // radius in kilometers
// Find documents within a bounding box (min_lat, min_lon, max_lat, max_lon)
let query = GeoQuery::within_bounding_box(
"location",
35.0, 139.0, // min (lat, lon)
36.0, 140.0, // max (lat, lon)
)?;
}
SpanQuery
ドキュメント内のタームの近接度に基づいてマッチングします。SpanTermQuery と SpanNearQuery を使用して近接クエリを構築します。
#![allow(unused)]
fn main() {
use laurus::lexical::query::span::{SpanQuery, SpanTermQuery, SpanNearQuery};
// Find documents where "quick" appears near "fox" (within 3 positions)
let query = SpanNearQuery::new(
"body",
vec![
Box::new(SpanTermQuery::new("body", "quick")) as Box<dyn SpanQuery>,
Box::new(SpanTermQuery::new("body", "fox")) as Box<dyn SpanQuery>,
],
3, // slop (max distance between terms)
true, // in_order (terms must appear in order)
);
}
スコアリング
Lexical 検索結果は BM25 を使用してスコアリングされます。スコアは、ドキュメントがクエリに対してどの程度関連性があるかを反映します。
- ドキュメント内のターム頻度が高いほどスコアが上昇する
- インデックス全体でタームが希少なほどスコアが上昇する
- 短いドキュメントは長いドキュメントに対してブーストされる
フィールドブースト
特定のフィールドをブーストして関連性に影響を与えることができます。
#![allow(unused)]
fn main() {
use laurus::LexicalSearchRequest;
let mut request = LexicalSearchRequest::new(Box::new(query));
request.field_boosts.insert("title".to_string(), 2.0); // title matches count double
request.field_boosts.insert("body".to_string(), 1.0);
}
LexicalSearchRequest のオプション
| オプション | デフォルト | 説明 |
|---|---|---|
query | (必須) | 実行するクエリ |
limit | 10 | 結果の最大件数 |
load_documents | true | ドキュメントの全内容をロードするかどうか |
min_score | 0.0 | 最小スコア閾値 |
timeout_ms | None | 検索タイムアウト(ミリ秒) |
parallel | false | セグメント間の並列検索を有効にする |
sort_by | Score | 関連性スコアでソート、またはフィールドでソート(asc / desc) |
field_boosts | 空 | フィールドごとのスコア倍率 |
ビルダーメソッド
LexicalSearchRequest はオプション設定のためのビルダースタイルの API をサポートします。
#![allow(unused)]
fn main() {
use laurus::LexicalSearchRequest;
use laurus::lexical::TermQuery;
let request = LexicalSearchRequest::new(Box::new(TermQuery::new("body", "rust")))
.limit(20)
.min_score(0.5)
.timeout_ms(5000)
.parallel(true)
.sort_by_field_desc("date")
.with_field_boost("title", 2.0)
.with_field_boost("body", 1.0);
}
Query DSL の使用
プログラマティックにクエリを構築する代わりに、テキストベースの Query DSL を使用できます。
#![allow(unused)]
fn main() {
use laurus::lexical::QueryParser;
use laurus::analysis::analyzer::standard::StandardAnalyzer;
use std::sync::Arc;
let analyzer = Arc::new(StandardAnalyzer::default());
let parser = QueryParser::new(analyzer).with_default_field("body");
// Simple term
let query = parser.parse("rust")?;
// Boolean
let query = parser.parse("rust AND programming")?;
// Phrase
let query = parser.parse("\"machine learning\"")?;
// Field-specific
let query = parser.parse("title:rust AND body:programming")?;
// Fuzzy
let query = parser.parse("programing~2")?;
// Range
let query = parser.parse("year:[2020 TO 2024]")?;
}
完全な構文リファレンスは Query DSL を参照してください。
次のステップ
Vector 検索
Vector 検索は、意味的類似性によってドキュメントを検索します。キーワードのマッチングではなく、ベクトル空間におけるクエリの意味とドキュメントエンベディングを比較します。
基本的な使い方
Builder API
#![allow(unused)]
fn main() {
use laurus::SearchRequestBuilder;
use laurus::vector::VectorSearchRequestBuilder;
let request = SearchRequestBuilder::new()
.vector_search_request(
VectorSearchRequestBuilder::new()
.add_text("embedding", "systems programming language")
.limit(10)
.build()
)
.build();
let results = engine.search(request).await?;
}
add_text() メソッドはテキストをクエリペイロードとして格納します。検索時に、エンジンが設定されたエンベッダーを使用してテキストをエンベディングし、ベクトルインデックスを検索します。
Query DSL
#![allow(unused)]
fn main() {
use laurus::vector::VectorQueryParser;
let parser = VectorQueryParser::new(embedder.clone())
.with_default_field("embedding");
let request = parser.parse(r#"embedding:~"systems programming""#).await?;
}
VectorSearchRequestBuilder
Builder API により、きめ細かな制御が可能です。
#![allow(unused)]
fn main() {
use laurus::vector::VectorSearchRequestBuilder;
use laurus::vector::store::request::QueryVector;
let request = VectorSearchRequestBuilder::new()
// Text query (will be embedded at search time)
.add_text("text_vec", "machine learning")
// Or use a pre-computed vector directly
.add_vector("embedding", vec![0.1, 0.2, 0.3, /* ... */])
// Search parameters
.limit(20)
.build();
}
メソッド
| メソッド | 説明 |
|---|---|
add_text(field, text) | 特定のフィールドに対するテキストクエリを追加(検索時にエンベディング) |
add_vector(field, vector) | 特定のフィールドに対する事前計算済みクエリベクトルを追加 |
add_vector_with_weight(field, vector, weight) | 明示的なウェイトを持つ事前計算済みベクトルを追加 |
add_payload(field, payload) | エンベディング対象の汎用 DataValue ペイロードを追加 |
add_bytes(field, bytes, mime) | バイナリペイロードを追加(例: マルチモーダル用の画像バイト) |
field(name) | 検索を特定のフィールドに制限 |
fields(names) | 検索を複数のフィールドに制限 |
limit(n) | 結果の最大件数(デフォルト: 10) |
score_mode(VectorScoreMode) | スコア結合モード(WeightedSum、MaxSim、LateInteraction) |
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 | クエリ句間の最大類似度スコア |
LateInteraction | ColBERT スタイルの Late Interaction スコアリング |
ウェイト
DSL では ^ ブースト構文を使用するか、QueryVector の weight で各フィールドの寄与度を調整します。
text_vec:~"cute kitten"^1.0 image_vec:~"fluffy cat"^0.5
これは、テキストの類似度が画像の類似度の 2 倍の重みを持つことを意味します。
フィルター付き Vector 検索
Lexical フィルターを適用して Vector 検索の結果を絞り込むことができます。
#![allow(unused)]
fn main() {
use laurus::{SearchRequestBuilder, LexicalSearchRequest};
use laurus::lexical::TermQuery;
use laurus::vector::VectorSearchRequestBuilder;
// Vector search with a category filter
let request = SearchRequestBuilder::new()
.vector_search_request(
VectorSearchRequestBuilder::new()
.add_text("embedding", "machine learning")
.build()
)
.filter_query(Box::new(TermQuery::new("category", "tutorial")))
.limit(10)
.build();
let results = engine.search(request).await?;
}
フィルタークエリはまず Lexical インデックス上で実行されて許可されるドキュメント ID のセットを特定し、その後 Vector 検索がそれらの ID に制限されます。
数値範囲によるフィルター
#![allow(unused)]
fn main() {
use laurus::lexical::NumericRangeQuery;
use laurus::lexical::core::field::NumericType;
let request = SearchRequestBuilder::new()
.vector_search_request(
VectorSearchRequestBuilder::new()
.add_text("embedding", "type systems")
.build()
)
.filter_query(Box::new(NumericRangeQuery::new(
"year", NumericType::Integer,
Some(2020.0), Some(2024.0), true, true
)))
.limit(10)
.build();
}
距離メトリクス(Distance Metrics)
距離メトリクスはスキーマでフィールドごとに設定されます(Vector インデキシング を参照)。
| メトリクス | 説明 | 小さい値 = より類似 |
|---|---|---|
| Cosine | 1 - コサイン類似度 | はい |
| Euclidean | L2 距離 | はい |
| Manhattan | L1 距離 | はい |
| DotProduct | 負の内積 | はい |
| Angular | 角度距離 | はい |
コード例: 完全な Vector 検索
use std::sync::Arc;
use laurus::{Document, Engine, Schema, SearchRequestBuilder, PerFieldEmbedder};
use laurus::lexical::TextOption;
use laurus::vector::HnswOption;
use laurus::vector::VectorSearchRequestBuilder;
use laurus::storage::memory::MemoryStorage;
#[tokio::main]
async fn main() -> laurus::Result<()> {
let storage = Arc::new(MemoryStorage::new(Default::default()));
let schema = Schema::builder()
.add_text_field("title", TextOption::default())
.add_hnsw_field("text_vec", HnswOption {
dimension: 384,
..Default::default()
})
.build();
// Set up per-field embedder
let embedder = Arc::new(my_embedder);
let pfe = PerFieldEmbedder::new(embedder.clone());
pfe.add_embedder("text_vec", embedder.clone());
let engine = Engine::builder(storage, schema)
.embedder(Arc::new(pfe))
.build()
.await?;
// Index documents (text in vector field is auto-embedded)
engine.add_document("doc-1", Document::builder()
.add_text("title", "Rust Programming")
.add_text("text_vec", "Rust is a systems programming language.")
.build()
).await?;
engine.commit().await?;
// Search by semantic similarity
let results = engine.search(
SearchRequestBuilder::new()
.vector_search_request(
VectorSearchRequestBuilder::new()
.add_text("text_vec", "systems language")
.build()
)
.limit(5)
.build()
).await?;
for r in &results {
println!("{}: score={:.4}", r.id, r.score);
}
Ok(())
}
次のステップ
ハイブリッド検索(Hybrid Search)
ハイブリッド検索は、Lexical 検索(キーワードマッチング)と Vector 検索(意味的類似性)を組み合わせることで、精度と意味的な関連性の両方を兼ね備えた結果を提供します。これは Laurus の最も強力な検索モードです。
なぜハイブリッド検索なのか
| 検索タイプ | 強み | 弱み |
|---|---|---|
| Lexical のみ | 正確なキーワードマッチング、希少なタームに強い | 同義語や言い換えを見逃す |
| Vector のみ | 意味を理解し、同義語に対応 | 正確なキーワードを見逃す場合がある、精度が低い |
| ハイブリッド | 両方の長所を活用 | 設定がやや複雑 |
仕組み
sequenceDiagram
participant User
participant Engine
participant Lexical as LexicalStore
participant Vector as VectorStore
participant Fusion
User->>Engine: SearchRequest\n(lexical + vector)
par Execute in parallel
Engine->>Lexical: BM25 keyword search
Lexical-->>Engine: Ranked hits (by relevance)
and
Engine->>Vector: ANN similarity search
Vector-->>Engine: Ranked hits (by distance)
end
Engine->>Fusion: Merge two result sets
Note over Fusion: RRF or WeightedSum
Fusion-->>Engine: Unified ranked list
Engine-->>User: Vec of SearchResult
基本的な使い方
Builder API
#![allow(unused)]
fn main() {
use laurus::{SearchRequestBuilder, LexicalSearchRequest, FusionAlgorithm};
use laurus::lexical::TermQuery;
use laurus::vector::VectorSearchRequestBuilder;
let request = SearchRequestBuilder::new()
// Lexical component
.lexical_search_request(
LexicalSearchRequest::new(
Box::new(TermQuery::new("body", "rust"))
)
)
// Vector component
.vector_search_request(
VectorSearchRequestBuilder::new()
.add_text("text_vec", "systems programming")
.build()
)
// Fusion algorithm
.fusion_algorithm(FusionAlgorithm::RRF { k: 60.0 })
.limit(10)
.build();
let results = engine.search(request).await?;
}
Query DSL
単一のクエリ文字列内で Lexical 句と Vector 句を混在させることができます。
#![allow(unused)]
fn main() {
use laurus::UnifiedQueryParser;
use laurus::lexical::QueryParser;
use laurus::vector::VectorQueryParser;
let unified = UnifiedQueryParser::new(
QueryParser::new(analyzer).with_default_field("body"),
VectorQueryParser::new(embedder),
);
// Lexical + vector in one query
let request = unified.parse(r#"body:rust text_vec:~"systems programming""#).await?;
let results = engine.search(request).await?;
}
~"..." 構文は Vector 句を識別します。それ以外はすべて Lexical として解析されます。
フュージョンアルゴリズム(Fusion Algorithms)
Lexical と Vector の両方の結果が存在する場合、それらを単一のランキングリストにマージする必要があります。Laurus は 2 つのフュージョンアルゴリズムをサポートしています。
RRF(Reciprocal Rank Fusion)
デフォルトのアルゴリズムです。生のスコアではなく、ランク位置に基づいて結果を結合します。
score(doc) = sum( 1 / (k + rank_i) )
rank_i は各結果リストにおけるドキュメントの位置、k はスムージングパラメータ(デフォルト 60)です。
#![allow(unused)]
fn main() {
use laurus::FusionAlgorithm;
let fusion = FusionAlgorithm::RRF { k: 60.0 };
}
利点:
- Lexical と Vector の結果間のスコア分布の違いに対してロバスト
- ウェイトのチューニングが不要
- すぐに使える(out of the box)
WeightedSum
正規化された Lexical スコアと Vector スコアを線形結合します。
score(doc) = lexical_weight * lexical_score + vector_weight * vector_score
#![allow(unused)]
fn main() {
use laurus::FusionAlgorithm;
let fusion = FusionAlgorithm::WeightedSum {
lexical_weight: 0.3,
vector_weight: 0.7,
};
}
使用場面:
- Lexical と Vector の関連性のバランスを明示的に制御したい場合
- 一方のシグナルが他方よりも重要であることがわかっている場合
SearchRequest のオプション
| オプション | デフォルト | 説明 |
|---|---|---|
lexical_search_request | None | Lexical クエリコンポーネント |
vector_search_request | None | Vector クエリコンポーネント |
filter_query | None | Lexical クエリによるプレフィルター(Lexical と Vector の両方の結果を制限) |
fusion_algorithm | None(両方の結果が存在する場合 RRF { k: 60.0 } を使用) | Lexical と Vector の結果をマージする方法 |
limit | 10 | 返される結果の最大件数 |
offset | 0 | スキップする結果の数(ページネーション用) |
SearchResult
各結果には以下が含まれます。
| フィールド | 型 | 説明 |
|---|---|---|
id | String | 外部ドキュメント ID |
score | f32 | フュージョン後の関連性スコア |
document | Option<Document> | ドキュメントの全内容(ロードされた場合) |
フィルター付きハイブリッド検索
フィルターを適用して、Lexical と Vector の両方の結果を制限できます。
#![allow(unused)]
fn main() {
let request = SearchRequestBuilder::new()
.lexical_search_request(
LexicalSearchRequest::new(Box::new(TermQuery::new("body", "rust")))
)
.vector_search_request(
VectorSearchRequestBuilder::new()
.add_text("text_vec", "systems programming")
.build()
)
// Only search within "tutorial" category
.filter_query(Box::new(TermQuery::new("category", "tutorial")))
.fusion_algorithm(FusionAlgorithm::RRF { k: 60.0 })
.limit(10)
.build();
}
フィルタリングの仕組み
- フィルタークエリが Lexical インデックス上で実行され、許可されるドキュメント ID のセットが生成される
- Lexical 検索: フィルターがユーザークエリとブーリアン AND で結合される
- Vector 検索: 許可された ID が ANN 検索の制限として渡される
ページネーション
offset と limit を使用してページネーションを実現します。
#![allow(unused)]
fn main() {
// Page 1: results 0-9
let page1 = SearchRequestBuilder::new()
.lexical_search_request(/* ... */)
.vector_search_request(/* ... */)
.offset(0)
.limit(10)
.build();
// Page 2: results 10-19
let page2 = SearchRequestBuilder::new()
.lexical_search_request(/* ... */)
.vector_search_request(/* ... */)
.offset(10)
.limit(10)
.build();
}
完全な例
use std::sync::Arc;
use laurus::{
Document, Engine, Schema, SearchRequestBuilder,
LexicalSearchRequest, FusionAlgorithm, PerFieldEmbedder,
};
use laurus::lexical::{TextOption, TermQuery};
use laurus::lexical::core::field::IntegerOption;
use laurus::vector::{HnswOption, VectorSearchRequestBuilder};
use laurus::storage::memory::MemoryStorage;
#[tokio::main]
async fn main() -> laurus::Result<()> {
let storage = Arc::new(MemoryStorage::new(Default::default()));
// Schema with both lexical and vector fields
let schema = Schema::builder()
.add_text_field("title", TextOption::default())
.add_text_field("body", TextOption::default())
.add_text_field("category", TextOption::default())
.add_integer_field("year", IntegerOption::default())
.add_hnsw_field("body_vec", HnswOption {
dimension: 384,
..Default::default()
})
.build();
// Configure analyzer and embedder (see Text Analysis and Embeddings docs)
// let analyzer = Arc::new(StandardAnalyzer::new()?);
// let embedder = Arc::new(CandleBertEmbedder::new("sentence-transformers/all-MiniLM-L6-v2")?);
let engine = Engine::builder(storage, schema)
// .analyzer(analyzer)
// .embedder(embedder)
.build()
.await?;
// Index documents with both text and vector fields
engine.add_document("doc-1", Document::builder()
.add_text("title", "Rust Programming Guide")
.add_text("body", "Rust is a systems programming language.")
.add_text("category", "programming")
.add_integer("year", 2024)
.add_text("body_vec", "Rust is a systems programming language.")
.build()
).await?;
engine.commit().await?;
// Hybrid search: keyword "rust" + semantic "systems language"
let results = engine.search(
SearchRequestBuilder::new()
.lexical_search_request(
LexicalSearchRequest::new(Box::new(TermQuery::new("body", "rust")))
)
.vector_search_request(
VectorSearchRequestBuilder::new()
.add_text("body_vec", "systems language")
.build()
)
.fusion_algorithm(FusionAlgorithm::RRF { k: 60.0 })
.limit(10)
.build()
).await?;
for r in &results {
println!("{}: score={:.4}", r.id, r.score);
}
Ok(())
}
次のステップ
- クエリ構文の完全なリファレンス: Query DSL
- ID 解決の仕組み: ID Management
- データの永続性: Persistence & WAL
Query DSL
Laurus は統合 Query DSL(Domain Specific Language)を提供しており、Lexical(キーワード)検索と Vector(意味的)検索を単一のクエリ文字列で記述できます。UnifiedQueryParser は入力を Lexical 部分と Vector 部分に分割し、適切なサブパーサーに委譲します。
概要
title:hello AND content:~"cute kitten"^0.8
|--- lexical --| |--- vector --------|
~" パターンが Vector 句を Lexical 句と区別します。それ以外はすべて Lexical クエリとして扱われます。
Lexical クエリ構文
Lexical クエリは、完全一致または近似のキーワードマッチングを使用して転置インデックスを検索します。
Term クエリ
フィールド(またはデフォルトフィールド)に対して単一のタームをマッチングします。
hello
title:hello
ブーリアン演算子
AND と OR(大文字小文字を区別しない)で句を結合します。
title:hello AND body:world
title:hello OR title:goodbye
明示的な演算子なしでスペース区切りされた句は、暗黙的なブーリアン(スコアリング付きの OR として動作)を使用します。
必須 / 禁止句
+(必ずマッチ)と -(マッチ禁止)を使用します。
+title:hello -title:goodbye
フレーズクエリ
ダブルクォートを使用して正確なフレーズをマッチングします。オプションの近接度(~N)で、ターム間に N 語を許可します。
"hello world"
"hello world"~2
ファジークエリ
編集距離を使用した近似マッチング。~ に続けてオプションで最大編集距離を指定します。
roam~
roam~2
ワイルドカードクエリ
?(1 文字)と *(0 文字以上)を使用します。
te?t
test*
範囲クエリ
包含的な [] または排他的な {} の範囲指定。数値フィールドや日付フィールドに有用です。
price:[100 TO 500]
date:{2024-01-01 TO 2024-12-31}
price:[* TO 100]
ブースト
^ で句のウェイトを増加させます。
title:hello^2
"important phrase"^1.5
グルーピング
括弧でサブ式を囲みます。
(title:hello OR title:hi) AND body:world
Lexical PEG 文法
完全な Lexical 文法(parser.pest):
query = { SOI ~ boolean_query ~ EOI }
boolean_query = { clause ~ (boolean_op ~ clause | clause)* }
clause = { required_clause | prohibited_clause | sub_clause }
required_clause = { "+" ~ sub_clause }
prohibited_clause = { "-" ~ sub_clause }
sub_clause = { grouped_query | field_query | term_query }
grouped_query = { "(" ~ boolean_query ~ ")" ~ boost? }
boolean_op = { ^"AND" | ^"OR" }
field_query = { field ~ ":" ~ field_value }
field_value = { range_query | phrase_query | fuzzy_term
| wildcard_term | simple_term }
phrase_query = { "\"" ~ phrase_content ~ "\"" ~ proximity? ~ boost? }
proximity = { "~" ~ number }
fuzzy_term = { term ~ "~" ~ fuzziness? ~ boost? }
wildcard_term = { wildcard_pattern ~ boost? }
simple_term = { term ~ boost? }
boost = { "^" ~ boost_value }
Vector クエリ構文
Vector クエリは、解析時にテキストをベクトルにエンベディングし、類似性検索を実行します。
基本構文
field:~"text"
field:~"text"^weight
| 要素 | 必須 | 説明 | 例 |
|---|---|---|---|
field: | いいえ | 対象のベクトルフィールド名 | content: |
~ | はい | Vector クエリマーカー | |
"text" | はい | エンベディングするテキスト | "cute kitten" |
^weight | いいえ | スコアウェイト(デフォルト: 1.0) | ^0.8 |
Vector クエリの例
# Single field
content:~"cute kitten"
# With boost weight
content:~"cute kitten"^0.8
# Default field (when configured)
~"cute kitten"
# Multiple clauses
content:~"cats" image:~"dogs"^0.5
# Nested field name (dot notation)
metadata.embedding:~"text"
複数句
複数の Vector 句はスペースで区切ります。すべての句が実行され、スコアは score_mode(デフォルト: WeightedSum)を使用して結合されます。
content:~"cats" image:~"dogs"^0.5
この場合のスコア計算:
score = similarity("cats", content) * 1.0
+ similarity("dogs", image) * 0.5
Vector DSL には AND/OR 演算子はありません。Vector 検索は本質的にランキング操作であり、ウェイト(^)が各句の寄与度を制御します。
スコアモード
| モード | 説明 |
|---|---|
WeightedSum(デフォルト) | すべてのクエリ句にわたる(類似度 * ウェイト)の合計 |
MaxSim | クエリ句間の最大類似度スコア |
LateInteraction | Late Interaction スコアリング |
スコアモードは DSL 構文からは設定できません。Rust API を使用してオーバーライドします。
#![allow(unused)]
fn main() {
let mut request = parser.parse(r#"content:~"cats" image:~"dogs""#).await?;
request.score_mode = VectorScoreMode::MaxSim;
}
Vector PEG 文法
完全な Vector 文法(parser.pest):
query = { SOI ~ vector_clause+ ~ EOI }
vector_clause = { field_prefix? ~ "~" ~ quoted_text ~ boost? }
field_prefix = { field_name ~ ":" }
field_name = @{ (ASCII_ALPHA | "_") ~ (ASCII_ALPHANUMERIC | "_" | ".")* }
quoted_text = ${ "\"" ~ inner_text ~ "\"" }
inner_text = @{ (!("\"") ~ ANY)* }
boost = { "^" ~ float_value }
float_value = @{ ASCII_DIGIT+ ~ ("." ~ ASCII_DIGIT+)? }
統合(ハイブリッド)クエリ構文
UnifiedQueryParser を使用すると、単一のクエリ文字列内で Lexical 句と Vector 句を自由に混在させることができます。
title:hello content:~"cute kitten"^0.8
仕組み
- 分割(Split): Vector 句(
field:~"text"^boostパターンに一致)が正規表現で抽出される - 委譲(Delegate): Vector 部分は
VectorQueryParserに、残りは Lexical のQueryParserに渡される - フュージョン(Fuse): Lexical と Vector の両方の結果が存在する場合、フュージョンアルゴリズムで結合される
曖昧性の解消
~" パターンは Vector 句を明確に識別します。Lexical 構文では、~ はターム或いはフレーズの後にのみ出現し(例: roam~2、"hello world"~10)、クォートの前には出現しないためです。
フュージョンアルゴリズム
クエリに Lexical 句と Vector 句の両方が含まれる場合、結果はフュージョンされます。
| アルゴリズム | 計算式 | 説明 |
|---|---|---|
| RRF(デフォルト) | score = sum(1 / (k + rank)) | Reciprocal Rank Fusion。異なるスコア分布に対してロバスト。デフォルト k=60。 |
| WeightedSum | score = lexical * a + vector * b | 設定可能なウェイトによる線形結合。 |
注意: フュージョンアルゴリズムは DSL 構文では指定できません。
UnifiedQueryParserの構築時に.with_fusion()で設定します。デフォルトは RRF(k=60)です。コード例はカスタムフュージョンを参照してください。
統合クエリの例
# Lexical only — no fusion
title:hello AND body:world
# Vector only — no fusion
content:~"cute kitten"
# Hybrid — fusion applied automatically
title:hello content:~"cute kitten"
# Hybrid with boolean operators
title:hello AND category:animal content:~"cute kitten"^0.8
# Multiple vector clauses + lexical
category:animal content:~"cats" image:~"dogs"^0.5
# Default fields (when configured)
hello ~"cats"
コード例
DSL による Lexical 検索
#![allow(unused)]
fn main() {
use std::sync::Arc;
use laurus::analysis::analyzer::standard::StandardAnalyzer;
use laurus::lexical::query::QueryParser;
let analyzer = Arc::new(StandardAnalyzer::new()?);
let parser = QueryParser::new(analyzer)
.with_default_field("title");
let query = parser.parse("title:hello AND body:world")?;
}
DSL による Vector 検索
#![allow(unused)]
fn main() {
use std::sync::Arc;
use laurus::vector::query::VectorQueryParser;
let parser = VectorQueryParser::new(embedder)
.with_default_field("content");
let request = parser.parse(r#"content:~"cute kitten"^0.8"#).await?;
}
統合 DSL によるハイブリッド検索
#![allow(unused)]
fn main() {
use laurus::engine::query::UnifiedQueryParser;
let unified = UnifiedQueryParser::new(lexical_parser, vector_parser);
let request = unified.parse(
r#"title:hello content:~"cute kitten"^0.8"#
).await?;
// request.lexical_search_request -> Some(...) — lexical query
// request.vector_search_request -> Some(...) — vector query
// request.fusion_algorithm -> Some(RRF) — fusion algorithm
}
カスタムフュージョン
#![allow(unused)]
fn main() {
use laurus::engine::search::FusionAlgorithm;
let unified = UnifiedQueryParser::new(lexical_parser, vector_parser)
.with_fusion(FusionAlgorithm::WeightedSum {
lexical_weight: 0.3,
vector_weight: 0.7,
});
}
ライブラリ概要
laurus クレートは検索エンジンのコアライブラリです。Lexical検索(転置インデックス(Inverted Index)によるキーワードマッチング)、Vector検索(Embeddingによるセマンティック類似度検索)、およびハイブリッド検索(両者の組み合わせ)を統一的なAPIで提供します。
モジュール構成
graph TB
LIB["laurus (lib.rs)"]
LIB --> engine["engine\nEngine, EngineBuilder\nSearchRequest, FusionAlgorithm"]
LIB --> analysis["analysis\nAnalyzer, Tokenizer\nToken Filters, Char Filters"]
LIB --> lexical["lexical\nInverted Index, BM25\nQuery Types, Faceting, Highlighting"]
LIB --> vector["vector\nFlat, HNSW, IVF\nDistance Metrics, Quantization"]
LIB --> embedding["embedding\nCandle BERT, OpenAI\nCLIP, Precomputed"]
LIB --> storage["storage\nMemory, File, Mmap\nColumnStorage"]
LIB --> store["store\nDocumentLog (WAL)"]
LIB --> spelling["spelling\nSpelling Correction\nSuggestion Engine"]
LIB --> data["data\nDataValue, Document"]
LIB --> error["error\nLaurusError, Result"]
主要な型
| 型 | モジュール | 説明 |
|---|---|---|
Engine | engine | Lexical検索とVector検索を統合する検索エンジン |
EngineBuilder | engine | Engineの設定・構築を行うBuilderパターン |
Schema | engine | フィールド定義とルーティング設定 |
SearchRequest | engine | 統一的な検索リクエスト(Lexical、Vector、またはハイブリッド) |
FusionAlgorithm | engine | 結果マージ戦略(RRFまたはWeightedSum) |
Document | data | 名前付きフィールド値のコレクション |
DataValue | data | すべてのフィールド型に対応する統一的な値のenum |
LaurusError | error | 各サブシステムのバリアントを含む包括的なエラー型 |
Feature Flag
laurus クレートはデフォルトではFeatureが有効化されていません。必要に応じてEmbeddingサポートを有効にしてください。
| Feature | 説明 | 依存クレート |
|---|---|---|
embeddings-candle | Hugging Face CandleによるローカルBERT Embedding | candle-core, candle-nn, candle-transformers, hf-hub, tokenizers |
embeddings-openai | OpenAI API Embedding | reqwest |
embeddings-multimodal | CLIPマルチモーダル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とEngineBuilderの内部構造
- スコアリングとランキング – BM25、TF-IDF、およびベクトル類似度スコアリング
- ファセット – 階層的なファセット検索
- ハイライト – 検索結果のハイライト表示
- スペル修正 – スペル候補の提案と自動修正
- ID管理 – 二層構造のドキュメントID管理
- 永続化とWAL – Write-Ahead Loggingとデータの耐久性
- 削除とコンパクション – 論理削除と領域の再利用
- エラーハンドリング – LaurusErrorとResult型
- 拡張性 – カスタムAnalyzer、Embedder、Storageバックエンド
- APIリファレンス – 主要な型とメソッドの一覧
Engine
Engine はLaurusの中心的な型です。Lexicalインデックス、Vectorインデックス、およびドキュメントログを単一の非同期APIで統合します。
Engine構造体
#![allow(unused)]
fn main() {
pub struct Engine {
schema: Schema,
lexical: LexicalStore,
vector: VectorStore,
log: Arc<DocumentLog>,
}
}
| フィールド | 型 | 説明 |
|---|---|---|
schema | Schema | フィールド定義とルーティングルール |
lexical | LexicalStore | キーワード検索用の転置インデックス(Inverted Index) |
vector | VectorStore | 類似度検索用のベクトルインデックス |
log | Arc<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> | StandardAnalyzer | Lexicalフィールド用のテキスト解析パイプライン |
embedder() | Arc<dyn Embedder> | None | Vectorフィールド用の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
- スキーマの分割 – Lexicalフィールド(Text、Integer、Floatなど)は
LexicalIndexConfigに、Vectorフィールド(HNSW、Flat、IVF)はVectorIndexConfigに分割されます - プレフィックス付きストレージの作成 – 各コンポーネントに独立した名前空間が割り当てられます(
lexical/、vector/、documents/) - ストアの初期化 –
LexicalStoreとVectorStoreがそれぞれの設定で作成されます - WALからのリカバリ – 前回のセッションでコミットされなかった操作がリプレイされます
スキーマの分割
Schemaにはlexicalフィールドとvectorフィールドの両方が含まれています。ビルド時に split_schema() がこれらを分離します。
graph LR
S["Schema<br/>title: Text<br/>body: Text<br/>page: Integer<br/>content_vec: HNSW"]
S --> LC["LexicalIndexConfig<br/>title: TextOption<br/>body: TextOption<br/>page: IntegerOption<br/>_id: KeywordAnalyzer"]
S --> VC["VectorIndexConfig<br/>content_vec: HnswOption<br/>(dim=384, m=16, ef=200)"]
予約フィールド _id は、完全一致検索のために KeywordAnalyzer を用いて常にLexical設定に追加されます。
フィールドごとのディスパッチ
PerFieldAnalyzer
PerFieldAnalyzer が指定された場合、テキスト解析はフィールドごとのAnalyzerにディスパッチされます。
graph LR
PFA["PerFieldAnalyzer"]
PFA -->|"title"| KA["KeywordAnalyzer"]
PFA -->|"body"| SA["StandardAnalyzer"]
PFA -->|"description"| JA["JapaneseAnalyzer"]
PFA -->|"_id"| KA2["KeywordAnalyzer<br/>(always)"]
PFA -->|other fields| DEF["Default Analyzer"]
PerFieldEmbedder
同様に、PerFieldEmbedder はフィールドごとのEmbedderにEmbedding処理をルーティングします。
graph LR
PFE["PerFieldEmbedder"]
PFE -->|"text_vec"| BERT["CandleBertEmbedder<br/>(384 dim)"]
PFE -->|"image_vec"| CLIP["CandleClipEmbedder<br/>(512 dim)"]
PFE -->|other fields| DEF["Default Embedder"]
Engineメソッド
ドキュメント操作
| メソッド | 説明 |
|---|---|
put_document(id, doc) | Upsert – 同じIDのドキュメントが既存の場合は置き換え |
add_document(id, doc) | 追加 – 新しいチャンクとして追加(複数のチャンクが同一IDを共有可能) |
get_documents(id) | 外部IDによるすべてのドキュメント/チャンクの取得 |
delete_documents(id) | 外部IDによるすべてのドキュメント/チャンクの削除 |
commit() | 保留中の変更をストレージにフラッシュ(ドキュメントが検索可能になる) |
recover() | クラッシュ後にWALをリプレイして未コミット状態を復元 |
add_field(name, field_option) | 稼働中のエンジンにフィールドを動的に追加し、更新後の Schema を返す |
delete_field(name) | 稼働中のエンジンからフィールドを動的に削除し、更新後の Schema を返す |
schema() | 現在の Schema への参照を返す |
検索
| メソッド | 説明 |
|---|---|
search(request) | 統一検索の実行(Lexical、Vector、またはハイブリッド) |
search() メソッドは SearchRequest を受け取ります。SearchRequestにはLexicalクエリ、Vectorクエリ、またはその両方を含めることができます。両方が指定された場合、結果は指定された FusionAlgorithm で統合されます。
#![allow(unused)]
fn main() {
use laurus::{SearchRequestBuilder, LexicalSearchRequest, FusionAlgorithm};
use laurus::lexical::TermQuery;
// Lexicalのみの検索
let request = SearchRequestBuilder::new()
.lexical_search_request(
LexicalSearchRequest::new(Box::new(TermQuery::new("body", "rust")))
)
.limit(10)
.build();
// RRFフュージョンによるハイブリッド検索
let request = SearchRequestBuilder::new()
.lexical_search_request(lexical_req)
.vector_search_request(vector_req)
.fusion_algorithm(FusionAlgorithm::RRF { k: 60.0 })
.limit(10)
.build();
let results = engine.search(request).await?;
}
SearchRequest
| フィールド | 型 | デフォルト | 説明 |
|---|---|---|---|
lexical_search_request | Option<LexicalSearchRequest> | None | Lexicalクエリ |
vector_search_request | Option<VectorSearchRequest> | None | Vectorクエリ |
limit | usize | 10 | 返却する最大結果数 |
offset | usize | 0 | ページネーションのオフセット |
fusion_algorithm | Option<FusionAlgorithm> | RRF (k=60) | LexicalとVectorの結果を統合する方法 |
filter_query | Option<Box<dyn Query>> | None | 両方の検索タイプに適用されるフィルタ |
FusionAlgorithm
| バリアント | 説明 |
|---|---|
RRF { k: f64 } | Reciprocal Rank Fusion – ランクベースの結合。スコア = sum(1 / (k + rank))。比較不可能なスコアの大きさを処理します。 |
WeightedSum { lexical_weight, vector_weight } | min-maxスコア正規化を用いた加重結合。重みは[0.0, 1.0]にクランプされます。 |
関連項目: アーキテクチャ – 高レベルのデータフロー図
スコアリングとランキング
Laurusは、Lexical検索に複数のスコアリングアルゴリズムを提供し、Vector検索には距離ベースの類似度を使用します。このページでは、すべてのスコアリングメカニズムとハイブリッド検索における相互作用について説明します。
Lexicalスコアリング
BM25(デフォルト)
BM25はLexical検索のデフォルトスコアリング関数です。単語頻度とドキュメント長の正規化をバランスさせます。
score = IDF * (tf * (k1 + 1)) / (tf + k1 * (1 - b + b * (doc_len / avg_doc_len)))
各パラメータの意味:
- tf – ドキュメント内の単語頻度(Term Frequency)
- IDF – 逆文書頻度(Inverse Document Frequency)。全ドキュメントにおける単語の希少性
- k1 – 単語頻度の飽和パラメータ
- b – ドキュメント長の正規化係数
- doc_len / avg_doc_len – ドキュメント長と平均ドキュメント長の比率
ScoringConfig
ScoringConfig はBM25およびその他のスコアリングパラメータを制御します。
| パラメータ | 型 | デフォルト | 説明 |
|---|---|---|---|
k1 | f32 | 1.2 | 単語頻度の飽和。値が大きいほど単語頻度の重みが増します。 |
b | f32 | 0.75 | フィールド長の正規化。0.0 = 正規化なし、1.0 = 完全な正規化。 |
tf_idf_boost | f32 | 1.0 | TF-IDFのグローバルブースト係数 |
enable_field_norm | bool | true | フィールド長の正規化を有効にする |
field_boosts | HashMap<String, f32> | empty | フィールドごとのスコア乗数 |
enable_coord | bool | true | クエリ調整係数(matched_terms / total_query_terms)を有効にする |
代替スコアリング関数
| 関数 | 説明 |
|---|---|
BM25ScoringFunction | 設定可能なk1とbを持つBM25(デフォルト) |
TfIdfScoringFunction | フィールド長正規化付きの対数正規化TF-IDF |
VectorSpaceScoringFunction | ドキュメントの単語ベクトル空間におけるコサイン類似度 |
CustomScoringFunction | カスタムスコアリングロジック用のユーザー定義クロージャ |
ScoringRegistry
ScoringRegistry はスコアリングアルゴリズムの中央レジストリを提供します。
#![allow(unused)]
fn main() {
// 事前登録済みアルゴリズム:
// - "bm25" -> BM25ScoringFunction
// - "tf_idf" -> TfIdfScoringFunction
// - "vector_space" -> VectorSpaceScoringFunction
}
フィールドブースト
フィールドブーストは、特定のフィールドからのスコア寄与に乗数を適用します。一部のフィールドが他よりも重要な場合に有用です。
#![allow(unused)]
fn main() {
use std::collections::HashMap;
let mut field_boosts = HashMap::new();
field_boosts.insert("title".to_string(), 2.0); // titleのマッチはスコア2倍
field_boosts.insert("body".to_string(), 1.0); // bodyのマッチはスコア1倍
}
調整係数(Coordination Factor)
enable_coord が true の場合、AdvancedScorer は調整係数を適用します。
coord = matched_query_terms / total_query_terms
これはより多くのクエリ単語にマッチするドキュメントに報酬を与えます。例えば、クエリが3つの単語を含み、ドキュメントがそのうち2つにマッチする場合、調整係数は 2/3 = 0.667 になります。
Vectorスコアリング
Vector検索は距離ベースの類似度で結果をランク付けします。
similarity = 1 / (1 + distance)
距離は設定された距離メトリクスを使用して計算されます。
| メトリクス | 説明 | 最適な用途 |
|---|---|---|
Cosine | 1 - コサイン類似度 | テキストEmbedding(最も一般的) |
Euclidean | L2距離 | 空間データ |
Manhattan | L1距離 | 特徴ベクトル |
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>,
}
}
| フィールド | 型 | 説明 |
|---|---|---|
path | FacetPath | ファセット値 |
count | u64 | マッチするドキュメント数 |
children | Vec<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);
}
設定オプション
| オプション | 型 | デフォルト | 説明 |
|---|---|---|---|
tag | String | "mark" | ハイライトに使用するHTMLタグ |
css_class | Option<String> | None | タグに追加するオプションのCSSクラス |
max_fragments | usize | 5 | 返却するフラグメントの最大数 |
fragment_size | usize | 150 | フラグメントの目標文字数 |
fragment_overlap | usize | 20 | 隣接するフラグメント間のオーバーラップ文字数 |
fragment_separator | String | " ... " | フラグメント間の区切り文字 |
return_entire_field_if_no_highlight | bool | false | マッチがない場合にフィールド全体の値を返却する |
max_analyzed_chars | usize | 1,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は最も関連性の高いフラグメントを選択します。
- テキストが
fragment_size文字のオーバーラップするウィンドウに分割されます - 各フラグメントは含まれるクエリ単語の数でスコアリングされます
- 上位
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_distance | usize | 2 | 候補提案のための最大Levenshtein編集距離 |
max_suggestions | usize | 5 | 単語あたりの最大候補数 |
min_frequency | u32 | 1 | 候補として提案されるために必要な辞書内の最小頻度 |
auto_correct | bool | false | trueの場合、しきい値を超える修正を自動的に適用 |
auto_correct_threshold | f64 | 0.8 | 自動修正に必要な信頼度スコア(0.0–1.0) |
use_index_terms | bool | true | 検索インデックスの単語を辞書として使用 |
learn_from_queries | bool | true | ユーザーの検索クエリから新しい単語を学習 |
CorrectionResult
correct() メソッドは詳細な情報を含む CorrectionResult を返します。
| フィールド | 型 | 説明 |
|---|---|---|
original | String | 元のクエリ文字列 |
corrected | Option<String> | 修正済みクエリ(自動修正が適用された場合) |
word_suggestions | HashMap<String, Vec<Suggestion>> | 誤入力単語ごとにグループ化された候補 |
confidence | f64 | 全体の信頼度スコア(0.0–1.0) |
auto_corrected | bool | 自動修正が適用されたかどうか |
ヘルパーメソッド
| メソッド | 戻り値 | 説明 |
|---|---|---|
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);
}
次のステップ
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兆ドキュメント) |
この構造を採用する理由
- ゼロコスト集約:
u64IDがグローバルにユニークであるため、アグリゲータはノード間のID衝突を気にせずに高速なソートと重複排除を実行できます。 - 高速ルーティング: アグリゲータは上位ビットを見るだけで、ドキュメントの担当物理ノードを即座に特定でき、コストの高いハッシュ検索を回避できます。
- 高性能フェッチ: 内部IDは物理データ構造に直接マッピングされます。これにより、Laurusは検索時に「外部IDから内部IDへの変換」ステップをスキップし、O(1) のアクセス速度を実現します。
IDライフサイクル
- 登録(
engine.put_document()/engine.add_document()): ユーザーが外部IDを持つドキュメントを提供します。 - ID割り当て:
Engineが現在のshard_idと新しいLocal IDを組み合わせて、Shard-Prefixed内部IDを発行します。 - マッピング: エンジンが外部IDと新しい内部IDの対応関係を維持します。
- 検索: 検索結果は内部IDから解決された外部ID(
String)を返します。 - 取得/削除: ユーザー向け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
主要な原則
- WALファースト: すべての書き込み(追加または削除)はインメモリ構造を更新する前にWALに追記されます
- バッファリング書き込み: インメモリバッファが
commit()が呼ばれるまで変更を蓄積します - アトミックコミット:
commit()はすべてのバッファリングされた変更をセグメントファイルにフラッシュし、WALを切り捨てます - クラッシュセーフティ: 書き込みとコミットの間にプロセスがクラッシュした場合、次回起動時に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
次のステップ
- 削除の処理方法: 削除とコンパクション
- ストレージバックエンド: Storage
削除とコンパクション
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"]
- ドキュメントの内部IDが**削除ビットマップ(Deletion Bitmap)**に追加されます
- 検索時にビットマップがチェックされ、削除されたドキュメントが結果からフィルタリングされます
- 元のデータはセグメントファイルに残ったままです
論理削除を採用する理由
| メリット | 説明 |
|---|---|
| 速度 | O(1) – ビットの反転は即座に完了 |
| 不変セグメント | セグメントファイルはインプレースで変更されないため、並行性の管理が簡素化 |
| 安全なリカバリ | クラッシュが発生しても、削除ビットマップはWALから再構築可能 |
Upsert(更新 = 削除 + 挿入)
既存の外部IDでドキュメントをインデックスすると、Laurusは自動的にUpsertを実行します。
- 古いドキュメントが論理削除されます(そのIDが削除ビットマップに追加)
- 新しい内部IDで新しいドキュメントが挿入されます
- 外部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
コンパクションの処理内容
- 既存セグメントからすべての生存(未削除)ドキュメントを読み取ります
- 削除済みエントリを含まない転置インデックスやベクトルインデックスを再構築します
- 新しいクリーンなセグメントファイルを書き込みます
- 古いセグメントファイルを削除します
- 削除ビットマップをリセットします
コストと頻度
| 側面 | 詳細 |
|---|---|
| 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です。
| バリアント | 説明 | 一般的な原因 |
|---|---|---|
Io | I/Oエラー | ファイルが見つからない、権限拒否、ディスク容量不足 |
Index | インデックス操作エラー | インデックスの破損、セグメント読み取り失敗 |
Schema | スキーマ関連のエラー | 不明なフィールド名、型の不一致 |
Analysis | テキスト解析エラー | トークナイザーの失敗、無効なフィルタ設定 |
Query | クエリの解析/実行エラー | 不正なQuery DSL、クエリ内の不明なフィールド |
Storage | ストレージバックエンドエラー | ストレージのオープン失敗、書き込み失敗 |
Field | フィールド定義エラー | 無効なフィールドオプション、重複するフィールド名 |
Json | JSONシリアライズエラー | 不正なドキュメントJSON |
InvalidOperation | 無効な操作 | コミット前の検索、二重クローズ |
ResourceExhausted | リソース制限超過 | メモリ不足、オープンファイル数超過 |
SerializationError | バイナリシリアライズエラー | ディスク上のデータ破損 |
OperationCancelled | 操作がキャンセルされた | タイムアウト、ユーザーによるキャンセル |
NotImplemented | 機能が利用不可 | 未実装の操作 |
Other | 汎用エラー | タイムアウト、無効な設定、無効な引数 |
基本的なエラーハンドリング
? 演算子の使用
最もシンプルなアプローチ – エラーを呼び出し元に伝播します。
#![allow(unused)]
fn main() {
use laurus::{Engine, Result};
async fn index_documents(engine: &Engine) -> Result<()> {
let doc = laurus::Document::builder()
.add_text("title", "Rust Programming")
.build();
engine.put_document("doc1", doc).await?;
engine.commit().await?;
Ok(())
}
}
エラーバリアントのマッチング
エラータイプごとに異なる動作が必要な場合:
#![allow(unused)]
fn main() {
use laurus::{Engine, LaurusError};
async fn safe_search(engine: &Engine, query: &str) {
match engine.search(/* request */).await {
Ok(results) => {
for result in results {
println!("{}: {}", result.id, result.score);
}
}
Err(LaurusError::Query(msg)) => {
eprintln!("Invalid query syntax: {}", msg);
}
Err(LaurusError::Io(e)) => {
eprintln!("Storage I/O error: {}", e);
}
Err(e) => {
eprintln!("Unexpected error: {}", e);
}
}
}
}
downcast によるエラータイプの確認
LaurusError は std::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::Error | LaurusError::Io |
serde_json::Error | LaurusError::Json |
anyhow::Error | LaurusError::Anyhow |
次のステップ
拡張性
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> | サポートする入力タイプを宣言(Text、Image) |
as_any(&self) -> &dyn Any | ダウンキャストを可能にする |
オプションメソッド
| メソッド | デフォルト | 説明 |
|---|---|---|
async embed_batch(&self, inputs) -> Result<Vec<Vector>> | embed への逐次呼び出し | バッチ最適化のためにオーバーライド |
name(&self) -> &str | "unknown" | ログ出力用の識別子 |
supports(&self, input_type) -> bool | supported_input_types をチェック | 入力タイプのサポート確認 |
supports_text() -> bool | Text を確認 | テキストサポートの簡略確認 |
supports_image() -> bool | Image を確認 | 画像サポートの簡略確認 |
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() -> LoadingMode | LoadingMode::Eager | 推奨されるデータロードモード |
スレッドセーフティ
3つのトレイトすべてが Send + Sync を要求します。つまり、実装はスレッド間で安全に共有できる必要があります。共有可能な可変状態には Arc<Mutex<_>> またはロックフリーデータ構造を使用してください。
次のステップ
- エラーハンドリング – カスタム実装でのエラー処理
- テキスト解析 – 組み込みのAnalyzerとパイプラインコンポーネント
- Embedding – 組み込みのEmbedderオプション
- Storage – 組み込みのStorageバックエンド
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_documentとadd_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 を構築 |
Search
SearchRequestBuilder
| メソッド | 説明 |
|---|---|
SearchRequestBuilder::new() | 新しいBuilderを作成 |
.lexical_search_request(req) | Lexical検索コンポーネントを設定 |
.vector_search_request(req) | Vector検索コンポーネントを設定 |
.filter_query(query) | プレフィルタクエリを設定 |
.fusion_algorithm(algo) | フュージョンアルゴリズムを設定(デフォルト: RRF) |
.limit(n) | 最大結果数(デフォルト: 10) |
.offset(n) | N件スキップ(デフォルト: 0) |
.build() | SearchRequest を構築 |
LexicalSearchRequest
| メソッド | 説明 |
|---|---|
LexicalSearchRequest::new(query) | クエリで作成 |
LexicalSearchRequest::from_dsl(query_str) | DSLクエリ文字列から作成 |
.limit(n) | 最大結果数 |
.load_documents(bool) | ドキュメント内容をロードするかどうか |
.min_score(f32) | 最小スコアしきい値 |
.timeout_ms(u64) | 検索タイムアウト(ミリ秒) |
.parallel(bool) | 並列検索を有効化 |
.sort_by_field_asc(field) | フィールドで昇順ソート |
.sort_by_field_desc(field) | フィールドで降順ソート |
.sort_by_score() | 関連度スコアでソート(デフォルト) |
.with_field_boost(field, boost) | フィールドレベルのブーストを追加 |
VectorSearchRequestBuilder
| メソッド | 説明 |
|---|---|
VectorSearchRequestBuilder::new() | 新しいBuilderを作成 |
.add_text(field, text) | フィールドのテキストクエリを追加 |
.add_vector(field, vector) | 事前計算済みクエリベクトルを追加 |
.add_bytes(field, bytes, mime) | バイナリペイロードを追加(マルチモーダル用) |
.limit(n) | 最大結果数 |
.score_mode(VectorScoreMode) | スコア結合モード(WeightedSum、MaxSim) |
.min_score(f32) | 最小スコアしきい値 |
.field(name) | 検索を特定のフィールドに制限 |
.build() | リクエストを構築 |
SearchResult
| フィールド | 型 | 説明 |
|---|---|---|
id | String | 外部ドキュメントID |
score | f32 | 関連度スコア |
document | Option<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
| 型 | 説明 |
|---|---|
StandardAnalyzer | RegexTokenizer + 小文字化 + ストップワード |
SimpleAnalyzer | トークン化のみ(フィルタリングなし) |
EnglishAnalyzer | RegexTokenizer + 小文字化 + 英語ストップワード |
JapaneseAnalyzer | 日本語形態素解析 |
KeywordAnalyzer | トークン化なし(完全一致) |
PipelineAnalyzer | カスタムTokenizer + フィルタチェーン |
PerFieldAnalyzer | フィールドごとのAnalyzerディスパッチ |
Embedder
| 型 | Feature Flag | 説明 |
|---|---|---|
CandleBertEmbedder | embeddings-candle | ローカルBERTモデル |
OpenAIEmbedder | embeddings-openai | OpenAI API |
CandleClipEmbedder | embeddings-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 serveで gRPC サーバーを起動
はじめに
# インストール
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"
詳細はサブセクションを参照してください:
- インストール – CLI のインストール方法
- コマンドリファレンス – 全コマンドの詳細
- スキーマフォーマット – スキーマ TOML フォーマットのリファレンス
- REPL – 対話モード
インストール
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 の使い方を順を追って説明します。
前提条件
- laurus CLI がインストール済み(インストール を参照)
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 を設定することで、フィールド指定なしのクエリは title と body の両方を検索します。
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"
デフォルトフィールド(title と body)が検索されます。doc001 と doc002 が返されます。
フィールド指定検索
title フィールドのみを検索します:
laurus --index-dir ./tutorial_data search "title:python"
doc003 のみが返されます。
カテゴリ検索
laurus --index-dir ./tutorial_data search "category:programming"
doc001 のみが返されます。
ブーリアンクエリ
+(必須)と -(除外)で条件を組み合わせます:
laurus --index-dir ./tutorial_data search "+body:rust -body:web"
“rust” を含み “web” を含まない doc001 のみが返されます。
フレーズ検索
完全一致するフレーズを検索します:
laurus --index-dir ./tutorial_data search 'body:"data science"'
doc003 のみが返されます。
あいまい検索
~ を使ってタイプミスに対応した検索を行います:
laurus --index-dir ./tutorial_data search "body:programing~1"
タイプミスがあっても “programming” にマッチします。
JSON 出力
プログラムでの利用に向けて JSON 形式で結果を取得します:
laurus --index-dir ./tutorial_data --format json search "rust"
Step 6: ドキュメントの取得
ID を指定して特定のドキュメントを取得します:
laurus --index-dir ./tutorial_data get doc --id doc001
Step 7: ドキュメントの削除
ドキュメントを削除してコミットします:
laurus --index-dir ./tutorial_data delete doc --id doc003
laurus --index-dir ./tutorial_data commit
削除されたことを確認します:
laurus --index-dir ./tutorial_data search "python"
結果は返されません。
Step 8: REPL を使う
REPL はインデックスを対話的に操作するためのインタラクティブセッションです:
laurus --index-dir ./tutorial_data repl
REPL で以下のコマンドを試してみてください:
> stats
> search rust
> doc add doc004 {"title":"Go Programming","body":"Go is a statically typed language designed for simplicity and efficiency.","category":"programming"}
> commit
> search programming
> doc get doc004
> doc delete doc004
> commit
> quit
REPL はコマンド履歴(上下キー)や行編集に対応しています。
Step 9: クリーンアップ
チュートリアルで作成したデータを削除します:
rm -rf ./tutorial_data schema.toml
次のステップ
- スキーマフォーマットで高度なフィールド設定を学ぶ
- コマンドリファレンスで全コマンドを確認する
- REPLでインタラクティブな操作を深める
- サーバーチュートリアルで gRPC/HTTP アクセスを試す
コマンドリファレンス
グローバルオプション
すべてのコマンドで以下のオプションが使用できます:
| オプション | 環境変数 | デフォルト | 説明 |
|---|---|---|---|
--index-dir <PATH> | LAURUS_INDEX_DIR | ./laurus_index | インデックスデータディレクトリのパス |
--format <FORMAT> | — | table | 出力形式: table または json |
# 例: カスタムデータディレクトリで JSON 出力を使用
laurus --index-dir /var/data/my_index --format json search "title:rust"
create — リソースの作成
create index
スキーマ TOML ファイルから新しいインデックスを作成します。
laurus create index --schema <FILE>
引数:
| フラグ | 必須 | 説明 |
|---|---|---|
--schema <FILE> | はい | インデックススキーマを定義する TOML ファイルのパス |
スキーマファイルの形式:
スキーマファイルは Laurus ライブラリの Schema 型と同じ構造に従います。詳細はスキーマフォーマットリファレンスを参照してください。例:
default_fields = ["title", "body"]
[fields.title.Text]
stored = true
indexed = true
[fields.body.Text]
stored = true
indexed = true
[fields.category.Text]
stored = true
indexed = true
例:
laurus --index-dir ./my_index create index --schema schema.toml
# Index created at ./my_index.
注意: インデックスが既に存在する場合はエラーが返されます。再作成するにはデータディレクトリを削除してください。
create schema
対話式ウィザードを通じてスキーマ TOML ファイルを生成します。
laurus create schema [--output <FILE>]
引数:
| フラグ | 必須 | デフォルト | 説明 |
|---|---|---|---|
--output <FILE> | いいえ | schema.toml | 生成されるスキーマの出力ファイルパス |
ウィザードは以下の手順で進みます:
- フィールド定義 — フィールド名を入力し、型を選択し、型固有のオプションを設定
- 繰り返し — 必要な数だけフィールドを追加
- デフォルトフィールド — デフォルトの検索対象とする Lexical フィールドを選択
- プレビュー — 保存前に生成された TOML を確認
- 保存 — スキーマファイルを書き出し
サポートされるフィールド型:
| 型 | カテゴリ | オプション |
|---|---|---|
Text | Lexical | indexed, stored, term_vectors |
Integer | Lexical | indexed, stored |
Float | Lexical | indexed, stored |
Boolean | Lexical | indexed, stored |
DateTime | Lexical | indexed, stored |
Geo | Lexical | indexed, stored |
Bytes | Lexical | stored |
Hnsw | Vector | dimension, distance, m, ef_construction |
Flat | Vector | dimension, distance |
Ivf | Vector | dimension, distance, n_clusters, n_probe |
例:
# schema.toml を対話的に生成
laurus create schema
# 出力パスを指定
laurus create schema --output my_schema.toml
# 生成されたスキーマからインデックスを作成
laurus create index --schema schema.toml
get — リソースの取得
get stats
インデックスの統計情報を表示します。
laurus get stats
テーブル出力の例:
Document count: 42
Vector fields:
╭──────────┬─────────┬───────────╮
│ Field │ Vectors │ Dimension │
├──────────┼─────────┼───────────┤
│ text_vec │ 42 │ 384 │
╰──────────┴─────────┴───────────╯
JSON 出力の例:
laurus --format json get stats
{
"document_count": 42,
"fields": {
"text_vec": {
"vector_count": 42,
"dimension": 384
}
}
}
get schema
現在のインデックスのスキーマを JSON 形式で表示します。
laurus get schema
例:
laurus get schema
# {
# "fields": { ... },
# "default_fields": ["title", "body"],
# ...
# }
get doc
外部 ID でドキュメント(およびすべてのチャンク)を取得します。
laurus get doc --id <ID>
テーブル出力の例:
╭──────┬─────────────────────────────────────────╮
│ ID │ Fields │
├──────┼─────────────────────────────────────────┤
│ doc1 │ body: This is a test, title: Hello World │
╰──────┴─────────────────────────────────────────╯
JSON 出力の例:
laurus --format json get doc --id doc1
[
{
"id": "doc1",
"document": {
"title": "Hello World",
"body": "This is a test document."
}
}
]
add — リソースの追加
add doc
インデックスにドキュメントを追加します。ドキュメントは commit を実行するまで検索対象になりません。
laurus add doc --id <ID> --data <JSON>
引数:
| フラグ | 必須 | 説明 |
|---|---|---|
--id <ID> | はい | 外部ドキュメント ID(文字列) |
--data <JSON> | はい | JSON 文字列としてのドキュメントフィールド |
JSON フォーマットはフィールド名と値を対応付けたフラットなオブジェクトです:
{
"title": "Introduction to Rust",
"body": "Rust is a systems programming language.",
"category": "programming"
}
例:
laurus add doc --id doc1 --data '{"title":"Hello World","body":"This is a test document."}'
# Document 'doc1' added. Run 'commit' to persist changes.
ヒント: 複数のドキュメントが同じ外部 ID を共有できます(チャンキングパターン)。各チャンクに対して
add docを使用してください。
add field
既存のインデックスにフィールドを動的に追加します。
laurus add field --index-dir ./data \
--name category \
--field-option '{"Text": {"indexed": true, "stored": true}}'
--field-option 引数はスキーマファイルと同じ外部タグ付き JSON 形式を受け付けます。
フィールド追加後、スキーマは自動的に永続化されます。
delete — リソースの削除
delete field
スキーマからフィールドを動的に削除します。既にインデックスされたデータは残りますが、削除されたフィールドにはアクセスできなくなります。
laurus delete field --name <FIELD_NAME>
例:
laurus delete field --name category
# Field 'category' deleted.
delete doc
外部 ID でドキュメント(およびすべてのチャンク)を削除します。
laurus delete doc --id <ID>
例:
laurus delete doc --id doc1
# Document 'doc1' deleted. Run 'commit' to persist changes.
commit
保留中の変更(追加と削除)をインデックスにコミットします。コミットするまで、変更は検索に反映されません。
laurus commit
例:
laurus --index-dir ./my_index commit
# Changes committed successfully.
search
Query DSL を使用して検索クエリを実行します。
laurus search <QUERY> [--limit <N>] [--offset <N>]
引数:
| 引数 / フラグ | 必須 | デフォルト | 説明 |
|---|---|---|---|
<QUERY> | はい | — | Laurus Query DSL によるクエリ文字列 |
--limit <N> | いいえ | 10 | 最大結果件数 |
--offset <N> | いいえ | 0 | スキップする結果件数 |
クエリ構文の例:
# Term クエリ
laurus search "body:rust"
# Phrase クエリ
laurus search 'body:"machine learning"'
# Boolean クエリ
laurus search "+body:programming -body:python"
# Fuzzy クエリ(タイポ許容)
laurus search "body:programing~2"
# Wildcard クエリ
laurus search "title:intro*"
# Range クエリ
laurus search "price:[10 TO 50]"
テーブル出力の例:
╭──────┬────────┬─────────────────────────────────────────╮
│ ID │ Score │ Fields │
├──────┼────────┼─────────────────────────────────────────┤
│ doc1 │ 0.8532 │ body: Rust is a systems..., title: Intr │
│ doc3 │ 0.4210 │ body: JavaScript powers..., title: Web │
╰──────┴────────┴─────────────────────────────────────────╯
JSON 出力の例:
laurus --format json search "body:rust" --limit 5
[
{
"id": "doc1",
"score": 0.8532,
"document": {
"title": "Introduction to Rust",
"body": "Rust is a systems programming language."
}
}
]
repl
対話型 REPL セッションを開始します。詳細は REPL を参照してください。
laurus repl
serve
gRPC サーバー(およびオプションで HTTP Gateway)を起動します。
laurus serve [OPTIONS]
起動オプション、設定、使用例については laurus-server のドキュメントを参照してください:
- はじめに — 起動オプションと gRPC 接続例
- 設定 — TOML 設定ファイル、環境変数、優先順位
- ハンズオンチュートリアル — ステップバイステップの操作ガイド
スキーマフォーマットリファレンス
スキーマファイルはインデックスの構造を定義します。どのフィールドが存在するか、その型、およびインデックスの方法を指定します。Laurus はスキーマファイルに TOML 形式を使用します。
概要
スキーマは2つのトップレベル要素で構成されます:
# クエリでフィールドが指定されていない場合にデフォルトで検索するフィールド。
default_fields = ["title", "body"]
# フィールド定義。各フィールドには名前と型付き設定があります。
[fields.<field_name>.<FieldType>]
# ... 型固有のオプション
default_fields— Query DSL でデフォルトの検索対象として使用されるフィールド名のリストです。Lexical フィールド(Text、Integer、Float など)のみデフォルトフィールドに指定できます。このキーはオプションで、デフォルトは空のリストです。fields— フィールド名とその型付き設定のマップです。各フィールドにはフィールド型を1つだけ指定する必要があります。
フィールド命名規則
- フィールド名は任意の文字列です(例:
title、body_vec、created_at)。 _idフィールドは Laurus が内部ドキュメント ID 管理用に予約しています。使用しないでください。- フィールド名はスキーマ内で一意である必要があります。
フィールド型
フィールドは Lexical(キーワード/全文検索用)と Vector(類似検索用)の2つのカテゴリに分類されます。1つのフィールドが両方を兼ねることはできません。
Lexical フィールド
Text
全文検索可能なフィールドです。テキストは解析パイプライン(トークン化、正規化、ステミングなど)によって処理されます。
[fields.title.Text]
indexed = true # このフィールドを検索用にインデックスするかどうか
stored = true # 取得用に元の値を保存するかどうか
term_vectors = false # タームの位置を保存するかどうか(フレーズクエリ、ハイライト用)
| オプション | 型 | デフォルト | 説明 |
|---|---|---|---|
indexed | bool | true | このフィールドの検索を有効にする |
stored | bool | true | 結果に返せるよう元の値を保存する |
term_vectors | bool | true | フレーズクエリ、ハイライト、More-Like-This 用にタームの位置を保存する |
Integer
64ビット符号付き整数フィールド。範囲クエリと完全一致をサポートします。
[fields.year.Integer]
indexed = true
stored = true
| オプション | 型 | デフォルト | 説明 |
|---|---|---|---|
indexed | bool | true | 範囲クエリおよび完全一致クエリを有効にする |
stored | bool | true | 元の値を保存する |
Float
64ビット浮動小数点フィールド。範囲クエリをサポートします。
[fields.rating.Float]
indexed = true
stored = true
| オプション | 型 | デフォルト | 説明 |
|---|---|---|---|
indexed | bool | true | 範囲クエリを有効にする |
stored | bool | true | 元の値を保存する |
Boolean
ブーリアンフィールド(true / false)。
[fields.published.Boolean]
indexed = true
stored = true
| オプション | 型 | デフォルト | 説明 |
|---|---|---|---|
indexed | bool | true | ブーリアン値によるフィルタリングを有効にする |
stored | bool | true | 元の値を保存する |
DateTime
UTC タイムスタンプフィールド。範囲クエリをサポートします。
[fields.created_at.DateTime]
indexed = true
stored = true
| オプション | 型 | デフォルト | 説明 |
|---|---|---|---|
indexed | bool | true | 日時の範囲クエリを有効にする |
stored | bool | true | 元の値を保存する |
Geo
地理座標フィールド(緯度/経度)。半径クエリおよびバウンディングボックスクエリをサポートします。
[fields.location.Geo]
indexed = true
stored = true
| オプション | 型 | デフォルト | 説明 |
|---|---|---|---|
indexed | bool | true | Geo クエリ(半径、バウンディングボックス)を有効にする |
stored | bool | true | 元の値を保存する |
Bytes
生バイナリデータフィールド。インデックスされず、保存のみです。
[fields.thumbnail.Bytes]
stored = true
| オプション | 型 | デフォルト | 説明 |
|---|---|---|---|
stored | bool | true | バイナリデータを保存する |
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
| オプション | 型 | デフォルト | 説明 |
|---|---|---|---|
dimension | integer | 128 | ベクトルの次元数(Embedding モデルの出力と一致させる必要あり) |
distance | string | "Cosine" | 距離メトリクス(距離メトリクスを参照) |
m | integer | 16 | ノードあたりの最大双方向接続数。大きいほど再現率が向上するがメモリ使用量が増加 |
ef_construction | integer | 200 | インデックス構築時の探索幅。大きいほど品質が向上するが構築が遅くなる |
base_weight | float | 1.0 | ハイブリッド検索のスコア融合における重み |
quantizer | object | なし | オプションの量子化方式(量子化を参照) |
チューニングガイドライン:
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
| オプション | 型 | デフォルト | 説明 |
|---|---|---|---|
dimension | integer | 128 | ベクトルの次元数 |
distance | string | "Cosine" | 距離メトリクス(距離メトリクスを参照) |
base_weight | float | 1.0 | ハイブリッド検索のスコア融合における重み |
quantizer | object | なし | オプションの量子化方式(量子化を参照) |
Ivf
IVF(Inverted File Index)。ベクトルをクラスタリングし、クラスタのサブセットのみを検索します。大規模データセットに適しています。
[fields.embedding.Ivf]
dimension = 384
distance = "Cosine"
n_clusters = 100
n_probe = 1
base_weight = 1.0
| オプション | 型 | デフォルト | 説明 |
|---|---|---|---|
dimension | integer | (必須) | ベクトルの次元数 |
distance | string | "Cosine" | 距離メトリクス(距離メトリクスを参照) |
n_clusters | integer | 100 | クラスタ数。多いほど細かい分割が可能 |
n_probe | integer | 1 | クエリ時に検索するクラスタ数。大きいほど再現率が向上するが遅くなる |
base_weight | float | 1.0 | ハイブリッド検索のスコア融合における重み |
quantizer | object | なし | オプションの量子化方式(量子化を参照) |
注意: 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_count | integer | サブベクトルの数。dimension を均等に割り切れる必要があります。 |
完全な例
全文検索のみ
Lexical 検索のみのシンプルなブログ記事インデックス:
default_fields = ["title", "body"]
[fields.title.Text]
indexed = true
stored = true
term_vectors = false
[fields.body.Text]
indexed = true
stored = true
term_vectors = false
[fields.category.Text]
indexed = true
stored = true
term_vectors = false
[fields.published_at.DateTime]
indexed = true
stored = true
Vector 検索のみ
セマンティック類似検索用の Vector のみのインデックス:
[fields.embedding.Hnsw]
dimension = 768
distance = "Cosine"
m = 16
ef_construction = 200
ハイブリッド検索(Lexical + Vector)
Lexical 検索と Vector 検索を組み合わせた両方の長所を活かす検索:
default_fields = ["title", "body"]
[fields.title.Text]
indexed = true
stored = true
term_vectors = false
[fields.body.Text]
indexed = true
stored = true
term_vectors = true
[fields.category.Text]
indexed = true
stored = true
term_vectors = false
[fields.body_vec.Hnsw]
dimension = 384
distance = "Cosine"
m = 16
ef_construction = 200
ヒント: 1つのフィールドが Lexical と Vector の両方を兼ねることはできません。別々のフィールド(例: テキスト用の
body、Embedding 用のbody_vec)を使用し、どちらも同じソースコンテンツにマッピングしてください。
E コマースの商品インデックス
複数のフィールド型を組み合わせたより複雑なスキーマ:
default_fields = ["name", "description"]
[fields.name.Text]
indexed = true
stored = true
term_vectors = false
[fields.description.Text]
indexed = true
stored = true
term_vectors = true
[fields.price.Float]
indexed = true
stored = true
[fields.in_stock.Boolean]
indexed = true
stored = true
[fields.created_at.DateTime]
indexed = true
stored = true
[fields.location.Geo]
indexed = true
stored = true
[fields.description_vec.Hnsw]
dimension = 384
distance = "Cosine"
スキーマの生成
CLI を使用して対話的にスキーマ TOML ファイルを生成できます:
laurus create schema
laurus create schema --output my_schema.toml
詳細は create schema を参照してください。
スキーマの使用
スキーマファイルが用意できたら、そこからインデックスを作成します:
laurus create index --schema schema.toml
または Rust でプログラム的に読み込みます:
#![allow(unused)]
fn main() {
use laurus::Schema;
let toml_str = std::fs::read_to_string("schema.toml")?;
let schema: Schema = toml::from_str(&toml_str)?;
}
REPL(対話モード)
REPL は、毎回 laurus コマンドをフルで入力することなく、インデックスを操作できる対話型セッションを提供します。
REPL の起動
laurus --index-dir ./my_index repl
Laurus REPL (type 'help' for commands, 'quit' to exit)
laurus>
REPL は起動時にインデックスを開き、セッション中ロードされた状態を維持します。
利用可能なコマンド
コマンドは CLI と同じ <操作> <リソース> の順序に従います。
| コマンド | 説明 |
|---|---|
search <query> | インデックスを検索 |
add field <name> <json> | スキーマにフィールドを追加 |
add doc <id> <json> | ドキュメントを追加 |
get stats | インデックスの統計情報を表示 |
get schema | 現在のスキーマを表示 |
get doc <id> | ID でドキュメントを取得 |
delete field <name> | スキーマからフィールドを削除 |
delete doc <id> | ID でドキュメントを削除 |
commit | 保留中の変更をコミット |
help | 利用可能なコマンドを表示 |
quit / exit | REPL を終了 |
使用例
検索
laurus> search body:rust
╭──────┬────────┬────────────────────────────────────╮
│ ID │ Score │ Fields │
├──────┼────────┼────────────────────────────────────┤
│ doc1 │ 0.8532 │ body: Rust is a systems..., title… │
╰──────┴────────┴────────────────────────────────────╯
フィールドの管理
laurus> add field category {"Text": {"indexed": true, "stored": true}}
Field 'category' added.
laurus> delete field category
Field 'category' deleted.
ドキュメントの追加とコミット
laurus> add doc doc4 {"title":"New Document","body":"Some content here."}
Document 'doc4' added.
laurus> commit
Changes committed.
情報の取得
laurus> get stats
Document count: 3
laurus> get schema
{
"fields": { ... },
"default_fields": ["title", "body"]
}
laurus> get doc doc4
╭──────┬───────────────────────────────────────────────╮
│ ID │ Fields │
├──────┼───────────────────────────────────────────────┤
│ doc4 │ body: Some content here., title: New Document │
╰──────┴───────────────────────────────────────────────╯
ドキュメントの削除
laurus> delete doc doc4
Document 'doc4' deleted.
laurus> commit
Changes committed.
機能
- 行編集 — 矢印キー、Home/End キー、および標準的な readline ショートカット
- 履歴 — 上下矢印キーで以前のコマンドを呼び出し
- Ctrl+C / Ctrl+D — REPL を正常に終了
サーバー概要
laurus-server クレートは、Laurus 検索エンジン用の gRPC サーバーとオプションの HTTP/JSON ゲートウェイを提供します。エンジンをメモリに常駐させることで、コマンド実行ごとの起動オーバーヘッドを排除します。
機能
- 永続エンジン – インデックスはリクエスト間で開いたまま維持され、呼び出しごとの WAL リプレイが不要
- フル gRPC API – インデックス管理、ドキュメント CRUD、コミット、検索(単発 + ストリーミング)
- HTTP ゲートウェイ – gRPC と併用可能なオプションの HTTP/JSON ゲートウェイで REST スタイルのアクセスを提供
- ヘルスチェック – ロードバランサーやオーケストレーター向けの標準ヘルスチェックエンドポイント
- グレースフルシャットダウン – Ctrl+C / SIGINT で保留中の変更を自動的にコミット
- TOML 設定 – オプションの設定ファイルと CLI・環境変数によるオーバーライド
アーキテクチャ
graph LR
subgraph "laurus-server"
GW["HTTP Gateway\n(axum)"]
GRPC["gRPC Server\n(tonic)"]
ENG["Engine\n(Arc<RwLock>)"]
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
セクション
- はじめに – 起動オプションと最初のステップ
- 設定 – TOML 設定、環境変数、優先順位
- gRPC API リファレンス – 全サービスと RPC の完全な API ドキュメント
- HTTP ゲートウェイ – HTTP/JSON エンドポイントリファレンス
gRPC サーバーをはじめる
サーバーの起動
gRPC サーバーは laurus CLI の serve サブコマンドで起動します。
laurus serve [OPTIONS]
オプション
| オプション | 短縮形 | 環境変数 | デフォルト | 説明 |
|---|---|---|---|---|
--config <PATH> | -c | LAURUS_CONFIG | – | TOML 設定ファイルのパス |
--host <HOST> | -H | LAURUS_HOST | 0.0.0.0 | リッスンアドレス |
--port <PORT> | -p | LAURUS_PORT | 50051 | リッスンポート |
--http-port <PORT> | – | LAURUS_HTTP_PORT | – | HTTP ゲートウェイポート(設定すると 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)を受信すると、自動的に以下を実行します。
- 新しい接続の受け付けを停止
- インデックスへの保留中の変更をコミット
- 正常に終了
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アナライザー(トークン化+小文字化)を使用。body—analyzersセクションで定義したカスタムbody_analyzer(NFKC 正規化+正規表現トークナイザー+小文字化+カスタムストップワード)を使用。category—keywordアナライザー(値全体を単一トークンとして扱い、完全一致用)を使用。embedding— HNSW ベクトルインデックス、4 次元、コサイン距離。embeddersで定義したmy_embedderを使用。このチュートリアルではprecomputed(外部で事前計算したベクトル)を使用。本番環境では、使用する埋め込みモデルに合わせた次元数(例: 384 や 768)を指定してください。
default_fields を設定することで、フィールド指定なしのクエリは title と body の両方を検索します。
組み込みアナライザー
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}'
デフォルトフィールド(title と body)が検索されます。doc001 と doc002 が返されます。
フィールド指定検索
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 のみが返されます。
ブーリアンクエリ
AND、OR、NOT で条件を組み合わせます:
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_bert | embeddings-candle | sentence-transformers/all-MiniLM-L6-v2 | 384 |
candle_clip | embeddings-multimodal | openai/clip-vit-base-patch32 | 512 |
openai | embeddings-openai | text-embedding-3-small | 1536 |
次のステップ
- ベクトル検索とハイブリッド検索で意味的な類似検索を試す
- gRPC API リファレンスで API 仕様の詳細を確認する
- 設定で本番環境向けの設定を行う
grpcurlや gRPC クライアントライブラリを使ったプログラムからのアクセスについてははじめにを参照
設定
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] セクション
| フィールド | 型 | デフォルト | 説明 |
|---|---|---|---|
host | String | "0.0.0.0" | gRPC サーバーのリッスンアドレス |
port | Integer | 50051 | gRPC サーバーのリッスンポート |
http_port | Integer | – | HTTP ゲートウェイポート。設定すると gRPC と並行して HTTP/JSON ゲートウェイが起動 |
[index] セクション
| フィールド | 型 | デフォルト | 説明 |
|---|---|---|---|
data_dir | String | "./laurus_index" | インデックスデータディレクトリのパス |
環境変数
| 変数 | 対応する設定 | 説明 |
|---|---|---|
LAURUS_HOST | server.host | リッスンアドレス |
LAURUS_PORT | server.port | gRPC リッスンポート |
LAURUS_HTTP_PORT | server.http_port | HTTP ゲートウェイポート |
LAURUS_INDEX_DIR | index.data_dir | インデックスデータディレクトリ |
RUST_LOG | – | ログフィルタディレクティブ(例: info, debug, laurus=debug,tonic=warn) |
LAURUS_CONFIG | – | TOML 設定ファイルのパス |
CLI 引数
| オプション | 短縮形 | デフォルト | 説明 |
|---|---|---|---|
--config <PATH> | -c | – | TOML 設定ファイルのパス |
--host <HOST> | -H | 0.0.0.0 | リッスンアドレス |
--port <PORT> | -p | 50051 | gRPC リッスンポート |
--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 | 説明 |
|---|---|---|
HealthService | Check | ヘルスチェック |
IndexService | CreateIndex, GetIndex, GetSchema, AddField, DeleteField | インデックスのライフサイクルとスキーマ |
DocumentService | PutDocument, AddDocument, GetDocuments, DeleteDocuments, Commit | ドキュメント CRUD とコミット |
SearchService | Search, SearchStream | 単発検索とストリーミング検索 |
HealthService
Check
サーバーの現在のサービング状態を返します。
rpc Check(HealthCheckRequest) returns (HealthCheckResponse);
レスポンスフィールド:
| フィールド | 型 | 説明 |
|---|---|---|
status | ServingStatus | サーバーの準備が完了している場合は SERVING_STATUS_SERVING |
IndexService
CreateIndex
指定されたスキーマで新しいインデックスを作成します。インデックスが既に開いている場合は ALREADY_EXISTS エラーを返します。
rpc CreateIndex(CreateIndexRequest) returns (CreateIndexResponse);
リクエストフィールド:
| フィールド | 型 | 必須 | 説明 |
|---|---|---|---|
schema | Schema | はい | インデックスのスキーマ定義 |
Schema 構造:
message Schema {
map<string, FieldOption> fields = 1;
repeated string default_fields = 2;
map<string, AnalyzerDefinition> analyzers = 3;
map<string, EmbedderConfig> embedders = 4;
}
fields— フィールド名をキーとしたフィールド定義。default_fields— クエリでフィールドを指定しない場合のデフォルト検索対象フィールド名。analyzers— 名前をキーとしたカスタムアナライザーパイプライン。TextOption.analyzerで参照。embedders— 名前をキーとしたエンベッダー設定。ベクトルフィールドオプション(HnswOption.embedderなど)で参照。
AnalyzerDefinition:
message AnalyzerDefinition {
repeated ComponentConfig char_filters = 1;
ComponentConfig tokenizer = 2;
repeated ComponentConfig token_filters = 3;
}
ComponentConfig(文字フィルター、トークナイザー、トークンフィルターに使用):
| フィールド | 型 | 説明 |
|---|---|---|
type | string | コンポーネントタイプ名(例: "whitespace", "lowercase", "unicode_normalization") |
params | map<string, string> | タイプ固有のパラメータ(文字列のキーと値のペア) |
EmbedderConfig:
| フィールド | 型 | 説明 |
|---|---|---|
type | string | エンベッダータイプ名(例: "precomputed", "candle_bert", "openai") |
params | map<string, string> | タイプ固有のパラメータ(例: "model" → "sentence-transformers/all-MiniLM-L6-v2") |
各 FieldOption は以下のフィールドタイプのいずれかを持つ oneof です。
| Lexical フィールド | Vector フィールド |
|---|---|
TextOption (indexed, stored, term_vectors, analyzer) | HnswOption (dimension, distance, m, ef_construction, base_weight, quantizer, embedder) |
IntegerOption (indexed, stored) | FlatOption (dimension, distance, base_weight, quantizer, embedder) |
FloatOption (indexed, stored) | IvfOption (dimension, distance, n_clusters, n_probe, base_weight, quantizer, embedder) |
BooleanOption (indexed, stored) | |
DateTimeOption (indexed, stored) | |
GeoOption (indexed, stored) | |
BytesOption (stored) |
ベクトルフィールドオプションの embedder フィールドには、Schema.embedders で定義したエンベッダー名を指定します。設定すると、インデックス時にドキュメントのテキストフィールドからベクトルを自動生成します。事前計算済みのベクトルを直接供給する場合は空のままにします。
距離メトリクス: COSINE, EUCLIDEAN, MANHATTAN, DOT_PRODUCT, ANGULAR
量子化手法: NONE, SCALAR_8BIT, PRODUCT_QUANTIZATION
QuantizationConfig 構造:
| フィールド | 型 | 説明 |
|---|---|---|
method | QuantizationMethod | 量子化手法(QUANTIZATION_METHOD_NONE, QUANTIZATION_METHOD_SCALAR_8BIT, または QUANTIZATION_METHOD_PRODUCT_QUANTIZATION) |
subvector_count | uint32 | サブベクトルの数(method が PRODUCT_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_count | uint64 | インデックス内のドキュメント総数 |
vector_fields | map<string, VectorFieldStats> | フィールドごとのベクトル統計情報 |
各 VectorFieldStats には vector_count と dimension が含まれます。
GetSchema
現在のインデックススキーマを取得します。
rpc GetSchema(GetSchemaRequest) returns (GetSchemaResponse);
レスポンスフィールド:
| フィールド | 型 | 説明 |
|---|---|---|
schema | Schema | インデックスのスキーマ |
AddField
稼働中のインデックスにフィールドを動的に追加します。
rpc AddField(AddFieldRequest) returns (AddFieldResponse);
| フィールド | 型 | 説明 |
|---|---|---|
name | string | フィールド名 |
field_option | FieldOption | フィールド設定 |
レスポンス: 更新後の Schema を返します。
DeleteField
稼働中のインデックスからフィールドを動的に削除します。既にインデックスされたデータは残りますが、削除されたフィールドにはアクセスできなくなります。
rpc DeleteField(DeleteFieldRequest) returns (DeleteFieldResponse);
message DeleteFieldRequest {
string name = 1;
}
message DeleteFieldResponse {
Schema schema = 1;
}
リクエストフィールド:
| フィールド | 型 | 必須 | 説明 |
|---|---|---|---|
name | string | はい | 削除するフィールド名 |
レスポンス: 更新後の Schema を返します。
DocumentService
PutDocument
ID を指定してドキュメントを挿入または置換します。同じ ID のドキュメントが既に存在する場合は置換されます。
rpc PutDocument(PutDocumentRequest) returns (PutDocumentResponse);
リクエストフィールド:
| フィールド | 型 | 必須 | 説明 |
|---|---|---|---|
id | string | はい | 外部ドキュメント ID |
document | Document | はい | ドキュメントの内容 |
Document 構造:
message Document {
map<string, Value> fields = 1;
}
各 Value は以下の型のいずれかを持つ oneof です。
| 型 | Proto フィールド | 説明 |
|---|---|---|
| Null | null_value | Null 値 |
| Boolean | bool_value | ブール値 |
| Integer | int64_value | 64 ビット整数 |
| Float | float64_value | 64 ビット浮動小数点数 |
| Text | text_value | UTF-8 文字列 |
| Bytes | bytes_value | バイト列 |
| Vector | vector_value | VectorValue(浮動小数点数のリスト) |
| DateTime | datetime_value | Unix マイクロ秒(UTC) |
| Geo | geo_value | GeoPoint(緯度、経度) |
AddDocument
ドキュメントを追加します。PutDocument と異なり、同じ ID の既存ドキュメントを置換しません。複数のドキュメントが同じ ID を共有できます(チャンキングパターン)。
rpc AddDocument(AddDocumentRequest) returns (AddDocumentResponse);
リクエストフィールドは PutDocument と同じです。
GetDocuments
指定された外部 ID に一致するすべてのドキュメントを取得します。
rpc GetDocuments(GetDocumentsRequest) returns (GetDocumentsResponse);
リクエストフィールド:
| フィールド | 型 | 必須 | 説明 |
|---|---|---|---|
id | string | はい | 外部ドキュメント ID |
レスポンスフィールド:
| フィールド | 型 | 説明 |
|---|---|---|
documents | repeated Document | 一致するドキュメント |
DeleteDocuments
指定された外部 ID に一致するすべてのドキュメントを削除します。
rpc DeleteDocuments(DeleteDocumentsRequest) returns (DeleteDocumentsResponse);
Commit
保留中の変更(追加および削除)をインデックスにコミットします。コミットされるまで、変更は検索に反映されません。
rpc Commit(CommitRequest) returns (CommitResponse);
SearchService
Search
検索クエリを実行し、結果を単一のレスポンスとして返します。
rpc Search(SearchRequest) returns (SearchResponse);
レスポンスフィールド:
| フィールド | 型 | 説明 |
|---|---|---|
results | repeated SearchResult | 関連度順の検索結果 |
total_hits | uint64 | マッチするドキュメントの総数(limit/offset 適用前) |
SearchStream
検索クエリを実行し、結果を 1 件ずつストリーミングで返します。
rpc SearchStream(SearchRequest) returns (stream SearchResult);
SearchRequest フィールド
| フィールド | 型 | 必須 | 説明 |
|---|---|---|---|
query | string | いいえ | Query DSL による Lexical 検索クエリ |
query_vectors | repeated QueryVector | いいえ | ベクトル検索クエリ |
limit | uint32 | いいえ | 最大結果件数(デフォルト: エンジンのデフォルト値) |
offset | uint32 | いいえ | スキップする結果件数 |
fusion | FusionAlgorithm | いいえ | ハイブリッド検索の Fusion アルゴリズム |
lexical_params | LexicalParams | いいえ | Lexical 検索パラメータ |
vector_params | VectorParams | いいえ | ベクトル検索パラメータ |
field_boosts | map<string, float> | いいえ | フィールドごとのスコアブースト |
query または query_vectors のいずれか 1 つ以上を指定する必要があります。
QueryVector
| フィールド | 型 | 説明 |
|---|---|---|
vector | repeated float | クエリベクトル |
weight | float | このベクトルの重み(デフォルト: 1.0) |
fields | repeated string | 対象のベクトルフィールド(空の場合は全フィールド) |
FusionAlgorithm
以下の 2 つのオプションを持つ oneof です。
- RRF (Reciprocal Rank Fusion):
kパラメータ(デフォルト: 60) - WeightedSum:
lexical_weightとvector_weight
LexicalParams
| フィールド | 型 | 説明 |
|---|---|---|
min_score | float | 最小スコア閾値 |
timeout_ms | uint64 | 検索タイムアウト(ミリ秒) |
parallel | bool | 並列検索を有効化 |
sort_by | SortSpec | スコアの代わりにフィールドでソート |
SortSpec
| フィールド | 型 | 説明 |
|---|---|---|
field | string | ソート対象のフィールド名。空文字列はスコアでソートすることを意味する |
order | SortOrder | SORT_ORDER_ASC(昇順)または SORT_ORDER_DESC(降順) |
VectorParams
| フィールド | 型 | 説明 |
|---|---|---|
fields | repeated string | 対象のベクトルフィールド |
score_mode | VectorScoreMode | WEIGHTED_SUM, MAX_SIM, または LATE_INTERACTION |
overfetch | float | オーバーフェッチ係数(デフォルト: 2.0) |
min_score | float | 最小スコア閾値 |
SearchResult
| フィールド | 型 | 説明 |
|---|---|---|
id | string | 外部ドキュメント ID |
score | float | 関連度スコア |
document | Document | ドキュメントの内容 |
例
{
"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 / JSON | INVALID_ARGUMENT | 不正なリクエストまたはスキーマ |
| インデックス未オープン | FAILED_PRECONDITION | CreateIndex の前に RPC が呼び出された場合 |
| インデックスが既に存在 | ALREADY_EXISTS | CreateIndex が 2 回呼び出された場合 |
| 未実装 | UNIMPLEMENTED | まだサポートされていない機能 |
| 内部エラー | INTERNAL | I/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/health | HealthService/Check | ヘルスチェック |
| POST | /v1/index | IndexService/CreateIndex | 新しいインデックスを作成 |
| GET | /v1/index | IndexService/GetIndex | インデックスの統計情報を取得 |
| GET | /v1/schema | IndexService/GetSchema | インデックスのスキーマを取得 |
| PUT | /v1/documents/:id | DocumentService/PutDocument | ドキュメントの Upsert |
| POST | /v1/documents/:id | DocumentService/AddDocument | ドキュメントの追加(チャンク) |
| GET | /v1/documents/:id | DocumentService/GetDocuments | ID でドキュメントを取得 |
| DELETE | /v1/documents/:id | DocumentService/DeleteDocuments | ID でドキュメントを削除 |
| POST | /v1/commit | DocumentService/Commit | 保留中の変更をコミット |
| POST | /v1/schema/fields | IndexService/AddField | フィールドの追加 |
| DELETE | /v1/schema/fields/:name | IndexService/DeleteField | フィールドの削除 |
| POST | /v1/search | SearchService/Search | 検索(単発) |
| POST | /v1/search/stream | SearchService/SearchStream | 検索(Server-Sent Events) |
API の使用例
ヘルスチェック
curl http://localhost:8080/v1/health
インデックスの作成
curl -X POST http://localhost:8080/v1/index \
-H 'Content-Type: application/json' \
-d '{
"schema": {
"fields": {
"title": {"text": {"indexed": true, "stored": true, "term_vectors": true}},
"body": {"text": {"indexed": true, "stored": true, "term_vectors": true}}
},
"default_fields": ["title", "body"]
}
}'
インデックス統計情報の取得
curl http://localhost:8080/v1/index
スキーマの取得
curl http://localhost:8080/v1/schema
ドキュメントの Upsert(PUT)
ドキュメントが既に存在する場合は置換します。
curl -X PUT http://localhost:8080/v1/documents/doc1 \
-H 'Content-Type: application/json' \
-d '{
"document": {
"fields": {
"title": "Hello World",
"body": "This is a test document."
}
}
}'
ドキュメントの追加(POST)
同じ ID の既存ドキュメントを置換せずに新しいチャンクを追加します。
curl -X POST http://localhost:8080/v1/documents/doc1 \
-H 'Content-Type: application/json' \
-d '{
"document": {
"fields": {
"title": "Hello World",
"body": "This is a test document."
}
}
}'
ドキュメントの取得
curl http://localhost:8080/v1/documents/doc1
ドキュメントの削除
curl -X DELETE http://localhost:8080/v1/documents/doc1
コミット
curl -X POST http://localhost:8080/v1/commit
検索
curl -X POST http://localhost:8080/v1/search \
-H 'Content-Type: application/json' \
-d '{"query": "body:test", "limit": 10}'
フィールドブースト付き検索
curl -X POST http://localhost:8080/v1/search \
-H 'Content-Type: application/json' \
-d '{
"query": "rust programming",
"limit": 10,
"field_boosts": {"title": 2.0}
}'
ハイブリッド検索
curl -X POST http://localhost:8080/v1/search \
-H 'Content-Type: application/json' \
-d '{
"query": "body:rust",
"query_vectors": [{"vector": [0.1, 0.2, 0.3], "weight": 1.0}],
"limit": 10,
"fusion": {"rrf": {"k": 60}}
}'
ストリーミング検索(SSE)
/v1/search/stream エンドポイントは Server-Sent Events(SSE)として結果を返します。各結果は個別のイベントとして送信されます。
curl -N -X POST http://localhost:8080/v1/search/stream \
-H 'Content-Type: application/json' \
-d '{"query": "body:test", "limit": 10}'
レスポンスは SSE イベントのストリームです。
data: {"id":"doc1","score":0.8532,"document":{...}}
data: {"id":"doc2","score":0.4210,"document":{...}}
リクエスト/レスポンス形式
すべてのリクエストおよびレスポンスボディは JSON を使用します。JSON の構造は gRPC の protobuf メッセージに対応しています。メッセージ定義の詳細は gRPC API リファレンスを参照してください。
MCP サーバー概要
laurus-mcp クレートは、Laurus 検索エンジン用の Model Context Protocol (MCP) サーバーを提供します。実行中の laurus-server インスタンスへの gRPC クライアントとして動作し、Claude などの AI アシスタントが標準 MCP stdio トランスポートを通じてドキュメントのインデックス登録や検索を行えるようにします。
機能
- MCP stdio トランスポート — サブプロセスとして起動し、stdin/stdout 経由で AI クライアントと通信
- gRPC クライアント — すべてのツール呼び出しを実行中の
laurus-serverインスタンスにプロキシ - 全 laurus 検索モード — Lexical(BM25)、Vector(HNSW/Flat/IVF)、ハイブリッド検索
- 動的接続 —
connectツールで任意の laurus-server エンドポイントに接続可能 - ドキュメントライフサイクル — MCP ツールを通じてドキュメントの追加・更新・削除・取得が可能
アーキテクチャ
graph LR
subgraph "laurus-mcp"
MCP["MCP Server\n(stdio)"]
end
AI["AI クライアント\n(Claude など)"] -->|"stdio (JSON-RPC)"| MCP
MCP -->|"gRPC"| SRV["laurus-server\n(常駐)"]
SRV --> Disk["ディスク上のインデックス"]
MCP サーバーは AI クライアントによって起動される子プロセスとして動作します。すべてのツール呼び出しを gRPC 経由で laurus-server インスタンスにプロキシします。laurus-server は MCP サーバーとは別途、事前に起動しておく必要があります。
クイックスタート
# ステップ 1: laurus-server を起動
laurus serve --grpc-port 50051
# ステップ 2: Claude Code で MCP サーバーを設定
claude mcp add laurus laurus mcp --endpoint http://localhost:50051
または手動で設定ファイルを編集:
{
"mcpServers": {
"laurus": {
"command": "laurus",
"args": ["mcp", "--endpoint", "http://localhost:50051"]
}
}
}
セクション
laurus-mcp をはじめる
前提条件
laurusCLI バイナリがインストール済み(cargo install laurus-cli)- 実行中の
laurus-serverインスタンス(laurus-server はじめにを参照) - MCP をサポートする AI クライアント(Claude Desktop、Claude Code など)
設定
ステップ 1: laurus-server を起動
laurus serve --grpc-port 50051
ステップ 2: MCP クライアントの設定
Claude Code
CLI コマンドで追加する方法(推奨):
claude mcp add laurus laurus mcp --endpoint http://localhost:50051
または ~/.claude/settings.json を直接編集:
{
"mcpServers": {
"laurus": {
"command": "laurus",
"args": ["mcp", "--endpoint", "http://localhost:50051"]
}
}
}
Claude Desktop
macOS の場合は ~/Library/Application Support/Claude/claude_desktop_config.json、
Windows の場合は %APPDATA%\Claude\claude_desktop_config.json を編集:
{
"mcpServers": {
"laurus": {
"command": "laurus",
"args": ["mcp", "--endpoint", "http://localhost:50051"]
}
}
}
使用ワークフロー
ワークフロー 1: 既存のインデックスを使用する
CLI でインデックスを事前に作成してから MCP サーバーで検索します:
# ステップ 1: スキーマファイルを作成
cat > schema.toml << 'EOF'
[fields.title]
Text = { indexed = true, stored = true }
[fields.body]
Text = { indexed = true, stored = true }
EOF
# ステップ 2: サーバーを起動してインデックスを作成
laurus serve --grpc-port 50051 &
laurus create index --schema schema.toml
# ステップ 3: MCP サーバーを起動(Claude が自動的に起動)
laurus mcp --endpoint http://localhost:50051
ワークフロー 2: AI 主導のインデックス作成
インデックスを事前に作成せず MCP サーバーを起動し、AI にインデックスを作成させます:
# laurus-server を起動(インデックス不要)
laurus serve --grpc-port 50051
次に Claude に依頼します:
「ブログ記事用の検索インデックスを作成してください。タイトルと本文テキストで検索できるようにして、著者と公開日も保存したいです。」
Claude は connect ツールを呼び出して接続し、スキーマを設計して create_index を自動的に呼び出します。
ワークフロー 3: 実行時に接続する
エンドポイントを指定せずに MCP サーバーを起動します:
laurus mcp
次に Claude に接続を依頼します:
「
http://localhost:50051の laurus サーバーに接続してください」
Claude は他のツールを使用する前に connect を呼び出します。
ライフサイクル
laurus-server 起動(別プロセス)
└─ gRPC ポート 50051 でリッスン
Claude 起動
└─ 起動: laurus mcp --endpoint http://localhost:50051
└─ stdio イベントループに入る
├─ stdin 経由でツール呼び出しを受信
├─ gRPC 経由で laurus-server にプロキシ
└─ stdout 経由で結果を送信
Claude 終了
└─ laurus-mcp プロセスが終了
└─ laurus-server は継続して動作
MCP ツールリファレンス
laurus MCP サーバーは以下のツールを公開しています。
connect
実行中の laurus-server gRPC エンドポイントに接続します。--endpoint フラグなしでサーバーを起動した場合や、実行時に別の laurus-server に切り替える場合に、他のツールを使用する前にこのツールを呼び出してください。
パラメーター
| 名前 | 型 | 必須 | 説明 |
|---|---|---|---|
endpoint | string | はい | gRPC エンドポイント URL(例: http://localhost:50051) |
例
Tool: connect
endpoint: "http://localhost:50051"
結果: Connected to laurus-server at http://localhost:50051.
create_index
指定されたスキーマで新しい検索インデックスを作成します。
パラメーター
| 名前 | 型 | 必須 | 説明 |
|---|---|---|---|
schema_json | string | はい | JSON 文字列としてのスキーマ定義 |
スキーマ JSON フォーマット
FieldOption は serde の externally-tagged 表現を使用します(バリアント名がキーになります):
{
"fields": {
"title": { "Text": { "indexed": true, "stored": true } },
"body": { "Text": {} },
"score": { "Float": {} },
"count": { "Integer": {} },
"active": { "Boolean": {} },
"created": { "DateTime": {} },
"embedding": { "Hnsw": { "dimension": 384 } }
}
}
例
Tool: create_index
schema_json: {"fields": {"title": {"Text": {}}, "body": {"Text": {}}}}
結果: Index created successfully at /path/to/index.
get_index
現在の検索インデックスの統計情報を取得します。
パラメーター
なし。
結果
{
"document_count": 42,
"vector_fields": ["embedding"]
}
add_document
インデックスにドキュメントを追加またはアップサートします。ドキュメントを追加した後は commit を呼び出してください。
パラメーター
| 名前 | 型 | 必須 | 説明 |
|---|---|---|---|
id | string | はい | 外部ドキュメント識別子 |
document | object | はい | JSON オブジェクトとしてのドキュメントフィールド |
mode | string | いいえ | "put"(デフォルト、アップサート)または "add"(チャンク追加) |
モード
put(デフォルト): 同じidを持つ既存のドキュメントを削除してから新しいものをインデックスします。add: 新しいチャンクとして追加します。複数のチャンクが同じidを持てます(大きなドキュメントの分割に便利)。
例
Tool: add_document
id: "doc-1"
document: {"title": "Hello World", "body": "これはテストドキュメントです。"}
結果: Document 'doc-1' added. Call commit to persist changes.
get_document
外部 ID でドキュメントを取得します。
パラメーター
| 名前 | 型 | 必須 | 説明 |
|---|---|---|---|
id | string | はい | 外部ドキュメント識別子 |
結果
{
"id": "doc-1",
"documents": [
{ "title": "Hello World", "body": "これはテストドキュメントです。" }
]
}
delete_document
外部 ID でドキュメントを削除します。削除後は commit を呼び出してください。
パラメーター
| 名前 | 型 | 必須 | 説明 |
|---|---|---|---|
id | string | はい | 外部ドキュメント識別子 |
結果: Document 'doc-1' deleted. Call commit to persist changes.
commit
保留中の変更をディスクにコミットします。変更を検索可能かつ永続的にするため、add_document または delete_document の後に必ず呼び出してください。
パラメーター
なし。
結果: Changes committed successfully.
add_field
インデックスにフィールドを追加します。
パラメーター
| 名前 | 型 | 必須 | 説明 |
|---|---|---|---|
name | string | はい | フィールド名 |
field_option_json | string | はい | JSON 形式のフィールド設定 |
例
{
"name": "category",
"field_option_json": "{\"Text\": {\"indexed\": true, \"stored\": true}}"
}
delete_field
インデックスからフィールドを削除します。既にインデックスされたデータは残りますが、削除されたフィールドにはアクセスできなくなります。
パラメーター
| 名前 | 型 | 必須 | 説明 |
|---|---|---|---|
name | string | はい | 削除するフィールド名 |
例
Tool: delete_field
name: "category"
結果: Field 'category' deleted.
search
laurus クエリ DSL を使用してドキュメントを検索します。
パラメーター
| 名前 | 型 | 必須 | 説明 |
|---|---|---|---|
query | string | はい | laurus クエリ DSL による検索クエリ |
limit | integer | いいえ | 最大結果数(デフォルト: 10) |
offset | integer | いいえ | ページネーション用スキップ数(デフォルト: 0) |
クエリ DSL の例
| クエリ | 説明 |
|---|---|
hello | デフォルトフィールド全体のターム検索 |
title:hello | フィールド指定のターム検索 |
title:hello AND body:world | ブール AND |
"exact phrase" | フレーズ検索 |
roam~2 | ファジー検索(編集距離 2) |
count:[1 TO 10] | 範囲検索 |
title:helo~1 | フィールド指定のファジー検索 |
結果
{
"total": 2,
"results": [
{
"id": "doc-1",
"score": 3.14,
"document": { "title": "Hello World", "body": "..." }
},
{
"id": "doc-2",
"score": 1.57,
"document": { "title": "Hello Again", "body": "..." }
}
]
}
典型的なワークフロー
1. connect → 実行中の laurus-server に接続
2. create_index → スキーマを定義(インデックスが存在しない場合)
3. add_field → フィールドを追加(必要に応じて)
4. add_document → ドキュメントをインデックス(必要に応じて繰り返し)
5. commit → 変更をディスクに永続化
6. search → インデックスを検索
7. add_document → ドキュメントを更新
8. delete_document → ドキュメントを削除
9. delete_field → 不要なフィールドを削除(必要に応じて)
10. commit → 変更を永続化
ビルドとテスト
前提条件
- Rust 1.85 以降(edition 2024)
- Cargo(Rust に付属)
- protobuf コンパイラ(
protoc)–laurus-serverのビルドに必要
ビルド
# すべてのクレートをビルド
cargo build
# 特定の Feature を指定してビルド
cargo build --features embeddings-candle
# リリースモードでビルド
cargo build --release
テスト
# すべてのテストを実行
cargo test
# 名前を指定して特定のテストを実行
cargo test <test_name>
# 特定のクレートのテストを実行
cargo test -p laurus
cargo test -p laurus-cli
cargo test -p laurus-server
Lint
# clippy を警告エラー扱いで実行
cargo clippy -- -D warnings
フォーマット
# フォーマットチェック
cargo fmt --check
# フォーマットを適用
cargo fmt
ドキュメント
API ドキュメント
# Rust API ドキュメントを生成して開く
cargo doc --no-deps --open
mdBook ドキュメント
# ドキュメントサイトをビルド
mdbook build docs
# ローカルプレビューサーバーを起動 (http://localhost:3000)
mdbook serve docs
# Markdown ファイルを Lint
markdownlint-cli2 "docs/src/**/*.md"
Feature Flags
laurus クレートはデフォルトでは Feature が無効の状態で提供されます。必要に応じて Embedding サポートを有効にしてください。
利用可能な Feature
| Feature | 説明 | 主な依存クレート |
|---|---|---|
embeddings-candle | Hugging Face Candle によるローカル BERT Embedding | candle-core, candle-nn, candle-transformers, hf-hub, tokenizers |
embeddings-openai | OpenAI API Embedding | reqwest |
embeddings-multimodal | CLIP マルチモーダル 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-cli と laurus-server はどちらも laurus ライブラリクレートに依存しています。
設計規約
- モジュールスタイル: ファイルベースのモジュール(Rust 2018 edition スタイル)、
mod.rsは使用しないsrc/tokenizer.rs+src/tokenizer/dictionary.rs- 不可:
src/tokenizer/mod.rs
- エラーハンドリング: ライブラリのエラー型には
thiserror、anyhowはバイナリクレートのみ unwrap()/expect()禁止: 本番コードでは使用不可(テストでは使用可)- 非同期: すべてのパブリック API は Tokio ランタイムで async/await を使用
- Unsafe: すべての
unsafeブロックに// SAFETY: ...コメントが必須 - ドキュメント: すべてのパブリックな型、関数、列挙型にドキュメントコメント(
///)が必須 - ライセンス: 依存クレートは MIT または Apache-2.0 互換であること