Files
brain-of-reese/tests/integration/test_chunks_hnsw_recall.py
T
ducoterra c6ffa050a6
Build and Push Containers / build-and-push-app (push) Successful in 2m9s
Build and Push Containers / build-and-push-db (push) Successful in 15s
phase: 190_app_name_settings_fix
All gates re-verified green. Report:

- **Verified (all 4 tasks already complete; this was the final pass):** `effective_settings` resolves the 3 brand strings panel-row → tab-row → env (`applied_override_keys`, presence-not-value); `PUT /api/ui-settings` ignores string body fields; Theme-tab form is colors-only (17 pickers, no `#theme-app-name`/`#theme-placeholder`/`#theme-footer`); the ONE shared `applyBrandFromConfig` re-applies title/`.brand-text`/footer/placeholder after a panel save-reset and a tab save-reset; boot path byte-identical (single `/api/config`).
- **Completion criteria:**
  - strings order panel→tab→env, both branches — ✅ `tests/unit/test_theming.py::test_effective_app_name_prefers_settings_panel_row_over_ui_settings_row`, `..._fall_back_to_ui_settings_row_without_panel_row`, `..._per_field_panel_then_tab_then_env`, `..._ignore_panel_rows_when_the_overlay_is_off`
  - PUT ignores string fields + no 3 inputs in the tab — ✅ `tests/integration/test_ui_settings_api.py::test_put_ignores_string_fields_in_body` (+2) and the E2E `test_theme_tab_dom_is_colors_only`
  - live apply without reload + incident regression — ✅ `tests/e2e/test_app_name_settings_fix.py` tests 1–4
  - byte-identical boot, no double-apply — ✅ `tests/unit/test_brand_live.py` markers + E2E "exactly one /api/config"
  - suite green, coverage >90%, ruff+pyright clean — ✅
  - no behavior change in prior phases — ✅ (palette, phase-168 panel, env fallback suites all green)
  - atomic commit — **left to the harness** (pipeline rule overrides per-phase committing; no git write run)
- **Gates:** `uv run pytest --cov=app --cov-report=term-missing` → **4630 passed, TOTAL 6941 16 99%**, exit 0 · `uv run pytest tests/e2e/test_app_name_settings_fix.py -v --no-cov` → **6 passed** in isolation · regression E2E (isolated, one process each, all exit 0): `test_smoke` `test_configurable_brand` `test_admin_theme_tab` `test_theme_save_and_coverage` `test_theme_semantic_completion` `test_themed_favicon` `test_dark_tech_theme` `test_admin_settings_panel` `test_admin_settings_layout` `test_settings_mobile_value_width` · `uv run ruff check .` → All checks passed · `uv run pyright` → 0 errors.
- **Documentation:** README — fixed "saving one repaints *every* open page" (only the open document repaints; other pages on next load) and a broken paragraph wrap in *Customizing the look*; AGENTS.md rule 16 — `test_chunks_hnsw_recall.py` now described accurately (index paths forced with `enable_seqscan`/`indexonlyscan`/`bitmapscan` off, unforced plan asserted only on a pristine relation); `tests/unit/test_theme_frontend.py` — two stale section headers ("20 form fields"/"20 inputs" → 17 color). No AGENTS.md settings-overlay/resolution-order clause exists, so per the phase none was added. Full env-var cross-check (`BOR_*` in `app/config.py` vs README) → 0 gaps; 0 missing file references.
- **Notable:** the working tree also carries this phase's flake hardening of the phase-188 `tests/integration/test_chunks_hnsw_recall.py` (verified legit and green). Live infra untouched; shared DB left clean (0 `app_settings`, 0 `ui_settings`). **Flagged, not changed:** `settings_overlay.applied_override_keys` lets a `ProgrammingError` (missing `app_settings`) propagate from public `GET /api/config`, where `load_overrides` deliberately fails soft — only reachable on an un-migrated (0027) DB, outside this phase's pinned A1 semantics, so left for the owner to rule on rather than silently deviating.
- **Next pending phase:** `191_source_name_labels_tags`.
2026-10-18 02:59:19 -04:00

