Files
vectorize-io__hindsight/hindsight-api-slim/tests/test_chunking_reference_matches_langchain.py
T
Nicolò Boschi 828a6e55d3 refactor(retain): format pgvector literals without orjson, and drop orjson entirely (#4040)
* refactor(retain): format pgvector literals without orjson

orjson was a single-purpose dependency: one function (`embedding_to_pgvector`),
on the retain write path only, rendering an embedding as the '[0.1,...]' literal
asyncpg binds to `vector`. It played no part in HTTP serialization — there is no
ORJSONResponse in the API, and recall builds its query literal with plain str().

Measurement decided it. On `perf-test --suite retain --scale small` (200 items,
384d, mock LLM + pg0) the orjson calls totalled 2.9ms of a ~1.9s retain, and
paired throughput runs could not tell the two implementations apart from noise
(orjson 75-124 items/s, replacement 81-115).

What replaces it is one C-level %-format call per vector against a per-dimension
cached "[%.9g,...]" template: ~1.8x faster than the repr() generator that was
already the non-finite fallback, ~5x slower than orjson. No pure-Python formatter
closes that gap — orjson formats floats with SIMD Ryu in Rust. Beating it means
not emitting text at all, which the benchmark now measures as `binary_pgvector`
(~2 orders of magnitude faster, 2.8x fewer wire bytes) but which needs an asyncpg
codec for the `vector` type and would still leave Oracle on the text path.

Values are narrowed to float32 before formatting, exactly once. Applying %.9g
straight to a float64 rounds twice — to nine digits, then to float32 — and the
two roundings pick different neighbouring float32s for ~0.8% of values. Narrowing
first makes the single remaining rounding the one PostgreSQL would have done, so
stored bytes are unchanged. The new regression test samples 4,096 elements, where
that rate misses by ~31.

The literal is now fixed-width rather than shortest-form (0.100000001 where orjson
wrote 0.1) — same float32, ~12% more SQL text.

The orjson version floor stays in pyproject as a transitive-only pin (langsmith
still pulls it), matching how pyjwt is handled.

* build: drop orjson from the runtime dependency closure

The previous commit removed the last import; this removes the package from
what production resolves.

orjson's only other path into the environment is
langchain-text-splitters -> langchain-core -> langsmith -> orjson, and
langchain has been test-only since #3756 — retain's chunker is its own
streaming RecursiveCharacterTextSplitter, and langchain survives purely as
the reference implementation test_chunking_matches_legacy.py diffs against
(with a guard test asserting the reference is still really langchain).

So the `orjson>=3.11.6` floor follows langsmith into the test extra and the
dev group, exactly as the langchain-core and langsmith floors already did
when langchain left the runtime. Deleting the floor outright would have
dropped the unbounded-recursion DoS bound for the version langsmith pulls.

    uv export --no-dev  ->  orjson absent
    uv export           ->  orjson 3.11.7 (via langsmith, test only)

uv.lock is unchanged.

* test: vendor the chunking oracle and drop langchain, langsmith and orjson

orjson's last path into the environment was the test extra:
langchain-text-splitters -> langchain-core -> langsmith -> orjson. langchain
was there for one reason — the differential chunking tests need the splitter
retain used before #3756 as their oracle, and that oracle has to be genuinely
the old implementation or the tests pass vacuously.

So the oracle moves in-tree. tests/chunking_reference.py transcribes langchain
1.1.2's RecursiveCharacterTextSplitter (MIT), specialised to the one
configuration retain ever used: chunk_overlap=0, keep_separator default "start",
length_function=len, non-regex separators. The branches that configuration cannot
reach are left out rather than transcribed untested.

The transcription was verified against the real splitter before the dependency
was removed: 20,079 comparisons — every case and size the chunking tests use,
plus 20,000 seeded fuzz documents built from the fragment shapes that break
splitters (empty strings, whitespace runs, CRLF, CJK, oversized tokens) — with
zero mismatches. tests/test_chunking_reference_matches_langchain.py keeps that
check runnable:

    uv run --with langchain-text-splitters pytest \
        tests/test_chunking_reference_matches_langchain.py

It skips by default, since langchain is deliberately absent.

test_the_legacy_reference_really_is_the_old_implementation loses its live
langchain call, so its anchor becomes a table of five expected splits — one per
separator tier, including the per-character fallback — produced BY langchain and
pasted in. A reference that quietly became the new implementation has to
reproduce all five to slip through.

    uv export --no-dev  ->  no orjson, no langchain
    uv export           ->  no orjson, no langchain

uv.lock loses 392 lines.
2026-09-02 17:58:22 +02:00

90 lines
3.3 KiB
Python

"""Re-verify the vendored chunking oracle against the real langchain splitter.
``tests/chunking_reference.py`` is a transcription of langchain's
``RecursiveCharacterTextSplitter``, kept so the differential chunking tests kept their
oracle when ``langchain-text-splitters`` left the dependency tree (it pulled in
``langchain-core`` -> ``langsmith`` -> ``orjson``).
A transcription is only as good as its last check against the original, so that check
lives here rather than in a commit message. **This test skips by default** — langchain is
deliberately not installed. To run it:
uv run --with langchain-text-splitters pytest tests/test_chunking_reference_matches_langchain.py
Do that after touching ``chunking_reference.py``, or when bumping the langchain version
the transcription claims to mirror. It matched byte-for-byte over 20,079 comparisons
against ``langchain-text-splitters`` 1.1.2 when the dependency was removed.
"""
import random
import pytest
from hindsight_api.engine.retain.fact_extraction import _RECURSIVE_TEXT_SEPARATORS
from tests.chunking_reference import recursive_split
from tests.test_chunking_streams import _PLAIN_TEXT_CASES
# Skips the whole module when langchain is absent, which is the normal state.
langchain_text_splitters = pytest.importorskip(
"langchain_text_splitters",
reason="langchain is not a dependency; install it explicitly to re-verify the oracle",
)
def _langchain_split(text: str, max_chars: int) -> list[str]:
splitter = langchain_text_splitters.RecursiveCharacterTextSplitter(
chunk_size=max_chars,
chunk_overlap=0,
length_function=len,
is_separator_regex=False,
separators=_RECURSIVE_TEXT_SEPARATORS,
)
return splitter.split_text(text)
def test_reference_matches_langchain_on_the_separator_tier_corpus():
"""Every case the streaming tests use, at every size they use."""
for name, text in _PLAIN_TEXT_CASES.items():
for max_chars in (10, 17, 40, 100, 1000):
assert recursive_split(text, max_chars, _RECURSIVE_TEXT_SEPARATORS) == _langchain_split(text, max_chars), (
f"{name} at max_chars={max_chars}"
)
def test_reference_matches_langchain_on_fuzzed_documents():
"""Randomly assembled documents, over the fragment shapes the legacy corpus uses.
Seeded so a failure is reproducible; the fragments deliberately include the cases that
break splitters — empty strings, whitespace runs, CRLF, CJK, and tokens larger than any
chunk size tried.
"""
fragments = [
"Alpha sentence one. ",
"Beta two! ",
"Gamma three? ",
"delta; ",
"eps, ",
"\n\n",
"\n",
" ",
"",
"文章です。",
"x" * 50,
"y" * 200,
'{"k": "v"}',
"...",
"\r\n",
"\t",
" ",
"Ålpha ",
"a" * 300,
]
rng = random.Random(20260902)
for case in range(2000):
document = "".join(rng.choice(fragments) for _ in range(rng.randint(1, 60)))
max_chars = rng.choice([1, 2, 5, 10, 37, 100, 256, 1500])
assert recursive_split(document, max_chars, _RECURSIVE_TEXT_SEPARATORS) == _langchain_split(
document, max_chars
), f"case {case} at max_chars={max_chars}: {document!r}"