Repository navigation
Native find_nearest: include_vector=False returns hits without the indexed vector (explicit opt-in) - #204
Conversation
|
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 I see them, with no choice made here: (1) an explicit opt-in on |
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 |
|
Timed A/B of the explicit opt-in on the native build (laptop timed lane,
Clean pairs per figure: 12. By the rule written before the run this is a SHIP as an explicit opt-in: the opt-in calls are faster on both dimensions, the default call (which keeps today's behaviour, every hit carrying its vector) shows no lower interval end above 1.03, and the A/A control is clean. Opting in and then reading every vector costs one lookup per hit: 1.0 [0.999, 1.004] at 1536 dimensions and 1.348 [1.34, 1.352] at 256, so the keyword is for callers that do not read the vector; its docs will say so, with these figures, before it merges. This PR is stacked on #203: it merges after #203, in the batch at the end of the current timed studies that pin main, with its docs (the keyword and these figures). |
#250) Describe the #203 behavior where users read about find_nearest: a paragraph and the A/B table in vectors.adoc (figures copied by script from the study's score file, p50 B/A with 95% intervals), a pointer from the statement-only "A query vector" section, and the query_vector Args text of find_nearest. The find_nearest_approximate Args stay as they are: its native path (_run_approximate) does not use the float32 buffer. The which-package vector-search medians predate #203, so they are labelled as measured before it instead of being replaced by numbers that were not re-measured. The sentence with the find_nearest signature is untouched so the include_vector change (#204) rebases cleanly. Closes #249 Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…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>
…mpare hit distances like the query-form tests The "Hits without the vector" section of vectors.adoc now carries the timed A/B of the opt-in (native wheel 26.11.1.dev0, engine 26.10.2, k = 10, 12 clean pairs per figure): p50 B/A with 95% intervals for include_vector=False (hits not read, ids read, vector of every hit read) and for the unchanged default call, at 1536 and 256 dimensions. The figures are generated from the study's score file by a script, not typed. The text says the keyword is an opt-in for callers that do not read the vector (256 dimensions: reading every vector afterwards costs more than the default). test_the_keyword_changes_no_answer compared distances with ==, which the JVM build does not guarantee between two calls (main's _assert_same rule): ids compare exactly, distances exactly on the native build and to a few float32 ulps on the JVM build. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
c91a828 to
ae8b1ef
Compare
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 a section "Hits without the vector" with the figures below).Why an opt-in and not the default. The first version of this PR made every hit lazy; its timed A/B showed that is a cost for callers that read the vector (256 dimensions: 1.336 [1.333, 1.346] for id and vector read, above the 1.10 cap for a default). So the default stays as it is. The timed A/B of this design (
tools/ab.py --noise, ABBA, round 0 dropped, an A/A control, answers identical in every run; native CI wheel of main 7e64c5e, engine inside it 26.10.2; arm A is #203's code, arm B is A plus this PR's files at its pre-rebase head c91a828, whose change the rebase left as it was (range-diff differs in context lines only);find_nearestk = 10 on 100,000 vectors per dimension count; 12 clean pairs per figure) is below. It was run by the rule written before it (the opt-in faster with the upper interval end below 1.00, the default call non-inferior with no lower end above 1.03), and the rule gives SHIP as an explicit opt-in. B/A of the p50 per call, 95% interval, below 1 is faster:include_vector=False, hits not readinclude_vector=False, hit ids readinclude_vector=False, then every vector read (one lookup per hit)The A/A control is clean and the statement control (
sql_id) is flat. The keyword is for callers that do not read the vector: at 256 dimensions the lookup per hit costs more than shipping the vector, so opting in and then reading every vector is slower than the default (1.348), which the docs say. The engine inside the wheel is 26.10.2, which still reads each record for the search row; whether the figures carry over to the 26.11.1 engine is not measured here.This PR was stacked on #203; #203 is merged, and the branch is rebased onto main (5d6e47c) with the diff reduced to this change (
src/arcadedb/_native/records.py,src/arcadedb/_native/schema_objects.py,src/arcadedb/vector.py) plus docs and tests. The conflicts were insaveandmodifyofrecords.py, next to main's lost-update check (base=, #194) and point-operation glue (own=, #230); both are kept: a lazy hit loads its record beforemodify()takes the valuessave()compares, so the check sees the whole record.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. Distances intest_the_keyword_changes_no_answercompare as main's_assert_samedoes: exactly on the native build, to a few float32 ulps on the JVM build. Not run locally after the rebase (no workload on this machine); CI runs them.compile, pyflakes and the repo's pre-commit hooks (black, isort) are 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.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.Edited 2026-11-05: rebased onto main (#203 merged), docs section carries the measured figures of the opt-in study, which replace the earlier "study not run yet" paragraph.
🤖 Generated with Claude Code