docir — design documents Graph

Documents / Issues / issue-cfeb6eaa31cc

A full rebuild on every upgrade, even when nothing changed

docir self upgrade ran an unconditional full reindex, and 96% of that is re-embedding: 58.4s of 60s on a 315-document store whose files had not moved.

issue-cfeb6eaa31ccissueresolved#cli#embeddings#material#release
View as Markdown◉ View in graph

What was measured

Every command in the CLI, timed as a whole python -m docir process against a copy of a real 315-document store (1,326 vectors: 315 document + 1,011 section). Apple M1, docir 0.15.0, --no-daemon, output captured so each one takes the JSON path an agent takes.

This is the half benchmarks/latency.py never covered. That harness times context, search and get, and concluded the read path is fast and interpreter startup dominates — both still true. Nothing measured the write and maintenance path, which is where a command that takes a minute was sitting unreported.

Numbers

p50 seconds, --no-daemon, 313–315 documents.

command s command s
docir check --fix 59.8 docir add / docir update 1.2–1.6
docir reindex 58.5 docir check 0.97
docir build 4.3 docir archive / docir delete ~0.7
docir lint --deep 2.4 docir version (floor) 0.5
docir query / search / get / context 1.3–1.7

Two outliers, and the second is the first: check --fix reindexes before allocating ids. Everything else is at or under 4.3 s.

Where the 58 seconds goes

Embedding, and nothing else. The same rebuild against the model-free hashing embedder (DOCIR_EMBEDDER=deterministic) costs 2.5 s, so the model accounts for 96%.

run time
full rebuild, real model 58.4 s
full rebuild, hashing embedder 2.5 s
docir reindex --changed, nothing moved 1.5 s

The 1.5 s row is the point: the work was already skippable, and the flag that skips it already shipped.

Why an unchanged corpus paid for it

docir self upgrade ran an unconditional full reindex — including the run that reports "already the newest build", where no package moved and no file changed. A full rebuild re-embeds every document it re-saves, so all 1,326 vectors were recomputed byte-identical to the ones already stored.

docir check --fix paid it twice: its repair reindexes in full before allocating ids, and again after re-issuing duplicates.

The fix

MaintenanceService.resync reads the index build stamp before rebuilding — both modes write it, so a cheap pass would erase the evidence a full one was needed — and takes the changed-only path only when the stamp equals the running version. A genuine version move still rebuilds in full, which is what adr-6a4718fa7a7d reserves it for.

It compares equality rather than reusing stale_index_build(), which folds "never recorded" into absent so docir check stays quiet. Here unknown has to mean rebuild: a store with no stamp was last built by code that did not write one.

Repair's two rebuilds are now changed-only. Agreeing with the files is all either is for.

Measured after

case before after
docir self upgrade, stamp equal 65 s 1.5 s
docir self upgrade, stamp moved 65 s 62 s
docir reindex 64 s 64 s

Batching and thread tuning were measured and rejected

The obvious next move — feed the model a batch instead of one text per call — makes it slower on this hardware, because onnxruntime already saturates the cores on a single ~300-token sequence and fastembed pads every batch to its longest member.

config ms/text
shipped default, one text per call 58.2
batch 16, length-sorted 79.3
batch 64, length-sorted 100.3
4 threads 66.1
8 threads 101.4

Do not retry either without a fresh measurement on different hardware.

What not to conclude

That embedding got cheaper. It did not: a real version move still costs ~62 s per 315 documents, and that is the floor for a release that changes how documents are read. The only win available was not embedding what did not change.

Nor that the daemon helps here. It keeps the model warm, which is worth ~0.5 s against a 58 s command — the observed 65 s through a warm daemon and 58.5 s in-process are the same number on a different day.

To amend: Re-verify: