Effect Validation

July 21, 2026 ยท View on GitHub

This document tracks small, reproducible KoutenDB effect-validation demos. These are generated local workloads, not universal performance claims.

The goal is to make KoutenDB's main claim testable:

  • reduce the candidate set that must be scanned;
  • reduce estimated downstream context tokens;
  • produce a compact prompt before any LLM is called;
  • keep the model choice optional and explicit.

Single Demo

examples/effect_validation_demo.sh

The demo generates a deterministic JSONL corpus, imports it into KoutenDB, and compares:

  • global retrieval across the whole generated corpus;
  • ring-routed retrieval scoped to docs/japan.

Representative local output:

docsglobal scannedrouted scannedglobal tokensrouted tokens
16816824692260

This means the demo reduced scanned records by 85.7% and estimated tokens by 62.4% before sending anything to an LLM.

Matrix Demo

examples/effect_validation_matrix.sh
KOUTEN_EFFECT_LARGE=1 examples/effect_validation_matrix.sh
KOUTEN_EFFECT_QUICK=1 examples/effect_validation_matrix.sh

This matrix is a manual validation path. It is intentionally not part of the default CI smoke suite because the default and large cases are meant to create millions of generated records.

The matrix uses several generated workload shapes:

  • small-balanced: small corpus with one useful ring and unrelated rings;
  • near-distractors: adds near-topic distractors to avoid a trivial clean split;
  • medium-noisy: adds a larger unrelated background corpus;
  • large-noisy: optional larger generated case enabled by KOUTEN_EFFECT_LARGE=1.

By default, the matrix uses KOUTEN_EFFECT_SCALE=1000, which reaches 13,500,000 documents in the standard run. KOUTEN_EFFECT_LARGE=1 adds a 98,000,000-document generated stress case. Use KOUTEN_EFFECT_QUICK=1 for the smaller fast matrix, or set KOUTEN_EFFECT_SCALE=N explicitly.

Bulk load uses chunked commits. The default matrix uses KOUTEN_EFFECT_BATCH_SIZE=10000; set it explicitly when comparing import behavior:

KOUTEN_EFFECT_SCALE=1000 KOUTEN_EFFECT_BATCH_SIZE=10000 examples/effect_validation_matrix.sh

The current matrix path uses disk-backed storage by default (KOUTEN_EFFECT_DISK_BACKED=1). Set KOUTEN_EFFECT_DISK_BACKED=0 only when you intentionally want to compare the legacy in-memory validation path.

Quick local sanity result from this repository state:

casedocsglobal budgetrouted budgetset latency usset us/recordpack latency uspack records/rings/bytesscannedtokensretrieve latency usscanned reductiontoken reductionprompt bytes
small-balanced168832250.06513.393244987.782168 / 4 / 61352168 -> 24692 -> 260321.089 -> 55.24985.714%62.428%1356
focused-pinpoint12045303166421.50113.81664656405.99512040 / 4 / 428676012045 -> 52408 -> 26019804.821 -> 60.41899.958%89.203%1357
focused-small-context12150305168488.49713.86736657476.12712150 / 5 / 433005012150 -> 502595 -> 43319583.824 -> 102.11799.588%83.314%2064
near-distractors186020528328.16415.2301969541.1601860 / 5 / 6817301860 -> 1201730 -> 4333375.936 -> 221.48693.548%74.971%2064
medium-noisy13500308190560.77514.11561362926.74213500 / 5 / 485595013500 -> 5002595 -> 69222898.644 -> 867.78496.296%73.333%3124

Larger local validation with KOUTEN_EFFECT_SCALE=100 and KOUTEN_EFFECT_BATCH_SIZE=10000:

casedocsglobal budgetrouted budgetset latency usset us/recordpack latency uspack records/rings/bytesscannedtokensretrieve latency usscanned reductiontoken reductionprompt bytes
small-balanced1680083235716.93214.03077082063.82116800 / 4 / 616836016800 -> 2400692 -> 26027384.316 -> 4239.18085.714%62.428%1360
focused-pinpoint120450030316409354.38713.6233746158327.7001204500 / 5 / 4326209501204500 -> 5002595 -> 2602730205.122 -> 901.45199.958%89.981%1361
focused-small-context121500030516650765.84013.7043345555622.2871215000 / 5 / 4367844501215000 -> 50002595 -> 4332353292.649 -> 8855.60799.588%83.314%2068
near-distractors1860002052529182.66913.597756835860.620186000 / 5 / 68640450186000 -> 120001730 -> 433325546.670 -> 20660.52293.548%74.971%2068
medium-noisy135000030817598824.70013.0361665875386.9871350000 / 5 / 4899144501350000 -> 500002595 -> 6922530113.316 -> 86388.77496.296%73.333%3128

The disk-backed path now separates two costs:

  • set latency: normal JSONL import into the WAL-backed store;
  • pack latency: an explicit physical-layout step that builds ring-local segment files for faster reads.

The WAL remains the source of truth. Ring segment files are rebuildable read layout, similar in operational role to a compaction or optimize step. They are not required for correctness.

The current segment-pack implementation uses buffered ring-local writes and skips very small rings. Large rings benefit from contiguous segment files, while tiny rings such as users/<id> can stay on WAL-offset reads without paying the cost of one segment file per ring. Explicit post-import packing is now fast enough for the scale-100 validation path, but it is still a visible cost at the largest multi-million stress scale. The effectPackRecords, effectPackRings, and effectPackBytes metrics expose that cost so future pack optimization can be measured directly. KOUTEN_EFFECT_PACK_DURING_IMPORT=1 can be used to test import-time segment construction, but it is not the default because it currently increases normal import latency too much.

Scale-1000 validation also completed on this machine with the current disk-backed segment path:

casedocsglobal budgetrouted budgetset latency usset us/recordpack latency uspack records/rings/bytesscannedtokensretrieve latency usscanned reductiontoken reductionprompt bytes
small-balanced168000832268312.83813.501862808368.180168000 / 4 / 61929560168000 -> 24000692 -> 260289622.808 -> 42232.27485.714%62.428%1362
near-distractors186000020524940651.10713.4089529361622.0011860000 / 5 / 6899444501860000 -> 1200001730 -> 4333481719.378 -> 227453.70193.548%74.971%2070
medium-noisy13500000308203851078.69415.10008072425799.77113500000 / 5 / 495587445013500000 -> 5000002595 -> 69230082232.678 -> 1101752.06396.296%73.333%3130

The scale-1000 stress path shows that the generated 13.5M-record case completes without the previous OOM kill. It also shows the next performance target: post-import segment packing is much faster than the earlier unbuffered path, but the largest pack step is still large enough to deserve more optimization before calling the v0.9 line final.

The important part is not that these generated numbers are universal. They show how to test the effect: compare broad retrieval with placement-aware retrieval, then report import latency, scanned records, estimated tokens, retrieval latency, and prompt bytes.

Pinpoint User Read

examples/pinpoint_user_read_bench.sh measures a common application pattern: many users exist, but a request already knows the target user. It compares two layouts with the same logical records:

  • broad layout: every profile is stored in one users ring and read with an id filter;
  • local layout: each profile is stored in users/<id> and read by ring.

Run:

KOUTEN_PINPOINT_USERS=100000 examples/pinpoint_user_read_bench.sh

Local disk-backed result with 100,000 users:

layoutset latency usset us/recordpack latency usread moderead latency us
broad users ring783773.6417.837736360499.095filter id in users1906854.458
local users/ ring10297185.434102.97185411704.754read users/ limit=170.755
local users/ ring10297185.434102.97185411704.754read users/ limit=2077.601

The local layout has a higher write/setup cost because it creates many ring metadata entries. The read path is the important result: once the application places data at users/<id>, the request does not need to scan the broad users ring and can read the target ring directly.

User Bundle Read Compared With PostgreSQL