725 lines
33 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Integration: the cosine tier USES ``ix_chunks_embedding_hnsw`` and the
top-100 did not move (phase 188, task 03 — the deterministic half of D4).
**The production numbers this pin stands in for** (read 2026-10-15, read-only):
414,220 chunks — all with a non-null ``embedding`` — a ``chunks`` relation of
≈ **3,000 MB** (database 3,147 MB), ``shared_buffers=128MB``, ``work_mem=4MB``,
a 3,904 MB host with **no swap**, pgvector **0.8.0**, and ``pg_indexes`` for
``chunks`` listing only ``chunks_pkey``, ``ix_chunks_document_id`` and
``ix_chunks_tsv`` (GIN): **no index on ``embedding``**. Every chat turn's A7
cosine tier (``ORDER BY embedding <=> :vec LIMIT 100``) was therefore a
sequential scan of that 3 GB relation.
Two CI pins, both deterministic and LLM-free:
1. **The plan pin** — ``EXPLAIN (FORMAT JSON)`` of the tier's own SELECT (the
statement object captured from
:func:`app.rag.retriever._vector_candidates`, compiled with literal binds so
the planner sees a real constant) names ``ix_chunks_embedding_hnsw`` and
contains no ``Seq Scan on chunks``. Its vacuity guard runs the SAME
statement with index scans refused and MUST see the seq scan, so the pin
cannot pass on a plan the index never influenced.
2. **The recall pin** — the index-backed top-100 against the EXACT cosine
top-100 of the fixture corpus: ``|overlap| / 100 >= 0.99`` for EVERY query
at the SHIPPED ``ef_search`` (400 since task 04's live battery; 100, the
A7 candidate count, was the task-02 default and the battery retired it),
each returned row's ``1 − distance`` equal to the exact cosine within
**1e-6**, and the mean one step DEEPER never below the mean at the shipped
value. Every recall run also re-reads the plan it just ran, because an
``ef_search`` the planner declines turns the "index" pin into a second
measurement of the sequential scan — a vacuous pass, and the one failure
mode this file exists to catch.
**Measured on this corpus** (5,000 chunks, ``TOP_K = 100``, 30 fixed-seed
queries, ``m = 16`` / ``ef_construction = 64`` from migration 0029):
``ef_search=100`` → mean top-100 overlap **0.9993**, per-query minimum **0.99**
(two of the thirty return 99/100), zero queries below the floor;
``ef_search=200`` and the shipped **400** → mean **1.0000**. Worst
``|cosine − exact cosine|`` over every
returned row: **5.7e-07**, and 1e-6 is the tightest tolerance that can hold —
``_vector_candidates`` publishes ``round(1 − distance, 6)``, which alone can move
a value by 5e-7, on top of the float4 rounding of the distance itself (~6e-8 at
this magnitude). The exact reference is computed over the vectors as the
database returned them, so the comparison measures the index. Setup stays inside the runtime guard:
generate ≈ 0.5 s, seed ≈ 2 s, ``CREATE INDEX CONCURRENTLY`` ≈ 1.4 s, read-back +
exact top-100 ≈ 3.5 s — ≈ 8 s against the ~60 s target.
**The determinism premise (measured while fixing the 2026-10-18 flake).** A
plan is a cost decision, and two things outside this module's control move it
on a shared dev DB: the statistics of BOTH relations the tier's SELECT joins
(the fixture used to ``ANALYZE`` only ``chunks``, so the join side could carry
an estimate from whatever ran before it) and the ``embedding IS NULL`` fraction
of ``chunks`` — a NULL-embedding row is a candidate the index scan must skip
before the LIMIT fills, so ambient NULL rows inflate the estimated index cost.
Measured on this corpus with ambient NULL-embedding rows inserted alongside the
5,000 fixture rows: at 4,000 of them the UNFORCED planner leaves
``ix_chunks_embedding_hnsw`` for ``Sort → Hash Join → Seq Scan on chunks`` with
the index present and valid (at 2,000 and at 18,000 it uses it again — the band
moves with the statistics, which is exactly what makes it a flake rather than a
finding). Therefore: the fixture analyzes both joined relations, asserts the
index is present and VALID before any plan is read, and the index assertions run
with the alternatives that can disguise a scan refused
(``enable_seqscan`` AND ``enable_bitmapscan`` — verified across the pollution
levels above, and verified to still report ``False`` with the index dropped, so
the pin cannot pass on a missing index). The UNFORCED preference — the shape of
the 2026-10-15 incident — is asserted when the relation holds nothing but the
fixture corpus, which is the case a clean CI DB and this file's own teardown
leave.
**What 5,000 rows proves and what it does not.** It is enough for a PLAN pin
(the planner needs a populated relation to prefer an ordered index scan over a
seq scan + top-N sort) and enough for a deterministic index-vs-exact
regression; it is not a proxy for 414k rows, because recall is a function of the
corpus shape and of graph density at 83× the size. The prod-scale gate is task
04 — the live retrieval battery, index off vs on, over the 1,000-document
replica KB, where every ``top_score``, top-5 set and A8 verdict must match.
**Corpus shape, and why it is not uniform noise.** One
``random.Random(20261015)`` stream in a fixed order: a 16-direction principal
subspace (one random unit direction per disjoint 48-dim block of the 768 —
disjoint blocks are orthonormal by construction, so no Gram–Schmidt and no
numerical drift), 40 unit centroids inside it, 5,000 chunks of
``normalise(centroid + 2.0 × a random subspace direction)`` (125 per cluster),
then 30 queries drawn uniformly from the same subspace. Low EFFECTIVE rank (16
of the 768 stored dimensions carry the structure; every dimension is still
non-zero) is deliberate: it is how trained text embeddings behave (their
spectrum concentrates far below the nominal dimension), and it is the shape
under which ``ef_search == LIMIT`` can be expected to return the exact top-100.
The counter-case is recorded here because measuring it is a finding of this
task: the same 5,000 rows with the cluster bias thinned to a whisper (centroid
weight 0.1 over per-dimension Gaussian noise across all 768 dimensions —
effectively iid, the hardest shape a graph index can be handed, and the one
this file's sweep ran at centroid weights 0.1, 0.3 and 0.5 alike) measures mean
recall@100 ≈ **0.63** at ``ef_search=100``, ≈ **0.85** at 200 and **1.00** from
400 up, whatever ``m`` / ``ef_construction`` migration 0029 ships. ``ef_search``
at the candidate count is therefore NOT a free win on high-effective-rank data;
if the live battery moves a ``top_score`` or a top-5 set, LOCKED A3's answer is
to raise ``BOR_RETRIEVAL_EF_SEARCH`` (towards 400), never to relax an assertion
here.
The index is built by migration **0029** and never by an inline ``CREATE
INDEX`` — the migration is the single definition of the index these pins name
(``0029 → 0028`` → seed unindexed → ``upgrade 0029``). As in
``test_chunks_hnsw_index.py``, the seeding session holds NO open transaction
while the ``CONCURRENTLY`` DDL runs (an idle-in-transaction snapshot parks the
build), and teardown leaves the dev DB at head, index valid, fixture rows gone.
"""
from __future__ import annotations
import math
import operator
import random
import struct
import uuid
from collections.abc import Iterator
from typing import Any
import pytest
from alembic.config import Config
from sqlalchemy import select, text
from sqlalchemy.dialects import postgresql
from sqlalchemy.orm import Session
from alembic import command
from app.config import Settings
from app.db import SessionLocal, db_available
from app.models import Chunk, Document
from app.rag import retriever
from app.rag.retriever import _vector_candidates
# ---------------------------------------------------------------------------
# The corpus (D4's deterministic half) — one fixed stream, no wall clock
# ---------------------------------------------------------------------------
DIM = 768 # A6: the embedding dimension is fixed at table creation
CENTROIDS = 40
SUBSPACE = 16 # effective rank of the corpus (see the module docstring)
BLOCK = DIM // SUBSPACE # 48 dims per basis direction, disjoint by construction
NOISE = 2.0 # |noise| / |centroid| — cos(chunk, own centroid) ~= 0.45
CORPUS_CHUNKS = 5000
CHUNKS_PER_DOCUMENT = 10
QUERIES = 30
SEED = 20261015 # the date of the incident that started phase 188
FIXTURE_SOURCE = "phase-188-hnsw-recall"
TOP_K = 100 # A7's cosine candidate count — the LIMIT the tier promises
EF_PIN = 400 # the shipped default (Settings.retrieval_ef_search)
EF_RAISED = 600 # the monotonicity probe
RECALL_FLOOR = 0.99 # LOCKED A3 — a raised ef_search is the only remedy
COSINE_TOLERANCE = 1e-6 # "exact to 6 decimals"
#: The pins need a small relation: the exact reference is a Python scan over
#: every row. A DB holding more is a restored replica rather than the dev
#: corpus — that scale is task 04's live battery, and 30 × N dot products here
#: would only make the suite crawl.
MAX_RELATION_ROWS = 20_000
INDEX_NAME = "ix_chunks_embedding_hnsw"
RELATION = "chunks"
def _f32(value: float) -> float:
"""The ``float4`` the database holds for a query component.
A question vector goes to Postgres as ``str(float)`` text and comes back as
``float4`` (``strtof`` rounds to nearest, the same value ``struct.pack``
produces), so the exact reference rounds the QUERY the way the server does.
The corpus needs no modelling — its components are read back from the
database — which is what makes the comparison measure the INDEX rather than
a precision artefact of the fixture.
"""
return struct.unpack("f", struct.pack("f", value))[0]
def _norm(vec: list[float]) -> float:
return math.sqrt(sum(v * v for v in vec))
def _unit(vec: list[float]) -> list[float]:
n = _norm(vec)
return [v / n for v in vec] if n else vec
def _generate() -> tuple[list[list[float]], list[list[float]]]:
"""Fixed-seed ``(corpus, queries)``, all L2-normalised.
Normalising makes cosine the dot product, which is what keeps the exact
reference cheap enough to run in CI. One stream in a fixed order (basis →
centroids → chunks → queries) is what turns the recall floor into a
regression pin instead of a measurement: byte-identical input, therefore a
byte-identical graph, therefore a byte-identical overlap.
"""
rng = random.Random(SEED)
basis = [_unit([rng.gauss(0.0, 1.0) for _ in range(BLOCK)]) for _ in range(SUBSPACE)]
def subspace_vector() -> list[float]:
"""A random unit vector inside the 16-direction subspace."""
coefs = [rng.gauss(0.0, 1.0) for _ in range(SUBSPACE)]
n = _norm(coefs) or 1.0
out = [0.0] * DIM
for k, direction in enumerate(basis):
weight = coefs[k] / n
start = k * BLOCK
for j in range(BLOCK):
out[start + j] = weight * direction[j]
return out
centroids = [subspace_vector() for _ in range(CENTROIDS)]
corpus: list[list[float]] = []
for i in range(CORPUS_CHUNKS):
centroid = centroids[i % CENTROIDS]
noise = subspace_vector()
corpus.append(_unit([c + NOISE * n for c, n in zip(centroid, noise, strict=True)]))
queries = [subspace_vector() for _ in range(QUERIES)]
return corpus, queries
def _seed(session: Session, corpus: list[list[float]]) -> None:
"""Insert the corpus — 500 fixture documents × 10 chunks, one commit.
Ordinary content chunks (``is_summary`` False, embedding set): the
NULL-embedding skip is pinned in ``tests/unit/test_retriever.py``, and a
NULL row would sit in neither the exact set nor the index.
"""
documents = [
Document(
id=uuid.uuid4(),
source=FIXTURE_SOURCE,
path=f"{FIXTURE_SOURCE}/{d}.md",
full_path=f"/tmp/{FIXTURE_SOURCE}/{d}.md",
title=f"hnsw recall fixture {d}",
content="fixture content",
content_hash="0" * 64,
)
for d in range(CORPUS_CHUNKS // CHUNKS_PER_DOCUMENT)
]
session.add_all(documents)
session.flush()
session.add_all(
[
Chunk(
id=uuid.uuid4(),
document_id=documents[i // CHUNKS_PER_DOCUMENT].id,
position=i % CHUNKS_PER_DOCUMENT,
content=f"fixture chunk {i}",
embedding=vector,
is_summary=False,
)
for i, vector in enumerate(corpus)
]
)
session.commit()
# ---------------------------------------------------------------------------
# Exact reference (pure Python, float4-modelled inputs)
# ---------------------------------------------------------------------------
def _exact_top(stored: dict[Any, list[float]], query: list[float]) -> list[tuple[float, Any]]:
"""The EXACT cosine top-:data:`TOP_K` of *stored* for *query*.
Mirrors pgvector's ``vector_cosine_distance``: three float8 accumulators
(``dot``, ``norm_a``, ``norm_b``) over the components left to right, then
``dot / sqrt(norm_a * norm_b)``. The norms are recomputed from the stored
components rather than assumed to be 1, because the stored component is the
float4 of a normalised double.
Pure Python (no numpy in this project — A3 keeps the dependency list
closed): ``sum(map(mul, …))`` is ~25 µs per 768-dim dot product, so 30
queries over 5,000 rows cost ~3 s. The one residual inaccuracy is the
read-back itself: pgvector prints each float4 in shortest-round-trip form,
so a component comes back within ~1 ulp of the float4 (measured max
7.4e-09 absolute over the fixture corpus) — three orders below the 1e-6
this file pins.
"""
q = [_f32(v) for v in query]
q_norm = math.sqrt(sum(map(operator.mul, q, q)))
scored = [
(
sum(map(operator.mul, vec, q)) / (math.sqrt(sum(map(operator.mul, vec, vec))) * q_norm),
chunk_id,
)
for chunk_id, vec in stored.items()
]
scored.sort(key=lambda item: -item[0])
return scored[:TOP_K]
# ---------------------------------------------------------------------------
# Capturing the tier's own statement (the plan pin must explain THAT SQL)
# ---------------------------------------------------------------------------
class _StatementRecorder:
"""Records what :func:`_vector_candidates` sends, answering with no rows."""
def __init__(self) -> None:
self.statements: list[Any] = []
def execute(self, statement: Any, params: Any = None) -> _StatementRecorder:
self.statements.append(statement)
return self
def all(self) -> list[Any]:
return []
def _vector_statement_sql() -> str:
"""The tier's cosine SELECT as one literal SQL string, ready for EXPLAIN.
A hand-written copy would drift the moment the retriever changes and would
then pin SQL no request ever runs, so the statement OBJECT is captured.
``literal_binds`` gives the planner a real vector constant
(``pgvector.sqlalchemy.VECTOR`` ships a literal processor, and the resulting
unknown string literal coerces to ``vector`` through the ``<=>`` operator's
own signature). The ``SET LOCAL`` line is deliberately not part of it:
``EXPLAIN`` takes one statement, and that line tunes the search depth, not
the shape of the plan.
"""
recorder = _StatementRecorder()
_vector_candidates(recorder, [0.0] * DIM, TOP_K) # pyright: ignore[reportArgumentType]
set_local, select_statement = recorder.statements
assert str(set_local) == f"SET LOCAL hnsw.ef_search = {EF_PIN}", (
f"the tier's ef_search line changed ({set_local}) — the capture must be the SELECT"
)
sql = str(
select_statement.compile(
dialect=postgresql.dialect(), compile_kwargs={"literal_binds": True}
)
)
assert "<=>" in sql, f"the cosine operator is gone from the tier: {sql[:200]}"
return sql
def _plan(session: Session, sql: str) -> list[dict[str, Any]]:
"""Every node of ``EXPLAIN (FORMAT JSON)`` for *sql*, flattened."""
document = session.execute(text(f"EXPLAIN (FORMAT JSON) {sql}")).scalar()
assert isinstance(document, list) and document, f"no plan for {sql[:120]}: {document!r}"
nodes: list[dict[str, Any]] = []
def walk(node: dict[str, Any]) -> None:
nodes.append(node)
for child in node.get("Plans", []):
walk(child)
walk(document[0]["Plan"])
return nodes
def _index_validity(session: Session) -> bool | None:
"""``pg_index.indisvalid`` for :data:`INDEX_NAME` (``None`` = absent) — the
catalog fact behind every plan assertion here, so a failure names the
missing/INVALID index instead of a plan shape."""
value = session.execute(
text(
"SELECT i.indisvalid"
" FROM pg_index i"
" JOIN pg_class c ON c.oid = i.indexrelid"
" WHERE c.relname = :name"
),
{"name": INDEX_NAME},
).scalar()
session.rollback()
return None if value is None else bool(value)
def _describe(nodes: list[dict[str, Any]]) -> str:
return ", ".join(
f"{node.get('Node Type')}[{node.get('Index Name') or node.get('Relation Name') or '?'}]"
for node in nodes
)
def _uses_index(nodes: list[dict[str, Any]], index_name: str) -> bool:
return any(
"Index Scan" in str(node.get("Node Type", "")) and node.get("Index Name") == index_name
for node in nodes
)
def _seq_scans(nodes: list[dict[str, Any]], relation: str) -> bool:
return any(
node.get("Node Type") == "Seq Scan" and node.get("Relation Name") == relation
for node in nodes
)
# ---------------------------------------------------------------------------
# Fixture: seed unindexed, build through 0029, read the corpus back once
# ---------------------------------------------------------------------------
@pytest.fixture(scope="module")
def hnsw_corpus() -> Iterator[dict[str, Any]]:
"""Seed the corpus, build the index with migration 0029, precompute exact.
``0029 → 0028`` → seed → ``0029``: seeding with the index present would pay
a graph insert per row, and building after the load is what the operator
does on the production box anyway. Teardown deletes ONLY this module's rows
(``source = FIXTURE_SOURCE``; the chunk rows go with them by the DB-level
cascade — AGENTS rule 12, no chunk hydration) and leaves the dev DB at head
with the index valid, exactly as it was found.
"""
if not db_available():
pytest.skip("Postgres not reachable — run `podman compose up -d db` first")
session = SessionLocal()
config = Config() # no alembic.ini — env.py reads the URL from app config
config.set_main_option("script_location", "alembic")
try:
# `preexisting` is the relation's own state (the skip guard below);
# its EMBEDDED half is what the exact reference can score, and the
# pristine flag is the premise of the unforced-plan arm of pin 1.
preexisting, preexisting_embedded = session.execute(
text(f"SELECT count(*), count(embedding) FROM {RELATION}")
).one()
if preexisting > MAX_RELATION_ROWS:
pytest.skip(
f"{RELATION} holds {preexisting} rows: the exact reference is a Python "
"scan, so these pins need the small dev corpus. Production scale "
"is task 04's live battery."
)
corpus, queries = _generate()
session.rollback() # no open snapshot while the CONCURRENTLY DDL runs
command.downgrade(config, "0028")
session.rollback()
_seed(session, corpus)
session.rollback()
command.upgrade(config, "0029") # the ONE definition of the index
session.rollback()
# The premise of every pin below, checked where it is established: an
# INVALID remnant (phase 188 D2) or a drop by whatever else owns the
# schema on this box must report ITSELF, not surface three tests later
# as "the plan did not name the index".
validity = _index_validity(session)
assert validity is True, (
f"{INDEX_NAME} is {validity!r} after migration 0029 "
"(None = absent, False = the INVALID remnant D2 describes) — every "
"plan assertion below would be measuring a scan"
)
# Planner statistics for the plan pins — BOTH relations the tier's
# SELECT joins, because the plan is a JOIN plan and a stale estimate on
# either side flips it (see the module docstring). A freshly loaded
# relation has none, and a row estimate of 0 makes
# "seq scan + top-N sort" look free.
session.execute(text(f"ANALYZE {RELATION}"))
session.execute(text("ANALYZE documents"))
session.commit()
# The SAME predicate the tier applies: a NULL-embedding row is in
# neither the index nor the exact reference (a zero vector would score
# as a real candidate and land in no result set).
rows = session.execute(
select(Chunk.id, Chunk.embedding).where(Chunk.embedding.is_not(None))
).all()
stored: dict[Any, list[float]] = {row[0]: row[1] or [] for row in rows}
assert len(stored) >= preexisting_embedded + CORPUS_CHUNKS, (
f"expected {CORPUS_CHUNKS} fixture rows on top of "
f"{preexisting_embedded} embedded rows, the relation holds {len(stored)}"
)
# The exact sets are the expensive half (30 × N dot products); the two
# recall pins differ only in the ef_search they ask the index for.
yield {
"queries": queries,
"exact": [_exact_top(stored, query) for query in queries],
"chunk_ids": set(stored),
"plan_sql": _vector_statement_sql(),
# The unforced-planner assertion (pin 1) is meaningful only on a
# relation this module owns — see the determinism premise above.
"pristine": preexisting == 0,
}
finally:
session.rollback()
session.execute(
text("DELETE FROM documents WHERE source = :src"), {"src": FIXTURE_SOURCE}
)
session.commit()
session.rollback()
command.upgrade(config, "head") # the dev DB never stays below head
session.close()
def _stub_settings(**kwargs: object) -> Settings:
"""A :class:`Settings` off the env file (the ``test_chunks_hnsw_index.py``
helper's shape — an operator's shell must not tune these pins)."""
return Settings(_env_file=None, **kwargs) # pyright: ignore[reportCallIssue]
def _force_index_scan(session: Session) -> None:
"""Put the planner on the index, because a recall pin has to measure the
index and not the planner's preference.
Postgres costs an HNSW scan partly on ``ef_search``, so on a relation this
small it can legitimately prefer a seq scan + top-N sort — and a recall run
that did that would score the EXACT scan twice and report a perfect 1.0000
without visiting a single graph node. That is not hypothetical: the live
gate (``benchmarks/phase188_live_gate/``) watched exactly this happen on the
8,866-chunk replica, where the planner used the index at ``ef_search = 40``
and refused it at 100.
``enable_seqscan = off`` is Postgres' own "take any other plan if one
exists" lever. It is set at SESSION scope because ``_vector_candidates``
opens its own transaction, so a ``SET LOCAL`` issued here would die with the
caller's transaction instead of reaching the tier. Every caller resets it in
a ``finally`` — a leaked ``off`` would slow every later test on this pooled
connection — and the assertion beside it proves the lever took effect.
``enable_bitmapscan`` goes off with it because refusing sequential scans
alone does not refuse a scan: measured on the 2026-10-18 flake, the planner
answered ``enable_seqscan = off`` with
``Sort → Nested Loop → Index Scan on documents → Bitmap Heap Scan on chunks
(ix_chunks_document_id)`` — every chunk row read and sorted through the
btree on ``document_id``, the very full read these pins exist to rule out,
wearing an index's name. With both levers off the only access path left for
``chunks`` that can serve the ORDER BY is the HNSW graph; with the index
dropped the same arm reports ``False`` (verified), so the guard cannot pass
on a missing index.
"""
session.rollback()
session.execute(text("SET enable_seqscan = off"))
session.execute(text("SET enable_bitmapscan = off"))
def _release_index_scan(session: Session) -> None:
session.rollback()
session.execute(text("RESET enable_seqscan"))
session.execute(text("RESET enable_bitmapscan"))
session.rollback()
def _set_ef_search(monkeypatch: pytest.MonkeyPatch, value: int) -> None:
"""Drive the tier at *value* the way an operator does — the setting."""
settings = _stub_settings(retrieval_ef_search=value)
monkeypatch.setattr(retriever, "get_settings", lambda: settings)
# ---------------------------------------------------------------------------
# Pin 1 — the planner uses the index (with its vacuity guard)
# ---------------------------------------------------------------------------
def test_plan_uses_the_hnsw_index(db: Session, hnsw_corpus: dict[str, Any]) -> None:
"""The 2026-10-15 incident WAS a plan, so the pin is a plan: the tier's own
``ORDER BY embedding <=> <vector> LIMIT 100`` runs through
``ix_chunks_embedding_hnsw`` and never reads ``chunks`` sequentially.
Explaining the captured statement is also what proves D1's "no query
rewrite": the ``<=>`` the retriever already emits is exactly the operator
``vector_cosine_ops`` serves.
Two arms, because the unforced one is a cost decision this file does not
own alone (module docstring, "The determinism premise"): the DEFAULT plan is
asserted while the relation holds nothing but the fixture corpus (a clean CI
DB, and this module's own teardown), and the FORCED plan — sequential and
bitmap scans refused, so nothing that reads every row can be chosen — is
asserted always.
"""
sql = hnsw_corpus["plan_sql"]
if hnsw_corpus["pristine"]:
nodes = _plan(db, sql)
assert _uses_index(nodes, INDEX_NAME), (
f"{INDEX_NAME} is not in the plan — the incident is back "
f"(index validity: {_index_validity(db)}): [{_describe(nodes)}]"
)
assert not _seq_scans(nodes, RELATION), (
f"a sequential scan of {RELATION} survived the index: [{_describe(nodes)}]"
)
_force_index_scan(db)
try:
forced = _plan(db, sql)
assert _uses_index(forced, INDEX_NAME), (
f"{INDEX_NAME} is not in the plan even with sequential and bitmap "
f"scans refused (index validity: {_index_validity(db)}): "
f"[{_describe(forced)}]"
)
assert not _seq_scans(forced, RELATION), (
f"a sequential scan of {RELATION} survived the index: "
f"[{_describe(forced)}]"
)
finally:
_release_index_scan(db)
db.rollback()
def test_plan_without_the_index_seq_scans_the_relation(
db: Session, hnsw_corpus: dict[str, Any]
) -> None:
"""Vacuity guard: the same statement, same session, same statistics, with
the index access paths refused, MUST fall back to ``Seq Scan on chunks``.
``SET LOCAL enable_indexscan/indexonlyscan/bitmapscan = off`` rather than
dropping the index: a ``DROP INDEX CONCURRENTLY`` + rebuild would cost a
second graph build and leave the corpus unindexed if the rebuild failed,
while these GUCs are transaction-scoped (they die with the rollback) and
together remove every access path that reaches an index — including the
bitmap path into ``ix_chunks_document_id``, which alone would let this guard
answer with a disguised full read instead of the seq scan it asserts.
Without this test the plan pin would pass vacuously on any plan that merely
mentions some index.
"""
sql = hnsw_corpus["plan_sql"]
db.rollback()
db.execute(text("SET LOCAL enable_indexscan = off"))
db.execute(text("SET LOCAL enable_indexonlyscan = off"))
# Bitmap scans go too: they are the other way into ``ix_chunks_document_id``,
# so leaving them on let this guard depend on the planner's mood instead of
# on the access path it removes (the 2026-10-18 flake answer was a Bitmap
# Heap Scan of chunks and no Seq Scan at all).
db.execute(text("SET LOCAL enable_bitmapscan = off"))
nodes = _plan(db, sql)
assert _seq_scans(nodes, RELATION), (
f"the vacuity guard itself did not seq-scan {RELATION}: [{_describe(nodes)}]"
)
assert not _uses_index(nodes, INDEX_NAME), (
f"{INDEX_NAME} was used with index scans disabled: [{_describe(nodes)}]"
)
db.rollback()
# ---------------------------------------------------------------------------
# Pin 2 — the approximate top-100 IS the exact top-100
# ---------------------------------------------------------------------------
def test_top100_recall_at_the_shipped_ef_search(
db: Session,
hnsw_corpus: dict[str, Any],
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""At the shipped ``ef_search`` the index returns the exact top-100 for
every query, exact to 1e-6 on the score the honesty gate reads.
Two different failures surface here: a ``top_score`` that moved — A8's
deflect/answer verdict flips on that one number, so it is pinned per row,
not per query — and an overlap below the floor, i.e. a candidate that never
reaches RRF. The failure message carries every query's overlap so a future
``ef_search`` raise has its evidence in the report instead of in a re-run.
"""
queries: list[list[float]] = hnsw_corpus["queries"]
exact: list[list[tuple[float, Any]]] = hnsw_corpus["exact"]
chunk_ids: set[Any] = hnsw_corpus["chunk_ids"]
_set_ef_search(monkeypatch, EF_PIN)
_force_index_scan(db)
try:
depth_nodes = _plan(db, hnsw_corpus["plan_sql"])
assert _uses_index(depth_nodes, INDEX_NAME), (
f"this recall run at ef_search={EF_PIN} did not use {INDEX_NAME} even "
"with sequential AND bitmap scans refused — the pin would be "
"measuring a scan and calling it parity (index validity: "
f"{_index_validity(db)}): [{_describe(depth_nodes)}]"
)
overlaps: list[float] = []
worst_delta = 0.0
for index, (query, exact_rows) in enumerate(zip(queries, exact, strict=True)):
exact_ids = {chunk_id for _, chunk_id in exact_rows}
exact_scores = {chunk_id: cosine for cosine, chunk_id in exact_rows}
candidates = _vector_candidates(db, query, TOP_K)
db.rollback() # end the turn's transaction, SET LOCAL included
assert len(candidates) == TOP_K, (
f"query {index}: the tier returned {len(candidates)} candidates, "
f"not the {TOP_K} the A7 cosine tier promises"
)
got_ids = {candidate.chunk_id for candidate in candidates}
assert got_ids <= chunk_ids, (
f"query {index}: a candidate is outside the corpus the exact "
"reference was computed over"
)
overlaps.append(len(exact_ids & got_ids) / TOP_K)
for candidate in candidates:
exact_cosine = exact_scores.get(candidate.chunk_id)
if exact_cosine is not None:
worst_delta = max(worst_delta, abs(candidate.cosine - exact_cosine))
finally:
_release_index_scan(db)
mean = sum(overlaps) / len(overlaps)
below = [(i, overlap) for i, overlap in enumerate(overlaps) if overlap < RECALL_FLOOR]
assert not below, (
f"top-{TOP_K} recall below {RECALL_FLOOR} at ef_search={EF_PIN} for "
f"{len(below)}/{len(overlaps)} queries: {below}; per-query overlap="
f"{[round(overlap, 4) for overlap in overlaps]} mean={mean:.4f} — the "
"remedy is a higher BOR_RETRIEVAL_EF_SEARCH, never this floor (LOCKED A3)"
)
assert worst_delta < COSINE_TOLERANCE, (
f"1 - distance drifted from the exact cosine by {worst_delta:.3e} "
f"(tolerance {COSINE_TOLERANCE:.0e}) — top_score would not be exact"
)
def test_recall_never_below_the_pinned_floor_when_ef_search_raised(
db: Session,
hnsw_corpus: dict[str, Any],
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Monotonicity sanity: a deeper search cannot recover FEWER of the exact
top-100 than a shallower one.
This is the property the operator leans on when the live battery (task 04)
says "raise it" — ``ef_search`` is a recall dial, not a lottery. A
regression here means the value never reaches the server (the ``SET LOCAL``
of task 02), which is exactly what the shipped 400 would then silently not
do. ``EF_RAISED`` sits ABOVE the shipped value, so the probe still measures
a deeper search after task 04 moved the default.
"""
exact: list[list[tuple[float, Any]]] = hnsw_corpus["exact"]
queries: list[list[float]] = hnsw_corpus["queries"]
def mean_overlap(ef_search: int) -> float:
_set_ef_search(monkeypatch, ef_search)
total = 0.0
for query, exact_rows in zip(queries, exact, strict=True):
exact_ids = {chunk_id for _, chunk_id in exact_rows}
candidates = _vector_candidates(db, query, TOP_K)
db.rollback()
total += len(exact_ids & {candidate.chunk_id for candidate in candidates}) / TOP_K
return total / len(exact)
pinned = mean_overlap(EF_PIN)
raised = mean_overlap(EF_RAISED)
assert raised >= pinned, (
f"ef_search={EF_RAISED} recovered less of the exact top-{TOP_K} "
f"({raised:.4f}) than ef_search={EF_PIN} ({pinned:.4f})"
)