You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Current behavior: Every external ingest call takes one document, walks the full stack (apply_dynamic_schema → log.append → store_document → lexical.upsert_document → vector.upsert_document), and acquires the writer mutex once per doc.
Why it's a bottleneck/risk: Per-doc Mutex-acquire is a real cost at high QPS. The dynamic-schema policy resnapshots the schema's RwLock<Schema> once per doc. WAL fsync also happens once per doc.
Reference precedent: Tantivy IndexWriter::add_document is the API but is intended to be called from N threads with a single shared writer; the commit is one fsync per batch. Lucene IndexWriter.addDocuments(Iterable) exists.
Suggested direction: Add LexicalStore::upsert_documents(&self, batch: Vec<(u64, Document)>) -> Result<()> and a HybridEngine::put_batch. Inside, snapshot schema once, hold writer mutex once, WAL-batch all records under group-commit.
Risk / scope: M. Pure addition; no on-disk format change.
ID: LI-14 — see ~/.claude/tasks/laurus/20260523_perf_round3_audit/task_list.md for the full Round-3 issue list.
Task list (2026-07-13 campaign start, jointly with #572)
API surface survey (every existing add/put entry point across engine/stores/bindings; insertion point for a batch API; existing ingestion benches/tests; Tantivy/Lucene batch-API shapes)
Opportunity ceiling (files/fsyncs/segments per commit today vs batched — deterministic metrics)
Round-3 perf push sub-issue (tracked under umbrella #533).
[L] Batch ingestion API (
add_documents/commit_batch) (parity:Tantivy)laurus/src/lexical/store.rs:145-151(single-docupsert_document),laurus/src/engine.rs:170-272(only single-docput).apply_dynamic_schema → log.append → store_document → lexical.upsert_document → vector.upsert_document), and acquires the writer mutex once per doc.RwLock<Schema>once per doc. WAL fsync also happens once per doc.IndexWriter::add_documentis the API but is intended to be called from N threads with a single shared writer; thecommitis one fsync per batch. LuceneIndexWriter.addDocuments(Iterable)exists.LexicalStore::upsert_documents(&self, batch: Vec<(u64, Document)>) -> Result<()>and aHybridEngine::put_batch. Inside, snapshot schema once, hold writer mutex once, WAL-batch all records under group-commit.ID:
LI-14— see~/.claude/tasks/laurus/20260523_perf_round3_audit/task_list.mdfor the full Round-3 issue list.Task list (2026-07-13 campaign start, jointly with #572)
Phase 0: Investigation
LexicalStore::commitalways writes a new segment #572's premise at line level)Phase 1: Plan
add_documents/commit_batch) #551 API / perf(lexical/index): no commit batching —LexicalStore::commitalways writes a new segment #572 batching / possible subsumption of perf(lexical/index): storagesync()on every commit even for in-memory backend #573, perf(lexical/index):inverted_index = TermPostingIndex::new()re-allocatesAHashMapper flush #575) → plan comment. Decision: batch API only;commit_batch/CommitPolicydeferred until perf(vector/index): commit pipeline: write payload incrementally instead of one giantcreate_outputat flush #634 (O(N²/c).hnswrewrite footgun). Sub-issues filed: feat(engine): batch ingestion APIput_documents/add_documentswith one WAL fsync per batch (#551 PR-1) #863 / perf(engine): retain vector writer across commits + lexical segment-meta cache (#551 PR-2) #864 / feat(server,cli): bulk document ingestion — gRPC PutDocuments/AddDocuments, HTTP bulk, MCP tool, CLI JSONL (#551 PR-3) #865 / feat(bindings):put_documents/add_documentsacross python, nodejs, wasm, ruby, php (#551 PR-4) #866Phase 2: Implementation
put_documents/add_documentswith one WAL fsync per batch (#551 PR-1) #863):Engine::put_documents/add_documents+ fail-fastBatchIngesterror + WAL deferred-fsync scope + tests/bench/docs — merged (PR feat(engine): batch ingestion APIput_documents/add_documentswith one WAL fsync per batch (#863) #867, main771c6f80; fsync N→1 asserted by a cfg(test) counter; FileStorage bench ~36x at n=1000, 144→5,250 docs/s; also fixed a global-deferral hazard so concurrent singular writes keep per-record durability)0d95906c; closed perf(lexical/index): no commit batching —LexicalStore::commitalways writes a new segment #572, perf(lexical/index):InvertedIndexWriter::find_segments_for_docscans all.metaper upsert #559, perf(lexical/index):mark_persisted_doc_deletedconstructs a freshDeletionManagerper call #571;.hnswreload per commit cycle → 0, per-upsert.metascans → 0, deterministic counter + fault-injection tests; a pre-commit adversarial review hardened 5 defect classes incl. error-path invalidation and ahas_pending_changes()gate that also eliminates the full.hnswrewrite on no-change commits; discovered pre-existing HNSW reachability bug filed as bug(vector/hnsw): incremental graph append can leave layer-0 nodes unreachable (nondeterministic recall loss across commit cycles) #868)PutDocuments/AddDocuments+ HTTP bulk + MCP tool + CLI JSONL ingestput_documents/add_documentsacross python, nodejs, wasm, ruby, php (#551 PR-4) #866): bindings ×5 + docs (closes perf(lexical/index): batch ingestion API (add_documents/commit_batch) #551)Wrap-up
sync()on every commit even for in-memory backend #573 (weak premise: memory-backendsync()is a no-op)