examples/user_bundle_postgres_bench.sh measures a user-detail page shape rather than a single primary-key lookup. Each user has 20 logical records: profile, addresses, career entries, preferences, and orders. KoutenDB stores them under coordinate-local rings such as users/<id>/profile and users/<id>/orders, then reads the bundle with a narrowed stellar read:

N=100000 READS=500 examples/user_bundle_postgres_bench.sh

PostgreSQL stores the same logical data in normalized indexed tables and is measured in two shapes:

  • five prepared indexed SELECT statements in one transaction;
  • one prepared JSON aggregate query over the same indexed tables.

Local disk-backed results:

userslogical recordsKoutenDB bundle read usPostgreSQL 5 SELECT usPostgreSQL JSON aggregate us
1,00020,000213.248424246
10,000200,000202.668442257
100,0002,000,000205.799407240

The KoutenDB read path stayed roughly flat in this benchmark because the query starts from users/<id> and only visits the requested subrings: profile, addresses, career, preferences, and orders. This is the intended locality model: related records can be retrieved together without turning every request into a broad collection scan or a multi-table join.

The write/setup side is still a performance target. At 100,000 users, KoutenDB inserted 2,000,000 logical records at 33.442073 us/record in this helper. That is good enough for the read-locality validation, but bulk load and many-ring metadata creation should be optimized before treating this shape as a final ingestion design.

Per-Subring Limit And Sort Case Study

The heterogeneous subring bundle benchmark checks a shape that is awkward to express as a plain relational join: one root user plus several related collections where each collection has a different limit and sort order.

Example read shape:

kouten get --ring=users/<id> \
  --subring=profile,addresses,career,preferences,orders,notifications \
  --subring-limit=profile:1,addresses:3,career:2,preferences:1,orders:10,notifications:5 \
  --subring-rsort=orders:time,notifications:time

Local disk-backed result, measured with N=10000 READS=1000 examples/subring_bundle_postgres_bench.sh on 2026-07-21:

userslogical recordsKoutenDB subring bundle read usPostgreSQL 6 SELECT usPostgreSQL JSON aggregate us
10,0001,050,000196.859515236

The important distinction is the model. KoutenDB treats the related collections as nearby subrings and applies the bounds at the read boundary. PostgreSQL can still express the shape with indexed limited subqueries and JSON aggregation, but it is no longer a simple join and it pushes more response-shaping work into SQL.

Offline Real-Data Copy

Generated workloads are useful for repeatability, but the next pre-production step is to run the same measurement against copied or exported real data:

KOUTEN_REAL_JSONL=/path/to/corpus.jsonl \
QUERY_RING=docs/japan \
GLOBAL_BUDGET=40 \
ROUTED_BUDGET=10 \
examples/offline_effect_validation.sh

Expected JSONL shape:

{"ring":"docs/japan","body":{"id":"doc-1","title":"...","text":"..."},"embedding":[1.0,0.0,0.0,0.0]}

This keeps the validation offline. It does not require production traffic and it does not call an LLM unless the user separately chooses to run the generated prompt through a model.

Optional Trusted LLM Step

The effect validation does not require a model download. It always writes a prompt first. A trusted local model can be added explicitly:

ollama pull gemma4:e2b
KOUTEN_TRUSTED_LLM_CMD='ollama run gemma4:e2b' examples/effect_validation_demo.sh

Gemma 4 E2B is used as the documented example because it is an official Google Gemma 4 edge-size model available through Ollama.

References:

JMeter Load Smoke

examples/jmeter_load_smoke.sh
KOUTEN_JMETER_THREADS=64 KOUTEN_JMETER_LOOPS=1000 examples/jmeter_load_smoke.sh

This is an optional Apache JMeter load smoke for the koutend TCP listener. It sends concurrent HEALTH requests and writes a JTL result file.

It is deliberately separate from retrieval-locality benchmarks:

  • JMeter health load checks listener stability and request/response behavior;
  • effect-validation scripts check retrieval working-set and token reduction.

When JMeter is not installed, the wrapper exits successfully with a clear skip message so the repository remains easy to test in minimal environments.