Repository navigation
Read a full scan's vectors through one blob handle per chunk - #17
Conversation
A vec0 full scan read each row's vector through vec0_get_vector_data(), which looks up the row's chunk, opens the chunk's vectors blob, reads one vector and closes the blob again. A chunk's blob holds chunk_size vectors (3 MiB for 1024 float[768]), and the first read through a newly opened handle walks the blob's overflow pages up to the row's offset, so the opens dominated the scan. The scan already visits rows in (chunk_id, chunk_offset) order, and the cursor now keeps one blob handle per vector column, moved to the next chunk with sqlite3_blob_reopen(). Each row's chunk position is looked up when its vector is read: the scan's rowid statement sorts every row when the scan starts, so its positions would miss a delete, insert or optimize the same connection runs during the scan. A write that replaces the chunk's row expires the handle, and a rollback of a transaction that changed the schema invalidates it (SQLITE_ABORT, or SQLITE_ABORT_ROLLBACK with extended result codes on); the read is then retried once on a newly opened handle. Any other failure to move the handle, such as an interrupt, fails the read. The position lookup and that failure handling follow photostructure/sqlite-vec@f793711 by Matthew McEachen, which corrected this change after it merged there as photostructure#1. On a 10,000-row float[768] table (medians of 3 interleaved runs of 7 queries), selecting every vector takes 11.7 ms instead of 760.8 ms in a database file and 5.5 ms instead of 88.1 ms in memory, and vec_distance_cosine() against each 32.2 ms instead of 777.1 ms and 24.6 ms instead of 110.6 ms. tests/test-fullscan.py checks every row's vectors against point lookups across chunks with gaps and two vector columns, reads a chunk rewritten in the middle of a scan, fails on a row deleted during the scan instead of returning another row's vector, reads rows that optimize moves during the scan, and fails a scan cancelled while its handle moves to the next chunk; tests/test-unit.c scans across a rollback that changed the schema, with extended result codes off and on.
0131d7d to
b223b93
Compare
|
Thanks for the update, and for pulling in mceachen's correction. The updated head resolves the three cases I had found on the previous one, and I re-ran them here:
The speedup is intact: a file-backed 10k x float[768] scan went from 1704 ms to 12.6 ms on the previous head here, and the per-row position lookup costs little on top of that. Full suite and Two optional follow-ups, not blocking: |
A
vec0full scan reads each row's vector throughvec0_get_vector_data(), which looks up the row's chunk, opens the chunk's vectors blob, reads one vector and closes the blob again. A chunk's blob holdschunk_sizevectors (3 MiB for 1024float[768]), and the first read through a newly opened handle walks the blob's overflow pages up to the row's offset, so the opens dominate the scan.On a 10,000-row
float[768]table (medians of 3 interleaved runs of 7 queries):select sum(length(embedding)) from vselect sum(vec_distance_cosine(embedding, ?)) from vThe scan already visits rows in
(chunk_id, chunk_offset)order, and the cursor now keeps one blob handle per vector column, moved to the next chunk withsqlite3_blob_reopen(). Each row's chunk position is looked up withvec0_get_chunk_position()when its vector is read: the scan's rowid statement sorts every row when the scan starts, so positions taken from it would miss a delete, insert oroptimizethe same connection runs during the scan. A write that replaces the chunk's row expires the handle, and a rollback of a transaction that changed the schema invalidates it (SQLITE_ABORT, orSQLITE_ABORT_ROLLBACKwith extended result codes on); the read is then retried once on a newly opened handle. Any other failure to move the handle, such as an interrupt raised by a progress handler, fails the read.The position lookup and that failure handling are mceachen's: photostructure/sqlite-vec@f793711 corrected this change after it merged there as photostructure#1, for the three problems his review found. Two adaptations here: this fork has no
vec0_error_rc(), so a failed move fails the read withSQLITE_ERRORlike the other failures in the function, and a cancellation inside vec0's own statements is reported here as a vec0 error rather thaninterrupted, so his interrupt test checks that the cancelled scan fails rather than its message.tests/test-fullscan.pychecks every row's vectors against point lookups across chunks with deleted rows and two vector columns, reads a chunk whose shadow row is rewritten in the middle of a scan, fails on a row deleted during the scan instead of returning the vector of the row that took its slot, reads rows thatoptimizemoves during the scan, and fails a scan cancelled while its handle moves to the next chunk.tests/test-unit.cscans across a rollback that changed the schema, with extended result codes off and on. Those last four fail on this PR's previous head and pass here. The full suite passes (148 passed, 4 skipped, 259 snapshots), as do the C unit tests.It merges cleanly with #9 through #14 and #18 through #24. The same change is merged on photostructure/sqlite-vec as photostructure#1 with f793711 on top, and open upstream as asg017#336.