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. |
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