Context — why this is a follow-up, not a rewrite
The vectors phase used to spend ~91s of kernel I/O on a 1167-file repo because coco.mount_each creates one Lance merge_insert transaction per file (1168 fragments, 1170 manifest commits). cocoindex flushes target writes once per processing component, and mount_each makes one component per source item. cocoindex does not yet batch target writes across mount_each components — this is upstream issue cocoindex#2219 ("Support batching for target state sinks from different components", filed 2026-06-25), which is literally this problem.
Current approach (this branch)
app_main now iterates the source in a single component (async for … in walk_dir().items() instead of coco.mount_each), so all declare_row calls flush as one merge_insert per table. process_*_file stay @coco.fn(memo=True), and _RowHandler.reconcile skips rows whose fingerprint is unchanged.
Measured on Shopizer (1167 files / ~3500 chunks):
| metric |
mount_each (before) |
single-component (after) |
| vectors wall |
119.7s |
40.4s (CPU) / 37.5s (MPS) |
kernel I/O (sys) |
91.15s |
4.2s |
| Lance fragments |
1168 |
1 |
| no-change increment |
— |
2.4s, 0 new fragments |
| 1-file-change increment |
— |
10.2s, +1 fragment |
Correctness holds: random uuid4() ids are only generated on a memo miss (changed file); on a memo hit coco replays the recorded target state (same ids, same content) and reconcile skips. No need to leave cocoindex or unify on FileHashTracker for vectors.
This issue — the residual cost + the upstream unlock
The single-component loop has one residual cost: on every increment it re-iterates all files so coco can fingerprint each for memo/reconcile (~3s fixed on 1167 files; grows linearly with repo size). mount_each avoids the re-visit (per-file source memo skips unchanged files entirely) — but mount_each is exactly what caused the per-file transaction storm.
When cocoindex#2219 ships and we adopt a compatible cocoindex version:
- Revert
app_main to mount_each (restores per-file source-memo granularity → unchanged files skipped entirely on increment, no re-visit, no re-fingerprint).
- Enable native cross-component sink batching → few
merge_insert transactions without sacrificing per-file memo granularity.
That gets the best of both: fast bulk init AND proportional, no-revisit increment. Until then, the single-component loop is the supported workaround and the init/reprocess win is already captured.
Related upstream: cocoindex#2220 (schema-aware LanceDB row fingerprinting) — would let reconcile skip unchanged rows even cheaper.
References
Context — why this is a follow-up, not a rewrite
The vectors phase used to spend ~91s of kernel I/O on a 1167-file repo because
coco.mount_eachcreates one Lancemerge_inserttransaction per file (1168 fragments, 1170 manifest commits). cocoindex flushes target writes once per processing component, andmount_eachmakes one component per source item. cocoindex does not yet batch target writes acrossmount_eachcomponents — this is upstream issue cocoindex#2219 ("Support batching for target state sinks from different components", filed 2026-06-25), which is literally this problem.Current approach (this branch)
app_mainnow iterates the source in a single component (async for … in walk_dir().items()instead ofcoco.mount_each), so alldeclare_rowcalls flush as onemerge_insertper table.process_*_filestay@coco.fn(memo=True), and_RowHandler.reconcileskips rows whose fingerprint is unchanged.Measured on Shopizer (1167 files / ~3500 chunks):
mount_each(before)sys)Correctness holds: random
uuid4()ids are only generated on a memo miss (changed file); on a memo hit coco replays the recorded target state (same ids, same content) and reconcile skips. No need to leave cocoindex or unify onFileHashTrackerfor vectors.This issue — the residual cost + the upstream unlock
The single-component loop has one residual cost: on every
incrementit re-iterates all files so coco can fingerprint each for memo/reconcile (~3s fixed on 1167 files; grows linearly with repo size).mount_eachavoids the re-visit (per-file source memo skips unchanged files entirely) — butmount_eachis exactly what caused the per-file transaction storm.When cocoindex#2219 ships and we adopt a compatible cocoindex version:
app_maintomount_each(restores per-file source-memo granularity → unchanged files skipped entirely on increment, no re-visit, no re-fingerprint).merge_inserttransactions without sacrificing per-file memo granularity.That gets the best of both: fast bulk init AND proportional, no-revisit increment. Until then, the single-component loop is the supported workaround and the init/reprocess win is already captured.
Related upstream: cocoindex#2220 (schema-aware LanceDB row fingerprinting) — would let reconcile skip unchanged rows even cheaper.
References