Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/ja/src/laurus/api_reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ cargo doc --open
| `Engine::builder(storage, schema)` | `EngineBuilder` を作成 |
| `engine.put_document(id, doc).await?` | ドキュメントのUpsert(IDが存在する場合は置き換え) |
| `engine.add_document(id, doc).await?` | ドキュメントをチャンクとして追加(複数のチャンクが同一IDを共有可能) |
| `engine.put_documents(docs).await?` | `(id, doc)` ペアのバッチ Upsert — WAL fsync はバッチごとに 1 回([バッチインジェスト](persistence.md#バッチインジェスト) 参照) |
| `engine.add_documents(docs).await?` | `(id, doc)` ペアのバッチチャンク追加 — 耐久性・エラー意味論は `put_documents` と同一 |
| `engine.delete_documents(id).await?` | 外部IDによるすべてのドキュメント/チャンクの削除 |
| `engine.get_documents(id).await?` | 外部IDによるすべてのドキュメント/チャンクの取得 |
| `engine.search(request).await?` | 検索リクエストの実行 |
Expand All @@ -26,6 +28,8 @@ cargo doc --open
| `engine.stats()?` | インデックス統計の取得 |

> **`put_document` と `add_document` の違い:** `put_document` はUpsertを実行します。同じ外部IDのドキュメントが既に存在する場合、削除して置き換えます。`add_document` は常に追加し、複数のドキュメントチャンクが同じ外部IDを共有できます。詳細は [Schema & Fields -- ドキュメントのインデキシング](../concepts/schema_and_fields.md#indexing-documents) を参照してください。
>
> **バッチ形式:** `put_documents` / `add_documents` は `(id, doc)` ペアを入力順に逐次適用します(1 回の `put_documents` バッチ内で重複した ID は逐次 put と同じくデデュープされ、最後の出現が勝ちます)。適用できないドキュメントに遭遇すると fail-fast で `LaurusError::BatchIngest { failed_index, failed_id, applied, .. }` を返します。バッチロールバックはありません: 適用済みドキュメントは WAL と NRT バッファに残るため、バッチ(またはその suffix)の再試行は冪等です。耐久性の詳細は[バッチインジェスト](persistence.md#バッチインジェスト)を参照してください。

### EngineBuilder

Expand Down
2 changes: 2 additions & 0 deletions docs/ja/src/laurus/engine.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,8 @@ graph LR
| :--- | :--- |
| `put_document(id, doc)` | Upsert -- 同じIDのドキュメントが既存の場合は置き換え |
| `add_document(id, doc)` | 追加 -- 新しいチャンクとして追加(複数のチャンクが同一IDを共有可能) |
| `put_documents(docs)` | バッチ Upsert -- `(id, doc)` ペアを順序どおり適用し、WAL fsync はバッチごとに 1 回 |
| `add_documents(docs)` | バッチ追加 -- `put_documents` と同様だが既存チャンクを削除しない |
| `get_documents(id)` | 外部IDによるすべてのドキュメント/チャンクの取得 |
| `delete_documents(id)` | 外部IDによるすべてのドキュメント/チャンクの削除 |
| `commit()` | 保留中の変更をストレージにフラッシュ(ドキュメントが検索可能になる) |
Expand Down
19 changes: 19 additions & 0 deletions docs/ja/src/laurus/persistence.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,25 @@ engine.add_document("doc-3", doc3).await?;

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

## バッチインジェスト

`put_documents` / `add_documents` は `put_document` / `add_document` のバッチ形式です。`(id, doc)` ペアを**入力順に逐次適用**し、既定の `PerRecord` ポリシー下ではレコードごとではなく**バッチ末尾の 1 回の WAL fsync** でバッチ全体を durable にします。

```rust
let docs: Vec<(String, Document)> = build_batch();
engine.put_documents(docs).await?; // fsync 1 回で、全ドキュメントが単発 put と同等に durable
engine.commit().await?; // バッチ全体で 1 回のセグメントフラッシュ
```

留意すべき意味論:

- **順序**: 1 回の `put_documents` バッチ内で重複した外部 ID は、同じ put を逐次発行した場合とまったく同じようにデデュープされます(最後の出現が勝ち)。`add_documents` での ID の繰り返しは正当なマルチチャンク追加です。
- **fail-fast・ロールバックなし**: 適用できない最初のドキュメントでバッチは停止し、`LaurusError::BatchIngest { failed_index, failed_id, applied, .. }` を返します。失敗前に適用された `applied` 件はロールバック**されません** — WAL と NRT バッファに残り(即座に検索可能、次のコミットで永続化、クラッシュ時はリカバリで再生)、エラー経路でもバッチ末尾の WAL フラッシュは実行されます。バッチ全体、または `failed_index` からの suffix の再試行は put 意味論の下で冪等です。
- **耐久性**: 呼び出しが `Ok` を返した時点で、バッチ内の全ドキュメントは成功した単発 put とまったく同等に durable です。呼び出し途中のクラッシュで失われるのは fsync 前の末尾のみで、リカバリは fsync 済み prefix をドキュメント単位で再生し、途中で切れた末尾レコードは通常どおり CRC フレーミングが破棄します。
- **サイズ指針**: エンジンは各ドキュメントを WAL へ順次クローンするため、バッチのメモリは呼び出し側の `Vec` が支配的です。1 回の呼び出しあたり 1,000〜10,000 ドキュメントが良い既定値で、より大きなコーパスは複数回の呼び出しに分割してください(セグメントサイズを抑えるため定期的なコミットも推奨)。
- **`Group` ポリシー下**: バッチ中もグループしきい値は発火し続けるため、同ポリシーの有界な損失ウィンドウは保たれます。バッチ末尾のフラッシュも実行されます。
- **並行する単発書き込み**: 別タスクのバッチ実行中に完了した `put_document` / `add_document` / `delete_documents` は per-record の耐久性を完全に維持します — 単発書き込みは ack 前に fsync を再アサートするため、バッチが他の呼び出し元の保証を弱めることはありません。

## WAL 耐久性ポリシー

既定では、各 `add`/`delete` は返る前に WAL を fsync するため、成功した書き込みがクラッシュで失われることはありません。大量に取り込む場合、この書き込みごとの fsync がスループットのボトルネックになります。`WalSyncPolicy` により、書き込みごとの耐久性とスループットをトレードオフできます。
Expand Down
4 changes: 4 additions & 0 deletions docs/src/laurus/api_reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ The central coordinator for all indexing and search operations.
| `Engine::builder(storage, schema)` | Create an `EngineBuilder` |
| `engine.put_document(id, doc).await?` | Upsert a document (replace if ID exists) |
| `engine.add_document(id, doc).await?` | Add a document as a chunk (multiple chunks can share an ID) |
| `engine.put_documents(docs).await?` | Batched upsert of `(id, doc)` pairs — one WAL fsync per batch (see [Batch Ingestion](persistence.md#batch-ingestion)) |
| `engine.add_documents(docs).await?` | Batched chunk append of `(id, doc)` pairs — same durability and error semantics as `put_documents` |
| `engine.delete_documents(id).await?` | Delete all documents/chunks by external ID |
| `engine.get_documents(id).await?` | Get all documents/chunks by external ID |
| `engine.search(request).await?` | Execute a search request |
Expand All @@ -26,6 +28,8 @@ The central coordinator for all indexing and search operations.
| `engine.stats()?` | Get index statistics |

> **`put_document` vs `add_document`:** `put_document` performs an upsert — if a document with the same external ID already exists, it is deleted and replaced. `add_document` always appends, allowing multiple document chunks to share the same external ID. See [Schema & Fields — Indexing Documents](../concepts/schema_and_fields.md#indexing-documents) for details.
>
> **Batch forms:** `put_documents` / `add_documents` apply their `(id, doc)` pairs sequentially in input order (duplicate IDs within one `put_documents` batch dedup exactly like sequential puts — the last occurrence wins) and fail fast with `LaurusError::BatchIngest { failed_index, failed_id, applied, .. }` on the first document that cannot be applied. There is no batch rollback: applied documents stay in the WAL and NRT buffers, so retrying the batch (or its suffix) is idempotent. See [Batch Ingestion](persistence.md#batch-ingestion) for the durability details.

### EngineBuilder

Expand Down
2 changes: 2 additions & 0 deletions docs/src/laurus/engine.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,8 @@ graph LR
| :--- | :--- |
| `put_document(id, doc)` | Upsert -- replaces any existing document with the same ID |
| `add_document(id, doc)` | Append -- adds as a new chunk (multiple chunks can share an ID) |
| `put_documents(docs)` | Batched upsert -- applies `(id, doc)` pairs in order with one WAL fsync per batch |
| `add_documents(docs)` | Batched append -- like `put_documents` but never deletes existing chunks |
| `get_documents(id)` | Retrieve all documents/chunks by external ID |
| `delete_documents(id)` | Delete all documents/chunks by external ID |
| `commit()` | Flush pending changes to storage (makes documents searchable) |
Expand Down
19 changes: 19 additions & 0 deletions docs/src/laurus/persistence.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,25 @@ engine.add_document("doc-3", doc3).await?;

> **Tip:** Commits are relatively expensive because they flush segments to storage. For bulk indexing, batch many documents before calling `commit()`.

## Batch Ingestion

`put_documents` / `add_documents` are the batched forms of `put_document` / `add_document`. They apply their `(id, doc)` pairs **sequentially, in input order**, and under the default `PerRecord` policy they make the whole batch durable with a **single WAL fsync at batch end** instead of one per record:

```rust
let docs: Vec<(String, Document)> = build_batch();
engine.put_documents(docs).await?; // one fsync, all docs as durable as singular puts
engine.commit().await?; // one segment flush for the whole batch
```

Semantics to be aware of:

- **Ordering**: duplicate external IDs within one `put_documents` batch dedup exactly like the same puts issued sequentially — the last occurrence wins. Repeating an ID in `add_documents` legitimately appends multiple chunks.
- **Fail-fast, no rollback**: the batch stops at the first document that cannot be applied and returns `LaurusError::BatchIngest { failed_index, failed_id, applied, .. }`. The `applied` documents before the failure are **not** rolled back — they stay in the WAL and NRT buffers (searchable immediately, durable at the next commit, replayed on crash recovery), and the batch-end WAL flush runs on the error path too. Retrying the batch, or its suffix starting at `failed_index`, is idempotent under put semantics.
- **Durability**: when the call returns `Ok`, every document in the batch is exactly as durable as a successful singular put. A crash mid-call loses at most the un-fsync'd tail; recovery replays the fsync'd prefix per document, and a torn trailing record is dropped by the CRC framing as usual.
- **Sizing**: the engine clones each document into the WAL as it goes, so batch memory is dominated by the caller's `Vec`. Batches of 1,000-10,000 documents per call are a good default; chunk larger corpora into multiple calls (and commit periodically to bound segment sizes).
- **Under `Group` policy**: the group thresholds keep firing mid-batch, so that policy's bounded loss window is preserved; the batch-end flush still runs.
- **Concurrent singular writes**: a `put_document` / `add_document` / `delete_documents` call that completes while another task's batch is in flight keeps its full per-record durability — singular writes re-assert the fsync before acknowledging, so a batch never weakens anyone else's guarantee.

## WAL Durability Policy

By default, each `add`/`delete` fsyncs the WAL before returning, so a successful write can never be lost to a crash. When ingesting at high volume, that per-write fsync becomes the throughput bottleneck. The `WalSyncPolicy` lets you trade per-write durability for throughput:
Expand Down
33 changes: 33 additions & 0 deletions laurus/benches/lexical_indexing_bench.rs
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,38 @@ fn bench_bulk_ingest(c: &mut Criterion) {
group.finish();
}

/// End-to-end `add_documents(N) + commit` — the batch-API twin of
/// `ingest/bulk` (#551). One WAL fsync per batch instead of one per record,
/// so the delta vs `ingest/bulk` is only visible on the FileStorage backend
/// (`LAURUS_BENCH_DISK=1`); on MemoryStorage the two should be equal.
fn bench_bulk_ingest_batch_api(c: &mut Criterion) {
let rt = Runtime::new().unwrap();
let mut group = c.benchmark_group("ingest/bulk_batch_api");
group.sample_size(SAMPLE_SIZE_SLOW);

for &n in &ingest_corpus_sizes() {
group.throughput(Throughput::Elements(n as u64));
group.bench_with_input(BenchmarkId::from_parameter(n), &n, |b, &n| {
b.iter_batched(
|| {
let engine = rt.block_on(build_empty_engine()).unwrap();
let docs = build_documents(n);
(engine, docs)
},
|(engine, docs)| {
rt.block_on(async {
engine.add_documents(docs).await.unwrap();
engine.commit().await.unwrap();
});
},
BatchSize::SmallInput,
);
});
}

group.finish();
}

/// `(add × K + commit) × M` so M segments accumulate. M = 8 typically
/// crosses the default `TieredMergePolicy::max_segments_per_tier = 4`
/// threshold, so the M-sweep also exposes any auto-merge work.
Expand Down Expand Up @@ -403,6 +435,7 @@ criterion_group!(
bench_add_documents,
bench_commit,
bench_bulk_ingest,
bench_bulk_ingest_batch_api,
bench_multi_segment_commit,
);
criterion_main!(benches);
Loading