Repository navigation
A long float list, or a float64 array in a named parameter, crosses as one buffer - #107
Conversation
…s one buffer (#106) JVM: a list of 32 or more Python floats and nothing else, or a native-order float64 vector, becomes one double[] that Java boxes into the same ArrayList<Double> the element-wise conversion built (505 us -> 44 us for 1536 numbers, 570 us -> 60 us for the named float64 array). Mixed lists, ints, bools, None, strings, numpy scalars and short lists keep the old path, so an int stays a Long. Native: a list of 64 or more Python floats that float32 holds exactly (the check double -> float32 -> double is the identity, finite numbers only) goes in the float32 side buffer under a new tag BL that Json.java reads as a List<Double>; every other list stays JSON text. _prep also stops walking a list of plain scalars element by element. Tests state the bytes (codec), the Java objects (spy on the one buffer crossing), and that IN, list properties, equality, CONTAINS and vectorNeighbors give the same results. The docs name the float32 array as the fast path. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Timed A/B done: base wheel (main
Every changed form is faster, about 16% (JVM) and 20% (native) per 1536-d query, and the cost on top of an unchanged float32 query ( Merging on green. |
…a Python list (#203) * Native find_nearest: send the query vector as one float32 array, not 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> * tests: isort order for the find_nearest query-form tests Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * tests: find_nearest query forms compare JVM distances to a few float32 ulps, native exactly The JVM leg of the PR run failed test_every_form_of_the_query_returns_the_same_neighbours_at_the_same_distances on one distance that differed in the last float32 bit between two calls with the same query values (0.9530812501907349 against 0.9530811309814453, same neighbour order); the same test passed on this head in the dispatched JVM run. Exact distances are not a contract on the JVM build. On the native build, the path this PR changes, the comparison stays exact, because a tolerance of float32 size could not tell a query that float32 rounds from its float32 form, which is what the float64 test guards. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Closes #106
Draft: a timed A/B (
ab.py) of the two input forms follows before this leaves draft. The numbers below are untimed encode costs on the laptop (cores 14,15, 2000 repetitions each), information and not a timing claim.Reasoning
A list is not always a vector, so the change takes only what is unambiguous and leaves every other value on the path it had. The engine must read exactly the object it read before, not an approximation of it. That rules out turning a list into float32 on the JVM build (
float[]is a different type fromArrayList<Double>, and float32 rounds doubles), and it rules out any change that depends on guessing from the SQL text whether a parameter is a vector.What changed
JVM build (
src/arcadedb/type_conversion.py,convert_python_to_java):set(map(type, value)) == {float}) crosses as onedouble[], and Java boxes it withArrays.stream(...).boxed().toList()into anArrayList. That is the sameArrayList<Double>, value for value (NaN, infinities and-0.0included), that the element-wise path built.float64array of 32 or more numbers takes the same route instead ofvalue.tolist()and the boxed list.Native build (
src/arcadedb/_native/codec.py,native/src/main/java/org/humemai/nativelib/Json.java):double -> float32 -> doubleis the identity and every number is finite, goes in the existing float32 side buffer under a new tagBL.Json.decodeTagreads it as aList<Double>(the float widens to the same double), the object the JSON text became. A list from a float32 model (ndarray.tolist()) passes; a list of other doubles,NaNor an infinity fails the check and stays JSON text._prepreturns a list of plain scalars as it is instead of rebuilding it element by element (69 us for 1536 numbers, no change in the text).The docs (
databases-and-queries.adoc, "A query vector") namenp.asarray(v, dtype=np.float32)as the fast path and state which forms are rounded to float32.Encode cost, 1536-d, same laptop and entry points as the issue's second table
On the native build the list went from 31686 bytes of text to a 38 byte stub and 6144 bytes of float32.
What deliberately did not change, and why
int,bool,None,stror numpy scalar among its floats, anintlist, and any list shorter than the threshold: the old path, so anintstays anint(Long) and every type survives. A list of numpy scalars (list(array)) also keeps the old path, afloatsubclass too.float32on either build. It would change the stored and compared values (0.1is not a float32), andfloat[]is notList<Double>forIN,CONTAINS, equality or an untyped list property.float64arrays (rounded to float32 on both builds, native any float array) andfloat32arrays: unchanged, they already cross as one buffer.tolist()as before (the rows then take the list path).Tests
tests/test_query_vector_params.py(both builds):TestNativeCodecstates the bytes handed over (stub text, side buffer, offsets with other arrays, fallbacks for the number cases above, text unchanged without the buffer).TestStatementsstates that the values and types come back unchanged and thatIN, a list property (insert, read back,=),CONTAINSandvectorNeighbors(positional and named; list, float32 array, float64 array, literal text) give the same results, for float32-exact lists and for doubles.tests/test_query_vector_params_jvm.py(JVM build; listed inNATIVE_NOT_APPLICABLE): counts the one-buffer crossings with a spy on_boxed_doublesand compares the Java objects (ArrayList,DoubleandLongclasses,equalsagainst the element-wise list).Local results on cores 14,15 against the 2026-10-26 wheels (main
e60a748, Python overlaid with this branch):test_type_conversion,test_parameter_binding,test_numpy_support,test_conversion_fixes,test_positional_count,test_vector_sql,test_float_vector_bulk,test_native_result_transporttest_parameter_bindingThe 5 native failures are the float32-exact list cases, which fail with
unknown parameter tag BLbecause the local native library predates the tag. The Java side of the tag (Json.java) compiles against Graal SDK 25.0.2 and the engine jars but has not run: the native end-to-end check is this PR's native CI, which builds the library. Against the old code the 6 codec tests and all 24 JVM spy tests fail (the new function is absent), and the statement tests pass, so they assert the old results.Static gate:
pre-commit run --files(black 26.10.1, isort) clean, bandit oversrcandtestsclean.🤖 Generated with Claude Code