Visitar URL original
Bridge: float vectors cross as one binary buffer, not as JSON text or one JPype call per element by tae898 · Pull Request #496 · humemai/arcadedb-embedded-python · GitHub
Skip to content

Bridge: float vectors cross as one binary buffer, not as JSON text or one JPype call per element - #496

Merged
tae898 merged 2 commits into
mainfrom
jpype-glue-crossings
Oct 24, 2026
Merged

tae898 merged 2 commits into
mainfrom
jpype-glue-crossings

Conversation

@tae898

@tae898 tae898 commented Oct 24, 2026

Copy link
Copy Markdown

Float vectors now cross the Python/Java bridge as one binary buffer per batch. Before, a vector property loaded with insert_many was written by Python as decimal text inside the JSON rows and parsed back by the engine, and in graph_batch.create_vertices, Document.set and the allowed_rids filter JPype converted every list element (or every RID) in its own call. For a property declared ARRAY_OF_FLOATS the engine converts each number to float anyway, so the vectors of a chunk now cross as one float32 matrix (new bridge class FloatVectors.Matrix, one buffer copy) beside the JSON, and Java builds each document's own float[] row. Properties that are not declared ARRAY_OF_FLOATS, and vectors that are ragged, missing or null, keep the exact JSON path. Also: insert_columns accepts a 2-D float32 array as a column of vectors, a numpy array is accepted as a property value everywhere (it raised TypeError before), and the allowed_rids filter builds its RID set in one crossing (GraphCalls.ridSet) instead of two per RID. This is the DSL-first rule applied to transport: the features stay SQL and Cypher, and the glue class carries the data where a crossing costs.

Measured

Two arms built from one base wheel (same engine jars and JRE, only the bindings' Python files and the bridge jar differ): A = origin/main bindings, B = this branch. 128-d float32 vectors on an ARRAY_OF_FLOATS property, JVM heap 4g, one fresh process per run, laptop pinned to 2 cores (x86-64, 16 cores), interleaved ABBA rounds with a quiet gate and a temperature gate before every run, round 0 dropped, tools/ab.py --noise per figure, 10 clean pairs each (2 pairs dropped by the noise rule). Ratios below 1 mean the branch is faster. The table is generated from the study output (ab.txt and the per-run files), and the same figures are in the docs through render_ab_tables.py (docs/benchmarks/ab-results/float-vector-crossing.json).

Step Before (ms, median) After (ms, median) After / before (95% interval) Pairs better / worse (sign test p)
insert_many, 100,000 dict rows, 128-d float32, chunks of 20,000 14,215 2,621 0.185 (0.180 to 0.191) 10 / 0 (p = 0.00195)
insert_columns, 100,000 rows, 128-d float32 609 521 0.862 (0.833 to 0.947) 8 / 2 (p = 0.109)
graph_batch create_vertices, 50,000 rows, 128-d float32 3,596 1,083 0.300 (0.289 to 0.315) 10 / 0 (p = 0.00195)
find_nearest(k=10) with 5,000 allowed RIDs, 200 queries, 20,000 vectors 2,296 569 0.246 (0.235 to 0.257) 10 / 0 (p = 0.00195)
all four steps of the probe 20,605 4,785 0.232 (0.230 to 0.236) 10 / 0 (p = 0.00195)

insert_columns is the one that differs by design: on main the best a column load offers is one float[] per row, so its row compares that with the new 2-D column, and its interval is wide enough that the sign test does not reach significance (8 of 10 pairs better). The other four figures are 10 of 10 pairs better.

The A/B was measured on the pre-rebase commit ad4e8a394e. This branch is that commit rebased onto origin/main at 43d723c9e1 (29 commits); the code is unchanged by the rebase except one conflict in tests/test_python_callbacks.py, where main added an xfail marker (free-threaded heap growth, #454) above test_registering_again_and_again_does_not_grow_the_python_heap and this change added test_a_numpy_array_is_a_return_value in the same place; both are kept. The rebased commit adds the docs table, the figure id in render_ab_tables.py and the result file. The study's base wheel was built before the rebase, so the speed table is not a measurement of the rebased tree.

Tests run

Locally, one batched job through quiet_build (this branch's src and a javac-recompiled bridge jar overlaid on an unpacked linux/amd64 cp312 wheel): test_float_vector_bulk.py, test_vector.py, test_python_callbacks.py, test_graph_batch.py, test_bulk_insert.py, test_insert_columns.py, test_dataframe_ingest.py, test_bulk_json_paths.py, test_bridge_fixes.py: 291 passed, 1 skipped (a polars import in test_dataframe_ingest.py, polars was not installed in the job). Lint: black, isort --profile black, and the repo's pre-commit hooks (including the A/B tables check) pass on the commit; bandit finds nothing at medium or above. The stock wheel build and the CI matrix are the remaining checks. Not run locally: the free-threaded and the other-OS wheels.

For a port to ArcadeData/arcadedb-python: its DocumentBatcher.java, VertexBatcher.java and GraphCalls.java are byte-identical to ours before this change, so the diff of those three files plus the new FloatVectors.java applies as is; it does not have PythonFunctions or PythonRecordEvents (not touched here).

🤖 Generated with Claude Code

tae898 and others added 2 commits October 24, 2026 14:48
… one JPype call per element

A vector load spent most of its time on text: Python wrote every float as a decimal number and the engine parsed it back (insert_many), or JPype converted every list element on its own (create_vertices, Document.set). For a property declared ARRAY_OF_FLOATS the engine converts each number of a list to float anyway, so the vectors of a chunk now cross as one float32 matrix (FloatVectors.Matrix, one buffer copy) beside the JSON, and each document gets its own float[] row Java-side. Properties that are not declared ARRAY_OF_FLOATS, and vectors that are ragged, missing or null, keep the exact JSON path.

Also: insert_columns accepts a 2-D float32 array as a column of vectors; a numpy array is accepted as a property value everywhere (Document.set, insert_many, create_vertices; it raised TypeError before); the allowed_rids filter of a vector search builds its RID set in one crossing (GraphCalls.ridSet) instead of two per RID. The speed table in docs/guide/vectors.md is generated by render_ab_tables.py from the laptop A/B study (tools/ab.py, ABBA, 10 clean pairs per figure).

Tests: tests/test_float_vector_bulk.py (39 tests: exact float32 equality with the engine's own list conversion on doubles a float32 cannot hold, which values keep the JSON path, every entry point, parallel mode); tests/test_vector.py rid-conversion test now passes a Java RID object so the list takes the per-RID path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…k.py for bandit B608

CI runs bandit at low severity and confidence; the query strings use the
test's own type names and RIDs. Same pattern as test_graph_batch.py.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@tae898
tae898 merged commit 8bbe458 into main Oct 24, 2026
42 of 44 checks passed
@tae898
tae898 deleted the jpype-glue-crossings branch October 24, 2026 06:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant