Skip to content

Stream search indexing per batch (opt-in unmerged Stage writes) for memory-flat large-object indexing #574

Description

@ddeboer

Problem

searchIndexWriter (from #534/#565) buffers a whole dataset's extracted quads – and the groupByRoot subject index it feeds – before projecting, so peak memory is O(dataset). Fine for the current scale (millions of objects on Typesense), but it caps large-object indexing and pairs with the disk-backed engine work in #572: a 500M-object index wants streamed ingestion, not a dataset buffered in the indexer.

Root cause: Stage.runWithSelector merges every selector batch into a single per-dataset write(), which erases the self-contained framing unit the selector already produced. Each selector batch (per-root CONSTRUCT with its one-hop closure) is independently framable; the merge is what forces the whole-dataset buffer.

Decision (settled in design discussion)

Keep @lde/pipeline quad-typed – no Stage<Out> / documents-as-pipeline-payload (that trades away the RDF-pipeline integrity for genericity we do not want). Instead:

  • @lde/pipeline: add an opt-in per-batch (unmerged) write mode on Stage – each self-contained selector batch is delivered as its own write(dataset, …) call, writes serialized (readers still concurrent).
  • @lde/search-pipeline: searchIndexWriter projects each write() chunk and streams the documents on, dropping the per-dataset buffer → peak memory O(batch). No @lde/search change (projectGraph on a bounded chunk suffices).

No Writer-interface change: the chunk is the existing write() call; the engine Writer<SearchDocument> already streams (BatchImporter).

Why opt-in, not the default

  • Concurrency: runWithSelector reads batches concurrently through one single-consumer queue, which serializes the write. Per-batch-by-default would force write serialization on the shared path, and firing concurrent write()s into the existing writers is unsafe (SparqlUpdateWriter's CLEAR-once check races; FileWriter's buffer interleaves).
  • Efficiency: SparqlUpdateWriter coalesces INSERT DATA by its own batchSize; per-batch writes mean more, smaller INSERTs. The production quad pipelines (NDE DKG, Linked Open Limburg) would pay that for no benefit.

Opt-in confines both to the search pipeline that benefits, at zero blast radius on the production pipelines.

Correctness contract

Per-batch projection is correct only if each root's CONSTRUCT inlines its one-hop closure, so a batch is self-contained to frame – which frameByType already assumes. State it explicitly for extraction authors.

Scope

  • @lde/pipeline: opt-in unmerged write mode on Stage (serialize writes, keep concurrent reads); tests; confirm SparqlUpdateWriter / FileWriter / @lde/pipeline-void unchanged in default mode.
  • @lde/search-pipeline: rework searchIndexWriter to project + stream per write(); enable the mode; drop the passes buffer.
  • ADR for the per-batch mode + one-hop contract (supersedes the searchIndexWriter-buffers-per-dataset note).

Relationships

Non-goal

  • Genericizing the pipeline over payload type (Stage<Out>, documents in the pipeline). Payload stays Quad.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Fields

    No fields configured for Feature.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions