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_query(
laurus::lexical::search::searcher::LexicalSearchQuery::Obj(
Box::new(TermQuery::new("body", "rust"))
)
)
.limit(10)
.build();
let results = engine.search(request).await?;
for r in &results {
println!("{}: score={:.4}", r.id, r.score);
}
Ok(())
}
ライセンス
Laurus は MIT ライセンス のもとで提供されています。
アーキテクチャ
このページでは、Laurus の内部構造について説明します。アーキテクチャを理解することで、スキーマ設計、Analyzer の選択、検索戦略についてより適切な判断ができるようになります。
プロジェクト構成
Laurus は Cargo workspace として 5 つのクレートで構成されています。
graph TB
CLI["laurus-cli\n(Binary Crate)\nCLI + REPL"]
SRV["laurus-server\n(Library + Binary)\ngRPC Server + HTTP Gateway"]
MCP["laurus-mcp\n(Library + Binary)\nMCP Server"]
PY["laurus-python\n(cdylib)\nPython Bindings"]
LIB["laurus\n(Library Crate)\nCore Search Engine"]
CLI --> LIB
CLI --> SRV
CLI --> MCP
SRV --> LIB
MCP --> SRV
MCP --> LIB
PY --> LIB
| クレート | 種類 | 説明 |
|---|---|---|
| laurus | Library | コア検索エンジン – Lexical 検索、Vector 検索、ハイブリッド検索 |
| laurus-cli | Binary | インデックス管理と検索のためのコマンドラインインターフェース |
| laurus-server | Library + Binary | オプションの HTTP/JSON ゲートウェイ付き gRPC サーバー |
| laurus-mcp | Library + Binary | MCP(Model Context Protocol)サーバー |
| laurus-python | cdylib | PyO3 による Python バインディング |
各クレートの詳細については以下を参照してください。
全体概要
Laurus は単一の Engine を中心に構成されており、4 つの内部コンポーネントを統括します。
graph TB
subgraph Engine
SCH["Schema"]
LS["LexicalStore\n(Inverted Index)"]
VS["VectorStore\n(HNSW / Flat / IVF)"]
DL["DocumentLog\n(WAL + Document Storage)"]
end
Storage["Storage (trait)\nMemory / File / File+Mmap"]
LS --- Storage
VS --- Storage
DL --- Storage
| コンポーネント | 役割 |
|---|---|
| Schema | フィールドとその型を宣言し、各フィールドのルーティング先を決定する |
| LexicalStore | キーワード検索のための転置インデックス(Inverted Index)(BM25 スコアリング) |
| VectorStore | 類似度検索のためのベクトルインデックス(Flat、HNSW、または IVF) |
| DocumentLog | 耐久性のための WAL(Write-Ahead Log)+ ドキュメントストレージ |
3 つのストアはすべて単一の Storage バックエンドを共有し、キープレフィックス(lexical/、vector/、documents/)によって分離されています。
Engine のライフサイクル
Engine の構築
EngineBuilder が各パーツから Engine を組み立てます。
#![allow(unused)]
fn main() {
let engine = Engine::builder(storage, schema)
.analyzer(analyzer) // optional: for text fields
.embedder(embedder) // optional: for vector fields
.build()
.await?;
}
sequenceDiagram
participant User
participant EngineBuilder
participant Engine
User->>EngineBuilder: new(storage, schema)
User->>EngineBuilder: .analyzer(analyzer)
User->>EngineBuilder: .embedder(embedder)
User->>EngineBuilder: .build().await
EngineBuilder->>EngineBuilder: split_schema()
Note over EngineBuilder: Separate fields into\nLexicalIndexConfig\n+ VectorIndexConfig
EngineBuilder->>Engine: Create LexicalStore
EngineBuilder->>Engine: Create VectorStore
EngineBuilder->>Engine: Create DocumentLog
EngineBuilder->>Engine: Recover from WAL
EngineBuilder-->>User: Engine ready
build() 時に Engine は以下の処理を行います。
- スキーマの分割 — 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;
use laurus::lexical::TermQuery;
use laurus::lexical::search::searcher::LexicalSearchQuery;
// Search for "rust" in the "body" field
let request = SearchRequestBuilder::new()
.lexical_query(
LexicalSearchQuery::Obj(
Box::new(TermQuery::new("body", "rust"))
)
)
.limit(10)
.build();
let results = engine.search(request).await?;
for result in &results {
println!("ID: {}, Score: {:.4}", result.id, result.score);
if let Some(doc) = &result.document {
if let Some(title) = doc.get("title") {
println!(" Title: {:?}", title);
}
}
}
}
完全なサンプル
以下は、コピー・ペーストしてそのまま実行できる完全なプログラムです:
use std::sync::Arc;
use laurus::{
Document, Engine, Result, Schema, SearchRequestBuilder,
};
use laurus::lexical::{TextOption, TermQuery};
use laurus::lexical::search::searcher::LexicalSearchQuery;
use laurus::storage::memory::MemoryStorage;
#[tokio::main]
async fn main() -> Result<()> {
// 1. Storage
let storage = Arc::new(MemoryStorage::new(Default::default()));
// 2. Schema
let schema = Schema::builder()
.add_text_field("title", TextOption::default())
.add_text_field("body", TextOption::default())
.add_default_field("body")
.build();
// 3. Engine
let engine = Engine::builder(storage, schema).build().await?;
// 4. Index documents
for (id, title, body) in [
("doc-1", "Introduction to Rust", "Rust is a systems programming language focused on safety."),
("doc-2", "Python for Data Science", "Python is widely used in machine learning."),
("doc-3", "Web Development", "JavaScript powers interactive web applications."),
] {
let doc = Document::builder()
.add_text("title", title)
.add_text("body", body)
.build();
engine.add_document(id, doc).await?;
}
engine.commit().await?;
// 5. Search
let request = SearchRequestBuilder::new()
.lexical_query(
LexicalSearchQuery::Obj(
Box::new(TermQuery::new("body", "rust"))
)
)
.limit(10)
.build();
let results = engine.search(request).await?;
for r in &results {
println!("{}: score={:.4}", r.id, r.score);
}
Ok(())
}
次のステップ
- 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、LexicalSearchQuery
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。
geo3d_search
Earth-Centered Earth-Fixed (ECEF) 座標系を用いた 3D 地理検索のサンプルです。6 つのランドマーク(東京タワー、東京スカイツリー、富士山頂、自由の女神像、シドニー・オペラハウス、ISS サンプル点)を wgs84_to_ecef で変換してインデックスします。
cargo run --example geo3d_search
デモ内容: Geo3dDistanceQuery(球)、Geo3dBoundingBoxQuery(3D AABB)、Geo3dNearestQuery(k-NN)、および wgs84_to_ecef 変換ユーティリティ。バウンディングボックスクエリは ISS サンプルを意図的に除外し、高度が第三の軸として機能することを示します。
search_with_candle
Hugging Face Candle を使用した実際の BERT エンベディングによる Vector 検索です。初回実行時にモデルが自動的にダウンロードされます(約 80 MB)。
cargo run --example search_with_candle --features embeddings-candle
必要条件: embeddings-candle Feature Flag
デモ内容: 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 --> G3["Geo3d"]
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() | 緯度/経度のペア。半径検索とバウンディングボックスクエリをサポート |
| Geo3d | Geo3dOption | add_geo3d_field() | 3D ECEF 直交座標ポイント(x, y, z、メートル)。3D 距離検索・バウンディングボックス・k-NN クエリをサポート。詳細は 3D 地理検索 を参照 |
| 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) | 2D 地理座標フィールドを追加(WGS84) |
add_geo_ecef(name, x, y, z) | (f64, f64, f64) | 3D ECEF 直交座標ポイントを追加(メートル) |
add_bytes(name, data) | Vec<u8> | バイナリデータを追加 |
add_field(name, value) | DataValue | 任意の値型を追加 |
DataValue
DataValue は Laurus におけるフィールド値を表す統合列挙型です。
#![allow(unused)]
fn main() {
pub enum DataValue {
Null,
Bool(bool),
Int64(i64),
Float64(f64),
Text(String),
Bytes(Vec<u8>, Option<String>), // (data, optional MIME type)
Vector(Vec<f32>),
DateTime(DateTime<Utc>),
Geo(GeoPoint), // 2D WGS84 ポイント (latitude, longitude)
GeoEcef(GeoEcefPoint), // 3D ECEF 直交座標ポイント (x, y, z)、メートル
Int64Array(Vec<i64>), // 多値整数フィールド
Float64Array(Vec<f64>), // 多値浮動小数点フィールド
}
}
DataValue は一般的な型に対して From<T> を実装しているため、.into() 変換が使用できます。
#![allow(unused)]
fn main() {
use laurus::DataValue;
let v: DataValue = "hello".into(); // Text
let v: DataValue = 42i64.into(); // Int64
let v: DataValue = 3.14f64.into(); // Float64
let v: DataValue = true.into(); // Bool
let v: DataValue = vec![0.1f32, 0.2].into(); // Vector
}
予約フィールド
アンダースコア(_)で始まるフィールド名はすべてエンジンの予約領域です。
ユーザーコードからそのような名前でフィールドを定義することはできず、
_ で始まるキーを含むドキュメントは投入時にエラーとなります。
唯一許可される _ プレフィックス名は、次に説明する _id システムフィールドです。
_id — 外部ドキュメント ID
put_document / add_document に渡された外部ドキュメント ID を格納します。
KeywordAnalyzer(完全一致)でインデクシングされ、自動的に挿入されるため
スキーマに追加する必要はありません。
動的スキーマ
Laurus はスキーマに宣言されていないフィールドを含むドキュメントも受け付けます。
挙動は Schema に設定する DynamicFieldPolicy で制御します:
| ポリシー | 未宣言フィールドに対する挙動 |
|---|---|
Strict | わかりやすいエラーメッセージで投入を拒否する |
Dynamic(デフォルト) | 値から型を推論してスキーマへ自動追加する |
Ignore | 未宣言フィールドを静かに破棄し、他のフィールドはインデックスする |
Builder でポリシーを設定します:
#![allow(unused)]
fn main() {
use laurus::{DynamicFieldPolicy, Schema};
let schema = Schema::builder()
.dynamic_field_policy(DynamicFieldPolicy::Dynamic)
.build();
}
型推論ルール(Dynamic ポリシー)
| 投入される値 | 推論されるフィールド型 |
|---|---|
string | Text(転置インデックス、BM25) |
integer | Integer(BKD tree) |
float | Float(BKD tree) |
bool | Boolean |
整数の配列(例: [1, 2, 3]) | Integer(multi_valued = true) |
浮動小数点を含む数値配列(例: [1.5, 2.0, 3]) | Float(multi_valued = true) |
緯度キー(lat または latitude)と経度キー(lon、lng、longitude のいずれか)を持ち、値が範囲内の object | Geo |
数値の x、y、z の 3 キーをすべて持つ object(有限値、ECEF メートル単位) | Geo3d |
ベクトルフィールド(Hnsw / Flat / Ivf)と Bytes は 自動推論の
対象外です。スキーマへ明示的に宣言してください。なお、同一 object 内で
2D 用キー(lat / lon)と 3D 用キー(x / y / z)を混在させた
場合は曖昧と判定してエラーとなります。いずれか一方の形式のみ使用して
ください。
多値数値フィールド
Integer と Float フィールドは multi_valued = true を指定することで、
1 ドキュメントに複数の値を保持できます。範囲クエリはいずれかの値が条件を満たせばマッチ
する Lucene 流の挙動で、スコアは constant(マッチ件数による加点なし)です。
多値フィールドに単一値を送った場合は要素 1 個の配列に自動ラップされます。 逆に単一値フィールドに配列を送ると、暗黙の切り捨てではなくエラーになります。
型衝突
既に宣言されているフィールドに別の型の値が到着した場合、Laurus は 宣言された型への変換を試みます。変換ルールは以下の通りです:
| 宣言型 | 受け取った値 | 結果 |
|---|---|---|
Integer | Int64 | そのまま格納 |
Integer | Float64(3.14) | 3 へ切り捨て(情報損失あり — 下の警告を参照) |
Integer | Text("42") | 42 としてパース |
Integer | Text("abc") | エラー |
Float | Int64 | f64 に拡張 |
Float | Text("3.14") | パース |
Boolean | Int64(0) / Int64(1) | false / true |
Boolean | Text("true"/"false") | 大文字小文字を無視してパース |
Text | 任意のスカラー値 | 文字列化 |
Geo / Geo3d / Bytes / ベクトル | 対応 variant 以外 | エラー |
変換エラーの扱いはポリシーに依存します:
Strict: ただちにエラーを返すDynamic: エラーを返す(安全とみなせる変換はこの層ですべて試し切っている)Ignore: 該当フィールドのみ破棄し、他のフィールドはインデックスする
⚠️ 警告: 静かな情報損失が発生しうる
いくつかの変換は、エラーを返さずに情報を失います:
Integerフィールドは受け取ったFloat値を切り捨てます (3.14→3、-3.9→-3)。投入は成功しますFloatフィールドはf64仮数部に収まらない巨大な整数で精度を失う可能性がありますTextフィールドはスカラーを文字列化して受け入れます(元の型情報は消えます)Ignoreは非互換なフィールドを静かに捨てますデータの正確性を優先したい場合は、
DynamicFieldPolicy::Strictを使う(あるいは必要なフィールドをすべて事前に宣言する)ことを推奨します。Dynamicポリシーは「ドキュメントを投入できる」ことを「入力データを 1 ビットも失わない」ことより優先します。
Query DSL と未宣言フィールド
スキーマが確定した後、クエリパーサは field:value 句で参照されるフィールドが
すべてスキーマに存在することを検証します。titl:hello(title:hello の打ち間違い)
のような typo は、結果が無言で空になるのではなく、明確なパースエラーとして返ります。
動的フィールド管理
稼働中のエンジンに対して、フィールドの追加および削除を動的に行えます。 フィールドの型変更はサポートされていません。
フィールドの追加
Engine::add_field() を使用すると、稼働中のエンジンにフィールドを動的に追加できます。
Lexical フィールドの追加
let updated_schema = engine.add_field(
"category",
FieldOption::Text(TextOption::default()),
).await?;
Vector フィールドの追加
let updated_schema = engine.add_field(
"embedding",
FieldOption::Flat(FlatOption::default().dimension(384)),
).await?;
既存のドキュメントには影響がありません(新しいフィールドの値が存在しないだけです)。
フィールドの削除
Engine::delete_field() を使用すると、稼働中のエンジンからフィールドを動的に削除できます。
let updated_schema = engine.delete_field("category").await?;
フィールド削除時の動作は以下の通りです。
- スキーマからフィールド定義が削除されます。
default_fieldsに含まれている場合、そこからも削除されます。- フィールドに紐づくアナライザーおよびエンベッダーの登録が解除されます。
- 既にインデックスされたデータは物理的に残りますが、スキーマから削除されたフィールドにはアクセスできなくなります。
共通の注意事項
返却された Schema は呼び出し側で永続化する必要があります(例: schema.toml への書き出し)。
スキーマ設計のヒント
-
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(日本語ストップワード)
JapaneseAnalyzer::new は LinderaTokenizer::new と同じ引数(segmentation mode、Lindera 辞書ディレクトリのパス、任意のユーザー辞書パス)を受け取ります。laurus は Lindera の embed-* features をデフォルトで有効化しないため、IPADIC 等の辞書を実ファイルパスとして必ず指定する必要があります。
#![allow(unused)]
fn main() {
use laurus::analysis::analyzer::language::japanese::JapaneseAnalyzer;
// Lindera 辞書を展開済みのパスを指定する。
let analyzer = JapaneseAnalyzer::new(
"normal",
"/var/lib/lindera/ipadic",
None,
)?;
// "東京都に住んでいる" → ["東京", "都", "住ん", "いる"]
}
Schema 経由で参照する場合は構造化された AnalyzerSpec 形式でパラメータを渡します(後述の PerFieldAnalyzer を参照)。
KeywordAnalyzer
入力全体を単一のトークンとして扱います。トークン化や正規化は行いません。
#![allow(unused)]
fn main() {
use laurus::analysis::analyzer::keyword::KeywordAnalyzer;
let analyzer = KeywordAnalyzer::new();
// "Hello World" → ["Hello World"]
}
完全一致が必要なフィールド(カテゴリ、タグ、ステータスコード)に使用してください。
SimpleAnalyzer
フィルタリングなしでテキストをトークン化します。元の大文字小文字とすべてのトークンが保持されます。解析パイプラインを完全に制御したい場合や、Tokenizer を単独でテストしたい場合に便利です。
パイプライン: ユーザー指定の Tokenizer のみ(Char Filter なし、Token Filter なし)
#![allow(unused)]
fn main() {
use laurus::analysis::analyzer::simple::SimpleAnalyzer;
use laurus::analysis::tokenizer::regex::RegexTokenizer;
use std::sync::Arc;
let tokenizer = Arc::new(RegexTokenizer::new()?);
let analyzer = SimpleAnalyzer::new(tokenizer);
// "Hello World" → ["Hello", "World"]
// (no lowercasing, no stop word removal)
}
Tokenizer のテストや、別のステップで手動で Token Filter を適用したい場合に使用してください。
EnglishAnalyzer
英語に特化した Analyzer です。トークン化、小文字化、一般的な英語ストップワードの除去を行います。
パイプライン: RegexTokenizer(Unicode 単語境界) → 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で解析されます。
Schema からの per-field analyzer 設定
実装で直接 PerFieldAnalyzer を組み立てる代わりに、スキーマ宣言で analyzer を割り当てる場合がほとんどです。テキストフィールドの analyzer 設定は次の 2 つの形式を受け付けます。
// 1. パラメータ不要の組込 analyzer、または schema.analyzers に登録した名前。
{ "analyzer": "standard" }
{ "analyzer": "english" }
{ "analyzer": "my_custom_pipeline" }
// 2. パラメータ付きの組込プリセット。現状は Japanese プリセットのみで、Lindera 辞書のパスが必須。
{
"analyzer": {
"language": "japanese",
"mode": "normal",
"dict": "/var/lib/lindera/ipadic"
}
}
文字列単独の "japanese" は辞書パスを伴わないためエラーとなります。既存スキーマで "analyzer": "japanese" を保存していた場合は、上記の構造化形式に移行してください。
プリセットに収まらないパイプラインを使いたい場合は、schema.analyzers に AnalyzerDefinition として登録し、フィールドからは名前で参照します。
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)
地理フィールドには 2 種類あり、いずれも同じ多次元 BKD-Tree プリミティブにバックアップされています:
| フィールド型 | 次元数 | 座標 | サポートされるクエリ |
|---|---|---|---|
Geo | 2 | WGS84 緯度・経度(度) | 半径検索、バウンディングボックス |
Geo3d | 3 | ECEF 直交座標 (x, y, z)(メートル) | 3D 距離検索(球)、3D バウンディングボックス、k-NN |
Geo3d は高度が一級の次元になる用途(ドローン・衛星・屋内 3D 測位など、
2D の Geo フィールドでは情報が失われたり極で歪んだりするケース)で
適しています。座標系・WGS84 変換ヘルパー・DSL 構文については
3D 地理検索 (ECEF) を参照してください。
セグメント(Segments)
Lexical インデックスはセグメントに分割されて構成されます。各セグメントはイミュータブルで自己完結型のミニインデックスです。
graph TB
LI["Lexical Index"]
LI --> S1["Segment 0"]
LI --> S2["Segment 1"]
LI --> S3["Segment 2"]
S1 --- F1[".dict (terms)"]
S1 --- F2[".post (postings)"]
S1 --- F3[".bkd (numerics)"]
S1 --- F4[".docs (doc store)"]
S1 --- F5[".dv (doc values)"]
S1 --- F6[".meta (metadata)"]
S1 --- F7[".lens (field lengths)"]
| ファイル拡張子 | 内容 |
|---|---|
.dict | Term Dictionary(ソート済みターム + メタデータオフセット) |
.post | Posting Lists(ドキュメント ID、ターム頻度、位置情報) |
.bkd | 数値・日付・Geo(2D)・Geo3d(3D ECEF)フィールドの 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;
use laurus::lexical::TermQuery;
use laurus::lexical::search::searcher::LexicalSearchQuery;
let request = SearchRequestBuilder::new()
.lexical_query(
LexicalSearchQuery::Obj(
Box::new(TermQuery::new("body", "rust"))
)
)
.limit(10)
.build();
let results = engine.search(request).await?;
}
クエリタイプ
TermQuery
特定のフィールドに完全一致するタームを含むドキュメントをマッチングします。
#![allow(unused)]
fn main() {
use laurus::lexical::TermQuery;
// Find documents where "body" contains the term "rust"
let query = TermQuery::new("body", "rust");
}
注意: タームは解析後にマッチングされます。フィールドが
StandardAnalyzerを使用している場合、インデキシングされたテキストとクエリタームの両方が小文字化されるため、TermQuery::new("body", "rust")は元テキスト内の “Rust” にもマッチします。
PhraseQuery
正確なタームの並びを含むドキュメントをマッチングします。
#![allow(unused)]
fn main() {
use laurus::lexical::query::phrase::PhraseQuery;
// Find documents containing the exact phrase "machine learning"
let query = PhraseQuery::new("body", vec!["machine".to_string(), "learning".to_string()]);
// Or use the convenience method from a phrase string:
let query = PhraseQuery::from_phrase("body", "machine learning");
}
フレーズクエリは、ターム位置情報が格納されている必要があります(TextOption のデフォルト設定)。
BooleanQuery
複数のクエリをブーリアン論理で結合します。
#![allow(unused)]
fn main() {
use laurus::lexical::query::boolean::{BooleanQuery, BooleanQueryBuilder, Occur};
let query = BooleanQueryBuilder::new()
.must(Box::new(TermQuery::new("body", "rust"))) // AND
.must(Box::new(TermQuery::new("body", "programming"))) // AND
.must_not(Box::new(TermQuery::new("body", "python"))) // NOT
.build();
}
| Occur | 意味 | DSL での表現 |
|---|---|---|
Must | ドキュメントが必ずマッチしなければならない | +term または AND |
Should | ドキュメントがマッチすべき(スコアをブースト) | term または OR |
MustNot | ドキュメントがマッチしてはならない | -term または NOT |
Filter | 必ずマッチする必要があるが、スコアには影響しない | (DSL に相当するものなし) |
FuzzyQuery
指定された編集距離(レーベンシュタイン距離)内のタームをマッチングします。
#![allow(unused)]
fn main() {
use laurus::lexical::query::fuzzy::FuzzyQuery;
// Find documents matching "programing" within edit distance 2
// This will match "programming", "programing", etc.
let query = FuzzyQuery::new("body", "programing"); // default max_edits = 2
}
WildcardQuery
ワイルドカードパターンを使用してタームをマッチングします。
#![allow(unused)]
fn main() {
use laurus::lexical::query::wildcard::WildcardQuery;
// '?' matches exactly one character, '*' matches zero or more
let query = WildcardQuery::new("filename", "*.pdf")?;
let query = WildcardQuery::new("body", "pro*")?;
let query = WildcardQuery::new("body", "col?r")?; // matches "color" and "colour"
}
PrefixQuery
特定のプレフィックスで始まるタームを含むドキュメントをマッチングします。
#![allow(unused)]
fn main() {
use laurus::lexical::query::prefix::PrefixQuery;
// Find documents where "body" contains terms starting with "pro"
// This matches "programming", "program", "production", etc.
let query = PrefixQuery::new("body", "pro");
}
RegexpQuery
正規表現パターンにマッチするタームを含むドキュメントをマッチングします。
#![allow(unused)]
fn main() {
use laurus::lexical::query::regexp::RegexpQuery;
// Find documents where "body" contains terms matching the regex
let query = RegexpQuery::new("body", "^pro.*ing$")?;
// Match version-like patterns
let query = RegexpQuery::new("version", r"^v\d+\.\d+")?;
}
注意:
RegexpQuery::new()はResultを返します。正規表現パターンは構築時にバリデーションされ、無効なパターンの場合はエラーが返されます。
NumericRangeQuery
数値フィールドの値が指定された範囲内にあるドキュメントをマッチングします。
#![allow(unused)]
fn main() {
use laurus::lexical::NumericRangeQuery;
use laurus::lexical::core::field::NumericType;
// Find documents where "price" is between 10.0 and 100.0 (inclusive)
let query = NumericRangeQuery::new(
"price",
NumericType::Float,
Some(10.0), // min
Some(100.0), // max
true, // include min
true, // include max
);
// Open-ended range: price >= 50
let query = NumericRangeQuery::new(
"price",
NumericType::Float,
Some(50.0),
None, // no upper bound
true,
false,
);
}
GeoQuery
2D 地理座標(WGS84 緯度・経度)に基づいてドキュメントをマッチングします。
#![allow(unused)]
fn main() {
use laurus::lexical::query::geo::GeoQuery;
// Find documents within 10 km (= 10 000 m) of Tokyo Station (35.6812, 139.7671)
let query = GeoQuery::within_radius("location", 35.6812, 139.7671, 10_000.0)?; // distance in metres
// Find documents within a bounding box (min_lat, min_lon, max_lat, max_lon)
let query = GeoQuery::within_bounding_box(
"location",
35.0, 139.0, // min (lat, lon)
36.0, 140.0, // max (lat, lon)
)?;
}
Geo3dDistanceQuery / Geo3dBoundingBoxQuery / Geo3dNearestQuery
3 種類のクエリが、ECEF 直交座標(メートル)にバックアップされた 3D の
Geo3d フィールドを対象とします。高度が意味を持つ用途や、2D の Geo
フィールドでは極特異点が問題になるケースで使ってください。座標系・WGS84
変換ヘルパー・実例については 3D 地理検索 を参照してください。
#![allow(unused)]
fn main() {
use laurus::GeoEcefPoint;
use laurus::lexical::query::geo3d::{
Geo3dDistanceQuery, Geo3dBoundingBoxQuery, Geo3dNearestQuery,
};
let centre = GeoEcefPoint::new(-3_955_182.0, 3_350_553.0, 3_700_276.0);
// 球: `centre` から 5 km 以内のドキュメント
let q = Geo3dDistanceQuery::new("position", centre, 5_000.0);
// 軸並行 3D バウンディングボックス(コンストラクタが軸ごとに min ≤ max を検証)
let min = GeoEcefPoint::new(-4_000_000.0, 3_300_000.0, 3_650_000.0);
let max = GeoEcefPoint::new(-3_900_000.0, 3_400_000.0, 3_750_000.0);
let q = Geo3dBoundingBoxQuery::new("position", min, max)?;
// k-NN: 最も近い 10 件、半径スケジュールをカスタマイズ
let q = Geo3dNearestQuery::new("position", centre, 10)
.with_initial_radius(500.0)
.with_max_radius(1_000_000.0);
}
| クエリ | スコア |
|---|---|
Geo3dDistanceQuery | 1 - distance / radius を [0, 1] にクランプ |
Geo3dBoundingBoxQuery | マッチした全ドキュメントで定数 1.0 |
Geo3dNearestQuery | 最も近いヒットが 1.0、返却された集合内で最も遠いヒットが 0.0 となるよう正規化 |
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::SearchRequestBuilder;
use laurus::lexical::search::searcher::LexicalSearchQuery;
let request = SearchRequestBuilder::new()
.lexical_query(LexicalSearchQuery::Obj(Box::new(query)))
.add_field_boost("title", 2.0) // title matches count double
.add_field_boost("body", 1.0)
.build();
}
Lexical 検索オプション(SearchRequestBuilder 経由)
Lexical 検索の動作パラメータは SearchRequestBuilder のメソッドで設定します。これらは SearchRequest の lexical_options フィールドに格納されます。
| オプション | デフォルト | 説明 |
|---|---|---|
field_boosts | 空 | フィールドごとのスコア倍率 |
min_score | 0.0 | 最小スコア閾値 |
timeout_ms | None | 検索タイムアウト(ミリ秒) |
parallel | false | セグメント間の並列検索を有効にする |
sort_by | Score | 関連性スコアでソート、またはフィールドでソート(asc / desc) |
ビルダーメソッド
SearchRequestBuilder を使用してクエリとオプションを設定します。
#![allow(unused)]
fn main() {
use laurus::SearchRequestBuilder;
use laurus::lexical::TermQuery;
use laurus::lexical::search::searcher::{LexicalSearchQuery, SortField};
let request = SearchRequestBuilder::new()
.lexical_query(LexicalSearchQuery::Obj(Box::new(TermQuery::new("body", "rust"))))
.limit(20)
.lexical_min_score(0.5)
.lexical_timeout_ms(5000)
.lexical_parallel(true)
.sort_by(SortField::FieldDesc("date".to_string()))
.add_field_boost("title", 2.0)
.add_field_boost("body", 1.0)
.build();
}
Query DSL の使用
プログラマティックにクエリを構築する代わりに、テキストベースの Query DSL を使用できます。
#![allow(unused)]
fn main() {
use laurus::lexical::QueryParser;
use laurus::analysis::analyzer::standard::StandardAnalyzer;
use std::sync::Arc;
let analyzer = Arc::new(StandardAnalyzer::default());
let parser = QueryParser::new(analyzer).with_default_field("body");
// Simple term
let query = parser.parse("rust")?;
// Boolean
let query = parser.parse("rust AND programming")?;
// Phrase
let query = parser.parse("\"machine learning\"")?;
// Field-specific
let query = parser.parse("title:rust AND body:programming")?;
// Fuzzy
let query = parser.parse("programing~2")?;
// Range
let query = parser.parse("year:[2020 TO 2024]")?;
}
完全な構文リファレンスは Query DSL を参照してください。
次のステップ
Vector 検索
Vector 検索は、意味的類似性によってドキュメントを検索します。キーワードのマッチングではなく、ベクトル空間におけるクエリの意味とドキュメントエンベディングを比較します。
基本的な使い方
Builder API
#![allow(unused)]
fn main() {
use laurus::SearchRequestBuilder;
use laurus::vector::VectorSearchRequestBuilder;
let request = SearchRequestBuilder::new()
.vector_query(
VectorSearchRequestBuilder::new()
.add_text("embedding", "systems programming language")
.limit(10)
.build()
)
.build();
let results = engine.search(request).await?;
}
add_text() メソッドはテキストをクエリペイロードとして格納します。検索時に、エンジンが設定されたエンベッダーを使用してテキストをエンベディングし、ベクトルインデックスを検索します。
Query DSL
#![allow(unused)]
fn main() {
use laurus::vector::VectorQueryParser;
let parser = VectorQueryParser::new(embedder.clone())
.with_default_field("embedding");
let request = parser.parse(r#"embedding:"systems programming""#).await?;
}
VectorSearchRequestBuilder
Builder API により、きめ細かな制御が可能です。
#![allow(unused)]
fn main() {
use laurus::vector::VectorSearchRequestBuilder;
use laurus::vector::store::request::QueryVector;
let request = VectorSearchRequestBuilder::new()
// Text query (will be embedded at search time)
.add_text("text_vec", "machine learning")
// Or use a pre-computed vector directly
.add_vector("embedding", vec![0.1, 0.2, 0.3, /* ... */])
// Search parameters
.limit(20)
.build();
}
メソッド
| メソッド | 説明 |
|---|---|
add_text(field, text) | 特定のフィールドに対するテキストクエリを追加(検索時にエンベディング) |
add_vector(field, vector) | 特定のフィールドに対する事前計算済みクエリベクトルを追加 |
add_vector_with_weight(field, vector, weight) | 明示的なウェイトを持つ事前計算済みベクトルを追加 |
add_payload(field, payload) | エンベディング対象の汎用 DataValue ペイロードを追加 |
add_bytes(field, bytes, mime) | バイナリペイロードを追加(例: マルチモーダル用の画像バイト) |
field(name) | 検索を特定のフィールドに制限 |
fields(names) | 検索を複数のフィールドに制限 |
limit(n) | 結果の最大件数(デフォルト: 10) |
score_mode(VectorScoreMode) | スコア結合モード(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;
use laurus::lexical::TermQuery;
use laurus::vector::VectorSearchRequestBuilder;
// Vector search with a category filter
let request = SearchRequestBuilder::new()
.vector_query(
VectorSearchRequestBuilder::new()
.add_text("embedding", "machine learning")
.build()
)
.filter_query(Box::new(TermQuery::new("category", "tutorial")))
.limit(10)
.build();
let results = engine.search(request).await?;
}
フィルタークエリはまず Lexical インデックス上で実行されて許可されるドキュメント ID のセットを特定し、その後 Vector 検索がそれらの ID に制限されます。
数値範囲によるフィルター
#![allow(unused)]
fn main() {
use laurus::lexical::NumericRangeQuery;
use laurus::lexical::core::field::NumericType;
let request = SearchRequestBuilder::new()
.vector_query(
VectorSearchRequestBuilder::new()
.add_text("embedding", "type systems")
.build()
)
.filter_query(Box::new(NumericRangeQuery::new(
"year", NumericType::Integer,
Some(2020.0), Some(2024.0), true, true
)))
.limit(10)
.build();
}
距離メトリクス(Distance Metrics)
距離メトリクスはスキーマでフィールドごとに設定されます(Vector インデキシング を参照)。
| メトリクス | 説明 | 小さい値 = より類似 |
|---|---|---|
| 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_query(
VectorSearchRequestBuilder::new()
.add_text("text_vec", "systems language")
.build()
)
.limit(5)
.build()
).await?;
for r in &results {
println!("{}: score={:.4}", r.id, r.score);
}
Ok(())
}
次のステップ
ハイブリッド検索(Hybrid Search)
ハイブリッド検索は、Lexical 検索(キーワードマッチング)と Vector 検索(意味的類似性)を組み合わせることで、精度と意味的な関連性の両方を兼ね備えた結果を提供します。これは Laurus の最も強力な検索モードです。
なぜハイブリッド検索なのか
| 検索タイプ | 強み | 弱み |
|---|---|---|
| Lexical のみ | 正確なキーワードマッチング、希少なタームに強い | 同義語や言い換えを見逃す |
| Vector のみ | 意味を理解し、同義語に対応 | 正確なキーワードを見逃す場合がある、精度が低い |
| ハイブリッド | 両方の長所を活用 | 設定がやや複雑 |
仕組み
sequenceDiagram
participant User
participant Engine
participant Lexical as LexicalStore
participant Vector as VectorStore
participant Fusion
User->>Engine: SearchRequest\n(lexical + vector)
par Execute in parallel
Engine->>Lexical: BM25 keyword search
Lexical-->>Engine: Ranked hits (by relevance)
and
Engine->>Vector: ANN similarity search
Vector-->>Engine: Ranked hits (by distance)
end
Engine->>Fusion: Merge two result sets
Note over Fusion: RRF or WeightedSum
Fusion-->>Engine: Unified ranked list
Engine-->>User: Vec of SearchResult
基本的な使い方
Builder API
#![allow(unused)]
fn main() {
use laurus::{SearchRequestBuilder, FusionAlgorithm};
use laurus::lexical::TermQuery;
use laurus::lexical::search::searcher::LexicalSearchQuery;
use laurus::vector::VectorSearchRequestBuilder;
let request = SearchRequestBuilder::new()
// Lexical component
.lexical_query(
LexicalSearchQuery::Obj(
Box::new(TermQuery::new("body", "rust"))
)
)
// Vector component
.vector_query(
VectorSearchRequestBuilder::new()
.add_text("text_vec", "systems programming")
.build()
)
// Fusion algorithm
.fusion_algorithm(FusionAlgorithm::RRF { k: 60.0 })
.limit(10)
.build();
let results = engine.search(request).await?;
}
Query DSL
単一のクエリ文字列内で Lexical 句と Vector 句を混在させることができます。
#![allow(unused)]
fn main() {
use laurus::UnifiedQueryParser;
use laurus::lexical::QueryParser;
use laurus::vector::VectorQueryParser;
let unified = UnifiedQueryParser::new(
QueryParser::new(analyzer).with_default_field("body"),
VectorQueryParser::new(embedder),
);
// Lexical + vector in one query
let request = unified.parse(r#"body:rust text_vec:"systems programming""#).await?;
let results = engine.search(request).await?;
}
Vector 句の識別はスキーマのフィールド型に基づいて行われます。ベクトルフィールドとして定義されたフィールド名を持つ句は Vector クエリとして、それ以外は Lexical クエリとして解析されます。
フュージョンアルゴリズム(Fusion Algorithms)
Lexical と Vector の両方の結果が存在する場合、それらを単一のランキングリストにマージする必要があります。Laurus は 2 つのフュージョンアルゴリズムをサポートしています。
RRF(Reciprocal Rank Fusion)
デフォルトのアルゴリズムです。生のスコアではなく、ランク位置に基づいて結果を結合します。
score(doc) = sum( 1 / (k + rank_i) )
rank_i は各結果リストにおけるドキュメントの位置、k はスムージングパラメータ(デフォルト 60)です。
#![allow(unused)]
fn main() {
use laurus::FusionAlgorithm;
let fusion = FusionAlgorithm::RRF { k: 60.0 };
}
利点:
- Lexical と Vector の結果間のスコア分布の違いに対してロバスト
- ウェイトのチューニングが不要
- すぐに使える(out of the box)
WeightedSum
正規化された Lexical スコアと Vector スコアを線形結合します。
score(doc) = lexical_weight * lexical_score + vector_weight * vector_score
#![allow(unused)]
fn main() {
use laurus::FusionAlgorithm;
let fusion = FusionAlgorithm::WeightedSum {
lexical_weight: 0.3,
vector_weight: 0.7,
};
}
使用場面:
- Lexical と Vector の関連性のバランスを明示的に制御したい場合
- 一方のシグナルが他方よりも重要であることがわかっている場合
SearchRequest のフィールド
| フィールド | 型 | デフォルト | 説明 |
|---|---|---|---|
query | SearchQuery | Dsl("") | 検索クエリ仕様(Dsl / Lexical / Vector / Hybrid) |
limit | usize | 10 | 返される結果の最大件数 |
offset | usize | 0 | スキップする結果の数(ページネーション用) |
fusion_algorithm | Option<FusionAlgorithm> | None(ハイブリッド時は RRF { k: 60.0 } を使用) | Lexical と Vector の結果をマージする方法 |
filter_query | Option<Box<dyn Query>> | None | Lexical クエリによるプレフィルター(Lexical と Vector の両方の結果を制限) |
lexical_options | LexicalSearchOptions | デフォルト | Lexical 検索の動作パラメータ |
vector_options | VectorSearchOptions | デフォルト | Vector 検索の動作パラメータ |
SearchResult
各結果には以下が含まれます。
| フィールド | 型 | 説明 |
|---|---|---|
id | String | 外部ドキュメント ID |
score | f32 | フュージョン後の関連性スコア |
document | Option<Document> | ドキュメントの全内容(ロードされた場合) |
フィルター付きハイブリッド検索
フィルターを適用して、Lexical と Vector の両方の結果を制限できます。
#![allow(unused)]
fn main() {
let request = SearchRequestBuilder::new()
.lexical_query(
LexicalSearchQuery::Obj(Box::new(TermQuery::new("body", "rust")))
)
.vector_query(
VectorSearchRequestBuilder::new()
.add_text("text_vec", "systems programming")
.build()
)
// Only search within "tutorial" category
.filter_query(Box::new(TermQuery::new("category", "tutorial")))
.fusion_algorithm(FusionAlgorithm::RRF { k: 60.0 })
.limit(10)
.build();
}
フィルタリングの仕組み
- フィルタークエリが Lexical インデックス上で実行され、許可されるドキュメント ID のセットが生成される
- Lexical 検索: フィルターがユーザークエリとブーリアン AND で結合される
- Vector 検索: 許可された ID が ANN 検索の制限として渡される
ページネーション
offset と limit を使用してページネーションを実現します。
#![allow(unused)]
fn main() {
// Page 1: results 0-9
let page1 = SearchRequestBuilder::new()
.lexical_query(/* ... */)
.vector_query(/* ... */)
.offset(0)
.limit(10)
.build();
// Page 2: results 10-19
let page2 = SearchRequestBuilder::new()
.lexical_query(/* ... */)
.vector_query(/* ... */)
.offset(10)
.limit(10)
.build();
}
完全な例
use std::sync::Arc;
use laurus::{
Document, Engine, Schema, SearchRequestBuilder,
FusionAlgorithm, PerFieldEmbedder,
};
use laurus::lexical::{TextOption, TermQuery};
use laurus::lexical::core::field::IntegerOption;
use laurus::lexical::search::searcher::LexicalSearchQuery;
use laurus::vector::{HnswOption, VectorSearchRequestBuilder};
use laurus::storage::memory::MemoryStorage;
#[tokio::main]
async fn main() -> laurus::Result<()> {
let storage = Arc::new(MemoryStorage::new(Default::default()));
// Schema with both lexical and vector fields
let schema = Schema::builder()
.add_text_field("title", TextOption::default())
.add_text_field("body", TextOption::default())
.add_text_field("category", TextOption::default())
.add_integer_field("year", IntegerOption::default())
.add_hnsw_field("body_vec", HnswOption {
dimension: 384,
..Default::default()
})
.build();
// Configure analyzer and embedder (see Text Analysis and Embeddings docs)
// let analyzer = Arc::new(StandardAnalyzer::new()?);
// let embedder = Arc::new(CandleBertEmbedder::new("sentence-transformers/all-MiniLM-L6-v2")?);
let engine = Engine::builder(storage, schema)
// .analyzer(analyzer)
// .embedder(embedder)
.build()
.await?;
// Index documents with both text and vector fields
engine.add_document("doc-1", Document::builder()
.add_text("title", "Rust Programming Guide")
.add_text("body", "Rust is a systems programming language.")
.add_text("category", "programming")
.add_integer("year", 2024)
.add_text("body_vec", "Rust is a systems programming language.")
.build()
).await?;
engine.commit().await?;
// Hybrid search: keyword "rust" + semantic "systems language"
let results = engine.search(
SearchRequestBuilder::new()
.lexical_query(
LexicalSearchQuery::Obj(Box::new(TermQuery::new("body", "rust")))
)
.vector_query(
VectorSearchRequestBuilder::new()
.add_text("body_vec", "systems language")
.build()
)
.fusion_algorithm(FusionAlgorithm::RRF { k: 60.0 })
.limit(10)
.build()
).await?;
for r in &results {
println!("{}: score={:.4}", r.id, r.score);
}
Ok(())
}
次のステップ
- クエリ構文の完全なリファレンス: Query DSL
- 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 句の区別は、フィールド名に基づいて行われます。スキーマ上でベクトルフィールドとして定義されたフィールド名が指定された場合、その句は Vector クエリとして扱われます。
フィールド検証
クエリ中の field:value 句は、パース時にスキーマと照合されます。スキーマに宣言されていないフィールドを参照したクエリは、結果が黙って空になるのではなく、エラーとして返されます。これにより typo(例: title:hello のつもりで titl:hello)を早期に検出できます。
未知のフィールドを含むドキュメントを受け付けたい場合は、スキーマの dynamic_field_policy を設定して、投入時にフィールドを追加する動作を有効にしてください。一度フィールドがスキーマに登録されれば、それを参照するクエリは正常に動作します。
Lexical クエリ構文
Lexical クエリは、完全一致または近似のキーワードマッチングを使用して転置インデックスを検索します。
Term クエリ
フィールド(またはデフォルトフィールド)に対して単一のタームをマッチングします。
hello
title:hello
ブーリアン演算子
AND と OR(大文字小文字を区別しない)で句を結合します。
title:hello AND body:world
title:hello OR title:goodbye
AND は対称的に動作します。両側の句を必須(Must)にマークするため、title:hello AND body:world は 両方 の句にマッチするドキュメントだけを返します。連鎖(a AND b AND c)でも 3 つすべてが必須となります。ただし +(必須)または -(禁止)が明示されている句は元の意図が維持され、AND で上書きされることはありません。
明示的な演算子なしでスペース区切りされた句は、暗黙的なブーリアン(スコアリング付きの OR として動作)を使用します。例えば a b AND c は「a は任意、b と c の両方が必須」と解釈されます。
必須 / 禁止句
+(必ずマッチ)と -(マッチ禁止)を使用します。
+title:hello -title:goodbye
フレーズクエリ
ダブルクォートを使用して正確なフレーズをマッチングします。オプションの近接度(~N)で、ターム間に N 語を許可します。
"hello world"
"hello world"~2
ファジークエリ
編集距離を使用した近似マッチング。~ に続けてオプションで最大編集距離を指定します。
roam~
roam~2
ワイルドカードクエリ
?(1 文字)と *(0 文字以上)を使用します。
te?t
test*
範囲クエリ
包含的な [] または排他的な {} の範囲指定。数値フィールドや日付フィールドに有用です。
price:[100 TO 500]
date:{2024-01-01 TO 2024-12-31}
price:[* TO 100]
2D 地理クエリ(geo_*)
2 種類の関数形式を Geo(2D 緯度 / 経度)フィールドに対して使えます。
緯度・経度は度単位、距離はメートル単位の符号付き浮動小数点数:
location:geo_distance(lat, lon, distance_m)
location:geo_bbox(min_lat, min_lon, max_lat, max_lon)
| 形式 | 動作 |
|---|---|
geo_distance(lat, lon, distance_m) | 中心 (lat, lon) から distance_m メートル以内の格納済み座標を返す |
geo_bbox(min_lat, min_lon, max_lat, max_lon) | 軸並行緯度・経度範囲に含まれる格納済み座標を返す |
例:
# 東京から 10 km(= 10 000 m)以内(35.6895, 139.6917)
location:geo_distance(35.6895, 139.6917, 10000)
# 軸並行緯度・経度範囲
location:geo_bbox(35.0, 139.0, 36.0, 140.0)
クエリ対象フィールドはスキーマで
Geoフィールドとして宣言されている必要が あります。緯度は[-90, 90]、経度は[-180, 180]の範囲外を拒否します。
3D 地理クエリ(geo3d_*)
3 種類の関数形式を Geo3d(3D ECEF 直交座標)フィールドに対して使えます。
k 以外の引数はすべてメートル単位の符号付き浮動小数点数、k のみ符号なし整数:
position:geo3d_distance(x, y, z, distance_m)
position:geo3d_bbox(min_x, min_y, min_z, max_x, max_y, max_z)
position:geo3d_nearest(x, y, z, k)
| 形式 | 動作 |
|---|---|
geo3d_distance(x, y, z, distance_m) | (x, y, z) から distance_m メートル以内の格納済みポイントを返す |
geo3d_bbox(min_x, min_y, min_z, max_x, max_y, max_z) | 軸並行 3D ボックスに含まれる格納済みポイントを返す |
geo3d_nearest(x, y, z, k) | (x, y, z) に最も近い k 件をユークリッド距離順で返す |
例:
# 東京タワーから 5 km 以内(ECEF 座標)
position:geo3d_distance(-3955182, 3350553, 3700276, 5000)
# 軸並行 ECEF バウンディングボックス内
position:geo3d_bbox(-4000000, 3300000, 3650000, -3900000, 3400000, 3750000)
# 最も近い 10 件
position:geo3d_nearest(-3955182, 3350553, 3700276, 10)
クエリ対象フィールドはスキーマで
Geo3dとして宣言されている必要があります。 座標系・WGS84 変換ヘルパー・詳細な意味論については 3D 地理検索 を参照してください。
ブースト
^ で句のウェイトを増加させます。
title:hello^2
"important phrase"^1.5
グルーピング
括弧でサブ式を囲みます。
(title:hello OR title:hi) AND body:world
Lexical PEG 文法
完全な Lexical 文法(parser.pest):
query = { SOI ~ boolean_query ~ EOI }
boolean_query = { clause ~ (boolean_op ~ clause | clause)* }
clause = { required_clause | prohibited_clause | sub_clause }
required_clause = { "+" ~ sub_clause }
prohibited_clause = { "-" ~ sub_clause }
sub_clause = { grouped_query | field_query | term_query }
grouped_query = { "(" ~ boolean_query ~ ")" ~ boost? }
boolean_op = { ^"AND" | ^"OR" }
field_query = { field ~ ":" ~ field_value }
field_value = { geo3d_query | geo_query | range_query | phrase_query
| fuzzy_term | wildcard_term | simple_term }
geo3d_query = { geo3d_distance | geo3d_bbox | geo3d_nearest }
geo3d_distance = { ^"geo3d_distance" ~ "(" ~ signed_float ~ "," ~ signed_float
~ "," ~ signed_float ~ "," ~ signed_float ~ ")" }
geo3d_bbox = { ^"geo3d_bbox" ~ "(" ~ signed_float ~ "," ~ signed_float
~ "," ~ signed_float ~ "," ~ signed_float ~ ","
~ signed_float ~ "," ~ signed_float ~ ")" }
geo3d_nearest = { ^"geo3d_nearest" ~ "(" ~ signed_float ~ "," ~ signed_float
~ "," ~ signed_float ~ "," ~ unsigned_int ~ ")" }
geo_query = { geo_distance | geo_bbox }
geo_distance = { ^"geo_distance" ~ "(" ~ signed_float ~ "," ~ signed_float
~ "," ~ signed_float ~ ")" }
geo_bbox = { ^"geo_bbox" ~ "(" ~ signed_float ~ "," ~ signed_float
~ "," ~ signed_float ~ "," ~ signed_float ~ ")" }
phrase_query = { "\"" ~ phrase_content ~ "\"" ~ proximity? ~ boost? }
proximity = { "~" ~ number }
fuzzy_term = { term ~ "~" ~ fuzziness? ~ boost? }
wildcard_term = { wildcard_pattern ~ boost? }
simple_term = { term ~ boost? }
boost = { "^" ~ boost_value }
Vector クエリ構文
Vector クエリは、解析時にテキストをベクトルにエンベディングし、類似性検索を実行します。
基本構文
field:"text"
field:text
field:"text"^weight
| 要素 | 必須 | 説明 | 例 |
|---|---|---|---|
field: | はい | 対象のベクトルフィールド名(スキーマでベクトルフィールドとして定義されている必要があります) | content: |
"text" または text | はい | エンベディングするテキスト(クォート付きまたはクォートなし) | "cute kitten"、python |
^weight | いいえ | スコアウェイト(デフォルト: 1.0) | ^0.8 |
Vector クエリの例
# Single field (quoted text)
content:"cute kitten"
# Single field (unquoted text)
content:python
# With boost weight
content:"cute kitten"^0.8
# Multiple clauses
content:"cats" image:"dogs"^0.5
# Nested field name (dot notation)
metadata.embedding:"text"
複数句
複数の Vector 句はスペースで区切ります。すべての句が実行され、スコアは score_mode(デフォルト: WeightedSum)を使用して結合されます。
content:"cats" image:"dogs"^0.5
この場合のスコア計算:
score = similarity("cats", content) * 1.0
+ similarity("dogs", image) * 0.5
Vector DSL には AND/OR 演算子はありません。Vector 検索は本質的にランキング操作であり、ウェイト(^)が各句の寄与度を制御します。
スコアモード
| モード | 説明 |
|---|---|
WeightedSum(デフォルト) | すべてのクエリ句にわたる(類似度 * ウェイト)の合計 |
MaxSim | クエリ句間の最大類似度スコア |
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 | unquoted_text) ~ boost? }
field_prefix = { field_name ~ ":" }
field_name = @{ (ASCII_ALPHA | "_") ~ (ASCII_ALPHANUMERIC | "_" | ".")* }
quoted_text = ${ "\"" ~ inner_text ~ "\"" }
unquoted_text = @{ (!(WHITE_SPACE | "^" | "\"") ~ ANY)+ }
inner_text = @{ (!("\"") ~ ANY)* }
boost = { "^" ~ float_value }
float_value = @{ ASCII_DIGIT+ ~ ("." ~ ASCII_DIGIT+)? }
統合(ハイブリッド)クエリ構文
UnifiedQueryParser を使用すると、単一のクエリ文字列内で Lexical 句と Vector 句を自由に混在させることができます。
title:hello content:"cute kitten"^0.8
仕組み
- 分割(Split): スキーマのフィールド型に基づいて、各句が Lexical か Vector かを判定する。ベクトルフィールドとして定義されたフィールド名を持つ句は Vector 句として抽出される
- 委譲(Delegate): Vector 部分は
VectorQueryParserに、残りは Lexical のQueryParserに渡される - フュージョン(Fuse): Lexical と Vector の両方の結果が存在する場合、フュージョンアルゴリズムで結合される
曖昧性の解消
Vector 句と Lexical 句の区別は、スキーマのフィールド型に基づいて行われます。フィールド名がスキーマ上でベクトルフィールド(HNSW、Flat、IVF など)として定義されている場合、その句は Vector クエリとして扱われます。Lexical 構文の ~(例: roam~2、"hello world"~10)はファジークエリや近接度クエリとして引き続き正しく解析されます。
フュージョンアルゴリズム
クエリに Lexical 句と Vector 句の両方が含まれる場合、結果はフュージョンされます。
| アルゴリズム | 計算式 | 説明 |
|---|---|---|
| RRF(デフォルト) | score = sum(1 / (k + rank)) | Reciprocal Rank Fusion。異なるスコア分布に対してロバスト。デフォルト k=60。 |
| WeightedSum | score = lexical * a + vector * b | 設定可能なウェイトによる線形結合。 |
注意: フュージョンアルゴリズムは DSL 構文では指定できません。
UnifiedQueryParserの構築時に.with_fusion()で設定します。デフォルトは RRF(k=60)です。コード例はカスタムフュージョンを参照してください。
ハイブリッド AND/OR セマンティクス(+ プレフィックス)
デフォルトでは、ハイブリッドクエリは union(OR) を使用します。Lexical 結果または Vector 結果のいずれかに含まれるドキュメントが返されます。Vector 句に + プレフィックスを付けると intersection(AND) に切り替わり、両方の結果セットに存在するドキュメントのみが返されます。
| 構文 | モード | 動作 |
|---|---|---|
title:Rust content:"system process" | OR(union) | Lexical クエリまたは Vector クエリにマッチするドキュメントが返される |
title:Rust +content:"system process" | AND(intersection) | Lexical と Vector の両方にマッチするドキュメントのみが返される |
+title:Rust +content:"system process" | AND(intersection) | 両方の句が必須。Lexical フィールドの + は既存の required clause として処理される |
ルール:
- Vector 句に
+プレフィックスがない場合、フュージョンは Lexical と Vector の結果を union(OR) で結合します - 1 つでも Vector 句に
+プレフィックスがある場合、フュージョンは intersection(AND) に切り替わり、Lexical と Vector の両方の結果セットに存在するドキュメントのみが返されます - Lexical フィールドの
+(例:+title:Rust)は、Lexical クエリパーサーによって required clause(必須句)として解釈されます。これは既存の Tantivy/Lucene スタイルの動作であり、それ自体ではハイブリッドフュージョンの intersection モードをトリガーしません
統合クエリの例
# Lexical only — no fusion
title:hello AND body:world
# Vector only — no fusion
content:"cute kitten"
# Vector only — unquoted text
content:python
# Hybrid — fusion applied automatically (OR / union)
title:hello content:"cute kitten"
# Hybrid with AND / intersection — both result sets required
title:hello +content:"cute kitten"
# Hybrid with boolean operators
title:hello AND category:animal content:"cute kitten"^0.8
# Multiple vector clauses + lexical
category:animal content:"cats" image:"dogs"^0.5
コード例
DSL による Lexical 検索
#![allow(unused)]
fn main() {
use std::sync::Arc;
use laurus::analysis::analyzer::standard::StandardAnalyzer;
use laurus::lexical::query::QueryParser;
let analyzer = Arc::new(StandardAnalyzer::new()?);
let parser = QueryParser::new(analyzer)
.with_default_field("title");
let query = parser.parse("title:hello AND body:world")?;
}
DSL による Vector 検索
#![allow(unused)]
fn main() {
use std::sync::Arc;
use laurus::vector::query::VectorQueryParser;
let parser = VectorQueryParser::new(embedder)
.with_default_field("content");
let request = parser.parse(r#"content:"cute kitten"^0.8"#).await?;
}
統合 DSL によるハイブリッド検索
#![allow(unused)]
fn main() {
use laurus::engine::query::UnifiedQueryParser;
let unified = UnifiedQueryParser::new(lexical_parser, vector_parser);
let request = unified.parse(
r#"title:hello content:"cute kitten"^0.8"#
).await?;
// request.query -> SearchQuery::Hybrid { lexical: ..., vector: ... }
// request.fusion_algorithm -> Some(RRF) — fusion algorithm
}
カスタムフュージョン
#![allow(unused)]
fn main() {
use laurus::engine::search::FusionAlgorithm;
let unified = UnifiedQueryParser::new(lexical_parser, vector_parser)
.with_fusion(FusionAlgorithm::WeightedSum {
lexical_weight: 0.3,
vector_weight: 0.7,
});
}
BKD-Tree
Laurus は数値・日時・地理ポイントなどのデータを BKD-Tree (Block KD-Tree) に格納する。BKD-Tree はディスク常駐の多次元インデックスで、レンジ・バウンディング ボックス・距離・k 近傍 (k-NN) の各クエリを単一のファイル形式で扱える。
「空間的な形」を持つあらゆるフィールド型はこの BKD プリミティブを共有する:
| フィールド型 | 次元数 | 座標空間 |
|---|---|---|
Integer / Float(単一値・多値) | 1 | スカラー |
DateTime | 1 | Unix マイクロ秒(UTC) |
Geo | 2 | 緯度・経度(度) |
Geo3d | 3 | ECEF 直交座標(メートル) |
新しい空間フィールド型を追加する作業は、次元数を選んでクエリ側の
IntersectVisitor を書くだけに帰着する。
ライタ・リーダ・オンディスクレイアウトはそのまま再利用される。
ファイルフォーマット (Version 2)
.bkd セグメントファイルは自己完結型のバイナリで、3 つの領域から成る:
+----------------------------------------+
| File Header | 固定長・バージョンタグ付き
+----------------------------------------+
| Leaf Blocks | row-major points + doc_ids
| leaf 0 |
| leaf 1 |
| ... |
+----------------------------------------+
| Index Nodes | 内部ナビゲーションノード
| node N-1 |
| ... |
| node 0 (root, written last) |
+----------------------------------------+
ヘッダ (BKDFileHeader) は magic、version(現在は 2)、num_dims、
bytes_per_dim、総ポイント数、リーフブロック数、全体の軸ごと min/max、
インデックス領域とルートノードへのオフセットを保持する。
Leaf Block レイアウト
各リーフブロックは、その部分木に属するポイントを格納する:
count u32 — リーフ内のポイント数
leaf_min [f64; num_dims] — リーフレベルの AABB 下端
leaf_max [f64; num_dims] — リーフレベルの AABB 上端
point_values [f64; count * num_dims] — row-major のポイント座標
doc_ids [u64; count] — ドキュメント ID の並列配列
リーフごとに AABB を持たせることで、クエリ領域がリーフの外側にある場合
(Outside) や全内包される場合 (Inside) には、ポイントを 1 つも読まずに
リーフ全体を判定できる。
内部 Index Node レイアウト
内部ノードは分割情報に加え、子ごとの AABB も保持する:
split_dim u32 — 分割する軸
split_value f64 — 分割しきい値
left_min [f64; num_dims] — 左部分木の AABB 下端
left_max [f64; num_dims] — 左部分木の AABB 上端
right_min [f64; num_dims] — 右部分木の AABB 下端
right_max [f64; num_dims] — 右部分木の AABB 上端
left_offset u64 — 左子ノードのファイルオフセット
right_offset u64 — 右子ノードのファイルオフセット
ノードごとの AABB(v2 で追加)は、分割値だけを持っていた v1 レイアウトを 置き換える。これにより
Inside/Outsideの枝刈りが、再帰的な 探索ではなく定数時間の矩形判定で済むようになった。
ビルドアルゴリズム
BKDWriter::write は、平坦な row-major のポイントバッファと並列の doc_ids
バッファからツリーを構築する。構築は 最も広い軸で分割する (widest-axis
split) ヒューリスティクスで進む:
- 入力部分集合の AABB を計算する。
(max - min)レンジが最も広い軸を選ぶ(同点時は次元番号の小さい方を 採用して決定的にする)。- インデックスの並びをその軸でソートし、中央値で分割する。
- 部分木が
block_size(既定512)以下のポイント数になるまで再帰し、 そうなったらリーフとして書き出す。 - 子が flush された後、各親の
left_offset/right_offsetを後埋めする。
ビルダはポイント/doc_id バッファ自体ではなくインデックスの並び (permutation)をソートするため、ポイント数に関わらずポイント単位の ヒープ確保を一切おこなわない。
数値ロバスト性
座標は全順序で比較できる必要がある。BKDWriter::write は NaN を明示的に
拒否する。NaN には順序が定義されておらず、分割判定とノードごとの AABB
不変条件を破壊するためである。±INFINITY は両方とも受理され、クエリでは
「無限大」を表す自然なセンチネルとして機能する。
IntersectVisitor プロトコル
BKD インデックスへのクエリは
IntersectVisitor
の実装として表現する。リーダはツリーを辿りながら、ビジタに 3 種類の
情報を尋ねる:
#![allow(unused)]
fn main() {
pub enum CellRelation {
Inside, // 部分木全体がヒット — ポイント単位の照合不要
Outside, // 部分木全体をスキップできる
Crosses, // 再帰、もしくはリーフをポイント単位で照合
}
pub trait IntersectVisitor {
fn compare(&self, cell: &AABB) -> CellRelation;
fn visit_inside(&mut self, doc_id: u64);
fn visit(&mut self, doc_id: u64, point: &[f64]);
}
}
リーダのトラバーサルは次のように進む:
graph TD
A["compare(node.aabb)"]
A -->|Inside| B["部分木の各 doc_id を visit_inside(doc_id) で報告<br/>(座標は読まない)"]
A -->|Outside| C["部分木をスキップ"]
A -->|Crosses, 内部ノード| D["子ノードへ再帰"]
A -->|Crosses, リーフ| E["各ポイントについて visit(doc_id, point)<br/>ヒット判定はビジタに委ねる"]
この 3 値分類こそが枝刈りを実現する鍵である。常に Crosses を返すビジタを
書いても結果は正しい — 単にリーフ全件走査に退化するだけだ。
レンジクエリ
レガシーの BKDTree::range_search API は intersect の薄いラッパに
なっている。RangeQueryVisitor を半開区間/閉区間のパラメータから組み立て、
無限大の None スロットを ±INFINITY に変換する。境界の包含・排他は
ビジタ自身が処理する。
3D 地理クエリ
3 つのビジタが laurus::lexical::query::geo3d に存在し、
Geo3d (3D ECEF) フィールドをターゲットにする:
| クエリ | compare の判定領域 | visit の点ごとの判定 |
|---|---|---|
Geo3dDistanceQuery | 球 (centre, radius) と AABB | ユークリッド距離 ≤ radius |
Geo3dBoundingBoxQuery | クエリ AABB と セル AABB | 点がクエリ AABB に内包 |
Geo3dNearestQuery (k-NN) | クエリ点を中心に拡大していく球 | 距離 ≤ 現在の k 番目最良値 |
同じプリミティブで将来のあらゆる空間クエリ(ポリゴンクエリや 2D Geo
の大円距離クエリなど)を新しいビジタとして実装できる。
リーダの内部実装
BKDReader::intersect は 1 クエリにつき 1 つのスクラッチバッファ
(IntersectScratch) を使う。バッファは出会った最大のリーフサイズまで
拡大されたあと、後続のリーフでも再利用される。結果として、何枚のリーフを
辿っても、1 クエリの間にアロケータに触れる回数はごく少数で済む。
単一リーフだけのツリー(非常に小さなフィールド)は特別扱いされる: 「ルートオフセット」がそのまま唯一のリーフを指すため、内部ノードの 降下処理は完全にスキップされる。
関連項目
- 3D 地理検索 (ECEF) — ECEF 距離・バウンディング ボックス・k-NN を実装した具体的な BKD ベースのビジタ。
- Lexical インデクシング —
.bkdセグメントファイルがセグメント全体のレイアウト内のどこに位置するか。 - Lexical 検索 —
NumericRangeQuery、GeoQuery、Geo3dDistanceQueryの Rust API エントリポイント。
3D 地理検索 (ECEF)
Laurus は用途の異なる 2 種類の地理フィールド型を提供する:
| フィールド型 | バックエンド構造 | 座標系 | こんなときに |
|---|---|---|---|
Geo | 2D BKD-Tree | WGS84 緯度・経度(度) | データが地表のみで、高度を考慮しなくてよい場合 |
Geo3d | 3D BKD-Tree | ECEF 直交座標 (x, y, z)(メートル) | 高度が一級の次元になる用途(ドローン・衛星・屋内測位・複数フロア建物) |
両者は同じ BKD-Tree プリミティブを共有しており、Geo3d
は単に第 3 の次元と異なるクエリ語彙を加えただけである。
なぜ ECEF なのか
(緯度, 経度, 高度) の組は人間にとっては便利だが、ユークリッド距離が
そのままでは使えない:経度 1 度は赤道で約 111 km だが極では 0 km、
そして「高度」は地球表面に沿って曲がっている。
ECEF (Earth-Centered Earth-Fixed) はこれを直交座標系にフラット化する:
- 原点は地球の質量中心。
- +X 軸は赤道上の経度 0° を貫く。
- +Y 軸は赤道上の経度 90° E を貫く。
- +Z 軸は地理北極を貫く。
- 3 軸とも単位はメートル。
この座標系では 2 点間の直線距離が単なるユークリッドノルムになる —
球面三角法も極特異点も経度のラップアラウンドも要らない。これこそが、
Geo3d クエリを 3D BKD-Tree 上で(特殊な空間コードなしで)使える
理由である。
Geo3d フィールドの定義
#![allow(unused)]
fn main() {
use laurus::Schema;
use laurus::lexical::core::field::Geo3dOption;
let schema = Schema::builder()
.add_geo3d_field("position", Geo3dOption::default())
.build();
}
Geo3dOption は他の lexical フィールドオプションと同じく
indexed / stored を提供する。インデックス済みの値はフィールドの
3D BKD-Tree に格納され、stored な値は get_documents で返ってくる。
TOML 形式(laurus-cli で使用):
[fields.position.Geo3d]
indexed = true
stored = true
3D ポイントのインデクシング
DocumentBuilder::add_geo_ecef を使って、メートル単位の生の直交座標で
ポイントを追加する:
#![allow(unused)]
fn main() {
use laurus::Document;
// (lat=35.6586°, lon=139.7454°, height=250m) のポイント
// 既に ECEF へ変換済み(後述「座標変換」参照)。
let doc = Document::builder()
.add_geo_ecef("position", -3_955_182.0, 3_350_553.0, 3_700_276.0)
.build();
engine.put_document("tokyo-tower", doc).await?;
engine.commit().await?;
}
値が既に GeoEcefPoint として手元にある場合は、統一 DataValue API を
使う:
#![allow(unused)]
fn main() {
use laurus::{DataValue, GeoEcefPoint};
let p = GeoEcefPoint::new(-3_955_182.0, 3_350_553.0, 3_700_276.0);
let doc = Document::builder()
.add_field("position", DataValue::GeoEcef(p))
.build();
}
座標変換 (WGS84 ↔ ECEF)
入力は通常 (緯度°, 経度°, 高度 m) でやって来る。Laurus は
laurus::util::ecef
で標準的な相互変換を提供する:
#![allow(unused)]
fn main() {
use laurus::util::ecef::{wgs84_to_ecef, ecef_to_wgs84};
// 順変換: lat/lon/height → ECEF
let p = wgs84_to_ecef(35.6586, 139.7454, 250.0);
// 逆変換: ECEF → lat/lon/height
let (lat, lon, height) = ecef_to_wgs84(&p);
}
| 関数 | 方向 | アルゴリズム |
|---|---|---|
wgs84_to_ecef(lat°, lon°, h_m) | 地理 → 直交 | 卯酉線(prime vertical)の閉形式公式 |
ecef_to_wgs84(&GeoEcefPoint) | 直交 → 地理 | Bowring 1985 の閉形式を初期値にした Newton-Raphson 3 反復 |
逆変換は地表下から LEO 軌道高度をはるかに超える領域まで、各軸で
サブ µm の精度で往復する。極付近では、cos(lat) → 0 で発散する
標準形 h = p / cos(lat) - N の代わりに、子午線形
h = z / sin(lat) - N(1 - e²) に切り替える。
内部で使う WGS84 楕円体定数は pub const として公開されている
(WGS84_A, WGS84_F, WGS84_B, WGS84_E2, WGS84_E_PRIME_SQ)ので、
外部コードからも laurus が使っているのと同じ値を参照できる。
クエリ種別
Geo3d フィールドを対象とするクエリは 3 種類ある。いずれも 3D BKD-Tree
を共有する IntersectVisitor
の実装である。
球(半径) — Geo3dDistanceQuery
中心点から distance_m メートル以内に格納済みポイントが入っているドキュメントを
すべて返す。スコアは 1 - distance / radius を [0, 1] にクランプした
値。
#![allow(unused)]
fn main() {
use laurus::lexical::query::geo3d::Geo3dDistanceQuery;
use laurus::GeoEcefPoint;
let centre = GeoEcefPoint::new(-3_955_182.0, 3_350_553.0, 3_700_276.0);
let query = Geo3dDistanceQuery::new("position", centre, 5_000.0); // 5 km 半径
}
ビジタは Outside 判定に AABB::min_distance_sq_to_point、
Inside 判定に AABB::max_distance_sq_to_point を使う。どちらも
2 乗距離空間で比較するため、sqrt を回避できる。
バウンディングボックス — Geo3dBoundingBoxQuery
(min_x, min_y, min_z) と (max_x, max_y, max_z) で定義された軸並行 3D
ボックスにポイントが入っているドキュメントを返す。
#![allow(unused)]
fn main() {
use laurus::lexical::query::geo3d::Geo3dBoundingBoxQuery;
use laurus::GeoEcefPoint;
let min = GeoEcefPoint::new(-4_000_000.0, 3_300_000.0, 3_650_000.0);
let max = GeoEcefPoint::new(-3_900_000.0, 3_400_000.0, 3_750_000.0);
let query = Geo3dBoundingBoxQuery::new("position", min, max)?;
}
コンストラクタが Result を返すのは、すべての軸で min[i] ≤ max[i] を
満たす必要があるため。境界がおかしい場合は構築時点で拒否され、
「黙って空結果」を防げる。
注意点。 クエリボックスは ECEF 直交座標で軸並行になっている。
(lat, lon, height)の 2 つの隅をそれぞれ ECEF に変換して作った ボックスは、ユーザーが直感的に思い描く球殻領域とは違う形をしている。 地表上の領域クエリを書くなら、Geo3dDistanceQuery(真の 3D 球)の方が 多くの場合ユーザー直感に近い。
k 近傍 — Geo3dNearestQuery
クエリ点に最も近い k 件のドキュメントを返す。クエリは小さなプローブ球
(既定 1 km)から始め、以下のいずれかを満たすまで半径を倍加していく:
- 重複なしで
k件以上の候補を集めた、 - 倍加しても何も新しいヒットが見つからなくなった、
- 半径が
max_radius_m(既定1.0e10m=10⁷ km)に達した。
#![allow(unused)]
fn main() {
use laurus::lexical::query::geo3d::Geo3dNearestQuery;
use laurus::GeoEcefPoint;
let centre = GeoEcefPoint::new(-3_955_182.0, 3_350_553.0, 3_700_276.0);
let query = Geo3dNearestQuery::new("position", centre, 10) // 上位 10 件
.with_initial_radius(500.0) // 500 m から開始
.with_max_radius(1_000_000.0); // 1 000 km で打ち切り
}
結果が k 件未満になるのは、単に max_radius_m 以内に k 件のポイントが
存在しないことを意味する。スコアは「最も近いヒットが 1.0、返却された
集合内で最も遠いヒットが 0.0」となるよう正規化される。
k-NN ビジタは compare から CellRelation::Inside を返さない —
Outside か Crosses のみ — ので、すべての候補がその座標と一緒に visit
へ届けられ、k-NN の順序付けに真の距離を使える。
Query DSL
Geo3d クエリは統一 Query DSL からも使える(Query DSL → Geo3d 関数
を参照):
position:geo3d_distance(-3955182, 3350553, 3700276, 5000)
position:geo3d_bbox(-4000000, 3300000, 3650000, -3900000, 3400000, 3750000)
position:geo3d_nearest(-3955182, 3350553, 3700276, 10)
| 形式 | 引数 |
|---|---|
geo3d_distance(x, y, z, distance_m) | 中心 (x, y, z) と最大距離(m) |
geo3d_bbox(min_x, min_y, min_z, max_x, max_y, max_z) | AABB の対角の 2 隅 |
geo3d_nearest(x, y, z, k) | 中心 (x, y, z) と整数 k |
数値引数はすべて符号付き浮動小数。geo3d_nearest の k のみ符号なし整数。
ワイヤーフォーマット
gRPC
Protocol Buffers では、3D 地理を専用の Value バリアント、Geo3dPoint
メッセージ、Geo3dOption スキーマオプションとして公開している:
message Geo3dPoint {
double x = 1;
double y = 2;
double z = 3;
}
message Value {
oneof kind {
// ...
Geo3dPoint geo3d_value = 12;
}
}
message Geo3dOption {
bool indexed = 1;
bool stored = 2;
}
完全な定義は common.proto
と index.proto
を参照。
HTTP gateway / MCP
JSON ドキュメントでは Geo3d 値を { "x": …, "y": …, "z": … } の
オブジェクトとして受け付ける:
{
"position": { "x": -3955182.0, "y": 3350553.0, "z": 3700276.0 }
}
HTTP gateway はこれをエンジンへ転送する前に DataValue::GeoEcef に変換する。
MCP サーバーも同じ規約に従う。
Geo3d を使うべきでないケース
- 純粋な 2D 地図クエリ(地表上の座標からの半径検索、タイル地図上の
単純なバウンディングボックスなど) — 引き続き
Geoを使うこと。 2D BKD はひとつ次元が軽く、WGS84 入力もユーザー直感に近い。 - 学習済みロケーション埋め込みの近似的な意味類似度 — それはベクトル
フィールド(
Hnsw,Flat,Ivf)の領分であって、Geo3dではない。
関連項目
- BKD-Tree —
Integer,Float,DateTime, 2DGeoも含めて支える、共通の多次元インデックス。 - スキーマとフィールド —
Geo3dを含む フィールド型の一覧。 - Query DSL — geo3d DSL 文法の詳細。
- Lexical 検索 — Lexical クエリ全般の Rust API エントリポイント。
laurus-wasm/examples/geo3d/— ブラウザ向けデモ。CesiumJS の 3D 地球儀上にリアルタイム航空機位置を プロットし、geo3d_bboxとgeo3d_nearestを実演します。
ライブラリ概要
laurus クレートは検索エンジンのコアライブラリです。Lexical検索(転置インデックス(Inverted Index)によるキーワードマッチング)、Vector検索(Embeddingによるセマンティック類似度検索)、およびハイブリッド検索(両者の組み合わせ)を統一的なAPIで提供します。
モジュール構成
graph TB
LIB["laurus (lib.rs)"]
LIB --> engine["engine\nEngine, EngineBuilder\nSearchRequest, FusionAlgorithm"]
LIB --> analysis["analysis\nAnalyzer, Tokenizer\nToken Filters, Char Filters"]
LIB --> lexical["lexical\nInverted Index, BM25\nQuery Types, Faceting, Highlighting"]
LIB --> vector["vector\nFlat, HNSW, IVF\nDistance Metrics, Quantization"]
LIB --> embedding["embedding\nCandle BERT, OpenAI\nCLIP, Precomputed"]
LIB --> storage["storage\nMemory, File, Mmap\nColumnStorage"]
LIB --> store["store\nDocumentLog (WAL)"]
LIB --> spelling["spelling\nSpelling Correction\nSuggestion Engine"]
LIB --> data["data\nDataValue, Document"]
LIB --> error["error\nLaurusError, Result"]
主要な型
| 型 | モジュール | 説明 |
|---|---|---|
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, FusionAlgorithm};
use laurus::lexical::TermQuery;
use laurus::lexical::search::searcher::LexicalSearchQuery;
// Lexicalのみの検索
let request = SearchRequestBuilder::new()
.lexical_query(LexicalSearchQuery::Obj(Box::new(TermQuery::new("body", "rust"))))
.limit(10)
.build();
// RRFフュージョンによるハイブリッド検索
let request = SearchRequestBuilder::new()
.lexical_query(lexical_query)
.vector_query(vector_query)
.fusion_algorithm(FusionAlgorithm::RRF { k: 60.0 })
.limit(10)
.build();
let results = engine.search(request).await?;
}
SearchRequest
| フィールド | 型 | デフォルト | 説明 |
|---|---|---|---|
query | SearchQuery | Dsl("") | 検索クエリ仕様(Dsl / Lexical / Vector / Hybrid) |
limit | usize | 10 | 返却する最大結果数 |
offset | usize | 0 | ページネーションのオフセット |
fusion_algorithm | Option<FusionAlgorithm> | None(ハイブリッド時はRRF k=60) | LexicalとVectorの結果を統合する方法 |
filter_query | Option<Box<dyn Query>> | None | 両方の検索タイプに適用されるフィルタ |
lexical_options | LexicalSearchOptions | デフォルト | Lexical検索の動作パラメータ(ブースト、タイムアウト等) |
vector_options | VectorSearchOptions | デフォルト | Vector検索の動作パラメータ(スコアモード等) |
FusionAlgorithm
| バリアント | 説明 |
|---|---|
RRF { k: f64 } | Reciprocal Rank Fusion – ランクベースの結合。スコア = sum(1 / (k + rank))。比較不可能なスコアの大きさを処理します。 |
WeightedSum { lexical_weight, vector_weight } | min-maxスコア正規化を用いた加重結合。重みは[0.0, 1.0]にクランプされます。 |
関連項目: アーキテクチャ – 高レベルのデータフロー図
スコアリングとランキング
Laurusは、Lexical検索に複数のスコアリングアルゴリズムを提供し、Vector検索には距離ベースの類似度を使用します。このページでは、すべてのスコアリングメカニズムとハイブリッド検索における相互作用について説明します。
Lexicalスコアリング
BM25(デフォルト)
BM25はLexical検索のデフォルトスコアリング関数です。単語頻度とドキュメント長の正規化をバランスさせます。
score = IDF * (tf * (k1 + 1)) / (tf + k1 * (1 - b + b * (doc_len / avg_doc_len)))
各パラメータの意味:
- tf – ドキュメント内の単語頻度(Term Frequency)
- IDF – 逆文書頻度(Inverse Document Frequency)。全ドキュメントにおける単語の希少性
- k1 – 単語頻度の飽和パラメータ
- b – ドキュメント長の正規化係数
- doc_len / avg_doc_len – ドキュメント長と平均ドキュメント長の比率
ScoringConfig
ScoringConfig はBM25およびその他のスコアリングパラメータを制御します。
| パラメータ | 型 | デフォルト | 説明 |
|---|---|---|---|
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
}
BM25Plan(事前計算済みクエリ)
同じクエリに対して多数のドキュメントをスコアリングする場合、BM25Plan はクエリビルド時に単語ごとの IDF、フィールド長正規化の不変項、k1 + 1 を一度だけ算出し、各ドキュメントを軽量な数値ループでスコアリングします。スコアリングメソッドは 2 つあります。
BM25Plan::score(&doc_stats)—BM25ScoringFunction::scoreのドロップイン高速版。DocumentStatsのHashMap<String, u64>経由で単語頻度を引きます。BM25Plan::score_packed(&term_freqs, doc_length)—query_termsでの位置でインデックスされた&[u64]をパック済み単語頻度として受け取ります。ドキュメントごとの文字列キーHashMap引きを完全に回避できるため、呼び出し側でドキュメントごとに頻度を 1 度だけパックできる場合に使ってください。
#![allow(unused)]
fn main() {
use laurus::lexical::search::scoring::bm25::{BM25Plan, ScoringConfig};
let plan = BM25Plan::new(&query_terms, &collection_stats, &ScoringConfig::default());
// HashMap パス(ドロップイン高速版):
let score = plan.score(&doc_stats);
// Packed パス(ドキュメントごとの HashMap 引きを回避):
let term_freqs: Vec<u64> = query_terms
.iter()
.map(|t| *doc_stats.term_frequencies.get(t).unwrap_or(&0))
.collect();
let score = plan.score_packed(&term_freqs, doc_stats.doc_length);
}
BM25ScoringFunction::score は 1 ドキュメントずつスコアリングする呼び出し側のために維持されています。
フィールドブースト
フィールドブーストは、特定のフィールドからのスコア寄与に乗数を適用します。一部のフィールドが他よりも重要な場合に有用です。
#![allow(unused)]
fn main() {
use std::collections::HashMap;
let mut field_boosts = HashMap::new();
field_boosts.insert("title".to_string(), 2.0); // titleのマッチはスコア2倍
field_boosts.insert("body".to_string(), 1.0); // bodyのマッチはスコア1倍
}
調整係数(Coordination Factor)
enable_coord が true の場合、AdvancedScorer は調整係数を適用します。
coord = matched_query_terms / total_query_terms
これはより多くのクエリ単語にマッチするドキュメントに報酬を与えます。例えば、クエリが3つの単語を含み、ドキュメントがそのうち2つにマッチする場合、調整係数は 2/3 = 0.667 になります。
Vectorスコアリング
Vector検索は距離ベースの類似度で結果をランク付けします。
similarity = 1 / (1 + distance)
距離は設定された距離メトリクスを使用して計算されます。
| メトリクス | 説明 | 最適な用途 |
|---|---|---|
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 | フィールド定義エラー | 無効なフィールドオプション、重複するフィールド名 |
BenchmarkFailed | ベンチマークエラー | ベンチマーク実行失敗 |
ThreadJoinError | スレッド join エラー | ワーカースレッドでのパニック |
Json | JSONシリアライズエラー | 不正なドキュメントJSON |
Anyhow | anyhow ラップエラー | anyhow 経由のサードパーティクレートエラー |
InvalidOperation | 無効な操作 | コミット前の検索、二重クローズ |
ResourceExhausted | リソース制限超過 | メモリ不足、オープンファイル数超過 |
SerializationError | バイナリシリアライズエラー | ディスク上のデータ破損 |
OperationCancelled | 操作がキャンセルされた | タイムアウト、ユーザーによるキャンセル |
NotImplemented | 機能が利用不可 | 未実装の操作 |
Other | 汎用エラー | タイムアウト、無効な設定、無効な引数 |
基本的なエラーハンドリング
? 演算子の使用
最もシンプルなアプローチ – エラーを呼び出し元に伝播します。
#![allow(unused)]
fn main() {
use laurus::{Engine, Result};
async fn index_documents(engine: &Engine) -> Result<()> {
let doc = laurus::Document::builder()
.add_text("title", "Rust Programming")
.build();
engine.put_document("doc1", doc).await?;
engine.commit().await?;
Ok(())
}
}
エラーバリアントのマッチング
エラータイプごとに異なる動作が必要な場合:
#![allow(unused)]
fn main() {
use laurus::{Engine, LaurusError};
async fn safe_search(engine: &Engine, query: &str) {
match engine.search(/* request */).await {
Ok(results) => {
for result in results {
println!("{}: {}", result.id, result.score);
}
}
Err(LaurusError::Query(msg)) => {
eprintln!("Invalid query syntax: {}", msg);
}
Err(LaurusError::Io(e)) => {
eprintln!("Storage I/O error: {}", e);
}
Err(e) => {
eprintln!("Unexpected error: {}", e);
}
}
}
}
downcast によるエラータイプの確認
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を作成 |
.query_dsl(dsl) | 統合DSLクエリ文字列を設定 |
.lexical_query(query) | Lexical検索クエリを設定(LexicalSearchQuery) |
.vector_query(query) | Vector検索クエリを設定(VectorSearchQuery) |
.filter_query(query) | プレフィルタクエリを設定 |
.fusion_algorithm(algo) | フュージョンアルゴリズムを設定(デフォルト: RRF) |
.limit(n) | 最大結果数(デフォルト: 10) |
.offset(n) | N件スキップ(デフォルト: 0) |
.add_field_boost(field, boost) | Lexical検索のフィールドブーストを追加 |
.lexical_min_score(f32) | Lexical検索の最小スコアしきい値 |
.lexical_timeout_ms(u64) | Lexical検索のタイムアウト(ミリ秒) |
.lexical_parallel(bool) | Lexical検索の並列実行を有効化 |
.sort_by(SortField) | Lexical検索のソート順を設定 |
.vector_score_mode(VectorScoreMode) | Vector検索のスコア結合モードを設定 |
.vector_min_score(f32) | Vector検索の最小スコアしきい値 |
.build() | SearchRequest を構築 |
VectorSearchRequestBuilder
| メソッド | 説明 |
|---|---|
VectorSearchRequestBuilder::new() | 新しいBuilderを作成 |
.add_text(field, text) | フィールドのテキストクエリを追加 |
.add_vector(field, vector) | 事前計算済みクエリベクトルを追加 |
.add_bytes(field, bytes, mime) | バイナリペイロードを追加(マルチモーダル用) |
.limit(n) | 最大結果数 |
.score_mode(VectorScoreMode) | スコア結合モード(WeightedSum、MaxSim) |
.min_score(f32) | 最小スコアしきい値 |
.field(name) | 検索を特定のフィールドに制限 |
.build() | リクエストを構築 |
SearchResult
| フィールド | 型 | 説明 |
|---|---|---|
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 docs --id doc001
Step 7: ドキュメントの削除
ドキュメントを削除してコミットします:
laurus --index-dir ./tutorial_data delete docs --id doc003
laurus --index-dir ./tutorial_data commit
削除されたことを確認します:
laurus --index-dir ./tutorial_data search "python"
結果は返されません。
Step 8: REPL を使う
REPL はインデックスを対話的に操作するためのインタラクティブセッションです:
laurus --index-dir ./tutorial_data repl
REPL で以下のコマンドを試してみてください:
> get stats
> search rust
> add doc doc004 {"title":"Go Programming","body":"Go is a statically typed language designed for simplicity and efficiency.","category":"programming"}
> commit
> search programming
> get docs doc004
> delete docs doc004
> commit
> quit
REPL はコマンド履歴(上下キー)や行編集に対応しています。
Step 9: クリーンアップ
チュートリアルで作成したデータを削除します:
rm -rf ./tutorial_data schema.toml
次のステップ
- スキーマフォーマットで高度なフィールド設定を学ぶ
- コマンドリファレンスで全コマンドを確認する
- 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
新しいインデックスを作成します。--schema が指定された場合はその TOML ファイルを使用し、省略された場合は対話型スキーマウィザードが起動します。
laurus create index [--schema <FILE>]
引数:
| フラグ | 必須 | 説明 |
|---|---|---|
--schema <FILE> | いいえ | インデックススキーマを定義する TOML ファイルのパス。省略時はインデックスディレクトリに既存の schema.toml があればそれを使用し、なければ対話型ウィザードが起動します。 |
スキーマファイルの形式:
スキーマファイルは Laurus ライブラリの Schema 型と同じ構造に従います。詳細はスキーマフォーマットリファレンスを参照してください。例:
default_fields = ["title", "body"]
[fields.title.Text]
stored = true
indexed = true
[fields.body.Text]
stored = true
indexed = true
[fields.category.Text]
stored = true
indexed = true
例:
# スキーマファイルから作成
laurus --index-dir ./my_index create index --schema schema.toml
# Index created at ./my_index.
# 対話型ウィザード(--schema フラグなし)
laurus --index-dir ./my_index create index
# === Laurus Schema Generator ===
# Field name: title
# ...
# Index created at ./my_index.
注意:
schema.tomlとstore/の両方が存在する場合はエラーが返されます。再作成するにはインデックスディレクトリを削除してください。schema.tomlのみ存在する場合(作成が中断された場合など)は、--schemaなしでcreate 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 |
Geo3d | 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 docs
外部 ID で全ドキュメント(チャンクを含む)を取得します。
laurus get docs --id <ID>
テーブル出力の例:
╭──────┬─────────────────────────────────────────╮
│ ID │ Fields │
├──────┼─────────────────────────────────────────┤
│ doc1 │ body: This is a test, title: Hello World │
╰──────┴─────────────────────────────────────────╯
JSON 出力の例:
laurus --format json get docs --id doc1
[
{
"id": "doc1",
"document": {
"title": "Hello World",
"body": "This is a test document."
}
}
]
add — リソースの追加
add doc
インデックスにドキュメントを追加します。ドキュメントは commit を実行するまで検索対象になりません。
laurus add doc --id <ID> --data <JSON>
引数:
| フラグ | 必須 | 説明 |
|---|---|---|
--id <ID> | はい | 外部ドキュメント ID(文字列) |
--data <JSON> | はい | JSON 文字列としてのドキュメントフィールド |
JSON フォーマットはフィールド名と値を対応付けたフラットなオブジェクトです:
{
"title": "Introduction to Rust",
"body": "Rust is a systems programming language.",
"category": "programming"
}
例:
laurus add doc --id doc1 --data '{"title":"Hello World","body":"This is a test document."}'
# Document 'doc1' added. Run 'commit' to persist changes.
ヒント: 複数のドキュメントが同じ外部 ID を共有できます(チャンキングパターン)。各チャンクに対して
add docを使用してください。
put — リソースの上書き(Upsert)
put doc
インデックスにドキュメントを上書き(upsert)します。同じ ID のドキュメントが既に存在する場合、全チャンクが削除されてから新しいドキュメントがインデックスされます。ドキュメントは commit を実行するまで検索対象になりません。
laurus put doc --id <ID> --data <JSON>
引数:
| フラグ | 必須 | 説明 |
|---|---|---|
--id <ID> | はい | 外部ドキュメント ID(文字列) |
--data <JSON> | はい | JSON 文字列としてのドキュメントフィールド |
例:
laurus put doc --id doc1 --data '{"title":"Updated Title","body":"This replaces the existing document."}'
# Document 'doc1' put (upserted). Run 'commit' to persist changes.
注意:
add docとは異なり、put docは指定 ID の既存チャンクをすべて置き換えます。チャンクを追記したい場合はadd docを、ドキュメント全体を置き換えたい場合はput docを使用してください。
add field
既存のインデックスにフィールドを動的に追加します。
laurus add field --index-dir ./data \
--name category \
--field-option '{"Text": {"indexed": true, "stored": true}}'
--field-option 引数はスキーマファイルと同じ外部タグ付き JSON 形式を受け付けます。
フィールド追加後、スキーマは自動的に永続化されます。
delete — リソースの削除
delete field
スキーマからフィールドを動的に削除します。既にインデックスされたデータは残りますが、削除されたフィールドにはアクセスできなくなります。
laurus delete field --name <FIELD_NAME>
例:
laurus delete field --name category
# Field 'category' deleted.
delete docs
外部 ID で全ドキュメント(チャンクを含む)を削除します。
laurus delete docs --id <ID>
例:
laurus delete docs --id doc1
# Documents 'doc1' deleted. Run 'commit' to persist changes.
commit
保留中の変更(追加と削除)をインデックスにコミットします。コミットするまで、変更は検索に反映されません。
laurus commit
例:
laurus --index-dir ./my_index commit
# Changes committed successfully.
search
Query DSL を使用して検索クエリを実行します。
laurus search <QUERY> [--limit <N>] [--offset <N>]
引数:
| 引数 / フラグ | 必須 | デフォルト | 説明 |
|---|---|---|---|
<QUERY> | はい | — | Laurus Query DSL によるクエリ文字列 |
--limit <N> | いいえ | 10 | 最大結果件数 |
--offset <N> | いいえ | 0 | スキップする結果件数 |
クエリ構文の例:
# Term クエリ
laurus search "body:rust"
# Phrase クエリ
laurus search 'body:"machine learning"'
# Boolean クエリ
laurus search "+body:programming -body:python"
# Fuzzy クエリ(タイポ許容)
laurus search "body:programing~2"
# Wildcard クエリ
laurus search "title:intro*"
# Range クエリ
laurus search "price:[10 TO 50]"
# 3D 地理クエリ(球 / バウンディングボックス / k-NN)
laurus search "position:geo3d_distance(-3955182, 3350553, 3700276, 5000)"
laurus search "position:geo3d_bbox(-4000000, 3300000, 3650000, -3900000, 3400000, 3750000)"
laurus search "position:geo3d_nearest(-3955182, 3350553, 3700276, 10)"
テーブル出力の例:
╭──────┬────────┬─────────────────────────────────────────╮
│ ID │ Score │ Fields │
├──────┼────────┼─────────────────────────────────────────┤
│ doc1 │ 0.8532 │ body: Rust is a systems..., title: Intr │
│ doc3 │ 0.4210 │ body: JavaScript powers..., title: Web │
╰──────┴────────┴─────────────────────────────────────────╯
JSON 出力の例:
laurus --format json search "body:rust" --limit 5
[
{
"id": "doc1",
"score": 0.8532,
"document": {
"title": "Introduction to Rust",
"body": "Rust is a systems programming language."
}
}
]
repl
対話型 REPL セッションを開始します。詳細は REPL を参照してください。
laurus repl
serve
gRPC サーバー(およびオプションで HTTP Gateway)を起動します。
laurus serve [OPTIONS]
起動オプション、設定、使用例については laurus-server のドキュメントを参照してください:
- はじめに — 起動オプションと gRPC 接続例
- 設定 — TOML 設定ファイル、環境変数、優先順位
- ハンズオンチュートリアル — ステップバイステップの操作ガイド
スキーマフォーマットリファレンス
スキーマファイルはインデックスの構造を定義します。どのフィールドが存在するか、その型、およびインデックスの方法を指定します。Laurus はスキーマファイルに TOML 形式を使用します。
概要
スキーマは 3 つのトップレベル要素で構成されます:
# スキーマに宣言されていないフィールドの扱い。省略時は "dynamic"。
dynamic_field_policy = "dynamic"
# クエリでフィールドが指定されていない場合にデフォルトで検索するフィールド。
default_fields = ["title", "body"]
# フィールド定義。各フィールドには名前と型付き設定があります。
[fields.<field_name>.<FieldType>]
# ... 型固有のオプション
dynamic_field_policy— スキーマに宣言されていないフィールドがドキュメントに含まれる場合の挙動を制御します。値は"strict"/"dynamic"/"ignore"。デフォルトは"dynamic"。詳細および「dynamicでは情報損失が起きうる」という警告は 動的スキーマ を参照してください。default_fields— Query DSL でデフォルトの検索対象として使用されるフィールド名のリストです。Lexical フィールド(Text、Integer、Float など)のみデフォルトフィールドに指定できます。このキーはオプションで、デフォルトは空のリストです。fields— フィールド名とその型付き設定のマップです。各フィールドにはフィールド型を1つだけ指定する必要があります。
フィールド命名規則
- フィールド名は任意の文字列です(例:
title、body_vec、created_at)。 - アンダースコア(
_)で始まるフィールド名はエンジンの予約領域です。例外として_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
multi_valued = false
| オプション | 型 | デフォルト | 説明 |
|---|---|---|---|
indexed | bool | true | 範囲クエリおよび完全一致クエリを有効にする |
stored | bool | true | 元の値を保存する |
multi_valued | bool | false | 整数の配列を受け付け、範囲クエリはいずれかの値が条件を満たせばマッチ(Lucene 流の “any match”、constant スコア) |
Float
64ビット浮動小数点フィールド。範囲クエリをサポートします。
[fields.rating.Float]
indexed = true
stored = true
multi_valued = false
| オプション | 型 | デフォルト | 説明 |
|---|---|---|---|
indexed | bool | true | 範囲クエリを有効にする |
stored | bool | true | 元の値を保存する |
multi_valued | bool | false | 浮動小数点の配列を受け付け、範囲クエリはいずれかの値が条件を満たせばマッチ(Lucene 流の “any match”、constant スコア) |
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 | 元の値を保存する |
Geo3d
3D Earth-Centered Earth-Fixed (ECEF) 直交座標系の点フィールド(x / y / z はメートル単位)。geo3d_distance(球)、geo3d_bbox(3D AABB)、geo3d_nearest(k-NN)クエリをサポートします。座標系および wgs84_to_ecef / ecef_to_wgs84 の変換ユーティリティについては 3D 地理検索 (ECEF) を参照してください。
[fields.position.Geo3d]
indexed = true
stored = true
| オプション | 型 | デフォルト | 説明 |
|---|---|---|---|
indexed | bool | true | 3D 地理クエリ(geo3d_distance、geo3d_bbox、geo3d_nearest)を有効にする |
stored | bool | true | 元の (x, y, z) 値を保存する |
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 が起動し、作成を案内します:
Laurus REPL — no index found at ./my_index.
Use 'create index <schema_path>' to create one, or 'help' for commands.
laurus>
利用可能なコマンド
コマンドは CLI と同じ <操作> <リソース> の順序に従います。
| コマンド | 説明 |
|---|---|
create index [schema_path] | インデックスを作成(パス省略時は対話型ウィザード) |
create schema <output_path> | 対話型スキーマ生成ウィザード |
search <query> | インデックスを検索 |
add field <name> <json> | スキーマにフィールドを追加 |
add doc <id> <json> | ドキュメントを追加(追記、同一 ID で複数チャンク可) |
put doc <id> <json> | ドキュメントを上書き(同一 ID の既存チャンクを置換) |
get stats | インデックスの統計情報を表示 |
get schema | 現在のスキーマを表示 |
get docs <id> | ID で全ドキュメント(チャンクを含む)を取得 |
delete field <name> | スキーマからフィールドを削除 |
delete docs <id> | ID で全ドキュメント(チャンクを含む)を削除 |
commit | 保留中の変更をコミット |
help | 利用可能なコマンドを表示 |
quit / exit | REPL を終了 |
注意:
create、help、quit以外のコマンドはインデックスがロードされている必要があります。インデックスがロードされていない場合、まずcreate indexを実行するようメッセージが表示されます。
使用例
インデックスの作成
laurus> create index ./schema.toml
Index created at ./my_index.
laurus> add doc doc1 {"title":"Hello","body":"World"}
Document 'doc1' added.
検索
laurus> search body:rust
╭──────┬────────┬────────────────────────────────────╮
│ ID │ Score │ Fields │
├──────┼────────┼────────────────────────────────────┤
│ doc1 │ 0.8532 │ body: Rust is a systems..., title… │
╰──────┴────────┴────────────────────────────────────╯
フィールドの管理
laurus> add field category {"Text": {"indexed": true, "stored": true}}
Field 'category' added.
laurus> delete field category
Field 'category' deleted.
ドキュメントの追加とコミット
laurus> add doc doc4 {"title":"New Document","body":"Some content here."}
Document 'doc4' added.
laurus> commit
Changes committed.
情報の取得
laurus> get stats
Document count: 3
laurus> get schema
{
"fields": { ... },
"default_fields": ["title", "body"]
}
laurus> get docs doc4
╭──────┬───────────────────────────────────────────────╮
│ ID │ Fields │
├──────┼───────────────────────────────────────────────┤
│ doc4 │ body: Some content here., title: New Document │
╰──────┴───────────────────────────────────────────────╯
ドキュメントの削除
laurus> delete docs doc4
Documents 'doc4' deleted.
laurus> commit
Changes committed.
機能
- 行編集 — 矢印キー、Home/End キー、および標準的な readline ショートカット
- 履歴 — 上下矢印キーで以前のコマンドを呼び出し
- Ctrl+C / Ctrl+D — REPL を正常に終了
サーバー概要
laurus-server クレートは、Laurus 検索エンジン用の gRPC サーバーとオプションの HTTP/JSON ゲートウェイを提供します。エンジンをメモリに常駐させることで、コマンド実行ごとの起動オーバーヘッドを排除します。
機能
- 永続エンジン – インデックスはリクエスト間で開いたまま維持され、呼び出しごとの WAL リプレイが不要
- フル gRPC API – インデックス管理、ドキュメント CRUD、コミット、検索(単発 + ストリーミング)
- HTTP ゲートウェイ – gRPC と併用可能なオプションの HTTP/JSON ゲートウェイで REST スタイルのアクセスを提供
- ヘルスチェック – ロードバランサーやオーケストレーター向けの標準ヘルスチェックエンドポイント
- グレースフルシャットダウン – Ctrl+C / SIGINT で保留中の変更を自動的にコミット
- TOML 設定 – オプションの設定ファイルと CLI・環境変数によるオーバーライド
アーキテクチャ
graph LR
subgraph "laurus-server"
GW["HTTP Gateway\n(axum)"]
GRPC["gRPC Server\n(tonic)"]
ENG["Engine\n(Arc<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;
DynamicFieldPolicy dynamic_field_policy = 5;
}
enum DynamicFieldPolicy {
DYNAMIC_FIELD_POLICY_UNSPECIFIED = 0;
DYNAMIC_FIELD_POLICY_STRICT = 1;
DYNAMIC_FIELD_POLICY_DYNAMIC = 2;
DYNAMIC_FIELD_POLICY_IGNORE = 3;
}
fields— フィールド名をキーとしたフィールド定義。default_fields— クエリでフィールドを指定しない場合のデフォルト検索対象フィールド名。analyzers— 名前をキーとしたカスタムアナライザーパイプライン。TextOption.analyzerで参照。embedders— 名前をキーとしたエンベッダー設定。ベクトルフィールドオプション(HnswOption.embedderなど)で参照。dynamic_field_policy— 投入されたドキュメントに含まれるがfieldsに宣言されていないフィールドの扱い。UNSPECIFIED(値 0)は後方互換のためDYNAMICとして解釈されます。挙動マトリクスおよびDYNAMICでの情報損失警告は スキーマとフィールド を参照してください。
AnalyzerDefinition:
message AnalyzerDefinition {
repeated ComponentConfig char_filters = 1;
ComponentConfig tokenizer = 2;
repeated ComponentConfig token_filters = 3;
}
ComponentConfig(文字フィルター、トークナイザー、トークンフィルターに使用):
| フィールド | 型 | 説明 |
|---|---|---|
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, multi_valued) | FlatOption (dimension, distance, base_weight, quantizer, embedder) |
FloatOption (indexed, stored, multi_valued) | IvfOption (dimension, distance, n_clusters, n_probe, base_weight, quantizer, embedder) |
BooleanOption (indexed, stored) | |
DateTimeOption (indexed, stored) | |
GeoOption (indexed, stored) | |
Geo3dOption (indexed, stored) | |
BytesOption (stored) |
ベクトルフィールドオプションの embedder フィールドには、Schema.embedders で定義したエンベッダー名を指定します。設定すると、インデックス時にドキュメントのテキストフィールドからベクトルを自動生成します。事前計算済みのベクトルを直接供給する場合は空のままにします。
距離メトリクス: COSINE, EUCLIDEAN, MANHATTAN, DOT_PRODUCT, ANGULAR
量子化手法: NONE, SCALAR_8BIT, PRODUCT_QUANTIZATION
QuantizationConfig 構造:
| フィールド | 型 | 説明 |
|---|---|---|
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(緯度、経度) |
| Geo3d | geo3d_value | Geo3dPoint(x, y, z メートル単位、ECEF 直交座標系) |
Geo3dPoint:
| フィールド | 型 | 説明 |
|---|---|---|
x | double | X 座標(メートル単位、ECEF: 赤道面、+X 方向は経度 0°) |
y | double | Y 座標(メートル単位、ECEF: 赤道面、+Y 方向は東経 90°) |
z | double | Z 座標(メートル単位、ECEF: +Z 方向は北極) |
座標系の詳細および wgs84_to_ecef / ecef_to_wgs84 の変換ユーティリティについては 3D 地理検索 (ECEF) を参照してください。
AddDocument
ドキュメントを追加します。PutDocument と異なり、同じ ID の既存ドキュメントを置換しません。複数のドキュメントが同じ ID を共有できます(チャンキングパターン)。
rpc AddDocument(AddDocumentRequest) returns (AddDocumentResponse);
リクエストフィールドは PutDocument と同じです。
GetDocuments
指定された外部 ID に一致するすべてのドキュメントを取得します。
rpc GetDocuments(GetDocumentsRequest) returns (GetDocumentsResponse);
リクエストフィールド:
| フィールド | 型 | 必須 | 説明 |
|---|---|---|---|
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 つ以上を指定する必要があります。
3D 地理クエリ
3D ECEF の地理クエリは SearchRequest.query に渡す Lexical DSL 文字列で表現します。専用のメッセージ型はなく、コアライブラリで使用される DSL 形式がそのまま gRPC 経由でも動作します。3 種類の形式があります(構文の詳細は Query DSL → 3D 地理クエリ を参照):
position:geo3d_distance(x, y, z, distance_m)—(x, y, z)を中心とした最大距離(メートル単位)の球position:geo3d_bbox(min_x, min_y, min_z, max_x, max_y, max_z)— 3D 軸並行バウンディングボックスposition:geo3d_nearest(x, y, z, k)—(x, y, z)に最も近い k 個の近傍点
position はフィールド名で、スキーマで宣言した実際の Geo3d 型フィールドに置き換えてください。すべての数値引数は符号付きの double 値で、k は符号なし整数です。
QueryVector
| フィールド | 型 | 説明 |
|---|---|---|
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": {
"dynamic_field_policy": "dynamic",
"fields": {
"title": {"text": {"indexed": true, "stored": true, "term_vectors": true}},
"body": {"text": {"indexed": true, "stored": true, "term_vectors": true}}
},
"default_fields": ["title", "body"]
}
}'
dynamic_field_policy は省略可能なキーで、スキーマに宣言されていないフィールドの扱いを制御します。指定できる値は "strict" / "dynamic"(デフォルト)/ "ignore" の 3 種類です。詳細および "dynamic" での情報損失に関する警告は スキーマとフィールド を参照してください。
インデックス統計情報の取得
curl http://localhost:8080/v1/index
スキーマの取得
curl http://localhost:8080/v1/schema
ドキュメントの Upsert(PUT)
ドキュメントが既に存在する場合は置換します。
curl -X PUT http://localhost:8080/v1/documents/doc1 \
-H 'Content-Type: application/json' \
-d '{
"document": {
"fields": {
"title": "Hello World",
"body": "This is a test document."
}
}
}'
ドキュメントの追加(POST)
同じ ID の既存ドキュメントを置換せずに新しいチャンクを追加します。
curl -X POST http://localhost:8080/v1/documents/doc1 \
-H 'Content-Type: application/json' \
-d '{
"document": {
"fields": {
"title": "Hello World",
"body": "This is a test document."
}
}
}'
ドキュメントの取得
curl http://localhost:8080/v1/documents/doc1
ドキュメントの削除
curl -X DELETE http://localhost:8080/v1/documents/doc1
コミット
curl -X POST http://localhost:8080/v1/commit
検索
curl -X POST http://localhost:8080/v1/search \
-H 'Content-Type: application/json' \
-d '{"query": "body:test", "limit": 10}'
フィールドブースト付き検索
curl -X POST http://localhost:8080/v1/search \
-H 'Content-Type: application/json' \
-d '{
"query": "rust programming",
"limit": 10,
"field_boosts": {"title": 2.0}
}'
ハイブリッド検索
curl -X POST http://localhost:8080/v1/search \
-H 'Content-Type: application/json' \
-d '{
"query": "body:rust",
"query_vectors": [{"vector": [0.1, 0.2, 0.3], "weight": 1.0}],
"limit": 10,
"fusion": {"rrf": {"k": 60}}
}'
ストリーミング検索(SSE)
/v1/search/stream エンドポイントは Server-Sent Events(SSE)として結果を返します。各結果は個別のイベントとして送信されます。
curl -N -X POST http://localhost:8080/v1/search/stream \
-H 'Content-Type: application/json' \
-d '{"query": "body:test", "limit": 10}'
レスポンスは SSE イベントのストリームです。
data: {"id":"doc1","score":0.8532,"document":{...}}
data: {"id":"doc2","score":0.4210,"document":{...}}
JSON フィールド値の型推論
ドキュメント投入リクエスト(PUT /v1/documents/:id または
POST /v1/documents/:id)のボディに含まれる document.fields の各値は、
スキーマレス取り込みと同じ推論ルールでエンジンの
DataValue に変換されます。これにより
HTTP 経路と gRPC 経路で挙動が一致します。
| JSON 値 | 推論されるフィールド型 | 備考 |
|---|---|---|
null | (スキップ) | NullValue として送出され、取り込み時に破棄されます。 |
true / false | boolean | |
整数(i64 に収まる) | integer | |
| 浮動小数点 / 巨大整数 | float | |
"text" | text | |
[1, 2, 3](全要素 integer) | integer(multi_valued: true) | 多値数値フィールド。 |
[1.0, 2.5](非整数を含む数値配列) | float(multi_valued: true) | |
[](空配列) | (スキップ) | 要素型を決定できないためフィールドはスキップされます。 |
{"latitude": ..., "longitude": ...} | geo | |
{"lat": ..., "lon": ...} / {"lat": ..., "lng": ...} | geo | latitude / longitude の短縮別名を受け付けます。 |
{"x": ..., "y": ..., "z": ...} | geo3d | 3 キーすべて必須、有限な数値、ECEF メートル単位。lat/lon キーとの混在は拒否されます。 |
以下の場合、ゲートウェイは HTTP 400(Bad Request)を返します:
- 配列が混在型もしくは非数値要素を含む(例:
[1, "x"]) - オブジェクトが有効な地理座標ではない(2D は latitude/longitude キーが、
3D は
x/y/zのいずれかが欠けている) - 緯度が
[-90, 90]の範囲外、または経度が[-180, 180]の範囲外 - 3D ECEF の座標が有限値でない(
NaN/Inf) - 同一オブジェクトに 2D(
lat/lon)と 3D(x/y/z)のキーが混在
ベクトルおよびバイト列フィールドは JSON だけからは推論できないため、
スキーマで明示的に宣言する必要があります。宣言済みのベクトルフィールドに
数値配列が送られた場合は自動的に f32 ベクトルへキャストされるので、
REST クライアントは埋め込みベクトルを通常の JSON 配列として送信できます。
3D 地理クエリ
3D ECEF クエリは query に渡す Lexical DSL 文字列をそのまま再利用します。ゲートウェイは文字列を変更せずエンジンへ転送するため、gRPC 経由と同じ DSL 形式が HTTP 経由でも動作します:
curl -X POST http://localhost:8080/v1/search \
-H 'Content-Type: application/json' \
-d '{
"query": "position:geo3d_distance(-3955182, 3350553, 3700276, 5000)",
"limit": 10
}'
geo3d_bbox および geo3d_nearest の構文は Query DSL → 3D 地理クエリ を参照してください。
リクエスト/レスポンス形式
すべてのリクエストおよびレスポンスボディは JSON を使用します。JSON の構造は gRPC の protobuf メッセージに対応しています。メッセージ定義の詳細は gRPC API リファレンスを参照してください。
MCP サーバー概要
laurus-mcp クレートは、Laurus 検索エンジン用の Model Context Protocol (MCP) サーバーを提供します。実行中の laurus-server インスタンスへの gRPC クライアントとして動作し、Claude などの AI アシスタントが標準 MCP stdio トランスポートを通じてドキュメントのインデックス登録や検索を行えるようにします。
機能
- MCP stdio トランスポート — サブプロセスとして起動し、stdin/stdout 経由で AI クライアントと通信
- gRPC クライアント — すべてのツール呼び出しを実行中の
laurus-serverインスタンスにプロキシ - 全 laurus 検索モード — Lexical(BM25)、Vector(HNSW/Flat/IVF)、ハイブリッド検索
- 動的接続 —
connectツールで任意の laurus-server エンドポイントに接続可能 - ドキュメントライフサイクル — MCP ツールを通じてドキュメントの追加・更新・削除・取得が可能
アーキテクチャ
graph LR
subgraph "laurus-mcp"
MCP["MCP Server\n(stdio)"]
end
AI["AI クライアント\n(Claude など)"] -->|"stdio (JSON-RPC)"| MCP
MCP -->|"gRPC"| SRV["laurus-server\n(常駐)"]
SRV --> Disk["ディスク上のインデックス"]
MCP サーバーは AI クライアントによって起動される子プロセスとして動作します。すべてのツール呼び出しを gRPC 経由で laurus-server インスタンスにプロキシします。laurus-server は MCP サーバーとは別途、事前に起動しておく必要があります。
クイックスタート
# ステップ 1: laurus-server を起動
laurus serve --port 50051
# ステップ 2: Claude Code で MCP サーバーを設定
claude mcp add laurus -- laurus mcp --endpoint http://localhost:50051
または手動で設定ファイルを編集:
{
"mcpServers": {
"laurus": {
"command": "laurus",
"args": ["mcp", "--endpoint", "http://localhost:50051"]
}
}
}
セクション
laurus-mcp をはじめる
前提条件
laurusCLI バイナリがインストール済み(cargo install laurus-cli)- 実行中の
laurus-serverインスタンス(laurus-server はじめにを参照) - MCP をサポートする AI クライアント(Claude Desktop、Claude Code など)
設定
ステップ 1: laurus-server を起動
laurus serve --port 50051
ステップ 2: MCP クライアントの設定
Claude Code
CLI コマンドで追加する方法(推奨):
claude mcp add laurus -- laurus mcp --endpoint http://localhost:50051
または ~/.claude/settings.json を直接編集:
{
"mcpServers": {
"laurus": {
"command": "laurus",
"args": ["mcp", "--endpoint", "http://localhost:50051"]
}
}
}
Claude Desktop
以下の設定ファイルを編集:
- macOS:
~/Library/Application Support/Claude/claude_desktop_config.json - Windows:
%APPDATA%\Claude\claude_desktop_config.json
{
"mcpServers": {
"laurus": {
"command": "laurus",
"args": ["mcp", "--endpoint", "http://localhost:50051"]
}
}
}
使用ワークフロー
ワークフロー 1: 既存のインデックスを使用する
CLI でインデックスを事前に作成してから MCP サーバーで検索します:
# ステップ 1: スキーマファイルを作成
cat > schema.toml << 'EOF'
[fields.title]
Text = { indexed = true, stored = true }
[fields.body]
Text = { indexed = true, stored = true }
EOF
# ステップ 2: サーバーを起動してインデックスを作成
laurus serve --port 50051 &
laurus create index --schema schema.toml
# ステップ 3: MCP サーバーを Claude Code に登録
claude mcp add laurus -- laurus mcp --endpoint http://localhost:50051
ワークフロー 2: AI 主導のインデックス作成
laurus-server を起動してから MCP サーバーを登録し、AI にインデックスを作成させます:
# ステップ 1: laurus-server を起動(インデックス不要)
laurus serve --port 50051
# ステップ 2: MCP サーバーを Claude Code に登録
claude mcp add laurus -- laurus mcp --endpoint http://localhost:50051
次に Claude に依頼します:
「ブログ記事用の検索インデックスを作成してください。タイトルと本文テキストで検索できるようにして、著者と公開日も保存したいです。」
Claude はスキーマを設計して create_index を自動的に呼び出します。
ワークフロー 3: 実行時に接続する
エンドポイントを指定せずに MCP サーバーを登録します:
claude mcp add laurus -- laurus mcp
または設定ファイルを直接編集:
{
"mcpServers": {
"laurus": {
"command": "laurus",
"args": ["mcp"]
}
}
}
次に Claude に接続を依頼します:
「
http://localhost:50051の laurus サーバーに接続してください」
Claude は他のツールを使用する前に connect を呼び出します。
MCP サーバーの削除
Claude Code から登録済みの MCP サーバーを削除するには:
claude mcp remove laurus
Claude Desktop の場合は、設定ファイルから laurus エントリを削除してアプリケーションを再起動してください。
ライフサイクル
laurus-server 起動(別プロセス)
└─ gRPC ポート 50051 でリッスン
Claude 起動
└─ 起動: laurus mcp --endpoint http://localhost:50051
└─ stdio イベントループに入る
├─ stdin 経由でツール呼び出しを受信
├─ gRPC 経由で laurus-server にプロキシ
└─ stdout 経由で結果を送信
Claude 終了
└─ laurus-mcp プロセスが終了
└─ laurus-server は継続して動作
MCP ツールリファレンス
laurus MCP サーバーは以下のツールを公開しています。
connect
実行中の laurus-server gRPC エンドポイントに接続します。--endpoint フラグなしでサーバーを起動した場合や、実行時に別の laurus-server に切り替える場合に、他のツールを使用する前にこのツールを呼び出してください。
パラメーター
| 名前 | 型 | 必須 | 説明 |
|---|---|---|---|
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 表現を使用します(バリアント名がキーになります):
{
"dynamic_field_policy": "Dynamic",
"fields": {
"title": { "Text": { "indexed": true, "stored": true } },
"body": { "Text": {} },
"score": { "Float": {} },
"count": { "Integer": {} },
"active": { "Boolean": {} },
"created": { "DateTime": {} },
"embedding": { "Hnsw": { "dimension": 384 } }
}
}
オプションの dynamic_field_policy キーは、スキーマに宣言されていないフィールドが投入ドキュメントに含まれる場合の挙動を制御します。指定可能な値は "Strict" / "Dynamic"(デフォルト)/ "Ignore"。警告: "Dynamic" では integer フィールドに入ってきた float 値が静かに切り捨てられます(3.14 → 3)。厳密さが必要なら "Strict" を使用してください。詳細な挙動マトリクスは スキーマとフィールド を参照してください。
例
Tool: create_index
schema_json: {"fields": {"title": {"Text": {}}, "body": {"Text": {}}}}
結果: Index created successfully at /path/to/index.
3D ECEF 座標を扱う Geo3d フィールドを含むスキーマ:
{
"fields": {
"title": { "Text": { "indexed": true, "stored": true } },
"position": { "Geo3d": { "indexed": true, "stored": true } }
}
}
座標系については 3D 地理検索 (ECEF) を参照してください。Geo3d フィールドは geo3d_distance / geo3d_bbox / geo3d_nearest の DSL 形式で検索できます(後述の search ツールを参照)。
get_stats
現在の検索インデックスの統計情報(ドキュメント数、ベクトルフィールド情報など)を取得します。
パラメーター
なし。
結果
{
"document_count": 42,
"vector_fields": ["embedding"]
}
get_schema
現在のインデックスのスキーマ(全フィールド定義と設定)を取得します。
パラメーター
なし。
結果
{
"fields": {
"title": { "Text": { "indexed": true, "stored": true } },
"body": { "Text": {} },
"embedding": { "Hnsw": { "dimension": 384 } }
},
"default_fields": ["title", "body"]
}
put_document
インデックスにドキュメントを上書き(upsert)します。同じ ID のドキュメントが既に存在する場合、全チャンクが削除されてから新しいドキュメントがインデックスされます。ドキュメント追加後は commit を呼び出してください。
パラメーター
| 名前 | 型 | 必須 | 説明 |
|---|---|---|---|
id | string | はい | 外部ドキュメント識別子 |
document | object | はい | JSON オブジェクトとしてのドキュメントフィールド |
例
Tool: put_document
id: "doc-1"
document: {"title": "Hello World", "body": "これはテストドキュメントです。"}
結果: Document 'doc-1' put (upserted). Call commit to persist changes.
Geo3d 値を含む例:
Tool: put_document
id: "drone-1"
document: {"title": "東京上空のドローン", "position": {"x": -3955182.0, "y": 3350553.0, "z": 3700276.0}}
MCP サーバーは 3D ECEF 点を x、y、z キーを持つ JSON オブジェクト(メートル単位)として受け付けます。これは HTTP ゲートウェイの挙動とは異なり、HTTP ゲートウェイでは現在 JSON から Geo3d を推論しません。MCP では書き込み・読み出しともに完全対応しています。
add_document
インデックスにドキュメントを新しいチャンクとして追加します。put_document とは異なり、同じ ID の既存ドキュメントを削除せずに追記します。大きなドキュメントをチャンクに分割する際に便利です。ドキュメント追加後は commit を呼び出してください。
パラメーター
| 名前 | 型 | 必須 | 説明 |
|---|---|---|---|
id | string | はい | 外部ドキュメント識別子 |
document | object | はい | JSON オブジェクトとしてのドキュメントフィールド |
例
Tool: add_document
id: "doc-1"
document: {"title": "Hello World - Part 2", "body": "これは続きです。"}
結果: Document 'doc-1' added as chunk. Call commit to persist changes.
get_documents
外部 ID で全ドキュメント(チャンクを含む)を取得します。
パラメーター
| 名前 | 型 | 必須 | 説明 |
|---|---|---|---|
id | string | はい | 外部ドキュメント識別子 |
結果
{
"id": "doc-1",
"documents": [
{ "title": "Hello World", "body": "これはテストドキュメントです。" }
]
}
delete_documents
外部 ID で全ドキュメント(チャンクを含む)を削除します。削除後は commit を呼び出してください。
パラメーター
| 名前 | 型 | 必須 | 説明 |
|---|---|---|---|
id | string | はい | 外部ドキュメント識別子 |
結果: Documents 'doc-1' deleted. Call commit to persist changes.
commit
保留中の変更をディスクにコミットします。変更を検索可能かつ永続的にするため、put_document、add_document、または delete_documents の後に必ず呼び出してください。
パラメーター
なし。
結果: 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 を使用してドキュメントを検索します。Lexical 検索、Vector 検索、ハイブリッド検索を単一のクエリ文字列でサポートします。
パラメーター
| 名前 | 型 | 必須 | 説明 |
|---|---|---|---|
query | string | はい | laurus 統一クエリ DSL による検索クエリ |
limit | integer | いいえ | 最大結果数(デフォルト: 10) |
offset | integer | いいえ | ページネーション用スキップ数(デフォルト: 0) |
fusion | string | いいえ | ハイブリッド検索用の融合アルゴリズム(JSON) |
field_boosts | string | いいえ | フィールド毎のブースト係数(JSON) |
クエリ DSL の例
Lexical 検索
| クエリ | 説明 |
|---|---|
hello | デフォルトフィールド全体のターム検索 |
title:hello | フィールド指定のターム検索 |
title:hello AND body:world | ブール AND |
"exact phrase" | フレーズ検索 |
roam~2 | ファジー検索(編集距離 2) |
count:[1 TO 10] | 範囲検索 |
title:helo~1 | フィールド指定のファジー検索 |
3D 地理検索
| クエリ | 説明 |
|---|---|
position:geo3d_distance(x, y, z, distance_m) | (x, y, z) を中心とした最大距離(メートル単位)の球 |
position:geo3d_bbox(min_x, min_y, min_z, max_x, max_y, max_z) | 3D 軸並行バウンディングボックス |
position:geo3d_nearest(x, y, z, k) | (x, y, z) に最も近い k 個の近傍点 |
position はフィールド名で、スキーマで宣言した実際の Geo3d 型フィールドに置き換えてください。完全な DSL 構文は Query DSL → 3D 地理クエリ を参照してください。
Vector 検索
| クエリ | 説明 |
|---|---|
content:"cute kitten" | 特定フィールドでの Vector 検索(クォート付き) |
content:python | 特定フィールドでの Vector 検索(クォートなし) |
content:"cute kitten"^0.8 | 重み付き Vector 検索 |
a:"cats" b:"dogs"^0.5 | 複数の Vector クエリ |
ハイブリッド検索
| クエリ | 説明 |
|---|---|
title:hello content:"cute kitten" | Lexical + Vector(OR/union — いずれかの結果を返す) |
title:hello +content:"cute kitten" | Lexical + Vector(AND/intersection — 両方にマッチした結果のみ) |
+title:hello +content:"cute kitten" | 両方必須(AND)。Lexical フィールドの + は required clause |
title:hello AND body:world content:"cats"^0.8 | ブール Lexical + 重み付き Vector |
融合アルゴリズムの例
{"rrf": {"k": 60.0}}
{"weighted_sum": {"lexical_weight": 0.7, "vector_weight": 0.3}}
フィールドブーストの例
{"title": 2.0, "body": 1.0}
結果
{
"total": 2,
"results": [
{
"id": "doc-1",
"score": 3.14,
"document": { "title": "Hello World", "body": "..." }
},
{
"id": "doc-2",
"score": 1.57,
"document": { "title": "Hello Again", "body": "..." }
}
]
}
典型的なワークフロー
1. connect → 実行中の laurus-server に接続
2. create_index → スキーマを定義(インデックスが存在しない場合)
3. add_field → フィールドを追加(必要に応じて)
delete_field → フィールドを削除(必要に応じて)
4. put_document → ドキュメントを上書き(必要に応じて繰り返し)
add_document → ドキュメントチャンクを追記(必要に応じて)
5. commit → 変更をディスクに永続化
6. search → インデックスを検索
7. get_documents → ID でドキュメントを取得
8. delete_documents → ドキュメントを削除
9. commit → 変更を永続化
Python バインディング概要
laurus-python パッケージは Laurus 検索エンジンの Python バインディングです。PyO3 と Maturin を使ってネイティブ Rust 拡張としてビルドされており、Python プログラムからネイティブに近いパフォーマンスで Laurus の Lexical 検索、Vector 検索、ハイブリッド検索機能を利用できます。
機能
- Lexical 検索 – BM25 スコアリングを備えた転置インデックスによる全文検索
- Vector 検索 – Flat、HNSW、IVF インデックスを使用した近似最近傍(ANN)検索
- ハイブリッド検索 – フュージョンアルゴリズム(RRF、WeightedSum)で Lexical と Vector の結果を統合
- 豊富なクエリ DSL – Term、Phrase、Fuzzy、Wildcard、NumericRange、Geo、Boolean、Span クエリ
- テキスト解析 – トークナイザー、フィルター、ステマー、同義語展開
- 柔軟なストレージ – インメモリ(一時的)またはファイルベース(永続的)インデックス
- Python らしい API – 型情報を備えた直感的な Python クラス
アーキテクチャ
graph LR
subgraph "laurus-python"
PyIndex["Index\n(Python クラス)"]
PyQuery["クエリクラス"]
PySearch["SearchRequest\n/ SearchResult"]
end
Python["Python アプリケーション"] -->|"メソッド呼び出し"| PyIndex
Python -->|"クエリオブジェクト"| PyQuery
PyIndex -->|"PyO3 FFI"| Engine["laurus::Engine\n(Rust)"]
PyQuery -->|"PyO3 FFI"| Engine
Engine --> Storage["ストレージ\n(Memory / File)"]
Python クラスは Rust エンジンの薄いラッパーです。 各呼び出しは PyO3 の FFI 境界を一度だけ越え、その後 Rust エンジンが操作をネイティブコードで実行します。
Rust エンジン内部は非同期 I/O を使用していますが、
Python 側のメソッドはすべて同期関数として公開されています。
これは Python の GIL(Global Interpreter Lock)の制約により、
単一インタプリタ内での真の並行実行ができないためです。
非同期 API にすると asyncio.run() が常に必要になり、
API が煩雑になります。代わりに、各メソッドは内部で
tokio::Runtime::block_on() を呼び出し、非同期 Rust を
同期 Python にブリッジしています。
注意: Node.js バインディング(
laurus-nodejs)では、 同じ Rust エンジンのメソッドをネイティブなasync/PromiseAPI として公開しています。 Node.js のイベントループは非同期をネイティブにサポート しているためです。
クイックスタート
import laurus
# インメモリインデックスを作成
index = laurus.Index()
# ドキュメントをインデックス
index.put_document("doc1", {"title": "Rust 入門", "body": "システムプログラミング言語です。"})
index.put_document("doc2", {"title": "Python データサイエンス", "body": "Python によるデータ解析。"})
index.commit()
# 検索
results = index.search("title:rust", limit=5)
for r in results:
print(f"[{r.id}] score={r.score:.4f} {r.document['title']}")
セクション
- インストール – パッケージのインストール方法
- クイックスタート – サンプルによるハンズオン入門
- API リファレンス – クラスとメソッドの完全リファレンス
インストール
PyPI からインストール
pip install laurus
ソースからビルド
ソースからビルドするには Rust ツールチェーン(1.75 以降)と Maturin が必要です。
# Maturin をインストール
pip install maturin
# リポジトリをクローン
git clone https://github.com/mosuka/laurus.git
cd laurus/laurus-python
# 開発モードでビルドとインストール
maturin develop
# またはリリースホイールをビルド
maturin build --release
pip install target/wheels/laurus-*.whl
動作確認
import laurus
index = laurus.Index()
print(index) # Index()
動作要件
- Python 3.8 以降
- コンパイル済みネイティブ拡張以外のランタイム依存関係なし
クイックスタート
1. インデックスを作成する
import laurus
# インメモリインデックス(一時的、プロトタイピングに最適)
index = laurus.Index()
# ファイルベースインデックス(永続的)
schema = laurus.Schema()
schema.add_text_field("title")
schema.add_text_field("body")
index = laurus.Index(path="./myindex", schema=schema)
2. ドキュメントをインデックスする
index.put_document("doc1", {
"title": "Rust 入門",
"body": "Rust は安全性とパフォーマンスに重点を置いたシステムプログラミング言語です。",
})
index.put_document("doc2", {
"title": "Python データサイエンス",
"body": "Python はデータ解析と機械学習に広く使われています。",
})
index.commit()
3. Lexical 検索
# DSL 文字列
results = index.search("title:rust", limit=5)
# クエリオブジェクト
results = index.search(laurus.TermQuery("body", "python"), limit=5)
# 結果を表示
for r in results:
print(f"[{r.id}] score={r.score:.4f} {r.document['title']}")
4. Vector 検索
Vector 検索にはベクトルフィールドを含むスキーマと事前計算済みエンベディングが必要です。
import laurus
schema = laurus.Schema()
schema.add_text_field("title")
schema.add_hnsw_field("embedding", dimension=4)
index = laurus.Index(schema=schema)
index.put_document("doc1", {"title": "Rust", "embedding": [0.1, 0.2, 0.3, 0.4]})
index.put_document("doc2", {"title": "Python", "embedding": [0.9, 0.8, 0.7, 0.6]})
index.commit()
query_vec = [0.1, 0.2, 0.3, 0.4]
results = index.search(laurus.VectorQuery("embedding", query_vec), limit=3)
5. ハイブリッド検索
request = laurus.SearchRequest(
lexical_query=laurus.TermQuery("title", "rust"),
vector_query=laurus.VectorQuery("embedding", query_vec),
fusion=laurus.RRF(k=60.0),
limit=5,
)
results = index.search(request)
6. 更新と削除
# 更新: put_document は同じ ID の全バージョンを置換する
index.put_document("doc1", {"title": "更新されたタイトル", "body": "新しいコンテンツ。"})
index.commit()
# 既存バージョンを削除せずに新しいバージョンを追記(RAG チャンキングパターン)
index.add_document("doc1", {"title": "チャンク 2", "body": "追加のチャンク。"})
index.commit()
# 全バージョンを取得
docs = index.get_documents("doc1")
# 削除
index.delete_documents("doc1")
index.commit()
7. スキーマ管理
schema = laurus.Schema()
schema.add_text_field("title")
schema.add_text_field("body")
schema.add_integer_field("year")
schema.add_float_field("score")
schema.add_boolean_field("published")
schema.add_bytes_field("thumbnail")
schema.add_geo_field("location")
schema.add_datetime_field("created_at")
schema.add_hnsw_field("embedding", dimension=384)
schema.add_flat_field("small_vec", dimension=64)
schema.add_ivf_field("ivf_vec", dimension=128, n_clusters=100)
8. インデックス統計
stats = index.stats()
print(stats["document_count"])
print(stats["vector_fields"])
API リファレンス
Index
Laurus 検索エンジンをラップするメインクラスです。
class Index:
def __init__(self, path: str | None = None, schema: Schema | None = None) -> None: ...
コンストラクタ
| パラメータ | 型 | デフォルト | 説明 |
|---|---|---|---|
path | str | None | None | 永続ストレージのディレクトリパス。None の場合はインメモリインデックスを作成します。 |
schema | Schema | None | None | スキーマ定義。省略時は空のスキーマが使用されます。 |
メソッド
| メソッド | 説明 |
|---|---|
put_document(id, doc) | ドキュメントをアップサート(upsert)します。同じ ID の既存バージョンをすべて置換します。 |
add_document(id, doc) | 既存バージョンを削除せずにドキュメントチャンクを追記します。 |
get_documents(id) -> list[dict] | 指定 ID の全保存バージョンを返します。 |
delete_documents(id) | 指定 ID の全バージョンを削除します。 |
commit() | バッファリングされた書き込みをフラッシュし、すべての保留中の変更を検索可能にします。 |
search(query, *, limit=10, offset=0) -> list[SearchResult] | 検索クエリを実行します。 |
stats() -> dict | インデックス統計(document_count、vector_fields)を返します。 |
search の query 引数
query パラメータは以下のいずれかを受け付けます:
- DSL 文字列(例:
"title:hello"、"content:\"memory safety\"") - Lexical クエリオブジェクト(
TermQuery、PhraseQuery、BooleanQueryなど) - Vector クエリオブジェクト(
VectorQuery、VectorTextQuery) SearchRequest(完全な制御が必要な場合)
Schema
Index のフィールドとインデックスタイプを定義します。
class Schema:
def __init__(self) -> None: ...
フィールドメソッド
| メソッド | 説明 |
|---|---|
add_text_field(name, *, stored=True, indexed=True, term_vectors=False, analyzer=None) | 全文フィールド(転置インデックス、BM25)。analyzer には組込名("standard" / "english" / "keyword" / "simple" / "noop"、または add_analyzer で登録したカスタム名)か、{"language": "japanese", "mode": "normal", "dict": "/var/lib/lindera/ipadic"} のようなパラメータ付きプリセットの dict を渡せます。文字列単独の "japanese" は Lindera 辞書パスが必須なため拒否されます。 |
add_integer_field(name, *, stored=True, indexed=True, multi_valued=False) | 64 ビット整数フィールド。multi_valued=True で整数配列を受け付け(範囲クエリは “any match”)。 |
add_float_field(name, *, stored=True, indexed=True, multi_valued=False) | 64 ビット浮動小数点フィールド。multi_valued=True で浮動小数点配列を受け付け(範囲クエリは “any match”)。 |
add_boolean_field(name, *, stored=True, indexed=True) | ブールフィールド。 |
add_bytes_field(name, *, stored=True) | 生バイトフィールド。 |
add_geo_field(name, *, stored=True, indexed=True) | 地理座標フィールド(緯度/経度)。 |
add_geo3d_field(name, *, stored=True, indexed=True) | 3D ECEF カルテシアン座標フィールド(x, y, z はメートル)。詳細は Geo3d の概念。 |
add_datetime_field(name, *, stored=True, indexed=True) | UTC 日時フィールド。 |
add_hnsw_field(name, dimension, *, distance="cosine", m=16, ef_construction=200, embedder=None) | HNSW 近似最近傍ベクトルフィールド。 |
add_flat_field(name, dimension, *, distance="cosine", embedder=None) | Flat(総当たり)ベクトルフィールド。 |
add_ivf_field(name, dimension, *, distance="cosine", n_clusters=100, n_probe=1, embedder=None) | IVF 近似最近傍ベクトルフィールド。 |
その他のメソッド
| メソッド | 説明 |
|---|---|
add_embedder(name, config) | 名前付きエンベダー定義を登録します。config は "type" キーを持つ辞書です(下記参照)。 |
set_default_fields(fields) | デフォルト検索フィールドを設定(文字列のリスト)。 |
set_dynamic_field_policy(policy) | 未宣言フィールドの扱いを設定。policy は "strict" / "dynamic"(デフォルト)/ "ignore"。詳細は下記を参照。 |
dynamic_field_policy() | 現在のポリシーを小文字の文字列で返す。 |
field_names() | 全フィールド名を返す。 |
Dynamic field policy(動的フィールドポリシー)
ドキュメントに含まれるがスキーマに宣言されていないフィールドの扱いを制御します:
"strict"— ドキュメントを拒否"dynamic"(デフォルト)— 各未宣言フィールドの型を推論してスキーマに追加。警告: integer フィールドに入ってきた float 値は静かに切り捨てられます(3.14→3)。厳密さが必要なら"strict"を使用してください"ignore"— 未宣言フィールドを静かに破棄
詳細な挙動マトリクスは スキーマとフィールド を参照してください。
エンベダータイプ
"type" | 必須キー | Feature Flag |
|---|---|---|
"precomputed" | – | (常に利用可能) |
"candle_bert" | "model" | embeddings-candle |
"candle_clip" | "model" | embeddings-multimodal |
"openai" | "model" | embeddings-openai |
距離メトリクス
| 値 | 説明 |
|---|---|
"cosine" | コサイン類似度(デフォルト) |
"euclidean" | ユークリッド距離 |
"dot_product" | 内積 |
"manhattan" | マンハッタン距離 |
"angular" | 角度距離 |
クエリクラス
TermQuery
TermQuery(field: str, term: str)
指定フィールドに完全一致する語句を含むドキュメントを検索します。
PhraseQuery
PhraseQuery(field: str, terms: list[str])
指定した語句が順序どおりに含まれるドキュメントを検索します。
FuzzyQuery
FuzzyQuery(field: str, term: str, *, max_edits: int = 2)
編集距離が max_edits 以内の近似一致を検索します。max_edits はキーワード専用引数です。
WildcardQuery
WildcardQuery(field: str, pattern: str)
ワイルドカードパターン検索。* は任意の文字列、? は任意の1文字に一致します。
NumericRangeQuery
NumericRangeQuery(field: str, *, min: int | float | None = None, max: int | float | None = None)
[min, max] の範囲内の数値を検索します。開いた境界には None を指定する
(または省略する)と開放されます。min と max はキーワード専用引数です。
数値型(整数または浮動小数点)は min/max の Python 型から推論されます。
GeoDistanceQuery
GeoDistanceQuery.within_radius(
field: str, lat: float, lon: float, distance_m: float,
)
地理的距離検索(半径指定)。指定した地点から distance_m メートル以内の
(lat, lon) 座標を持つドキュメントを返します。
GeoBoundingBoxQuery
GeoBoundingBoxQuery.within_bounding_box(
field: str,
min_lat: float, min_lon: float,
max_lat: float, max_lon: float,
)
地理的範囲(バウンディングボックス)検索。軸並行 [min_lat, max_lat] × [min_lon, max_lon] 内の (lat, lon) 座標を持つドキュメントを返します。
Geo3dDistanceQuery
Geo3dDistanceQuery.within_sphere(
field: str, x: float, y: float, z: float, distance_m: float,
)
3D ECEF 座標フィールドへの球距離検索。中心 (x, y, z) から distance_m メートル以内
の座標を持つドキュメントを返します。ECEF の理論については
Geo3d の概念 を参照。
Geo3dBoundingBoxQuery
Geo3dBoundingBoxQuery.within_box(
field: str,
min_x: float, min_y: float, min_z: float,
max_x: float, max_y: float, max_z: float,
)
軸並行 3D 範囲(AABB)検索。[min_x, max_x] × [min_y, max_y] × [min_z, max_z] 内
にある ECEF 座標を持つドキュメントを返します。
Geo3dNearestQuery
Geo3dNearestQuery.k_nearest(
field: str,
x: float, y: float, z: float,
k: int,
*,
initial_radius_m: float | None = None,
max_radius_m: float | None = None,
)
3D ECEF 座標フィールドへの k 最近傍検索。(x, y, z) から最も近い k 件のドキュ
メントを返します。initial_radius_m / max_radius_m は反復拡張サーチの探索コーン
を調整します。
BooleanQuery
bq = BooleanQuery()
bq.must(query)
bq.should(query)
bq.must_not(query)
複合ブールクエリ。引数なしでコンストラクタを呼び出し、must / should /
must_not メソッドで節を一つずつ追加します。各メソッドは任意のクエリ
オブジェクト(ネストされた BooleanQuery も含む)を受け付けます。
must 節はすべて一致する必要があり、must_not 節は一致してはなりません。
should 節はスコアリングに寄与し、must 節が無い場合は少なくとも1つが
一致する必要があります。
SpanQuery
# 単一語句
SpanQuery.term(field: str, term: str)
# Near: slop 位置以内の語句
SpanQuery.near(field: str, terms: list[str], *, slop: int = 0, ordered: bool = True)
# ネストされた SpanQuery 句を使った Near
SpanQuery.near_spans(field: str, clauses: list[SpanQuery], *, slop: int = 0, ordered: bool = True)
# Containing: big スパンが little スパンを含む
SpanQuery.containing(field: str, big: SpanQuery, little: SpanQuery)
# Within: 最大距離での include スパンと exclude スパン
SpanQuery.within(field: str, include: SpanQuery, exclude: SpanQuery, distance: int)
位置・近接スパンクエリ。静的ファクトリメソッドで構築します。near は語句
文字列のリストを受け取り、near_spans はネスト式のために SpanQuery
オブジェクトのリストを受け取ります。slop と ordered はキーワード専用
引数です。
VectorQuery
VectorQuery(field: str, vector: list[float])
事前計算済みエンベディングベクトルを使った近似最近傍検索を行います。
VectorTextQuery
VectorTextQuery(field: str, text: str)
クエリ時に text をエンベディングに変換してベクトル検索を行います。インデックスにエンベダーの設定が必要です。
SearchRequest
高度な制御が必要な場合の完全なリクエストクラスです。
class SearchRequest:
def __init__(
self,
*,
query=None,
lexical_query=None,
vector_query=None,
filter_query=None,
fusion=None,
limit: int = 10,
offset: int = 0,
) -> None: ...
| パラメータ | 説明 |
|---|---|
query | DSL 文字列または単一クエリオブジェクト。lexical_query / vector_query と排他的。 |
lexical_query | 明示的なハイブリッド検索の Lexical コンポーネント。 |
vector_query | 明示的なハイブリッド検索の Vector コンポーネント。 |
filter_query | スコアリング後に適用する Lexical フィルター。 |
fusion | フュージョンアルゴリズム(RRF または WeightedSum)。両コンポーネント指定時のデフォルトは RRF(k=60)。 |
limit | 最大結果件数(デフォルト 10)。 |
offset | ページネーションオフセット(デフォルト 0)。 |
SearchResult
Index.search() が返すクラスです。
class SearchResult:
id: str # 外部ドキュメント識別子
score: float # 関連性スコア
document: dict | None # 取得されたフィールド値。stored=False の場合は None
フュージョンアルゴリズム
RRF
RRF(k: float = 60.0)
逆順位フュージョン(Reciprocal Rank Fusion)。Lexical と Vector の結果リストを順位位置によってマージします。k は平滑化定数で、値が大きいほど上位ランクの影響が小さくなります。
WeightedSum
WeightedSum(lexical_weight: float = 0.5, vector_weight: float = 0.5)
両スコアリストをそれぞれ正規化した後、lexical_weight * lexical_score + vector_weight * vector_score として結合します。
テキスト解析
SynonymDictionary
class SynonymDictionary:
def __init__(self) -> None: ...
def add_synonym_group(self, synonyms: list[str]) -> None: ...
WhitespaceTokenizer
class WhitespaceTokenizer:
def __init__(self) -> None: ...
def tokenize(self, text: str) -> list[Token]: ...
SynonymGraphFilter
class SynonymGraphFilter:
def __init__(
self,
dictionary: SynonymDictionary,
keep_original: bool = True,
boost: float = 1.0,
) -> None: ...
def apply(self, tokens: list[Token]) -> list[Token]: ...
Token
class Token:
text: str
position: int
start_offset: int
end_offset: int
boost: float
stopped: bool
position_increment: int
position_length: int
フィールド値の型マッピング
Python の値は自動的に Laurus の DataValue 型に変換されます:
| Python 型 | Laurus 型 | 備考 |
|---|---|---|
None | Null | |
bool | Bool | int より先にチェック |
int | Int64 | |
float | Float64 | |
str | Text | |
bytes | Bytes | |
list[float] | Vector | 要素は f32 に変換 |
(lat, lon) タプル | Geo | 2 つの float 値 |
datetime.datetime | DateTime | isoformat() 経由で変換 |
Node.js バインディング概要
laurus-nodejs パッケージは、Laurus 検索エンジンの
Node.js/TypeScript バインディングです。
napi-rs を使用したネイティブアドオンとして
ビルドされており、Node.js プログラムから Laurus の Lexical 検索、
Vector 検索、ハイブリッド検索機能にネイティブに近い性能で
アクセスできます。
特徴
- Lexical 検索 – BM25 スコアリングによる転置インデックスベースの全文検索
- Vector 検索 – Flat、HNSW、IVF インデックスによる近似最近傍(ANN)検索
- ハイブリッド検索 – RRF、WeightedSum による Lexical と Vector の結果融合
- 豊富なクエリ DSL – Term、Phrase、Fuzzy、Wildcard、 NumericRange、Geo、Boolean、Span クエリ
- テキスト解析 – トークナイザー、フィルター、ステマー、同義語展開
- 柔軟なストレージ – インメモリ(揮発性)またはファイルベース(永続)
- TypeScript 型定義 –
.d.tsファイルの自動生成 - 非同期 API – 全 I/O 操作が Promise を返す
アーキテクチャ
graph LR
subgraph "laurus-nodejs"
JsIndex["Index\n(JS クラス)"]
JsQuery["Query クラス群"]
JsSearch["SearchRequest\n/ SearchResult"]
end
Node["Node.js アプリケーション"] -->|"メソッド呼び出し"| JsIndex
Node -->|"クエリオブジェクト"| JsQuery
JsIndex -->|"napi-rs FFI"| Engine["laurus::Engine\n(Rust)"]
JsQuery -->|"napi-rs FFI"| Engine
Engine --> Storage["Storage\n(Memory / File)"]
JavaScript クラスは Rust エンジンの薄いラッパーです。 各呼び出しは napi-rs の FFI 境界を一度だけ越え、 Rust エンジンが完全にネイティブコードで処理を実行します。
全 I/O メソッド(search、commit、putDocument 等)は
async で Promise を返します。napi-rs 内蔵の tokio
ランタイムで実行され、Node.js のイベントループをブロック
しません。Schema 構築、Query 作成、stats() は I/O を
伴わないため同期メソッドです。
注意: Python バインディング(
laurus-python)では、 同じ Rust エンジンのメソッドを同期関数として公開 しています。Python の GIL(Global Interpreter Lock)の 制約により非同期 API が煩雑になるためです。Node.js には この制約がないため、非同期 Rust エンジンを直接 Promise として公開しています。
クイックスタート
import { Index, Schema } from "laurus-nodejs";
// インメモリインデックスを作成
const schema = new Schema();
schema.addTextField("name");
schema.addTextField("description");
schema.setDefaultFields(["name", "description"]);
const index = await Index.create(null, schema);
// ドキュメントをインデックス
await index.putDocument("express", {
name: "Express",
description: "Fast minimalist web framework for Node.js.",
});
await index.putDocument("fastify", {
name: "Fastify",
description: "Fast and low overhead web framework.",
});
await index.commit();
// 検索
const results = await index.search("framework", 5);
for (const r of results) {
console.log(`[${r.id}] score=${r.score.toFixed(4)} ${r.document.name}`);
}
セクション
- インストール – パッケージのインストール方法
- クイックスタート – サンプルを使ったハンズオン入門
- API リファレンス – クラスとメソッドの完全なリファレンス
- 開発 – ソースからのビルド、テスト、プロジェクト構成
インストール
npm から
npm install laurus-nodejs
ソースから
ソースからビルドするには Rust ツールチェーン(1.85 以降)と Node.js 18 以上が必要です。
# リポジトリをクローン
git clone https://github.com/mosuka/laurus.git
cd laurus/laurus-nodejs
# 依存パッケージのインストール
npm install
# ネイティブモジュールのビルド(リリース)
npm run build
# デバッグモード(ビルドが速い)
npm run build:debug
確認
import { Index } from "laurus-nodejs";
const index = await Index.create();
console.log(index.stats());
// { documentCount: 0, vectorFields: {} }
要件
- Node.js 18 以上
- コンパイル済みネイティブアドオン以外のランタイム依存なし
クイックスタート
1. インデックスの作成
import { Index, Schema } from "laurus-nodejs";
// インメモリインデックス(揮発性、プロトタイピング向け)
const index = await Index.create();
// ファイルベースインデックス(永続化)
const schema = new Schema();
schema.addTextField("name");
schema.addTextField("description");
const persistentIndex = await Index.create("./myindex", schema);
2. ドキュメントのインデックス
await index.putDocument("express", {
name: "Express",
description: "Fast minimalist web framework for Node.js.",
});
await index.putDocument("fastify", {
name: "Fastify",
description: "Fast and low overhead web framework.",
});
await index.commit();
3. Lexical 検索
// DSL 文字列
const results = await index.search("name:express", 5);
// Term クエリ
const results2 = await index.searchTerm(
"description", "framework", 5,
);
// 結果の表示
for (const r of results) {
console.log(`[${r.id}] score=${r.score.toFixed(4)} ${r.document.name}`);
}
4. Vector 検索
Vector 検索にはベクトルフィールドを持つスキーマと 事前計算済みの埋め込みベクトルが必要です。
import { Index, Schema } from "laurus-nodejs";
const schema = new Schema();
schema.addTextField("name");
schema.addHnswField("embedding", 4);
const index = await Index.create(null, schema);
await index.putDocument("express", {
name: "Express",
embedding: [0.1, 0.2, 0.3, 0.4],
});
await index.putDocument("pg", {
name: "pg",
embedding: [0.9, 0.8, 0.7, 0.6],
});
await index.commit();
const results = await index.searchVector(
"embedding", [0.1, 0.2, 0.3, 0.4], 3,
);
5. ハイブリッド検索
import {
Index,
RRF,
SearchRequest,
TermQuery,
VectorQuery,
} from "laurus-nodejs";
const req = new SearchRequest({ limit: 5 });
req.setLexicalTerm(new TermQuery("name", "express"));
req.setVectorQuery(new VectorQuery("embedding", [0.1, 0.2, 0.3, 0.4]));
req.setRrfFusion(new RRF(60.0));
const results = await index.searchWithRequest(req);
6. 更新と削除
// 更新: putDocument は既存バージョンをすべて置換
await index.putDocument("express", {
name: "Express v5",
description: "Updated content.",
});
await index.commit();
// バージョン追記(RAG チャンキングパターン)
await index.addDocument("express", {
name: "Express chunk 2",
description: "Additional chunk.",
});
await index.commit();
// 全バージョンの取得
const docs = await index.getDocuments("express");
// 削除
await index.deleteDocuments("express");
await index.commit();
7. スキーマ管理
const schema = new Schema();
schema.addTextField("name");
schema.addTextField("description");
schema.addIntegerField("stars");
schema.addFloatField("score");
schema.addBooleanField("published");
schema.addBytesField("thumbnail");
schema.addGeoField("location");
schema.addDatetimeField("createdAt");
schema.addHnswField("embedding", 384);
schema.addFlatField("smallVec", 64);
schema.addIvfField("ivfVec", 128, "cosine", 100, 1);
8. インデックス統計
const stats = index.stats();
console.log(stats.documentCount);
console.log(stats.vectorFields);
API リファレンス
Index
主要なエントリポイント。Laurus 検索エンジンをラップします。
class Index {
static create(
path?: string | null,
schema?: Schema,
): Promise<Index>;
}
ファクトリメソッド
| パラメータ | 型 | デフォルト | 説明 |
|---|---|---|---|
path | string | null | null | 永続化ストレージのディレクトリ。null でインメモリ。 |
schema | Schema | 空 | スキーマ定義。 |
メソッド
| メソッド | 説明 |
|---|---|
putDocument(id, doc) | ドキュメントを上書き保存。 |
addDocument(id, doc) | 既存バージョンを残してチャンクを追記。 |
getDocuments(id) | 指定 ID の全バージョンを取得。 |
deleteDocuments(id) | 指定 ID の全バージョンを削除。 |
commit() | 書き込みをフラッシュし変更を検索可能にする。 |
search(query, limit?, offset?) | DSL 文字列で検索。 |
searchTerm(field, term, limit?, offset?) | 完全一致 Term 検索。 |
searchVector(field, vector, limit?, offset?) | 事前計算ベクトルで検索。 |
searchVectorText(field, text, limit?, offset?) | テキストを自動埋め込みして検索。 |
searchWithRequest(request) | SearchRequest で検索。 |
stats() | インデックス統計を返す。 |
ドキュメント操作と検索メソッドはすべて非同期で Promise を返します。
stats() は同期メソッドです。
Schema
Index のフィールドとインデックス型を定義します。
class Schema {
constructor();
}
フィールドメソッド
| メソッド | 説明 |
|---|---|
addTextField(name, stored?, indexed?, termVectors?, analyzer?) | 全文検索フィールド(転置インデックス、BM25)。analyzer にはパラメータ不要の組込名("standard" / "english" / "keyword" / "simple" / "noop"、または addAnalyzer で登録したカスタム名)を指定します。Lindera 辞書パスが必要な Japanese プリセットを使う場合は、lindera tokenizer を含むカスタム analyzer を登録して、その名前を参照してください。 |
addIntegerField(name, stored?, indexed?, multiValued?) | 64 ビット整数フィールド。multiValued: true で整数配列を受け付け(範囲クエリは “any match”)。 |
addFloatField(name, stored?, indexed?, multiValued?) | 64 ビット浮動小数点フィールド。multiValued: true で浮動小数点配列を受け付け(範囲クエリは “any match”)。 |
addBooleanField(name, stored?, indexed?) | 真偽値フィールド。 |
addBytesField(name, stored?) | バイナリデータフィールド。 |
addGeoField(name, stored?, indexed?) | 地理座標フィールド。 |
addGeo3dField(name, stored?, indexed?) | 3D ECEF カルテシアン座標フィールド(x, y, z はメートル)。詳細は Geo3d の概念。 |
addDatetimeField(name, stored?, indexed?) | UTC 日時フィールド。 |
addHnswField(name, dimension, distance?, m?, efConstruction?, embedder?) | HNSW ベクトルフィールド。 |
addFlatField(name, dimension, distance?, embedder?) | Flat(全探索)ベクトルフィールド。 |
addIvfField(name, dimension, distance?, nClusters?, nProbe?, embedder?) | IVF ベクトルフィールド。 |
addEmbedder(name, config) | 名前付き Embedder を登録。 |
setDefaultFields(fields) | デフォルト検索フィールドを設定。 |
setDynamicFieldPolicy(policy) | 未宣言フィールドの扱いを設定。policy は "strict" / "dynamic"(デフォルト)/ "ignore"。詳細は下記を参照。 |
dynamicFieldPolicy() | 現在のポリシーを小文字の文字列で返す。 |
fieldNames() | 全フィールド名を返す。 |
toString() | スキーマの文字列表現("Schema(fields=[...])" 形式)を返す。 |
Dynamic field policy(動的フィールドポリシー)
ドキュメントに含まれるがスキーマに宣言されていないフィールドの扱いを制御します:
"strict"— ドキュメントを拒否"dynamic"(デフォルト)— 各未宣言フィールドの型を推論してスキーマに追加。警告: integer フィールドに入ってきた float 値は静かに切り捨てられます(3.14→3)。厳密さが必要なら"strict"を使用してください"ignore"— 未宣言フィールドを静かに破棄
詳細な挙動マトリクスは スキーマとフィールド を参照してください。
距離指標
| 値 | 説明 |
|---|---|
"cosine" | コサイン類似度(デフォルト) |
"euclidean" | ユークリッド距離 |
"dot_product" | 内積 |
"manhattan" | マンハッタン距離 |
"angular" | 角度距離 |
クエリクラス
TermQuery
new TermQuery(field: string, term: string)
指定フィールドで完全一致する Term を含むドキュメントにマッチ。
PhraseQuery
new PhraseQuery(field: string, terms: string[])
指定順序で Term を含むドキュメントにマッチ。
FuzzyQuery
new FuzzyQuery(field: string, term: string, maxEdits?: number)
最大 maxEdits 編集距離までの近似マッチ(デフォルト 2)。
WildcardQuery
new WildcardQuery(field: string, pattern: string)
パターンマッチ。* は任意の文字列、? は任意の1文字。
NumericRangeQuery
new NumericRangeQuery(
field: string,
min?: number | null,
max?: number | null,
numericType?: "integer" | "float",
)
[min, max] 範囲の数値にマッチします。null(または省略)で開放端。
numericType は内部の範囲型を選択します("integer"(デフォルト)または
"float")。それ以外の値は例外をスローします。
GeoDistanceQuery
GeoDistanceQuery.withinRadius(
field: string, lat: number, lon: number, distanceM: number,
): GeoDistanceQuery
地理的距離検索(半径指定)。
GeoBoundingBoxQuery
GeoBoundingBoxQuery.withinBoundingBox(
field: string,
minLat: number, minLon: number,
maxLat: number, maxLon: number,
): GeoBoundingBoxQuery
地理的バウンディングボックス検索。
Geo3dDistanceQuery
Geo3dDistanceQuery.withinSphere(
field: string,
x: number, y: number, z: number,
distanceM: number,
): Geo3dDistanceQuery
3D ECEF 座標フィールドへの球距離検索。中心から distanceM メートル以内の (x, y, z)
座標を持つドキュメントを返します。ECEF の理論については
Geo3d の概念 を参照。
Geo3dBoundingBoxQuery
Geo3dBoundingBoxQuery.withinBox(
field: string,
minX: number, minY: number, minZ: number,
maxX: number, maxY: number, maxZ: number,
): Geo3dBoundingBoxQuery
軸並行 3D 範囲(AABB)検索。
Geo3dNearestQuery
Geo3dNearestQuery.kNearest(
field: string,
x: number, y: number, z: number,
k: number,
initialRadiusM?: number,
maxRadiusM?: number,
): Geo3dNearestQuery
3D ECEF 座標フィールドへの k 最近傍検索。initialRadiusM / maxRadiusM は
反復拡張サーチの探索コーンを調整します。
BooleanQuery
class BooleanQuery {
constructor();
// 各クエリタイプ X について(X は次のいずれか):
// { Term, Phrase, Fuzzy, Wildcard, NumericRange,
// GeoDistance, GeoBoundingBox,
// Geo3dDistance, Geo3dBoundingBox, Geo3dNearest,
// Boolean, Span }
mustX(query: X): void;
shouldX(query: X): void;
mustNotX(query: X): void;
}
MUST / SHOULD / MUST_NOT 句による複合ブーリアンクエリ。各句は対応するクエリ
クラスのインスタンスを引数に取ります。例:
mustTerm(new TermQuery("body", "rust")) や
shouldGeo3dNearest(Geo3dNearestQuery.kNearest(...))。
Node.js バインディングは多態 must(query) ではなく 36 個の per-type メソッド
(12 クエリタイプ × 3 極性)を公開しています。これは js_name を上書きした
クラスに対する napi-derive の Either<&T, ...> 引数バリデーションの制限を
回避するためです。
must 節はすべて一致する必要があり、mustNot 節は一致してはなりません。
should 節はスコアリングに寄与し、must 節が無い場合は少なくとも1つが
一致する必要があります。
const bq = new BooleanQuery();
bq.mustTerm(new TermQuery("body", "programming"));
bq.mustNotTerm(new TermQuery("title", "python"));
bq.shouldFuzzy(new FuzzyQuery("body", "data", 1));
SpanQuery
SpanQuery.term(field: string, term: string): SpanQuery
SpanQuery.near(
field: string, terms: string[],
slop?: number, ordered?: boolean,
): SpanQuery
SpanQuery.nearSpans(
field: string, clauses: SpanQuery[],
slop?: number, ordered?: boolean,
): SpanQuery
SpanQuery.containing(
field: string, big: SpanQuery, little: SpanQuery,
): SpanQuery
SpanQuery.within(
field: string,
include: SpanQuery, exclude: SpanQuery, distance: number,
): SpanQuery
位置・近接ベースのスパンクエリ。
VectorQuery
new VectorQuery(field: string, vector: number[])
事前計算済み埋め込みベクトルによる最近傍検索。
VectorTextQuery
new VectorTextQuery(field: string, text: string)
クエリ時にテキストを埋め込みに変換して検索。 インデックスに Embedder の設定が必要。
SearchRequest
高度な制御のための全機能検索リクエスト。
interface SearchRequestOptions {
queryDsl?: string;
limit?: number; // デフォルト 10
offset?: number; // デフォルト 0
}
class SearchRequest {
constructor(options?: SearchRequestOptions);
}
コンストラクタにはプリミティブな options を渡し、多態クエリ句は下記の
per-type セッターで設定します。BooleanQuery 同様、napi-derive の
Either<&T, ...> バリデーション制限を回避するため per-type 化されています。
DSL とフュージョンセッター
| メソッド | 説明 |
|---|---|
setQueryDsl(dsl: string) | DSL 文字列クエリを設定。 |
setRrfFusion(rrf: RRF) | RRF フュージョンを使用。 |
setWeightedSumFusion(ws: WeightedSum) | 加重和フュージョンを使用。 |
ベクトルセッター
| メソッド | 説明 |
|---|---|
setVectorQuery(query: VectorQuery) | 事前計算ベクトルクエリを設定。 |
setVectorTextQuery(query: VectorTextQuery) | テキストベースのベクトルクエリを設定(登録 Embedder で自動埋め込み)。 |
Lexical セッター(per-type)
X を { Term, Phrase, Fuzzy, Wildcard, NumericRange, GeoDistance, GeoBoundingBox, Geo3dDistance, Geo3dBoundingBox, Geo3dNearest, Boolean, Span } の各クエリタイプとして、以下のメソッドが公開されています:
| メソッド | 説明 |
|---|---|
setLexicalX(query: X) | 明示的なハイブリッドリクエストの Lexical コンポーネントを設定。 |
setFilterX(query: X) | スコアリング後のフィルタコンポーネントを設定。 |
合計 24 個の per-type セッター(12 lexical + 12 filter)に加え、上記の DSL / ベクトル / フュージョンセッターが利用可能です。
const req = new SearchRequest({ limit: 5 });
req.setLexicalTerm(new TermQuery("title", "rust"));
req.setVectorQuery(new VectorQuery("embedding", [0.1, 0.2, 0.3, 0.4]));
req.setRrfFusion(new RRF(60.0));
const results = await index.searchWithRequest(req);
SearchResult
検索メソッドが配列として返す結果。
interface SearchResult {
id: string; // 外部ドキュメント識別子
score: number; // 関連度スコア
document: object | null; // 取得フィールド、stored=false の場合は null
}
融合アルゴリズム
RRF
new RRF(k?: number) // デフォルト 60.0
Reciprocal Rank Fusion。ランク位置で Lexical と Vector の 結果リストを統合。
WeightedSum
new WeightedSum(
lexicalWeight?: number, // デフォルト 0.5
vectorWeight?: number, // デフォルト 0.5
)
両スコアリストを個別に正規化し、加重和で結合。
テキスト解析
SynonymDictionary
class SynonymDictionary {
constructor();
addSynonymGroup(terms: string[]): void;
}
WhitespaceTokenizer
class WhitespaceTokenizer {
constructor();
tokenize(text: string): Token[];
}
SynonymGraphFilter
class SynonymGraphFilter {
constructor(
dictionary: SynonymDictionary,
keepOriginal?: boolean, // デフォルト true
boost?: number, // デフォルト 1.0
);
apply(tokens: Token[]): Token[];
}
Token
interface Token {
text: string;
position: number;
startOffset: number;
endOffset: number;
boost: number;
stopped: boolean;
positionIncrement: number;
positionLength: number;
}
フィールド値の型
JavaScript の値は自動的に Laurus の DataValue 型に変換されます:
| JavaScript 型 | Laurus 型 | 備考 |
|---|---|---|
null | Null | |
boolean | Bool | |
number(整数) | Int64 | |
number(浮動小数点) | Float64 | |
string | Text | ISO 8601 文字列は DateTime になる |
number[] | Vector | f32 に変換 |
{ lat, lon } | Geo | 2 つの number 値 |
{ x, y, z } | GeoEcef | 3 つの number 値(メートル単位、3D ECEF 直交座標) |
開発環境のセットアップ
laurus-nodejs バインディングのローカル開発環境の構築、
ビルド、テスト実行について説明します。
前提条件
- Rust 1.85 以降(Cargo 含む)
- Node.js 18 以降(npm 含む)
- リポジトリがローカルにクローン済み
git clone https://github.com/mosuka/laurus.git
cd laurus
ビルド
開発ビルド
デバッグモードで Rust ネイティブアドオンをコンパイルします。 Rust ソースを変更した後は再実行してください。
cd laurus-nodejs
npm install
npm run build:debug
リリースビルド
npm run build
ビルドの確認
node -e "
const { Index } = require('./index.js');
Index.create().then(idx => console.log(idx.stats()));
"
// { documentCount: 0, vectorFields: {} }
テスト
テストには Vitest を使用し、
__tests__/ に配置されています。
# 全テスト実行
npm test
特定のテストを名前で実行:
npx vitest run -t "searches with DSL string"
リントとフォーマット
# Rust リント(Clippy)
cargo clippy -p laurus-nodejs -- -D warnings
# Rust フォーマットチェック
cargo fmt -p laurus-nodejs --check
# フォーマット適用
cargo fmt -p laurus-nodejs
クリーンアップ
# ビルド成果物の削除
rm -f *.node index.js index.d.ts
# node_modules の削除
rm -rf node_modules
プロジェクト構成
laurus-nodejs/
├── Cargo.toml # Rust クレートマニフェスト
├── build.rs # napi-build セットアップ
├── package.json # npm パッケージメタデータ
├── README.md # 英語 README
├── README_ja.md # 日本語 README
├── src/ # Rust ソース(napi-rs バインディング)
│ ├── lib.rs # モジュール登録
│ ├── index.rs # Index クラス
│ ├── schema.rs # Schema クラス
│ ├── query.rs # Query クラス群
│ ├── search.rs # SearchRequest / SearchResult / Fusion
│ ├── analysis.rs # Tokenizer / Filter / Token
│ ├── convert.rs # JS ↔ DataValue 変換
│ └── errors.rs # エラーマッピング
├── __tests__/ # Vitest 統合テスト
│ └── index.spec.mjs
└── examples/ # 実行可能な Node.js サンプル
├── quickstart.mjs
├── lexical-search.mjs
├── vector-search.mjs
└── hybrid-search.mjs
WASM バインディング概要
laurus-wasm パッケージは、Laurus 検索エンジンの WebAssembly バインディングです。
サーバーなしで、ブラウザやエッジランタイム(Cloudflare Workers、Vercel Edge Functions、Deno Deploy)
上で直接、レキシカル検索・ベクトル検索・ハイブリッド検索を実行できます。
機能
- レキシカル検索 – BM25 スコアリングによる転置インデックスベースの全文検索
- ベクトル検索 – Flat、HNSW、IVF インデックスによる近似最近傍探索
- ハイブリッド検索 – RRF、WeightedSum による融合アルゴリズム
- クエリ DSL – Term、Phrase、Fuzzy、Wildcard、NumericRange、Geo、Boolean、Span
- テキスト分析 – トークナイザー、フィルター、同義語展開
- インメモリストレージ – 高速な一時インデックス
- OPFS 永続化 – Origin Private File System によるページリロード後のデータ保持
- TypeScript 型定義 – 自動生成される
.d.tsファイル - 非同期 API – すべての I/O 操作は Promise を返す
アーキテクチャ
graph LR
subgraph "laurus-wasm"
WASM[wasm-bindgen API]
end
subgraph "laurus(コア)"
Engine
MemoryStorage
end
subgraph "ブラウザ"
JS[JavaScript / TypeScript]
OPFS[Origin Private File System]
end
JS --> WASM
WASM --> Engine
Engine --> MemoryStorage
WASM -.->|永続化| OPFS
Embedding 戦略
ネイティブ環境では Laurus に複数の組み込み Embedder(Candle BERT、Candle CLIP、
OpenAI API)が用意されており、ドキュメントのインデックス時や
searchVectorText("field", "query text") 実行時にエンジンが自動で呼び出します。
これらネイティブ Embedder は wasm32-unknown-unknown 上で動作しないため、
WASM ビルドでは無効化されています:
| Embedder | Dependency | Why it cannot run in WASM |
|---|---|---|
candle_bert | candle (GPU/SIMD) | Requires native SIMD intrinsics and file system for models |
candle_clip | candle | Same as above |
openai | reqwest (HTTP) | Requires a full async HTTP client (tokio + TLS) |
(これらは embeddings-candle / embeddings-openai Feature Flags で管理されており、
wasm32-unknown-unknown で無効化される native feature に依存するため
WASM ビルドから除外されます。)
laurus-wasm ではその代わりに以下 2 種類の addEmbedder タイプを公開しています:
"precomputed"— 呼び出し側がputDocument()/searchVector()経由で ベクトルを直接渡します。エンジンは埋め込みを行いません。"callback"— JavaScript コールバックembed: (text) => Promise<number[]>を登録し、エンジンがインジェスト時およびsearchVectorText()から呼び出します。Transformers.js 等のブラウザ内埋め込み ライブラリと組み合わせることでエンジン内自動埋め込みが実現でき、ネイティブ環境と 同じくsearchVectorText("field", "query text")を呼び出すだけで利用できます。
Option A — 事前計算済みベクトル
JavaScript 側で埋め込みを計算し、事前計算済みベクトルを putDocument() と
searchVector() に渡します:
// Transformers.js を使用(all-MiniLM-L6-v2、384次元)
import { pipeline } from '@huggingface/transformers';
const embedder = await pipeline('feature-extraction', 'Xenova/all-MiniLM-L6-v2');
async function embed(text) {
const output = await embedder(text, { pooling: 'mean', normalize: true });
return Array.from(output.data);
}
// 事前計算済み埋め込みでインデックス
const vec = await embed("Rust 入門");
await index.putDocument("doc1", { title: "Rust 入門", embedding: vec });
await index.commit();
// 事前計算済みクエリ埋め込みで検索
const queryVec = await embed("安全なシステムプログラミング");
const results = await index.searchVector("embedding", queryVec);
このアプローチにより、ネイティブ環境と同じ Sentence Transformer モデルを使った セマンティック検索がブラウザ内で実現できます。埋め込み計算は candle ではなく Transformers.js(ONNX Runtime Web)が担当します。
Option B — Callback Embedder
Transformers.js の同じパイプラインを "callback" Embedder として登録すれば、
エンジンが自動で呼び出してくれます。登録後は呼び出し側がベクトルを管理することなく、
インジェストおよび searchVectorText() が透過的に動作します:
import { pipeline } from '@huggingface/transformers';
const extractor = await pipeline('feature-extraction', 'Xenova/all-MiniLM-L6-v2');
schema.addEmbedder("transformers", {
type: "callback",
embed: async (text) => {
const output = await extractor(text, { pooling: 'mean', normalize: true });
return Array.from(output.data);
},
});
schema.addHnswField("embedding", 384, "cosine", undefined, undefined, "transformers");
const index = await Index.create(schema);
await index.putDocument("doc1", { title: "Rust 入門" });
await index.commit();
const results = await index.searchVectorText("embedding", "安全なシステムプログラミング");
Option A と比べると、Callback アプローチではエンジンがインジェスト時に埋め込みを
キャッシュでき、書き込み側と読み出し側で埋め込みコードを重複させずに済みます。
ただし commit() のたびに JS コールバックの解決を待つため、大量バルク投入時には
事前計算ベクトルのほうが有利な場合があります。
laurus-wasm と laurus-nodejs の使い分け
| 基準 | laurus-wasm | laurus-nodejs |
|---|---|---|
| 実行環境 | ブラウザ、エッジランタイム | Node.js サーバー |
| パフォーマンス | 良好(シングルスレッド) | 最高(ネイティブ、マルチスレッド) |
| ストレージ | インメモリ + OPFS | インメモリ + ファイルシステム |
| 埋め込み | 事前計算 + JS コールバック | Candle、OpenAI、事前計算 |
| パッケージ | npm install laurus-wasm | npm install laurus-nodejs |
| バイナリサイズ | 約 5-10 MB(WASM) | プラットフォームネイティブ |
インストール
npm / yarn / pnpm
npm install laurus-wasm
# または
yarn add laurus-wasm
# または
pnpm add laurus-wasm
CDN(ES Module)
<script type="module">
import init, { Index, Schema } from 'https://unpkg.com/laurus-wasm/laurus_wasm.js';
await init();
// ...
</script>
ソースからビルド
前提条件:
git clone https://github.com/mosuka/laurus.git
cd laurus/laurus-wasm
# バンドラー向け(webpack、vite 等)
wasm-pack build --target bundler --release
# ブラウザ直接利用向け(<script type="module">)
wasm-pack build --target web --release
出力は pkg/ ディレクトリに生成されます。
ブラウザ対応状況
laurus-wasm は以下をサポートするブラウザが必要です:
- WebAssembly(すべてのモダンブラウザ)
- ES Modules
OPFS 永続化には以下のブラウザが対応しています:
| ブラウザ | 最小バージョン |
|---|---|
| Chrome | 102+ |
| Firefox | 111+ |
| Safari | 15.2+ |
| Edge | 102+ |
クイックスタート
基本的な使い方(インメモリ)
import init, { Index, Schema } from 'laurus-wasm';
// WASM モジュールを初期化
await init();
// スキーマを定義
const schema = new Schema();
schema.addTextField("title");
schema.addTextField("body");
schema.setDefaultFields(["title", "body"]);
// インメモリインデックスを作成
const index = await Index.create(schema);
// ドキュメントを追加
await index.putDocument("doc1", {
title: "Rust 入門",
body: "Rust はシステムプログラミング言語です"
});
await index.putDocument("doc2", {
title: "WebAssembly ガイド",
body: "WASM はブラウザでネイティブに近いパフォーマンスを実現します"
});
await index.commit();
// 検索
const results = await index.search("rust");
for (const result of results) {
console.log(`${result.id}: ${result.score}`);
console.log(result.document);
}
永続化ストレージ(OPFS)
import init, { Index, Schema } from 'laurus-wasm';
await init();
const schema = new Schema();
schema.addTextField("title");
schema.addTextField("body");
// 永続化インデックスを開く(ページリロード後もデータが保持される)
const index = await Index.open("my-index", schema);
// ドキュメントを追加
await index.putDocument("doc1", {
title: "Hello",
body: "World"
});
// commit() で自動的に OPFS に永続化される
await index.commit();
// 次のページロード時、Index.open("my-index") でデータが復元される
日本語形態素検索
ブラウザ WASM では Lindera 辞書をファイルシステムパスで指定できないため、 OPFS にロードした IPADIC のバイト列から analyzer を構築します。
import init, { Index, Schema, JapaneseAnalyzer } from 'laurus-wasm';
import {
downloadDictionary,
loadDictionaryFiles,
hasDictionary,
} from 'laurus-wasm/opfs';
await init();
// 1. 初回訪問時に IPADIC アーカイブを OPFS にキャッシュする。zip は
// アプリと同一オリジンで配信する必要がある(GitHub Releases は CORS
// でブロックされる)。圧縮 ~16 MB / 展開後 ~58 MB。
if (!(await hasDictionary("ipadic"))) {
await downloadDictionary("./dict/lindera-ipadic.zip", "ipadic", {
onProgress: ({ phase, loaded, total }) => console.log(phase, loaded, total),
});
}
// 2. 8 つのコンポーネントファイルを読み出して analyzer を構築する。
const f = await loadDictionaryFiles("ipadic");
const ja = JapaneseAnalyzer.fromBytes(
f.metadata, f.dictDa, f.dictVals, f.dictWordsIdx,
f.dictWords, f.matrixMtx, f.charDef, f.unk,
"normal",
);
// 3. analyzer をスキーマに登録し、テキストフィールドから名前で参照する。
const schema = new Schema();
schema.addAnalyzer("ja-ipadic", ja);
schema.addTextField("title", undefined, undefined, undefined, "ja-ipadic");
schema.addTextField("body", undefined, undefined, undefined, "ja-ipadic");
schema.setDefaultFields(["title", "body"]);
const index = await Index.create(schema);
await index.putDocument("doc1", {
title: "形態素解析",
body: "Lindera は Rust 製の形態素解析ライブラリです。",
});
await index.commit();
const results = await index.search("形態素");
console.log(results[0].document.title); // "形態素解析"
JapaneseAnalyzer.fromBytes の完全なシグネチャと OPFS ヘルパ API は
API リファレンス を参照してください。
ベクトル検索
import init, { Index, Schema } from 'laurus-wasm';
await init();
const schema = new Schema();
schema.addTextField("title");
schema.addHnswField("embedding", 3); // 3次元ベクトル
const index = await Index.create(schema);
await index.putDocument("doc1", {
title: "Rust",
embedding: [1.0, 0.0, 0.0]
});
await index.putDocument("doc2", {
title: "Python",
embedding: [0.0, 1.0, 0.0]
});
await index.commit();
// ベクトル類似度で検索
const results = await index.searchVector("embedding", [0.9, 0.1, 0.0]);
console.log(results[0].document.title); // "Rust"
バンドラーでの利用
Vite
// vite.config.js
import wasm from 'vite-plugin-wasm';
export default {
plugins: [wasm()]
};
Webpack 5
Webpack 5 は asyncWebAssembly で WASM をネイティブサポートしています:
// webpack.config.js
module.exports = {
experiments: {
asyncWebAssembly: true
}
};
API リファレンス
Index
検索インデックスの作成・クエリを行うメインエントリポイントです。
静的メソッド
Index.create(schema?)
新しいインメモリ(一時)インデックスを作成します。
- 引数:
schema(Schema, 省略可) – スキーマ定義
- 戻り値:
Promise<Index>
Index.open(name, schema?)
OPFS で永続化されたインデックスを開くか、新規作成します。
- 引数:
name(string) – インデックス名(OPFS サブディレクトリ)schema(Schema, 省略可) – スキーマ定義
- 戻り値:
Promise<Index>
インスタンスメソッド
putDocument(id, document)
ドキュメントを置換(upsert)します。
- 引数:
id(string) – ドキュメント識別子document(object) – スキーマフィールドに対応するキーバリューペア
- 戻り値:
Promise<void>
addDocument(id, document)
ドキュメントバージョンを追加します(マルチバージョン RAG パターン)。
- 引数・戻り値:
putDocumentと同じ
getDocuments(id)
ドキュメントの全バージョンを取得します。
- 引数:
id(string) - 戻り値:
Promise<object[]>
deleteDocuments(id)
ドキュメントの全バージョンを削除します。
- 引数:
id(string) - 戻り値:
Promise<void>
commit()
書き込みをフラッシュし、変更を検索可能にします。
Index.open() で作成したインデックスの場合、OPFS にも自動永続化されます。
- 戻り値:
Promise<void>
search(query, limit?, offset?)
DSL 文字列クエリで検索します。
- 引数:
query(string) – クエリ DSL(例:"title:hello")limit(number, デフォルト 10)offset(number, デフォルト 0)
- 戻り値:
Promise<SearchResult[]>
searchTerm(field, term, limit?, offset?)
完全一致タームで検索します。
- 引数:
field(string) – フィールド名term(string) – 検索タームlimit,offset(number, 省略可)
- 戻り値:
Promise<SearchResult[]>
searchVector(field, vector, limit?, offset?)
ベクトル類似度で検索します。
- 引数:
field(string) – ベクトルフィールド名vector(number[]) – クエリ埋め込みベクトルlimit,offset(number, 省略可)
- 戻り値:
Promise<SearchResult[]>
searchVectorText(field, text, limit?, offset?)
テキストで検索します(登録された埋め込み器で変換)。
- 引数:
field(string) – ベクトルフィールド名text(string) – 埋め込み対象テキストlimit,offset(number, 省略可)
- 戻り値:
Promise<SearchResult[]>
searchGeo3dDistance(field, x, y, z, distanceM, limit?, offset?)
3D ECEF 座標フィールドへの球距離検索。中心 (x, y, z) から distanceM メートル以内
の座標を持つドキュメントを返します。ECEF の理論については
Geo3d の概念 を参照。
- 引数:
field(string) – Geo3d フィールド名x,y,z(number) – 中心 ECEF 座標(メートル)distanceM(number) – 中心からの最大距離(メートル)limit,offset(number, 省略可)
- 戻り値:
Promise<SearchResult[]>
searchGeo3dBoundingBox(field, minX, minY, minZ, maxX, maxY, maxZ, limit?, offset?)
3D ECEF 座標フィールドへの軸並行範囲(AABB)検索。
- 引数:
field(string) – Geo3d フィールド名minX,minY,minZ,maxX,maxY,maxZ(number) – 範囲境界(メートル)limit,offset(number, 省略可)
- 戻り値:
Promise<SearchResult[]>
searchGeo3dNearest(field, x, y, z, k, limit?, offset?, initialRadiusM?, maxRadiusM?)
3D ECEF 座標フィールドへの k 最近傍検索。(x, y, z) から最も近い k 件のドキュ
メントを返します。initialRadiusM / maxRadiusM(オプション)で反復拡張サーチの
探索コーンを調整できます。
- 引数:
field(string) – Geo3d フィールド名x,y,z(number) – 中心 ECEF 座標(メートル)k(number) – 返す近傍件数limit,offset(number, 省略可)initialRadiusM,maxRadiusM(number, 省略可)
- 戻り値:
Promise<SearchResult[]>
stats()
インデックス統計を返します。
- 戻り値:
{ documentCount: number, vectorFields: { [name]: { count, dimension } } }
Schema
インデックスフィールドと埋め込み器を定義するビルダーです。
コンストラクタ
new Schema()
空のスキーマを作成します。
メソッド
addTextField(name, stored?, indexed?, termVectors?, analyzer?)
全文検索テキストフィールドを追加します。analyzer にはパラメータ不要の
組込名("standard" / "english" / "keyword" / "simple" /
"noop")または addAnalyzer() で登録したランタイム analyzer 名を
指定します。
日本語の形態素解析を行う場合は、まず JapaneseAnalyzer を IPADIC の
バイト列から構築し、addAnalyzer() で登録してください。
JapaneseAnalyzer.fromBytes
と addAnalyzer を参照。
addIntegerField(name, stored?, indexed?, multiValued?)
64 ビット整数フィールドを追加します。multiValued: true を指定すると整数配列を受け付け、
範囲クエリはいずれかの値が条件を満たせばマッチ(Lucene 流の “any match”、constant スコア)します。
addFloatField(name, stored?, indexed?, multiValued?)
64 ビット浮動小数点フィールドを追加します。multiValued: true を指定すると浮動小数点配列を受け付け、
範囲クエリはいずれかの値が条件を満たせばマッチ(Lucene 流の “any match”、constant スコア)します。
addBooleanField(name, stored?, indexed?)
真偽値フィールドを追加します。
addDatetimeField(name, stored?, indexed?)
日時フィールドを追加します。
addGeoField(name, stored?, indexed?)
地理座標フィールドを追加します。
addGeo3dField(name, stored?, indexed?)
3D ECEF カルテシアン座標フィールド(x, y, z はメートル)を追加します。値は
{ x, y, z } オブジェクトで投入します。詳細は
Geo3d の概念 を参照。
WASM バインディングは Geo3dDistanceQuery / Geo3dBoundingBoxQuery /
Geo3dNearestQuery を JS クラスとして公開していません(wasm-bindgen は
dyn Query トレイトオブジェクトを公開できないため)。代わりに上記の
Index.searchGeo3dDistance / Index.searchGeo3dBoundingBox /
Index.searchGeo3dNearest メソッドを使用してください。
addBytesField(name, stored?)
バイナリデータフィールドを追加します。
addHnswField(name, dimension, distance?, m?, efConstruction?, embedder?)
HNSW ベクトルインデックスフィールドを追加します。
distance:"cosine"(デフォルト)、"euclidean"、"dot_product"、"manhattan"、"angular"m: 分岐係数(デフォルト 16)efConstruction: 構築時の探索幅(デフォルト 200)
addFlatField(name, dimension, distance?, embedder?)
全探索ベクトルインデックスフィールドを追加します。
addIvfField(name, dimension, distance?, nClusters?, nProbe?, embedder?)
IVF ベクトルインデックスフィールドを追加します。
nClusters: パーティショニングクラスタ数(デフォルト 100)nProbe: 検索時にプローブするクラスタ数(デフォルト 1)
addAnalyzer(name, analyzer)
事前に構築した analyzer インスタンスを name で登録します。テキスト
フィールドが Named 形式で analyzer を参照するときに、組込名や
schema.analyzers 定義よりも先に解決されます。
現状は JapaneseAnalyzer.fromBytes
で構築した JapaneseAnalyzer のみ受け付けます。ブラウザ WASM では
{ "language": "japanese", "dict": ... } プリセットがファイルシステム
パスを解決できないため、ランタイムレジストリ経由が日本語 analyzer を
利用する唯一の現実的な経路です。
import { JapaneseAnalyzer, Schema } from "laurus-wasm";
import { downloadDictionary, loadDictionaryFiles } from "laurus-wasm/opfs";
await downloadDictionary("./dict/lindera-ipadic.zip", "ipadic");
const f = await loadDictionaryFiles("ipadic");
const ja = JapaneseAnalyzer.fromBytes(
f.metadata, f.dictDa, f.dictVals, f.dictWordsIdx,
f.dictWords, f.matrixMtx, f.charDef, f.unk, "normal",
);
const schema = new Schema();
schema.addAnalyzer("ja-ipadic", ja);
schema.addTextField("body", undefined, undefined, undefined, "ja-ipadic");
addEmbedder(name, config)
名前付き埋め込み器を登録します。WASM では以下の 2 種類の type をサポートします:
"precomputed"— 埋め込みは行いません。ベクトルはputDocument()/searchVector()経由で直接渡します。"callback"— JavaScript コールバックembed: (text) => Promise<number[]>を 登録します。エンジンがインジェスト時およびsearchVectorText()で呼び出します。 Transformers.js などのブラウザ内埋め込みライブラリと組み合わせることで、 エンジン内自動埋め込みが可能になります。
// Precomputed embedder
schema.addEmbedder("precomputed-embedder", { type: "precomputed" });
// Callback embedder(例: Transformers.js)
schema.addEmbedder("callback-embedder", {
type: "callback",
embed: async (text) => {
const output = await pipeline(text, { pooling: "mean", normalize: true });
return Array.from(output.data);
},
});
setDefaultFields(fields)
デフォルト検索フィールドを設定します。
setDynamicFieldPolicy(policy)
ドキュメントに含まれるがスキーマに宣言されていないフィールドの扱いを設定します。policy は "strict" / "dynamic"(デフォルト)/ "ignore" のいずれか(大文字小文字を無視)。不正な値を渡すと例外をスローします。
"strict"— ドキュメントを拒否"dynamic"— 各未宣言フィールドの型を推論してスキーマに追加。警告: integer フィールドに入ってきた float 値は静かに切り捨てられます(3.14→3)"ignore"— 未宣言フィールドを静かに破棄
詳細な挙動マトリクスは スキーマとフィールド を参照してください。
dynamicFieldPolicy()
現在のポリシーを小文字の文字列で返します。
fieldNames()
定義済みフィールド名の配列を返します。
toString()
スキーマの文字列表現("Schema(fields=[...])" 形式)を返します。
SearchResult
interface SearchResult {
id: string;
score: number;
document: object | null;
}
Analysis
JapaneseAnalyzer
Lindera 辞書のバイト列から構築する日本語形態素解析 analyzer。
ブラウザ WASM には実ファイルシステムが無いため、標準の
{ "language": "japanese", "dict": "/path/to/ipadic" } プリセットは
利用できません。代わりに Lindera 辞書アーカイブ(典型的には
lindera-ipadic-X.Y.Z.zip)を取得して OPFS ヘルパ で
OPFS に保存し、8 つのコンポーネントバイト配列を
JapaneseAnalyzer.fromBytes に渡してください。
JapaneseAnalyzer.fromBytes(metadata, dictDa, ..., mode?)
IPADIC のバイト列から analyzer を構築する static ファクトリ。
引数(mode 以外はすべて Uint8Array):
| 引数 | 対応するファイル |
|---|---|
metadata | metadata.json |
dictDa | dict.da(Double-Array Trie) |
dictVals | dict.vals |
dictWordsIdx | dict.wordsidx |
dictWords | dict.words |
matrixMtx | matrix.mtx |
charDef | char_def.bin |
unk | unk.bin |
mode | "normal"(デフォルト)/ "search" / "decompose" |
いずれかのコンポーネントの deserialization に失敗した場合、または mode 文字列が不正な場合は throw します。
import { JapaneseAnalyzer } from "laurus-wasm";
import { loadDictionaryFiles } from "laurus-wasm/opfs";
const f = await loadDictionaryFiles("ipadic");
const ja = JapaneseAnalyzer.fromBytes(
f.metadata, f.dictDa, f.dictVals, f.dictWordsIdx,
f.dictWords, f.matrixMtx, f.charDef, f.unk,
"normal",
);
パイプラインは
NFKC 正規化 → 日本語 iteration mark 正規化 → Lindera 形態素解析 → lowercase → 日本語 stop word フィルタ
で、ネイティブ側の japanese プリセットと完全に一致します。
OPFS ヘルパ
laurus-wasm/opfs サブパスは、Lindera 辞書をブラウザの Origin
Private File System にダウンロード・保存・読込するヘルパを提供します。
JapaneseAnalyzer.fromBytes と組み合わせて使用します。
import {
downloadDictionary,
loadDictionaryFiles,
hasDictionary,
listDictionaries,
removeDictionary,
} from "laurus-wasm/opfs";
| 関数 | 説明 |
|---|---|
downloadDictionary(url, name, options?) | .zip を fetch し、Web の DecompressionStream API で展開して、Lindera 8 ファイルを OPFS の laurus/dictionaries/<name>/ 配下に保存します。options.onProgress({ phase, loaded?, total? }) で進捗通知を受け取れます。 |
loadDictionaryFiles(name) | 8 ファイルを { metadata, dictDa, dictVals, dictWordsIdx, dictWords, matrixMtx, charDef, unk } オブジェクトとして読み出し、JapaneseAnalyzer.fromBytes にそのまま渡せる形にします。 |
hasDictionary(name) | 辞書ディレクトリが OPFS にあれば true。 |
listDictionaries() | 保存済み辞書名の配列を返します。 |
removeDictionary(name) | 辞書ディレクトリを削除します。 |
ブラウザ CORS の制約により GitHub Releases から直接 fetch できないため、
zip はアプリと同一オリジンで配信してください(Laurus デモではデプロイ
時に ./dict/lindera-ipadic.zip を WASM と同じパスに同梱します)。
WhitespaceTokenizer
const tokenizer = new WhitespaceTokenizer();
const tokens = tokenizer.tokenize("hello world");
// [{ text, position, startOffset, endOffset, boost, stopped, positionIncrement, positionLength }]
空白を境界としてテキストを分割し、Token オブジェクトの配列を返します。
SynonymDictionary
const dict = new SynonymDictionary();
dict.addSynonymGroup(["ml", "machine learning"]);
同義語グループの辞書。グループ内のすべての語句が互いに同義語として扱われます。
SynonymGraphFilter
new SynonymGraphFilter(dictionary, keepOriginal = true, boost = 1.0)
dictionary(SynonymDictionary) — 同義語グループのソース。keepOriginal(boolean, デフォルトtrue) — 元のトークンを挿入された同義語と 並べて保持します。boost(number, デフォルト1.0) — 挿入される同義語トークンに適用される スコアブースト。
const filter = new SynonymGraphFilter(dict, true, 0.8);
const expanded = filter.apply(tokens);
SynonymDictionary の同義語でトークンを展開するトークンフィルターです。
開発
前提条件
rustup target add wasm32-unknown-unknown
cargo install wasm-pack
ビルド
cd laurus-wasm
# デバッグビルド(コンパイル高速)
wasm-pack build --target web --dev
# リリースビルド(最適化)
wasm-pack build --target web --release
# バンドラーターゲット(webpack、vite 等)
wasm-pack build --target bundler --release
プロジェクト構成
laurus-wasm/
├── Cargo.toml # Rust 依存関係(wasm-bindgen、laurus コア)
├── package.json # npm パッケージメタデータ
├── src/
│ ├── lib.rs # モジュール宣言
│ ├── index.rs # Index クラス(CRUD + 検索)
│ ├── schema.rs # Schema ビルダー
│ ├── search.rs # SearchRequest / SearchResult
│ ├── query.rs # クエリ型定義
│ ├── convert.rs # JsValue ↔ Document 変換
│ ├── analysis.rs # トークナイザー / フィルターラッパー
│ ├── errors.rs # LaurusError → JsValue 変換
│ └── storage.rs # OPFS 永続化レイヤー
└── js/
└── opfs_bridge.js # Origin Private File System 用 JS グルーコード
アーキテクチャノート
ストレージ戦略
laurus-wasm は二層ストレージアプローチを採用しています:
-
MemoryStorage(ランタイム) – すべての読み書き操作は Laurus の インメモリストレージを経由します。これは
StorageトレイトのSend + Sync要件を満たします。 -
OPFS(永続化) –
commit()時に MemoryStorage の全状態が OPFS ファイルにシリアライズされます。Index.open()時に OPFS ファイルが MemoryStorage にロードされます。
この設計により、JS ハンドルの Send + Sync 非互換性を回避しつつ、
コアエンジンを変更せずに永続化を実現しています。
Feature Flags
laurus コアは Feature Flags で WASM をサポートしています:
# laurus-wasm はデフォルト機能なしで laurus に依存
laurus = { workspace = true, default-features = false }
これにより、ネイティブ専用の依存関係(tokio/full、rayon、memmap2 等)が
除外され、#[cfg(target_arch = "wasm32")] フォールバックで並列処理が
逐次処理に切り替わります。
テスト
# ビルド確認
cargo build -p laurus-wasm --target wasm32-unknown-unknown
# Clippy
cargo clippy -p laurus-wasm --target wasm32-unknown-unknown -- -D warnings
ブラウザテストは wasm-pack test で実行できます:
wasm-pack test --headless --chrome
Ruby バインディング概要
laurus gem は Laurus 検索エンジンの Ruby バインディングです。Magnus と rb_sys を使ってネイティブ Rust 拡張としてビルドされており、Ruby プログラムからネイティブに近いパフォーマンスで Laurus の Lexical 検索、Vector 検索、ハイブリッド検索機能を利用できます。
機能
- Lexical 検索 – BM25 スコアリングを備えた転置インデックスによる全文検索
- Vector 検索 – Flat、HNSW、IVF インデックスを使用した近似最近傍(ANN)検索
- ハイブリッド検索 – フュージョンアルゴリズム(RRF、WeightedSum)で Lexical と Vector の結果を統合
- 豊富なクエリ DSL – Term、Phrase、Fuzzy、Wildcard、NumericRange、Geo、Boolean、Span クエリ
- テキスト解析 – トークナイザー、フィルター、ステマー、同義語展開
- 柔軟なストレージ – インメモリ(一時的)またはファイルベース(永続的)インデックス
- Ruby らしい API –
Laurus::名前空間の直感的な Ruby クラス
アーキテクチャ
graph LR
subgraph "laurus-ruby (gem)"
RbIndex["Index\n(Ruby クラス)"]
RbQuery["クエリクラス"]
RbSearch["SearchRequest\n/ SearchResult"]
end
Ruby["Ruby アプリケーション"] -->|"メソッド呼び出し"| RbIndex
Ruby -->|"クエリオブジェクト"| RbQuery
RbIndex -->|"Magnus FFI"| Engine["laurus::Engine\n(Rust)"]
RbQuery -->|"Magnus FFI"| Engine
Engine --> Storage["ストレージ\n(Memory / File)"]
Ruby クラスは Rust エンジンの薄いラッパーです。 各呼び出しは Magnus の FFI 境界を一度だけ越え、その後 Rust エンジンが操作をネイティブコードで実行します。
Rust エンジン内部は非同期 I/O を使用していますが、
Ruby 側のメソッドはすべて同期関数として公開されています。
各メソッドは内部で tokio::Runtime::block_on() を呼び出し、
非同期 Rust を同期 Ruby にブリッジしています。
クイックスタート
require "laurus"
# インメモリインデックスを作成
index = Laurus::Index.new
# ドキュメントをインデックス
index.put_document("doc1", { "title" => "Rust 入門", "body" => "システムプログラミング言語です。" })
index.put_document("doc2", { "title" => "Ruby Web 開発", "body" => "Ruby による Web アプリケーション。" })
index.commit
# 検索
results = index.search("title:rust", limit: 5)
results.each do |r|
puts "[#{r.id}] score=#{format('%.4f', r.score)} #{r.document['title']}"
end
セクション
- インストール – gem のインストール方法
- クイックスタート – サンプルによるハンズオン入門
- API リファレンス – クラスとメソッドの完全リファレンス
- 開発 – ソースからのビルドとテスト実行
インストール
RubyGems からインストール
gem install laurus
または Gemfile に追加します:
gem "laurus"
その後、以下を実行します:
bundle install
ソースからビルド
ソースからビルドするには Rust ツールチェーン(1.85 以降)と rb_sys が必要です。
# リポジトリをクローン
git clone https://github.com/mosuka/laurus.git
cd laurus/laurus-ruby
# 依存関係をインストール
bundle install
# ネイティブ拡張をコンパイル
bundle exec rake compile
# またはローカルに gem をインストール
gem build laurus.gemspec
gem install laurus-*.gem
動作確認
require "laurus"
index = Laurus::Index.new
puts index # Index()
動作要件
- Ruby 3.1 以降
- Rust ツールチェーン(gem インストール時に
rb_sys経由で自動的に呼び出されます) - コンパイル済みネイティブ拡張以外のランタイム依存関係なし
クイックスタート
1. インデックスを作成する
require "laurus"
# インメモリインデックス(一時的、プロトタイピングに最適)
index = Laurus::Index.new
# ファイルベースインデックス(永続的)
schema = Laurus::Schema.new
schema.add_text_field("title")
schema.add_text_field("body")
index = Laurus::Index.new(path: "./myindex", schema: schema)
2. ドキュメントをインデックスする
index.put_document("doc1", {
"title" => "Rust 入門",
"body" => "Rust は安全性とパフォーマンスに重点を置いたシステムプログラミング言語です。",
})
index.put_document("doc2", {
"title" => "Ruby Web 開発",
"body" => "Ruby は Web アプリケーションと高速プロトタイピングに広く使われています。",
})
index.commit
3. Lexical 検索
# DSL 文字列
results = index.search("title:rust", limit: 5)
# クエリオブジェクト
results = index.search(Laurus::TermQuery.new("body", "ruby"), limit: 5)
# 結果を表示
results.each do |r|
puts "[#{r.id}] score=#{format('%.4f', r.score)} #{r.document['title']}"
end
4. Vector 検索
Vector 検索にはベクトルフィールドを含むスキーマと事前計算済みエンベディングが必要です。
require "laurus"
schema = Laurus::Schema.new
schema.add_text_field("title")
schema.add_hnsw_field("embedding", 4)
index = Laurus::Index.new(schema: schema)
index.put_document("doc1", { "title" => "Rust", "embedding" => [0.1, 0.2, 0.3, 0.4] })
index.put_document("doc2", { "title" => "Ruby", "embedding" => [0.9, 0.8, 0.7, 0.6] })
index.commit
query_vec = [0.1, 0.2, 0.3, 0.4]
results = index.search(Laurus::VectorQuery.new("embedding", query_vec), limit: 3)
5. ハイブリッド検索
request = Laurus::SearchRequest.new(
lexical_query: Laurus::TermQuery.new("title", "rust"),
vector_query: Laurus::VectorQuery.new("embedding", query_vec),
fusion: Laurus::RRF.new(k: 60.0),
limit: 5,
)
results = index.search(request)
6. 更新と削除
# 更新: put_document は同じ ID の全バージョンを置換する
index.put_document("doc1", { "title" => "更新されたタイトル", "body" => "新しいコンテンツ。" })
index.commit
# 既存バージョンを削除せずに新しいバージョンを追記(RAG チャンキングパターン)
index.add_document("doc1", { "title" => "チャンク 2", "body" => "追加のチャンク。" })
index.commit
# 全バージョンを取得
docs = index.get_documents("doc1")
# 削除
index.delete_documents("doc1")
index.commit
7. スキーマ管理
schema = Laurus::Schema.new
schema.add_text_field("title")
schema.add_text_field("body")
schema.add_integer_field("year")
schema.add_float_field("score")
schema.add_boolean_field("published")
schema.add_bytes_field("thumbnail")
schema.add_geo_field("location")
schema.add_datetime_field("created_at")
schema.add_hnsw_field("embedding", 384)
schema.add_flat_field("small_vec", 64)
schema.add_ivf_field("ivf_vec", 128, n_clusters: 100)
8. インデックス統計
stats = index.stats
puts stats["document_count"]
puts stats["vector_fields"]
API リファレンス
Index
Laurus 検索エンジンをラップするメインクラスです。
Laurus::Index.new(path: nil, schema: nil)
コンストラクタ
| パラメータ | 型 | デフォルト | 説明 |
|---|---|---|---|
path: | String | nil | nil | 永続ストレージのディレクトリパス。nil の場合はインメモリインデックスを作成します。 |
schema: | Schema | nil | nil | スキーマ定義。省略時は空のスキーマが使用されます。 |
メソッド
| メソッド | 説明 |
|---|---|
put_document(id, doc) | ドキュメントをアップサート(upsert)します。同じ ID の既存バージョンをすべて置換します。 |
add_document(id, doc) | 既存バージョンを削除せずにドキュメントチャンクを追記します。 |
get_documents(id) -> Array<Hash> | 指定 ID の全保存バージョンを返します。 |
delete_documents(id) | 指定 ID の全バージョンを削除します。 |
commit | バッファリングされた書き込みをフラッシュし、すべての保留中の変更を検索可能にします。 |
search(query, limit: 10, offset: 0) -> Array<SearchResult> | 検索クエリを実行します。 |
stats -> Hash | インデックス統計("document_count"、"vector_fields")を返します。 |
search の query 引数
query パラメータは以下のいずれかを受け付けます:
- DSL 文字列(例:
"title:hello"、"content:\"memory safety\"") - Lexical クエリオブジェクト(
TermQuery、PhraseQuery、BooleanQueryなど) - Vector クエリオブジェクト(
VectorQuery、VectorTextQuery) SearchRequest(完全な制御が必要な場合)
Schema
Index のフィールドとインデックスタイプを定義します。
Laurus::Schema.new
フィールドメソッド
| メソッド | 説明 |
|---|---|
add_text_field(name, stored: true, indexed: true, term_vectors: false, analyzer: nil) | 全文フィールド(転置インデックス、BM25)。analyzer: にはパラメータ不要の組込名("standard" / "english" / "keyword" / "simple" / "noop"、または add_analyzer で登録したカスタム名)を指定します。Lindera 辞書パスが必要な Japanese プリセットは、lindera tokenizer を含むカスタム analyzer として登録し、名前で参照してください。 |
add_integer_field(name, stored: true, indexed: true, multi_valued: false) | 64 ビット整数フィールド。multi_valued: true で整数配列を受け付け(範囲クエリは “any match”)。 |
add_float_field(name, stored: true, indexed: true, multi_valued: false) | 64 ビット浮動小数点フィールド。multi_valued: true で浮動小数点配列を受け付け(範囲クエリは “any match”)。 |
add_boolean_field(name, stored: true, indexed: true) | ブールフィールド。 |
add_bytes_field(name, stored: true) | 生バイトフィールド。 |
add_geo_field(name, stored: true, indexed: true) | 地理座標フィールド(緯度/経度)。 |
add_geo3d_field(name, stored: true, indexed: true) | 3D ECEF カルテシアン座標フィールド(x, y, z はメートル)。詳細は Geo3d の概念。 |
add_datetime_field(name, stored: true, indexed: true) | UTC 日時フィールド。 |
add_hnsw_field(name, dimension, distance: "cosine", m: 16, ef_construction: 200, embedder: nil) | HNSW 近似最近傍ベクトルフィールド。 |
add_flat_field(name, dimension, distance: "cosine", embedder: nil) | Flat(総当たり)ベクトルフィールド。 |
add_ivf_field(name, dimension, distance: "cosine", n_clusters: 100, n_probe: 1, embedder: nil) | IVF 近似最近傍ベクトルフィールド。 |
その他のメソッド
| メソッド | 説明 |
|---|---|
add_embedder(name, config) | 名前付きエンベダー定義を登録します。config は "type" キーを持つ Hash です(下記参照)。 |
set_default_fields(fields) | クエリでフィールドが指定されていない場合に使用するデフォルトフィールドを設定します。fields は文字列の配列です。 |
set_dynamic_field_policy(policy) | 未宣言フィールドの扱いを設定します。policy は "strict" / "dynamic"(デフォルト)/ "ignore"。詳細は下記を参照。 |
dynamic_field_policy -> String | 現在のポリシーを小文字の文字列で返します。 |
field_names -> Array<String> | このスキーマに定義されたフィールド名のリストを返します。 |
Dynamic field policy(動的フィールドポリシー)
ドキュメントに含まれるがスキーマに宣言されていないフィールドの扱いを制御します:
"strict"— ドキュメントを拒否"dynamic"(デフォルト)— 各未宣言フィールドの型を推論してスキーマに追加。警告: integer フィールドに入ってきた float 値は静かに切り捨てられます(3.14→3)。厳密さが必要なら"strict"を使用してください"ignore"— 未宣言フィールドを静かに破棄
詳細な挙動マトリクスは スキーマとフィールド を参照してください。
エンベダータイプ
"type" | 必須キー | Feature Flag |
|---|---|---|
"precomputed" | – | (常に利用可能) |
"candle_bert" | "model" | embeddings-candle |
"candle_clip" | "model" | embeddings-multimodal |
"openai" | "model" | embeddings-openai |
距離メトリクス
| 値 | 説明 |
|---|---|
"cosine" | コサイン類似度(デフォルト) |
"euclidean" | ユークリッド距離 |
"dot_product" | 内積 |
"manhattan" | マンハッタン距離 |
"angular" | 角度距離 |
クエリクラス
TermQuery
Laurus::TermQuery.new(field, term)
指定フィールドに完全一致する語句を含むドキュメントを検索します。
PhraseQuery
Laurus::PhraseQuery.new(field, terms)
指定した語句が順序どおりに含まれるドキュメントを検索します。terms は文字列の配列です。
FuzzyQuery
Laurus::FuzzyQuery.new(field, term, max_edits: 2)
編集距離が max_edits 以内の近似一致を検索します。
WildcardQuery
Laurus::WildcardQuery.new(field, pattern)
ワイルドカードパターン検索。* は任意の文字列、? は任意の1文字に一致します。
NumericRangeQuery
Laurus::NumericRangeQuery.new(field, min: nil, max: nil)
[min, max] の範囲内の数値を検索します。開いた境界には nil を指定します。型(整数または浮動小数点)は min/max の Ruby 型から推論されます。
GeoDistanceQuery
Laurus::GeoDistanceQuery.within_radius(field, lat, lon, distance_m)
地理的距離検索(半径指定)。指定した地点から distance_m メートル以内の
(lat, lon) 座標を持つドキュメントを返します。
GeoBoundingBoxQuery
Laurus::GeoBoundingBoxQuery.within_bounding_box(
field, min_lat, min_lon, max_lat, max_lon,
)
地理的範囲(バウンディングボックス)検索。軸並行 [min_lat, max_lat] × [min_lon, max_lon] 内の (lat, lon) 座標を持つドキュメントを返します。
Geo3dDistanceQuery
Laurus::Geo3dDistanceQuery.within_sphere(field, x, y, z, distance_m)
3D ECEF 座標フィールドへの球距離検索。中心 (x, y, z) から distance_m メートル以内
の座標を持つドキュメントを返します。ECEF の理論については
Geo3d の概念 を参照。
Geo3dBoundingBoxQuery
Laurus::Geo3dBoundingBoxQuery.within_box(
field,
min_x, min_y, min_z,
max_x, max_y, max_z,
)
軸並行 3D 範囲(AABB)検索。
Geo3dNearestQuery
Laurus::Geo3dNearestQuery.k_nearest(
field, x, y, z, k,
initial_radius_m: nil,
max_radius_m: nil,
)
3D ECEF 座標フィールドへの k 最近傍検索。initial_radius_m: / max_radius_m:
キーワード引数(オプション)で反復拡張サーチの探索コーンを調整できます。
BooleanQuery
bq = Laurus::BooleanQuery.new
bq.must(query)
bq.should(query)
bq.must_not(query)
複合ブールクエリ。must 節はすべて一致する必要があり、must_not 節は一致してはなりません。should 節はスコアリングに寄与し、must 節が無い場合は少なくとも1つが一致する必要があります。
SpanQuery
# 単一語句
Laurus::SpanQuery.term(field, term)
# Near: slop 位置以内の語句
Laurus::SpanQuery.near(field, terms, slop: 0, ordered: true)
# ネストされた SpanQuery 句を使った Near
Laurus::SpanQuery.near_spans(field, clauses, slop: 0, ordered: true)
# Containing: big スパンが little スパンを含む
Laurus::SpanQuery.containing(field, big, little)
# Within: 最大距離での include スパンと exclude スパン
Laurus::SpanQuery.within(field, include_span, exclude_span, distance)
位置・近接スパンクエリ。near は語句文字列の配列を受け取り、near_spans はネスト式のために SpanQuery オブジェクトの配列を受け取ります。
VectorQuery
Laurus::VectorQuery.new(field, vector)
事前計算済みエンベディングベクトルを使った近似最近傍検索を行います。vector は Float の配列です。
VectorTextQuery
Laurus::VectorTextQuery.new(field, text)
クエリ時に text をエンベディングに変換してベクトル検索を行います。インデックスにエンベダーの設定が必要です。
SearchRequest
高度な制御が必要な場合の完全なリクエストクラスです。
Laurus::SearchRequest.new(
query: nil,
lexical_query: nil,
vector_query: nil,
filter_query: nil,
fusion: nil,
limit: 10,
offset: 0,
)
| パラメータ | 説明 |
|---|---|
query: | DSL 文字列または単一クエリオブジェクト。lexical_query: / vector_query: と排他的。 |
lexical_query: | 明示的なハイブリッド検索の Lexical コンポーネント。 |
vector_query: | 明示的なハイブリッド検索の Vector コンポーネント。 |
filter_query: | スコアリング後に適用する Lexical フィルター。 |
fusion: | フュージョンアルゴリズム(RRF または WeightedSum)。両コンポーネント指定時のデフォルトは RRF(k: 60)。 |
limit: | 最大結果件数(デフォルト 10)。 |
offset: | ページネーションオフセット(デフォルト 0)。 |
SearchResult
Index#search が返すクラスです。
result.id # => String -- 外部ドキュメント識別子
result.score # => Float -- 関連性スコア
result.document # => Hash|nil -- 取得されたフィールド値。削除済みの場合は nil
フュージョンアルゴリズム
RRF
Laurus::RRF.new(k: 60.0)
逆順位フュージョン(Reciprocal Rank Fusion)。Lexical と Vector の結果リストを順位位置によってマージします。k は平滑化定数で、値が大きいほど上位ランクの影響が小さくなります。
WeightedSum
Laurus::WeightedSum.new(lexical_weight: 0.5, vector_weight: 0.5)
両スコアリストをそれぞれ正規化した後、lexical_weight * lexical_score + vector_weight * vector_score として結合します。
テキスト解析
SynonymDictionary
dict = Laurus::SynonymDictionary.new
dict.add_synonym_group(["fast", "quick", "rapid"])
同義語グループの辞書です。グループ内のすべての語句は互いの同義語として扱われます。
WhitespaceTokenizer
tokenizer = Laurus::WhitespaceTokenizer.new
tokens = tokenizer.tokenize("hello world")
空白で分割してテキストをトークン化し、Token オブジェクトの配列を返します。
SynonymGraphFilter
filter = Laurus::SynonymGraphFilter.new(dictionary, keep_original: true, boost: 1.0)
expanded = filter.apply(tokens)
SynonymDictionary の同義語でトークンを展開するトークンフィルターです。
Token
token.text # => String -- トークンテキスト
token.position # => Integer -- トークンストリーム内の位置
token.start_offset # => Integer -- 元テキスト内の文字開始オフセット
token.end_offset # => Integer -- 元テキスト内の文字終了オフセット
token.boost # => Float -- スコアブースト係数(1.0 = 調整なし)
token.stopped # => Boolean -- ストップフィルターによって除去されたかどうか
token.position_increment # => Integer -- 前のトークンの位置との差分
token.position_length # => Integer -- このトークンがカバーする位置数
フィールド値の型マッピング
Ruby の値は自動的に Laurus の DataValue 型に変換されます:
| Ruby 型 | Laurus 型 | 備考 |
|---|---|---|
nil | Null | |
true / false | Bool | |
Integer | Int64 | |
Float | Float64 | |
String | Text | |
Array(数値) | Vector | 要素は f32 に変換 |
Hash("lat", "lon") | Geo | 2 つの Float 値 |
Hash("x", "y", "z") | GeoEcef | 3 つの Float 値(メートル単位、3D ECEF 直交座標) |
Time / String(iso8601 に応答) | DateTime | iso8601 経由で変換 |
開発環境のセットアップ
このページでは laurus-ruby バインディングのローカル開発環境の構築、ビルド、テストスイートの実行方法について説明します。
前提条件
- Rust 1.85 以降(Cargo 含む)
- Ruby 3.1 以降(Bundler 含む)
- リポジトリがローカルにクローンされていること
git clone https://github.com/mosuka/laurus.git
cd laurus
ビルド
開発ビルド
Rust ネイティブ拡張をデバッグモードでコンパイルします。Rust ソースを変更した場合は再実行してください。
cd laurus-ruby
bundle install
bundle exec rake compile
リリースビルド
gem build laurus.gemspec
ビルドの確認
ruby -e "
require 'laurus'
index = Laurus::Index.new
puts index.stats
"
# {"document_count"=>0, "vector_fields"=>{}}
テスト
テストは Minitest を使用しており、test/ ディレクトリにあります。
# 全テスト実行
bundle exec rake test
特定のテストファイルを実行する場合:
bundle exec ruby -Ilib -Itest test/test_index.rb
Lint とフォーマット
# Rust lint(Clippy)
cargo clippy -p laurus-ruby -- -D warnings
# Rust フォーマットチェック
cargo fmt -p laurus-ruby --check
# フォーマット適用
cargo fmt -p laurus-ruby
クリーンアップ
# ビルド成果物を削除
bundle exec rake clean
# インストールされた gem を削除
rm -rf vendor/bundle
プロジェクト構成
laurus-ruby/
├── Cargo.toml # Rust クレートマニフェスト
├── laurus.gemspec # Gem 仕様
├── Gemfile # Bundler 依存関係ファイル
├── Rakefile # Rake タスク(compile、test、clean)
├── lib/
│ └── laurus.rb # Ruby エントリポイント(ネイティブ拡張をロード)
├── ext/
│ └── laurus_ruby/ # ネイティブ拡張ビルド設定
│ └── extconf.rb # rb_sys 拡張設定
├── src/ # Rust ソース(Magnus バインディング)
│ ├── lib.rs # モジュール登録
│ ├── index.rs # Index クラス
│ ├── schema.rs # Schema クラス
│ ├── query.rs # クエリクラス
│ ├── search.rs # SearchRequest / SearchResult / Fusion
│ ├── analysis.rs # Tokenizer / Filter / Token
│ ├── convert.rs # Ruby ↔ DataValue 変換
│ └── errors.rs # エラーマッピング
├── test/ # Minitest テスト
│ ├── test_helper.rb
│ └── test_index.rb
└── examples/ # 実行可能な Ruby サンプル
PHP バインディング概要
laurus PHP エクステンションは Laurus 検索エンジンの PHP バインディングです。ext-php-rs を使ってネイティブ Rust 拡張としてビルドされており、PHP プログラムからネイティブに近いパフォーマンスで Laurus の Lexical 検索、Vector 検索、ハイブリッド検索機能を利用できます。
機能
- Lexical 検索 – BM25 スコアリングを備えた転置インデックスによる全文検索
- Vector 検索 – Flat、HNSW、IVF インデックスを使用した近似最近傍(ANN)検索
- ハイブリッド検索 – フュージョンアルゴリズム(RRF、WeightedSum)で Lexical と Vector の結果を統合
- 豊富なクエリ DSL – Term、Phrase、Fuzzy、Wildcard、NumericRange、Geo、Boolean、Span クエリ
- テキスト解析 – トークナイザー、フィルター、ステマー、同義語展開
- 柔軟なストレージ – インメモリ(一時的)またはファイルベース(永続的)インデックス
- PHP らしい API –
Laurus\名前空間の直感的な PHP クラス
アーキテクチャ
graph LR
subgraph "laurus-php (extension)"
PhpIndex["Index\n(PHP クラス)"]
PhpQuery["クエリクラス"]
PhpSearch["SearchRequest\n/ SearchResult"]
end
PHP["PHP アプリケーション"] -->|"メソッド呼び出し"| PhpIndex
PHP -->|"クエリオブジェクト"| PhpQuery
PhpIndex -->|"ext-php-rs FFI"| Engine["laurus::Engine\n(Rust)"]
PhpQuery -->|"ext-php-rs FFI"| Engine
Engine --> Storage["ストレージ\n(Memory / File)"]
PHP クラスは Rust エンジンの薄いラッパーです。 各呼び出しは ext-php-rs の FFI 境界を一度だけ越え、その後 Rust エンジンが操作をネイティブコードで実行します。
Rust エンジン内部は非同期 I/O を使用していますが、
PHP 側のメソッドはすべて同期関数として公開されています。
各メソッドは内部で tokio::Runtime::block_on() を呼び出し、
非同期 Rust を同期 PHP にブリッジしています。
クイックスタート
<?php
use Laurus\Index;
// インメモリインデックスを作成
$index = new Index();
// ドキュメントをインデックス
$index->putDocument("doc1", ["title" => "Introduction to Rust", "body" => "Systems programming language."]);
$index->putDocument("doc2", ["title" => "PHP for Web Development", "body" => "Web applications with PHP."]);
$index->commit();
// 検索
$results = $index->search("title:rust", 5);
foreach ($results as $r) {
printf("[%s] score=%.4f %s\n", $r->getId(), $r->getScore(), $r->getDocument()["title"]);
}
セクション
- インストール – エクステンションのインストール方法
- クイックスタート – サンプルによるハンズオン入門
- API リファレンス – クラスとメソッドの完全リファレンス
- 開発 – ソースからのビルドとテスト実行
インストール
ソースからビルド
ソースからビルドするには Rust ツールチェーン(1.85 以降)と PHP 8.1 以降(開発ヘッダー付き)が必要です。
# リポジトリをクローン
git clone https://github.com/mosuka/laurus.git
cd laurus/laurus-php
# ネイティブ拡張をビルド
cargo build --release
# 共有ライブラリを PHP エクステンションディレクトリにコピー
# (正確なパスは OS と PHP バージョンによって異なります)
cp ../target/release/liblaurus_php.so $(php -r "echo ini_get('extension_dir');")
次に php.ini にエクステンションを追加します:
extension=laurus_php.so
または、コマンドラインでエクステンションをロードすることもできます:
php -d extension=liblaurus_php.so your_script.php
動作確認
<?php
use Laurus\Index;
$index = new Index();
echo $index; // Index()
動作要件
- PHP 8.1 以降(開発ヘッダー付き:
php-dev/php-devel) - Rust ツールチェーン 1.85 以降(Cargo 含む)
- コンパイル済みネイティブ拡張以外のランタイム依存関係なし
クイックスタート
1. インデックスを作成する
<?php
use Laurus\Index;
use Laurus\Schema;
// インメモリインデックス(一時的、プロトタイピングに最適)
$index = new Index();
// ファイルベースインデックス(永続的)
$schema = new Schema();
$schema->addTextField("title");
$schema->addTextField("body");
$index = new Index("./myindex", $schema);
2. ドキュメントをインデックスする
$index->putDocument("doc1", [
"title" => "Introduction to Rust",
"body" => "Rust is a systems programming language focused on safety and performance.",
]);
$index->putDocument("doc2", [
"title" => "PHP for Web Development",
"body" => "PHP is widely used for web applications and rapid prototyping.",
]);
$index->commit();
3. Lexical 検索
// DSL 文字列
$results = $index->search("title:rust", 5);
// クエリオブジェクト
$results = $index->search(new \Laurus\TermQuery("body", "php"), 5);
// 結果を表示
foreach ($results as $r) {
printf("[%s] score=%.4f %s\n", $r->getId(), $r->getScore(), $r->getDocument()["title"]);
}
4. Vector 検索
Vector 検索にはベクトルフィールドを含むスキーマと事前計算済みエンベディングが必要です。
<?php
use Laurus\Index;
use Laurus\Schema;
use Laurus\VectorQuery;
$schema = new Schema();
$schema->addTextField("title");
$schema->addHnswField("embedding", 4);
$index = new Index(null, $schema);
$index->putDocument("doc1", ["title" => "Rust", "embedding" => [0.1, 0.2, 0.3, 0.4]]);
$index->putDocument("doc2", ["title" => "PHP", "embedding" => [0.9, 0.8, 0.7, 0.6]]);
$index->commit();
$queryVec = [0.1, 0.2, 0.3, 0.4];
$results = $index->search(new VectorQuery("embedding", $queryVec), 3);
5. ハイブリッド検索
use Laurus\SearchRequest;
use Laurus\TermQuery;
use Laurus\VectorQuery;
use Laurus\RRF;
$request = new SearchRequest(
query: null,
lexicalQuery: new TermQuery("title", "rust"),
vectorQuery: new VectorQuery("embedding", $queryVec),
filterQuery: null,
fusion: new RRF(60.0),
limit: 5,
);
$results = $index->search($request);
6. 更新と削除
// 更新: putDocument は同じ ID の全バージョンを置換する
$index->putDocument("doc1", ["title" => "Updated Title", "body" => "New content."]);
$index->commit();
// 既存バージョンを削除せずに新しいバージョンを追記(RAG チャンキングパターン)
$index->addDocument("doc1", ["title" => "Chunk 2", "body" => "Additional chunk."]);
$index->commit();
// 全バージョンを取得
$docs = $index->getDocuments("doc1");
// 削除
$index->deleteDocuments("doc1");
$index->commit();
7. スキーマ管理
$schema = new \Laurus\Schema();
$schema->addTextField("title");
$schema->addTextField("body");
$schema->addIntegerField("year");
$schema->addFloatField("score");
$schema->addBooleanField("published");
$schema->addBytesField("thumbnail");
$schema->addGeoField("location");
$schema->addDatetimeField("created_at");
$schema->addHnswField("embedding", 384);
$schema->addFlatField("small_vec", 64);
$schema->addIvfField("ivf_vec", 128, "cosine", 100, 1);
8. インデックス統計
$stats = $index->stats();
echo $stats["documentCount"];
echo $stats["vectorFields"];
API リファレンス
Index
Laurus 検索エンジンをラップするメインクラスです。
new \Laurus\Index(?string $path = null, ?Schema $schema = null)
コンストラクタ
| パラメータ | 型 | デフォルト | 説明 |
|---|---|---|---|
$path | string|null | null | 永続ストレージのディレクトリパス。null の場合はインメモリインデックスを作成します。 |
$schema | Schema|null | null | スキーマ定義。省略時は空のスキーマが使用されます。 |
メソッド
| メソッド | 説明 |
|---|---|
putDocument(string $id, array $doc): void | ドキュメントをアップサート(upsert)します。同じ ID の既存バージョンをすべて置換します。 |
addDocument(string $id, array $doc): void | 既存バージョンを削除せずにドキュメントチャンクを追記します。 |
getDocuments(string $id): array | 指定 ID の全保存バージョンを返します。 |
deleteDocuments(string $id): void | 指定 ID の全バージョンを削除します。 |
commit(): void | バッファリングされた書き込みをフラッシュし、すべての保留中の変更を検索可能にします。 |
search(mixed $query, int $limit = 10, int $offset = 0): array | 検索クエリを実行します。SearchResult の配列を返します。 |
stats(): array | インデックス統計("documentCount"、"vectorFields")を返します。 |
search の query 引数
$query パラメータは以下のいずれかを受け付けます:
- DSL 文字列(例:
"title:hello"、"embedding:\"memory safety\"") - Lexical クエリオブジェクト(
TermQuery、PhraseQuery、BooleanQueryなど) - Vector クエリオブジェクト(
VectorQuery、VectorTextQuery) SearchRequest(完全な制御が必要な場合)
Schema
Index のフィールドとインデックスタイプを定義します。
new \Laurus\Schema()
フィールドメソッド
| メソッド | 説明 |
|---|---|
addTextField(string $name, bool $stored = true, bool $indexed = true, bool $termVectors = false, ?string $analyzer = null): void | 全文フィールド(転置インデックス、BM25)。$analyzer にはパラメータ不要の組込名("standard" / "english" / "keyword" / "simple" / "noop"、または addAnalyzer で登録したカスタム名)を指定します。Lindera 辞書パスが必要な Japanese プリセットは、lindera tokenizer を含むカスタム analyzer として登録し、名前で参照してください。 |
addIntegerField(string $name, bool $stored = true, bool $indexed = true, bool $multiValued = false): void | 64 ビット整数フィールド。$multiValued = true で整数配列を受け付け(範囲クエリは “any match”)。 |
addFloatField(string $name, bool $stored = true, bool $indexed = true, bool $multiValued = false): void | 64 ビット浮動小数点フィールド。$multiValued = true で浮動小数点配列を受け付け(範囲クエリは “any match”)。 |
addBooleanField(string $name, bool $stored = true, bool $indexed = true): void | ブールフィールド。 |
addBytesField(string $name, bool $stored = true): void | 生バイトフィールド。 |
addGeoField(string $name, bool $stored = true, bool $indexed = true): void | 地理座標フィールド(緯度/経度)。 |
addGeo3dField(string $name, bool $stored = true, bool $indexed = true): void | 3D ECEF カルテシアン座標フィールド(x, y, z はメートル)。詳細は Geo3d の概念。 |
addDatetimeField(string $name, bool $stored = true, bool $indexed = true): void | UTC 日時フィールド。 |
addHnswField(string $name, int $dimension, ?string $distance = "cosine", int $m = 16, int $efConstruction = 200, ?string $embedder = null): void | HNSW 近似最近傍ベクトルフィールド。 |
addFlatField(string $name, int $dimension, ?string $distance = "cosine", ?string $embedder = null): void | Flat(総当たり)ベクトルフィールド。 |
addIvfField(string $name, int $dimension, ?string $distance = "cosine", int $nClusters = 100, int $nProbe = 1, ?string $embedder = null): void | IVF 近似最近傍ベクトルフィールド。 |
その他のメソッド
| メソッド | 説明 |
|---|---|
addEmbedder(string $name, array $config): void | 名前付きエンベダー定義を登録します。$config は "type" キーを持つ連想配列です(下記参照)。 |
setDefaultFields(array $fieldNames): void | クエリでフィールドが指定されていない場合に使用するデフォルトフィールドを設定します。$fieldNames は文字列の配列です。 |
setDynamicFieldPolicy(string $policy): void | 未宣言フィールドの扱いを設定します。$policy は "strict" / "dynamic"(デフォルト)/ "ignore"。詳細は下記を参照。 |
dynamicFieldPolicy(): string | 現在のポリシーを小文字の文字列で返します。 |
fieldNames(): array | このスキーマに定義されたフィールド名のリストを返します。 |
Dynamic field policy(動的フィールドポリシー)
ドキュメントに含まれるがスキーマに宣言されていないフィールドの扱いを制御します:
"strict"— ドキュメントを拒否"dynamic"(デフォルト)— 各未宣言フィールドの型を推論してスキーマに追加。警告: integer フィールドに入ってきた float 値は静かに切り捨てられます(3.14→3)。厳密さが必要なら"strict"を使用してください"ignore"— 未宣言フィールドを静かに破棄
詳細な挙動マトリクスは スキーマとフィールド を参照してください。
エンベダータイプ
"type" | 必須キー | Feature Flag |
|---|---|---|
"precomputed" | – | (常に利用可能) |
"candle_bert" | "model" | embeddings-candle |
"candle_clip" | "model" | embeddings-multimodal |
"openai" | "model" | embeddings-openai |
距離メトリクス
| 値 | 説明 |
|---|---|
"cosine" | コサイン類似度(デフォルト) |
"euclidean" | ユークリッド距離 |
"dot_product" | 内積 |
"manhattan" | マンハッタン距離 |
"angular" | 角度距離 |
クエリクラス
TermQuery
new \Laurus\TermQuery(string $field, string $term)
指定フィールドに完全一致する語句を含むドキュメントを検索します。
PhraseQuery
new \Laurus\PhraseQuery(string $field, array $terms)
指定した語句が順序どおりに含まれるドキュメントを検索します。$terms は文字列の配列です。
FuzzyQuery
new \Laurus\FuzzyQuery(string $field, string $term, int $maxEdits = 2)
編集距離が $maxEdits 以内の近似一致を検索します。
WildcardQuery
new \Laurus\WildcardQuery(string $field, string $pattern)
ワイルドカードパターン検索。* は任意の文字列、? は任意の1文字に一致します。
NumericRangeQuery
new \Laurus\NumericRangeQuery(string $field, mixed $min, mixed $max, ?string $numericType = "integer")
[$min, $max] の範囲内の数値を検索します。開いた境界には null を指定します。$numericType には "integer" または "float" を設定します。
GeoDistanceQuery
\Laurus\GeoDistanceQuery::withinRadius(
string $field, float $lat, float $lon, float $distanceM,
): GeoDistanceQuery
地理的距離検索(半径指定)。指定した地点から $distanceM メートル以内の
(lat, lon) 座標を持つドキュメントを返します。
GeoBoundingBoxQuery
\Laurus\GeoBoundingBoxQuery::withinBoundingBox(
string $field,
float $minLat, float $minLon,
float $maxLat, float $maxLon,
): GeoBoundingBoxQuery
地理的範囲(バウンディングボックス)検索。軸並行 [$minLat, $maxLat] × [$minLon, $maxLon] 内の (lat, lon) 座標を持つドキュメントを返します。
Geo3dDistanceQuery
\Laurus\Geo3dDistanceQuery::withinSphere(
string $field,
float $x, float $y, float $z,
float $distanceM,
): Geo3dDistanceQuery
3D ECEF 座標フィールドへの球距離検索。中心 (x, y, z) から $distanceM メートル以内
の座標を持つドキュメントを返します。ECEF の理論については
Geo3d の概念 を参照。
Geo3dBoundingBoxQuery
\Laurus\Geo3dBoundingBoxQuery::withinBox(
string $field,
float $minX, float $minY, float $minZ,
float $maxX, float $maxY, float $maxZ,
): Geo3dBoundingBoxQuery
軸並行 3D 範囲(AABB)検索。
Geo3dNearestQuery
\Laurus\Geo3dNearestQuery::kNearest(
string $field,
float $x, float $y, float $z,
int $k,
?float $initialRadiusM = null,
?float $maxRadiusM = null,
): Geo3dNearestQuery
3D ECEF 座標フィールドへの k 最近傍検索。$initialRadiusM / $maxRadiusM
(オプション)で反復拡張サーチの探索コーンを調整できます。
BooleanQuery
$bq = new \Laurus\BooleanQuery();
$bq->must($query);
$bq->should($query);
$bq->mustNot($query);
複合ブールクエリ。must 節はすべて一致する必要があり、mustNot 節は一致してはなりません。should 節はスコアリングに寄与し、must 節が無い場合は少なくとも1つが一致する必要があります。
SpanQuery
// 単一語句
\Laurus\SpanQuery::term(string $field, string $term): SpanQuery
// Near: slop 位置以内の語句
\Laurus\SpanQuery::near(string $field, array $terms, int $slop = 0, bool $ordered = true): SpanQuery
// NearSpans: slop 位置以内のネストされた SpanQuery 句
\Laurus\SpanQuery::nearSpans(string $field, array $clauses, int $slop = 0, bool $ordered = true): SpanQuery
// Containing: big スパンが little スパンを含む
\Laurus\SpanQuery::containing(string $field, SpanQuery $big, SpanQuery $little): SpanQuery
// Within: 最大距離での include スパンと exclude スパン
\Laurus\SpanQuery::within(string $field, SpanQuery $include, SpanQuery $exclude, int $distance): SpanQuery
位置・近接スパンクエリ。near は語句文字列の配列を受け取り、nearSpans は
ネスト式のために SpanQuery オブジェクトの配列を受け取ります(各句のフィールド
は外側の $field に再ルートされます)。
VectorQuery
new \Laurus\VectorQuery(string $field, array $vector)
事前計算済みエンベディングベクトルを使った近似最近傍検索を行います。$vector は Float の配列です。
VectorTextQuery
new \Laurus\VectorTextQuery(string $field, string $text)
クエリ時に $text をエンベディングに変換してベクトル検索を行います。インデックスにエンベダーの設定が必要です。
SearchRequest
高度な制御が必要な場合の完全なリクエストクラスです。
new \Laurus\SearchRequest(
mixed $query = null,
mixed $lexicalQuery = null,
mixed $vectorQuery = null,
mixed $filterQuery = null,
mixed $fusion = null,
int $limit = 10,
int $offset = 0,
)
| パラメータ | 説明 |
|---|---|
$query | DSL 文字列または単一クエリオブジェクト。$lexicalQuery / $vectorQuery と排他的。 |
$lexicalQuery | 明示的なハイブリッド検索の Lexical コンポーネント。 |
$vectorQuery | 明示的なハイブリッド検索の Vector コンポーネント。 |
$filterQuery | スコアリング後に適用する Lexical フィルター。 |
$fusion | フュージョンアルゴリズム(RRF または WeightedSum)。両コンポーネント指定時のデフォルトは RRF(k: 60)。 |
$limit | 最大結果件数(デフォルト 10)。 |
$offset | ページネーションオフセット(デフォルト 0)。 |
SearchResult
Index->search() が返すクラスです。
$result->getId() // string -- 外部ドキュメント識別子
$result->getScore() // float -- 関連性スコア
$result->getDocument() // array|null -- 取得されたフィールド値。stored=false の場合は null
フュージョンアルゴリズム
RRF
new \Laurus\RRF(float $k = 60.0)
逆順位フュージョン(Reciprocal Rank Fusion)。Lexical と Vector の結果リストを順位位置によってマージします。$k は平滑化定数で、値が大きいほど上位ランクの影響が小さくなります。
WeightedSum
new \Laurus\WeightedSum(float $lexicalWeight = 0.5, float $vectorWeight = 0.5)
両スコアリストをそれぞれ正規化した後、$lexicalWeight * lexical_score + $vectorWeight * vector_score として結合します。
テキスト解析
SynonymDictionary
$dict = new \Laurus\SynonymDictionary();
$dict->addSynonymGroup(["fast", "quick", "rapid"]);
同義語グループの辞書です。グループ内のすべての語句は互いの同義語として扱われます。
WhitespaceTokenizer
$tokenizer = new \Laurus\WhitespaceTokenizer();
$tokens = $tokenizer->tokenize("hello world");
空白で分割してテキストをトークン化し、Token オブジェクトの配列を返します。
SynonymGraphFilter
new \Laurus\SynonymGraphFilter(SynonymDictionary $dictionary, bool $keepOriginal = true, float $boost = 1.0)
| パラメータ | 説明 |
|---|---|
$dictionary | 同義語グループのソース。 |
$keepOriginal | true(デフォルト)の場合は元のトークンも同義語と並べて保持します。 |
$boost | 挿入される同義語トークンに適用されるスコアブースト(デフォルト 1.0)。 |
$filter = new \Laurus\SynonymGraphFilter($dictionary, true, 1.0);
$expanded = $filter->apply($tokens);
SynonymDictionary の同義語でトークンを展開するトークンフィルターです。
Token
$token->getText() // string -- トークンテキスト
$token->getPosition() // int -- トークンストリーム内の位置
$token->getStartOffset() // int -- 元テキスト内の文字開始オフセット
$token->getEndOffset() // int -- 元テキスト内の文字終了オフセット
$token->getBoost() // float -- スコアブースト係数(1.0 = 調整なし)
$token->isStopped() // bool -- ストップフィルターによって除去されたかどうか
$token->getPositionIncrement() // int -- 前のトークンの位置との差分
$token->getPositionLength() // int -- このトークンがカバーする位置数
フィールド値の型マッピング
PHP の値は自動的に Laurus の DataValue 型に変換されます:
| PHP 型 | Laurus 型 | 備考 |
|---|---|---|
null | Null | |
true / false | Bool | |
int | Int64 | |
float | Float64 | |
string | Text | |
array(数値) | Vector | 要素は f32 に変換 |
array("lat", "lon") | Geo | 2 つの float 値 |
array("x", "y", "z") | GeoEcef | 3 つの float 値(メートル単位、3D ECEF 直交座標) |
string(ISO 8601) | DateTime | ISO 8601 形式からパース |
開発環境のセットアップ
このページでは laurus-php バインディングのローカル開発環境の構築、ビルド、テストスイートの実行方法について説明します。
前提条件
- Rust 1.85 以降(Cargo 含む)
- PHP 8.1 以降(開発ヘッダー付き:
php-dev/php-devel) - Composer(依存関係管理用)
- リポジトリがローカルにクローンされていること
git clone https://github.com/mosuka/laurus.git
cd laurus
ビルド
開発ビルド
Rust ネイティブ拡張をデバッグモードでコンパイルします。Rust ソースを変更した場合は再実行してください。
cd laurus-php
cargo build
ビルド成果物は ../target/debug/liblaurus_php.so に生成されます。
リリースビルド
cd laurus-php
cargo build --release
ビルド成果物は ../target/release/liblaurus_php.so に生成されます。
ビルドの確認
php -d extension=../target/release/liblaurus_php.so -r "
use Laurus\Index;
\$index = new Index();
print_r(\$index->stats());
"
# Array ( [documentCount] => 0 [vectorFields] => Array ( ) )
テスト
テストは PHPUnit を使用しており、tests/ ディレクトリにあります。
# テスト依存関係をインストール
composer install
# 全テスト実行
php -d extension=../target/release/liblaurus_php.so vendor/bin/phpunit tests/
特定のテストファイルを実行する場合:
php -d extension=../target/release/liblaurus_php.so vendor/bin/phpunit tests/LaurusTest.php
Lint とフォーマット
# Rust lint(Clippy)
cargo clippy -p laurus-php -- -D warnings
# Rust フォーマットチェック
cargo fmt -p laurus-php --check
# フォーマット適用
cargo fmt -p laurus-php
クリーンアップ
# ビルド成果物を削除
cargo clean
# Composer 依存関係を削除
rm -rf vendor/
Workspace 統合と clang-sys パッチ
laurus-php は ext-php-rs を使用しており、
ext-php-rs は ext-php-rs-clang-sys(clang-sys のフォーク)に依存しています。
一方、laurus-ruby は magnus → rb-sys → bindgen → clang-sys(オリジナル)に依存しています。
両方のクレートが links = "clang" を宣言しており、Cargo は同一 workspace 内で同じ links 値を持つ
パッケージを 2 つ許可しません。
laurus-php と laurus-ruby を workspace メンバーとして共存させるため、ルートの Cargo.toml で
ext-php-rs-clang-sys を links 宣言を除去したローカルコピーに patch しています:
# Cargo.toml(workspace ルート)
[patch.crates-io]
ext-php-rs-clang-sys = { path = "patches/ext-php-rs-clang-sys" }
パッチは patches/ext-php-rs-clang-sys/ にあります。上流クレートからの唯一の変更点は
Cargo.toml の links = "clang" の除去です。clang-sys と ext-php-rs-clang-sys は
どちらも libclang をビルド時のみ使用し(bindgen によるヘッダー解析)、最終バイナリにはリンク
されないため、この変更は安全です。
パッチが必要な条件
このパッチは laurus-php と laurus-ruby が同一の Cargo workspace のメンバーである場合にのみ
必要です。laurus-ruby を workspace から除外するか、laurus-php を [workspace] exclude で
除外すれば、links = "clang" の競合は発生しないため、パッチとルート Cargo.toml の
[patch.crates-io] セクションを削除できます。
パッチの更新
ext-php-rs をアップグレードして新しいバージョンの ext-php-rs-clang-sys が
使われるようになった場合、パッチを更新してください:
# 1. laurus-php/Cargo.toml で ext-php-rs を更新した後:
cargo update -p ext-php-rs
# 2. 新しい ext-php-rs-clang-sys ソースをコピー
cp -r ~/.cargo/registry/src/index.crates.io-*/ext-php-rs-clang-sys-<NEW_VERSION>/* \
patches/ext-php-rs-clang-sys/
# 3. links 宣言を除去
sed -i 's/^links = "clang"/# links = "clang"/' patches/ext-php-rs-clang-sys/Cargo.toml
# 4. ビルドを確認
cargo build -p laurus-php -p laurus-ruby
macOS リンカーフラグ (-undefined dynamic_lookup)
PHP 拡張は共有ライブラリ(.so / .dylib)であり、実行時に PHP インタプリタに
ロードされます。PHP API シンボル(zend_*, php_* 等)は PHP バイナリ本体に
定義されており、拡張がリンクするライブラリには含まれません。Linux ではリンカーが
共有ライブラリ内の未定義シンボルをデフォルトで許容するため問題ありませんが、
macOS ではリンカーが未定義シンボルをエラーとして扱い、ビルドが失敗します:
ld: symbol(s) not found for architecture arm64
修正方法は -Wl,-undefined,dynamic_lookup をリンカーに渡すことです。これにより
シンボル解決がロード時(PHP が拡張を dlopen する時点)まで延期されます。
このフラグは .cargo/config.toml には設定しません。設定すると workspace 内の
全クレートに適用され、PHP 以外のクレートでも未定義シンボルがエラーにならなくなる
ためです。代わりに laurus-php のビルド時のみ適用します:
Makefile(ローカル開発):
build-laurus-php:
ifeq ($(shell uname -s),Darwin)
RUSTFLAGS="-C link-args=-Wl,-undefined,dynamic_lookup" cargo build -p laurus-php --release
else
cargo build -p laurus-php --release
endif
CI(GitHub Actions):
- name: Build PHP extension
shell: bash
run: |
if [ "$RUNNER_OS" == "macOS" ]; then
export RUSTFLAGS="-C link-args=-Wl,-undefined,dynamic_lookup"
fi
cargo build --release -p laurus-php
macOS でビルドする際は、cargo build -p laurus-php を直接実行するのではなく、
make build-laurus-php または make test-laurus-php を使用してください。
プロジェクト構成
laurus-php/
├── Cargo.toml # Rust クレートマニフェスト
├── composer.json # Composer パッケージ定義
├── composer.lock # ロックされた依存関係バージョン
├── src/ # Rust ソース(ext-php-rs バインディング)
│ ├── lib.rs # モジュール登録
│ ├── index.rs # Index クラス
│ ├── schema.rs # Schema クラス
│ ├── query.rs # クエリクラス
│ ├── search.rs # SearchRequest / SearchResult / Fusion
│ ├── analysis.rs # Tokenizer / Filter / Token
│ ├── convert.rs # PHP <-> DataValue 変換
│ └── errors.rs # エラーマッピング
├── tests/ # PHPUnit テスト
│ └── LaurusTest.php
└── examples/ # 実行可能な PHP サンプル
ビルドとテスト
前提条件
- Rust 1.85 以降(edition 2024)
- Cargo(Rust に付属)
- protobuf コンパイラ(
protoc)–laurus-serverのビルドに必要
ビルド
# すべてのクレートをビルド
cargo build
# 特定の Feature を指定してビルド
cargo build --features embeddings-candle
# リリースモードでビルド
cargo build --release
テスト
# すべてのテストを実行
cargo test
# 名前を指定して特定のテストを実行
cargo test <test_name>
# 特定のクレートのテストを実行
cargo test -p laurus
cargo test -p laurus-cli
cargo test -p laurus-server
Lint
# clippy を警告エラー扱いで実行
cargo clippy -- -D warnings
フォーマット
# フォーマットチェック
cargo fmt --check
# フォーマットを適用
cargo fmt
ドキュメント
API ドキュメント
# Rust API ドキュメントを生成して開く
cargo doc --no-deps --open
mdBook ドキュメント
# ドキュメントサイトをビルド
mdbook build docs
# ローカルプレビューサーバーを起動 (http://localhost:3000)
mdbook serve docs
# Markdown ファイルを Lint
markdownlint-cli2 "docs/src/**/*.md"
ベンチマーク
このガイドでは、laurus のベンチマークの実行方法、ベースライン(baseline)の保存と比較方法、プルリクエストでの結果報告方法について説明します。
ベンチマークスイートは laurus/benches/ 配下にあり、Criterion で構築されています。衛生ルール(決定的シード、ファイル冒頭ドキュメント、sanity assert、sample_size ポリシー)は laurus/benches/common.rs で一元管理されています。
スイート一覧
| ファイル | スコープ |
|---|---|
bkd_bench.rs | BKD ツリーの範囲検索(range search)、交差判定(intersect)、構築(1D / 2D / 3D、10k / 100k / 1M ポイント) |
distance_bench.rs | DistanceMetric::distance の cosine / Euclidean / Manhattan / dot product(現状は単一次元、次元スイープは #424 で対応予定) |
lexical_search_bench.rs | Engine::search 経由のエンドツーエンド lexical 検索(term / boolean / phrase / fuzzy / DSL) |
search_perf.rs | Posting iterator の skip_to、BM25Scorer::score、SIMD バッチスコアリング、コンパクト posting 変換 |
spell_correction_bench.rs | SpellingCorrector::correct、各イテレーションごとに fresh corrector を渡す cold-state 計測 |
synonym_bench.rs | SynonymDictionary::get_synonyms のルックアップ(100 / 1k / 10k グループ)と構築コスト |
text_analysis_bench.rs | StandardAnalyzer::analyze のシングルドキュメントとバッチ(100 ドキュメント)解析 |
vector_search_bench.rs | Flat / IVF / HNSW の構築と検索(1k / 5k ベクタ、dim 128、top-10) |
各ファイル冒頭の //! ドキュメントコメントにスコープ・シナリオ・フィルタ方法が書かれています。実行前に確認してください。
ベンチマークの実行
単一ベンチファイルを実行:
cargo bench -p laurus --bench distance_bench
criterion id でフィルタ(部分一致):
cargo bench -p laurus --bench distance_bench -- cosine
cargo bench -p laurus --bench vector_search_bench -- "HNSW Search/top10"
コンパイル確認のみ(CI やリファクタリング時に有用):
cargo bench -p laurus --bench distance_bench --no-run
ワークスペースの全ベンチを実行:
cargo bench -p laurus
ベースラインの保存と比較
Criterion は名前付きベースラインをサポートしており、フィーチャーブランチを main(または任意の参照状態)と比較できます。
現在の状態を main という名前のベースラインとして保存:
cargo bench -p laurus --bench distance_bench -- --save-baseline main
その後の実行結果をベースラインと比較:
cargo bench -p laurus --bench distance_bench -- --baseline main
出力には change: 行がベンチマーク単位で表示され、変化率と判定(No change in performance detected、Performance has improved、Performance has regressed)が示されます。Criterion はベースラインを target/criterion/<bench-id>/<baseline>/ 配下に保存します。
perf PR の推奨フロー:
main(または変更前の状態)で —cargo bench --bench RELEVANT -- --save-baseline main- ブランチで変更を実装
- ブランチで —
cargo bench --bench RELEVANT -- --baseline main change:行を PR 説明にコピーする
推奨環境
µs / ns 単位のマイクロベンチマークはシステムノイズに敏感です。意味のある数値を得るには:
-
CPU governor:
performanceに設定(Linux):sudo cpupower frequency-set -g performance -
Turbo boost: 周波数スケーリングが結果を歪めないように無効化:
echo 1 | sudo tee /sys/devices/system/cpu/intel_pstate/no_turbo # IntelAMD システムや BIOS レベルの設定は異なるためベンダーのドキュメントを参照してください。
-
バックグラウンド負荷: ブラウザ・IDE・ビルドウォッチャー・Docker を停止する。CPU を共有するものは短時間ベンチを歪めます。
-
コア固定(任意): 利用可能なら固定コアにピン留め:
taskset -c 2 cargo bench -p laurus --bench distance_bench -
再実行: 2 回実行して比較する。チューニング済みマシンで ~5 % 以下の差はノイズ。共有ワークステーションでは ~5 % を超える差もノイズの可能性がある。1 回の実行結果を過大解釈しない。
環境を安定化できない場合は、不安定な数値を権威ある数値として提示せず、PR で明示的に断ること(例:「共有ノート PC で測定、~10 % のノイズを想定」)。
Make ターゲット
Makefile から共通エントリポイントを利用できます:
make bench # cargo bench -p laurus
make bench-baseline # cargo bench -p laurus -- --save-baseline main
make bench-compare # cargo bench -p laurus -- --baseline main
単一ベンチを指定する場合は BENCH=name を渡します:
make bench BENCH=distance_bench
make bench-baseline BENCH=distance_bench
make bench-compare BENCH=distance_bench
PR 説明テンプレート
PR で計測可能なパフォーマンス変化を主張する場合、下記のようなテーブルを説明に貼り付けてください:
## Performance
Environment: <CPU モデル>, governor=performance, turbo disabled, dedicated machine.
Baseline: `main` at <commit-sha>. After: this branch at <commit-sha>.
| Bench | Before | After | Δ | Verdict |
| --- | --- | --- | --- | --- |
| `distance_metrics/cosine` | 4.20 µs | 3.10 µs | -26 % | improved |
| `distance_metrics/euclidean` | 2.18 µs | 2.16 µs | -1 % | no change |
Reproduce: `cargo bench -p laurus --bench distance_bench -- --baseline main`
比較が再現可能となるように、ベースラインと変更後の commit SHA を必ず含めてください。チューニング済みマシンで実行した場合でも環境を明示してください。
新しいベンチマークの追加
新規ベンチファイルを追加する際は、laurus/benches/common.rs のスイート全体衛生ルールに従ってください:
common::DEFAULT_SEED(またはlcg_*ヘルパ)で決定的シードを使う。rand::rng()は使わない。- ファイル冒頭に
//!ドキュメントコメントでスコープ・シナリオ・実行コマンド・フィルタ例を記載する。 - タイミング
b.iterの外側で 1 度だけassert!を実行し、空結果を出す regression を黙ってパスさせない。 SAMPLE_SIZE_FAST(デフォルト、50 ms 以下の操作向け)またはSAMPLE_SIZE_SLOW(構築パス向け)のいずれかを選ぶ。中間値は使わない。laurus/Cargo.tomlに[[bench]] name = "..." harness = falseで登録する。クレートはautobenches = falseを設定しているため、benches/配下のファイルは自動検出されない。
ファイル間でヘルパを共有する必要がある場合は、benches/common.rs を拡張してコード重複を避けてください。
CI 連携
現状、CI ではリグレッション検出のベンチジョブは実行していません。perf 系の PR は、推奨環境下で取得した baseline-vs-after 数値を手動で投稿することが想定されています。
将来的には大きなリグレッションで失敗するスモークセットのベンチジョブを追加する案があり、アンブレラ Issue #429 で追跡されています。
Feature Flags
laurus クレートはデフォルトでは Feature が無効の状態で提供されます。必要に応じて Embedding サポートを有効にしてください。
利用可能な Feature
| Feature | 説明 | 主な依存クレート |
|---|---|---|
embeddings-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 互換であること