Skip to content

Native find_nearest: a hit loads its vector on first use, not with every search (draft, stacked on #523) - #533

Closed
tae898 wants to merge 1 commit into
perf/native-find-nearest-ndarrayfrom
perf/native-find-nearest-lazy-vector
Closed

tae898 wants to merge 1 commit into
perf/native-find-nearest-ndarrayfrom
perf/native-find-nearest-lazy-vector

Conversation

@tae898

@tae898 tae898 commented Oct 25, 2026

Copy link
Copy Markdown

Native find_nearest no longer ships each hit's vector. Part of backlog row B-01 (the native half, N-08); the JVM half is a separate PR. Stacked on #523 (perf/native-find-nearest-ndarray), which edits the same function (the query side); this PR is the result side. Merge #523 first, or retarget this PR to native-preview once it is in.

The statement behind it, SELECT expand(vectorNeighbors(..)), returned every hit's whole record twice: nested in the record column and flattened into the row, vector included both times. The search is now SELECT *, !, !record FROM (SELECT expand(vectorNeighbors(?, ?, ?))), with a fourth parameter for the efSearch/filter options as before. The hit is built from the row's @rid, @type and distance and the remaining properties, without the vector. Type and kind come from @type of the row (a hit of a subtype keeps its own type) and _kind_of.

The documented hit contract is unchanged: a hit is (record, distance) with a full record. The record fills itself once, by one lookup_by_rid, the first time the vector is read (get, has_property) or the whole record is needed (to_dict, get_property_names, modify, save), so values, types and property order are those of lookup_by_rid. A search whose hits never touch the vector ships and looks up no vector. A hit deleted since the search raises ArcadeDBError on that first read. docs/api/vector.md says so.

Side fix: the engine's search row hides a record property called record or distance behind its own columns (ScoredRecordRow), and on native a type with a property called record made find_nearest fail with AttributeError: 'str' object has no attribute 'get' (the new shared test failed that way before the change). Such properties are now held back with the vector and load with it, and distance of the row is always the search score.

Untimed counts (dim 128, k 10, four buckets, 300 records): floats in the returned rows 2570 before, 10 after (the distances); lookups after reading id and text of all 10 hits 0, after also reading the vector 10 (one per hit). No timing was taken; the microseconds belong to a study.

Local checks (native image: a copy of native-lib/nnext, Python only, no rebuild): test_vector_hit_records.py (8 tests, both wheels: hits equal lookup_by_rid for a four-bucket type with a subtype and properties called record and distance, every first-access order, modify and save, ef_search, allowed_rids, find_nearest_by_key) and test_native_vector_hit_lazy.py (3 tests, native only: nothing shipped, one lookup per record, closed database). Written first: the shared file failed 8 of 8 on unchanged native. With the stack: 97 passed, 24 skipped (the usual NOTYET skips) over test_native_find_nearest_params.py, test_vector.py, test_vector_second_pass.py, test_vector_params_verification.py, test_vector_hit_records.py, test_native_vector_hit_lazy.py and test_core.py; the shared tests also pass on the JVM wheel (55 passed with test_vector.py). A fixed-seed dump of 120 find_nearest calls (rid, repr of score, k 1 to 50, 1, 4 and 8 buckets) is identical before and after, as is the shape of a hit (type, property names and order, vector length). black, isort and the pre-commit hooks pass.

Overlap with the open native PRs, by git merge-tree of this branch against each head: #523 conflicts in _native/schema_objects.py and is why this PR is stacked on it. #514, #520, #521, #525, #526 and #531 are clean. #522 and #529 each add a collect_ignore.append line next to mine in tests/conftest.py, a trivial adjacent-line conflict for whichever merges second. No native-image rebuild is needed.

🤖 Generated with Claude Code

…ery search

The statement behind find_nearest, SELECT expand(vectorNeighbors(..)), sent every hit's whole record twice, nested in the record column and flattened into the row, vector included. The search now drops the indexed vector (and the record column) in SQL, takes rid, type and distance from the row, and builds the hit without the vector. The record loads it once, by RID, when the vector is read or the whole record is needed (get, has_property, to_dict, get_property_names, modify, save). Values, property order and types stay those of lookup_by_rid.

A property of the record called record or distance was hidden by the search row's own columns, and a type with a property called record made find_nearest fail with an AttributeError. Such a property now loads with the vector.

Tests: test_vector_hit_records.py runs on both wheels (hits equal lookup_by_rid for a four-bucket type with a subtype and properties called record and distance, every first-access order, modify and save, ef_search, allowed_rids, find_nearest_by_key); test_native_vector_hit_lazy.py counts what is shipped and looked up.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@tae898

tae898 commented Oct 26, 2026

Copy link
Copy Markdown
Author

Closing here: the bindings moved to ArcadeData/arcadedb-python. This change has no counterpart there yet and no queued A/B. If it is wanted, it goes there as its own PR: it is now a line in the tracking issue ArcadeData/arcadedb-python#19. The branch stays.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant