Repository navigation
Native find_nearest: include_vector=False returns hits without the indexed vector (explicit opt-in) - #204
Native find_nearest: include_vector=False returns hits without the indexed vector (explicit opt-in)#204tae898 wants to merge 4 commits into
Conversation
…a Python list NativeVectorIndex._run_search turned every query into a list with tolist(). The codec then checked the list for float32-exactness (_exact_float_list: a pass of numpy over the list, tag BL) or, for a float64 query whose values float32 rounds, printed JSON number text (about 611 us at 1536 dimensions, #107). A numeric 1-d query (an ndarray of any numeric dtype, or a list/tuple of numbers, which numpy walks in C) now goes to the codec as a float32 array and travels through the float32 side buffer (tag B) directly. The engine reads the vector as float32 whatever it is given, so the rounding is unchanged. Anything else (text or None entries, a generator, ragged input, numpy missing) stays a list and reaches the engine as before. Port of humemai/arcadedb-embedded-python#523 (backlog row N-07) onto main after #107: the per-element _prep calls it removed are gone since #107, what remains is tolist() plus the exactness check, and the float64 text path. Untimed; a timed A/B decides it. Tests: test_find_nearest_query_forms.py (both backends: every form answers the same neighbours at the same distances, refusals unchanged; passes before and after) and test_native_find_nearest_params.py (native: where the floats travel and that the list exactness check is not run; 5 of its 6 tests fail on the code before). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
No textual conflicts: main's #202 (record ids as a :rid parameter, src/arcadedb/_native/records.py and database.py) and #201 (run_in_transaction witness) touch other hunks than this branch. The record-id rule of #202 is kept: this branch pastes no record id into SQL text (the lazy per-hit vector load, if present, goes through lookup_by_rid, which keeps its literal id by design, see tests/test_native_statement_texts.py). Static checks after the merge: compile() and pyflakes of the changed files (the unused typing.Optional in records.py is on main too), pre-commit black and isort pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Timed A/B (native wheel; arm A = main's CI native wheel, arm B = the same tree with only this PR's files, A/A control; laptop timed lane,
Clean pairs per figure: 12; answers equal on both arms in every round. By the rule written before the run this is a DROP as the default: a caller who reads the vector of each hit pays one extra lookup per hit, 1.336 [1.333, 1.346] at 256 dimensions, above the rule's 1.10 regression cap, while callers who do not read the vector gain clearly. So the cut is real but only for callers who do not need the vectors, which makes it an API question rather than a default. The options as we see them, with no choice made here: (1) an explicit opt-in on |
…dexed vector, loaded by RID on first read find_nearest keeps its default: every hit carries its vector, and the default search is the code of the query-as-float32 change (#203), untouched. A new keyword-only include_vector=False asks for hits built without the vector, for callers that need only ids or distances. On the native wheel the search then runs SELECT *, !<vector property>, !record FROM (SELECT expand(vectorNeighbors(...))) and each hit loads its record once by RID the first time the vector or the whole record is needed. The JVM build accepts the keyword and ignores it (it loads whole records either way). Reworks the earlier lazy-by-default version after its timed A/B: hits not read 0.66 and 0.805 (1536 and 256 dims), but vector read 1.011 and 1.336, so lazy-by-default is a cost for callers that read the vector. Tests run on both backends (values equal lookup_by_rid, keyword accepted) and on the native wheel (default ships the vector with no lookup and no dropped column, opt-in ships none and looks up once per record). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
9e0c931 to
c91a828
Compare
|
Change of design, 2026-11-04. The first version of this PR made every The PR is rebased onto #203 (head 02103b5) and its base is now |
find_nearestkeeps its default: every hit carries its indexed vector, and the default search is the code of #203 (query as one float32 buffer), unchanged. This PR adds a keyword-onlyinclude_vector=Falsefor callers that need only the ids, the distances or other properties of the hits.On the native build,
find_nearest(q, k, include_vector=False)runsSELECT *, !<vector property>, !record FROM (SELECT expand(vectorNeighbors(...)))and builds each hit without the vector (768 or 1536 floats per hit as JSON text, most of it read by nobody). A hit loads its record once, 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). Values, property order and types are those oflookup_by_rid, and a hit deleted since the search raisesArcadeDBErroron that first read. A caller that opts in and then reads the vector of every hit pays one extra lookup per hit, so the keyword is for searches that do not read it. The JVM build accepts the keyword and ignores it: it loads whole records either way, so the call costs the same (stated in the docstring and indocs/modules/ROOT/pages/how-to/vectors.adoc, which gets one short paragraph).Why an opt-in and not the default. The first version of this PR made every hit lazy. Its timed A/B (native wheel of main 7e64c5e, engine 26.10.2,
find_nearestk = 10, ab.py 95% CI, 12 clean pairs) was good for callers that do not read the vector and bad for callers that do:At 256 dimensions the lookup per hit costs more than shipping the vector, which is above the 1.10 cap for a default. The statement control was flat. So the default stays as it is, and a caller who knows it does not need the vector says so.
Stacked on #203: the base of this PR is
perf/native-find-nearest-ndarray, and the diff is the three files of the change (src/arcadedb/_native/records.py,src/arcadedb/_native/schema_objects.py,src/arcadedb/vector.py) plus docs and tests. Retarget tomainafter #203 merges.Tests.
tests/test_vector_hit_records.pyruns on both backends: the default call and the opt-in call equallookup_by_ridin type, property names and order, every value and the vector, for a four-bucket type with a subtype and in every first-access order; a hit can be modified and saved;ef_searchandallowed_ridswork with the keyword; the keyword is keyword-only and changes no answer; an opt-in hit of a type with properties calledrecordanddistance(the names of the search row's own columns) equals the lookup.tests/test_native_vector_hit_lazy.pyis native only: the default search ships the vector, runs a statement that drops nothing and looks up nothing; the opt-in search ships no vector and looks up nothing, then one lookup per record however the vector is reached, the loaded vector equals the default's, and a closed database does a lookup for none. Not run locally (no workload on this machine); CI runs them.black26.10.1,isort9.0.2 (profile black),banditandcompileare clean on the changed files.Known and left alone: on the default path (this PR does not touch it), a type with a property called
recordordistancestill gets a wrong or failing hit on nativefind_nearest, because those names are the search row's own columns. The opt-in path handles them. A fix of the default path would be a separate change.The timing of this design (the opt-in against the default, and the default path against #203's head, both dims) is a pre-registered study, not run yet; this PR makes no speed claim until it is. It stays a draft.
Refs #19
Edited 2026-11-04: reworked from "lazy by default" to an explicit opt-in (include_vector=False), stacked on #203; the first body described the lazy-by-default design and its side fix for properties named
recordordistanceon the default path.🤖 Generated with Claude Code