Repository navigation
Conversation
8 tasks
…ry entry and answers as the JSON route does (refs #388, #315) find_nearest_approximate sent the query as a JSON list of d numbers and read the hits from a JSON list of [rid, score]. The tests pin the binary route that replaces it, before it exists, so a run of this commit shows them failing on the old code. test_native_vindex_glue.py needs no library: a stand-in for arcadedb_n_vindex_bin reads the buffers by their addresses. It pins the floats the library would be given for every numeric form (bit for bit against a C cast through struct, with the exact ties, a subnormal and an underflow), the filter as int64 pairs and the plain-RID-text rule, the buffer size and the second call when the answer is larger, the answer's order, RID texts and score bits against the JSON route's, and every shape that must keep the JSON route (a generator, None, bool, NaN, infinity, beyond float32, empty, integer array, 2-d, numpy scalars, big-endian, an int beyond 2**53, a k beyond a C int, a buffer beyond a gibibyte). test_native_vindex_bin.py needs the library: a four-bucket PRODUCT index of 1200 vectors, k of 3 and 500, three query forms, the scores of both routes compared as packed doubles, queries that float32 rounds (ties, a subnormal), a filter by text and by record, the second call for an answer larger than the first buffer, and the outcome of the shapes that keep the JSON route and of a wrong dimension, which must be the same with the entry switched off. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… the hits from one buffer (arcadedb_n_vindex_bin) (refs #388, #315) The query crossed as a JSON list of d numbers that Java parsed into a list and narrowed to float[], and the hits came back as a JSON list of [rid, score] that Python parsed again. Now a float32 query crosses as a pointer and a count, a filter as int64 (bucket, position) pairs, and the library writes the hits into a buffer that Python provides: three int64 words per hit, the RID's bucket and position and the score as the bits of a double. The query is narrowed to float32 in Python with the same rounding as Java's floatValue(). The score is Double.parseDouble(Float.toString(f)), the double that the JSON route's number text of the same float parsed to, so the two routes return the same bits (the exact float widened would differ in the last digits of nearly every score, which is a visible change and not this one). The entry returns the hit count, or -1 with the error text in arcadedb_n_last_error. A buffer too small gets the count and nothing is written; Python asks again with room, once for a type whose bucket indexes outnumber the first guess of eight, and remembers the size. What the entry cannot carry keeps arcadedb_n_vindex: a query that is not a float array, a float or int list (a generator, numpy scalars, None, bool, an int beyond 2**53, NaN, infinity, an empty query), a filter entry that is not plain #bucket:position text, a k beyond a C int, an answer buffer beyond a gibibyte. ARCADEDB_NATIVE_VINDEX_BIN=0 selects the JSON route, and a library without the entry does by itself. find_nearest and the other searches are statements (vectorNeighbors): they already send a NumPy query as float32 and read rows, a different Java op, and are not changed. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
tae898
force-pushed
the
perf/388-native-vindex-bin
branch
from
November 14, 2026 09:28
f567c50 to
03e9b88
Compare
…nes (refs #388, #398) The one-letter ctypes aliases of the sigs table are defined a few lines above it, and the letter for the integer is N on main and I on the 26.11.1.post3 release line. A table line added on one base and merged on the other is a NameError on the first load of the native library, which no single branch shows and no test of a function finds: the post4 integration branch had exactly this for arcadedb_n_vindex_bin. The test reads the source of lib.py with ast: every name the table uses must be assigned in _load, with no library and no native backend, on any base. A planted fault shows that it finds a letter that is not defined. This branch itself is correct (its base is main, where the integer is N). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this PR do?
On the
arcadedb(native) wheel,find_nearest_approximatesent the query vector to the library as a JSON list of d numbers (text that Python printed and Java parsed into a list of boxed doubles, then narrowed to afloat[]) and read the hits back as a JSON list of[rid, score]that Python parsed again. Now a query that float32 carries crosses as a pointer and a count, a filter crosses as int64 (bucket, position) pairs, and the library writes the hits into a buffer that Python provides. It is the same shape as the other address entries (arcadedb_n_insert_colp,arcadedb_n_exec_bin,arcadedb_n_gb_bin). This is rank 5 of the scan in #388, the one that was not in the first audit.The new entry, in
NativeExtras.java:It returns the number of hits n. When
n <= capHitsthe hits are inout, three int64 words each: the RID's bucket, its position, and the score as the bits of a double. When n is larger nothing is written and Python asks again with room for n. -1 is an error, and its text is read witharcadedb_n_last_error, as forarcadedb_n_tx.arcadedb_n_vindexstays for themetadataandbuildops and for the queries the new entry cannot carry. Both entries share one lookup of the bucket indexes (vectorParts) and one search loop (approximateHits).On the Python side (
schema_objects.py,lib.py),NativeVectorIndex._run_approximatetries_approximate_packedand falls back to_approximate_json, which is the old code. The merge of the bucket answers (_sort_results, cut to k) and the record loads by RID are unchanged.What the answer is, and what stays on the JSON route
The call returns the same list as before: the same records in the same order, the same RID texts, and scores of the same type with the same bits. A score on the JSON route is the double that the number text of the engine's float parses to (
Float.toString, then Python'sfloat()), not the float widened. The new entry computes that same double in Java withDouble.parseDouble(Float.toString(f)), so the two routes agree by construction and not by a rounding argument. That is why the buffer holds a double and not a float32. The query is narrowed to float32 in Python with numpy's cast, which rounds to nearest even as Java'sfloatValue()does, and the tests compare both routes bit for bit, with exact ties and a subnormal in the query.A query crosses by address when it is a one-dimensional
float16,float32orfloat64array of the native byte order, or a list or tuple offloatandint(anintbeyond 2**53 is not one). Everything else keeps the JSON route, which returns the same hits: a generator, a NumPy scalar,Noneor a bool in a list, an integer array, a 2-d array, an empty query, and a NaN, an infinity or a value that float32 overflows to. A filter crosses as int64 pairs when every entry is plain#bucket:positiontext (a record is itsget_rid()text, as before); anything else goes to the engine's own RID parser through the JSON route. Akbeyond a C int, an answer buffer of more than a gibibyte, a missing numpy,ARCADEDB_NATIVE_VINDEX_BIN=0and a library built before this entry also keep the JSON route. The switch and the older-library fallback are the ones the other address entries have.What it allocates
Per call, in Python: the float32 query array (d x 4 bytes; none when the caller passes a contiguous float32 array, which is used as it is), the answer buffer (24 bytes per hit; room for 8 bucket indexes of k hits at first, so 192 x k bytes, and larger once for a type with more bucket indexes, remembered per index), and for a filter of m RIDs the int64 pair array (16 x m bytes) plus a transient list of 2m short strings from the digit scan. After the call: one tuple per hit and one RID text per hit, as the JSON route built. In Java: the
float[]of d floats and theHashSetof the filter, as before, and noStringBuilderfor the hits and no parsed list of boxed doubles for the query. Nothing holds more than one query or one answer.Other searches
Only the approximate search used this JSON shape.
find_nearest(also withinclude_vector=False, andfind_nearest_by_key) runsvectorNeighborsas a statement: its query is already a float32 array in the side buffer ofarcadedb_n_exec_bin, and its hits are result rows (the rank 6 path of #388), a different Java op, so this entry does not cover it and I changed nothing there.get_metadata,get_stats,get_sizeandbuild_graph_nowuse themetadataandbuildops ofarcadedb_n_vindexor statements, once per call and with no vector, and stay as they are.Related issues
Refs #388 and #315.
Performance changes
Not measured yet, and no number is stated here. The change removes the number text of the query on both sides and the hit text of the answer; the tests pin what crosses, not how long it takes. The figure for the timed A/B is pre-registered as follows.
find_nearest_approximate(query, k)call on thearcadedb(native) wheel, against the released26.11.1.post3wheel as the baseline, ABBA with an A/A control, one fresh process per run.pq_clusters), so the search is approximate. The sizes that matter: d of 128, 768 and 1536; k of 1, 10 and 100; a query as a float32 array (the form that is not copied) and as a float64 array (one narrowing copy); one filtered case with a few thousand RIDs.krecord loads by RID that the call makes, so the figure is what a caller sees. The loads are unchanged by this PR; k of 1 is where they weigh least.find_nearest(query, k)on the same index (the statement route, untouched by this change), measured in the same runs.Docs
docs/modules/ROOT/pages/how-to/vectors.adoc: one paragraph after the quantization rules on howfind_nearest_approximatecrosses onarcadedb, which queries keep the text route, and the switch. No other page or the README describes it.How I checked it
The tests are in the first commit and the change in the second, so the first commit fails on the old code: every test uses
lib.VINDEX_BINorlib.vindex_bin, which the parent does not have.tests/test_native_vindex_glue.pyneeds no library. A stand-in forarcadedb_n_vindex_binreads the query and the filter by their addresses, as the library does, and writes hits into the buffer. It pins the floats the library is given for every numeric form (bit for bit against a C cast throughstruct, so numpy is not its own witness), the filter pairs and the plain-text rule, the buffer size and the second call, the order, RID texts and score bits against the JSON route, and each shape that keeps the JSON route.tests/test_native_vindex_bin.pyneeds the native library (not collected on the JVM wheel): a four-bucket PRODUCT index of 1200 vectors, k of 3 and 500, three query forms, the scores of both routes compared as packed doubles, queries that float32 rounds, a filter by text and by record, the second call for an answer larger than the first buffer, and the same outcome as the JSON route for each shape that keeps it and for a wrong dimension and an unindexed property.uvx pre-commit run --fileson the changed files, Bandit as CI runs it,compile()and pyflakes. I did not run the tests: the laptop's queue guard blocks the test runner, and there is no JDK 25 here for the Java, so CI has to prove the Java compiles and every test passes. A draft pull request skips the platform legs, so I dispatched the native test workflow on the branch myself.CI: the native workflow that I dispatched on the branch (run 43568546290) passed on all four platforms. The Java compiles, and the parity suite passed with no failure, error or skip, including
test_native_vindex_bin.py(19 tests) andtest_native_vindex_glue.py(51 tests) on linux-x86_64 with Python 3.12. That run was on the commit before I rebased the branch ontomain. The pre-commit check of this pull request then failed with ruff F821 on the ctypes type letterIinlib.py, becausemainrenamed it toN. I renamed it in my entry, rebased, and every hook ofmain's configuration passes locally. The native workflow has not run on the rebased commit: the only code difference from the tested one is that one-letter rename.Checklist
uvx pre-commit run --files <changed files>passes🤖 Generated with Claude Code