study-buddy / tests /rag /test_dedup_mechanism.py
GitHub Actions
deploy d092bea3608b7a29952f16357fda39b7a29e399b
2e818da
Raw
History Blame Contribute Delete
9.69 kB
"""Behavioral test for the `rag-dedup-v2` over-fetch + dedup + truncate
mechanism (Task 8, Step 2/3). Written and confirmed FAILING before
`ChromaDBClient.query_observed` gained the mechanism.
Fixture design (deliberately avoids the "just returns fewer candidates" trap
the plan preamble's acceptance gate calls out): 5 documents ("alpha".."epsilon"),
each contributing 3 near-duplicate chunks (adjacent-chunker-overlap style --
differ only in a trailing variant token), ranked by embedding distance so
that document "alpha"'s 3 duplicates are the 3 nearest neighbors, "beta"'s 3
duplicates are next, and so on -- 15 raw candidates total, ranked in 5 clean
groups of 3.
Requesting top-5:
- `rag-naive-v1` (default, no over-fetch) can only ever see the nearest 5
raw candidates -- all 3 of "alpha"'s duplicates plus the first 2 of
"beta"'s -- so it returns a low-diversity, high-duplication set.
- `rag-dedup-v2` (over-fetch factor 3 -> fetches all 15, dedups, truncates
back to 5) can see every group, dedup down to one representative per
group, and return exactly 5 candidates again -- but now one from each of
the 5 distinct documents. Same final count (5), strictly higher diversity,
strictly lower duplication -- the fair comparison the plan requires.
v1's own behavior (default/unspecified pipeline_version) must be completely
unchanged by any of this -- asserted directly against the same fixture.
"""
from __future__ import annotations
import os
os.environ.setdefault("CEREBRAS_API_KEY", "test-key")
import chromadb
import pytest
from app.observability import bootstrap
from app.observability.config import ObservabilityConfig
from app.observability.retrieval import candidate_stats
# 12+-token base phrases, no shared vocabulary across groups, matching the
# Jaccard-clearing design in tests/observability/test_candidate_stats.py.
_GROUP_BASES = {
"alpha": "alpha membrane transport channel proteins regulate ion flow across the lipid bilayer",
"beta": "beta ribosome translation machinery assembles polypeptide chains from messenger rna transcripts today",
"gamma": "gamma mitochondrial electron transport chain produces cellular energy via oxidative phosphorylation",
"delta": "delta cytoskeleton microtubule dynamics control cell shape and intracellular cargo transport",
"epsilon": "epsilon nuclear pore complex regulates macromolecule trafficking between nucleus and cytoplasm",
}
_GROUP_ORDER = ["alpha", "beta", "gamma", "delta", "epsilon"]
_VARIANTS = ["one", "two", "three"]
def _text(group: str, variant: str) -> str:
return f"{_GROUP_BASES[group]} variant {variant}"
# Rank 1..15, strictly increasing distance from the query origin -> Chroma's
# L2-nearest-neighbor order is exactly _GROUP_ORDER x _VARIANTS, in order.
_ALL_TEXTS: list[str] = [
_text(group, variant) for group in _GROUP_ORDER for variant in _VARIANTS
]
_QUERY_TEXT = "find the most relevant passages"
_VECTORS: dict[str, list[float]] = {_QUERY_TEXT: [0.0, 0.0]}
for _rank, _text_value in enumerate(_ALL_TEXTS, start=1):
_VECTORS[_text_value] = [float(_rank), 0.0]
class _FakeEmbedder:
"""Deterministic embedder: exact string -> fixed vector (see _VECTORS)."""
def embed(self, texts: list[str]) -> list[list[float]]:
return [list(_VECTORS[t]) for t in texts]
@pytest.fixture
def isolated_db(monkeypatch):
from chromadb.api.client import SharedSystemClient
SharedSystemClient.clear_system_cache()
monkeypatch.setattr(
chromadb, "PersistentClient", lambda *a, **k: chromadb.EphemeralClient()
)
from app.rag.chromadb_client import ChromaDBClient
return ChromaDBClient(embedder=_FakeEmbedder())
def _seed(db) -> None:
documents = list(_ALL_TEXTS)
metadatas = [
{
"source": f"paper-{group}.pdf",
"document_id": f"doc-{group}",
"chunk_index": i,
"type": "content",
"project_id": "proj-1",
}
for i, group in enumerate(
g for g in _GROUP_ORDER for _ in _VARIANTS
)
]
ids = [f"chunk-{i}" for i in range(len(documents))]
db.upsert("library", documents, metadatas, ids)
def _query_vec(db):
return db.embedder.embed([_QUERY_TEXT])[0]
# --- v1: unchanged, low-diversity, high-duplication top-5 -------------------
def test_naive_v1_default_returns_low_diversity_top5(isolated_db):
_seed(isolated_db)
out = isolated_db.query(
"library", _query_vec(isolated_db), n_results=5, where={"project_id": "proj-1"}
)
assert len(out) == 5
stats = candidate_stats(out)
# Nearest 5 raw candidates = all 3 "alpha" duplicates + first 2 "beta" duplicates.
assert stats["document_diversity"] == 2
assert stats["duplicate_candidate_ratio"] == pytest.approx(3 / 5)
def test_naive_v1_explicit_matches_default(isolated_db):
_seed(isolated_db)
q = _query_vec(isolated_db)
default_out = isolated_db.query_observed(
"library", q, n_results=5, where={"project_id": "proj-1"}
)
explicit_out = isolated_db.query_observed(
"library", q, n_results=5, where={"project_id": "proj-1"}, pipeline_version="rag-naive-v1"
)
assert default_out.candidates == explicit_out.candidates
# --- v2: same final count, strictly more diverse, strictly less duplicated --
def test_dedup_v2_returns_same_count_more_diverse_top5(isolated_db):
_seed(isolated_db)
out = isolated_db.query(
"library",
_query_vec(isolated_db),
n_results=5,
where={"project_id": "proj-1"},
pipeline_version="rag-dedup-v2",
)
# Same final count as v1 -- the acceptance-gate-critical assertion: this
# is NOT just "fewer candidates returned."
assert len(out) == 5
stats = candidate_stats(out)
assert stats["document_diversity"] == 5
assert stats["duplicate_candidate_ratio"] == 0.0
# One representative from each of the 5 distinct documents.
assert {c["document_id"] for c in out} == {f"doc-{g}" for g in _GROUP_ORDER}
def test_dedup_v2_keeps_the_most_relevant_member_of_each_group(isolated_db):
_seed(isolated_db)
out = isolated_db.query(
"library",
_query_vec(isolated_db),
n_results=5,
where={"project_id": "proj-1"},
pipeline_version="rag-dedup-v2",
)
# Within each kept document, the surviving chunk is the nearest-ranked
# ("variant one") member of that group, not an arbitrary one.
for c in out:
assert c["text"].endswith("variant one")
# --- `duplicates_removed_from_pool`: direct pool-level evidence (final-review fix) --
#
# `duplicate_candidate_ratio` (see test_candidate_stats.py) only ever sees the
# final, post-truncate candidate list -- it reads ~0.0 whether the dedup step
# removed nothing from the over-fetched pool or removed several duplicates
# that then got truncated away, and can't tell those two cases apart on its
# own. `duplicates_removed_from_pool` is a direct count taken from the raw
# pool *before* truncation, closing that gap. This fixture (5 documents x 3
# near-duplicate variants each) gives the over-fetched v2 pool of 15 real,
# removable duplicates, so the count below is provably non-zero -- unlike the
# real corpus analyzed in `docs/observability/dedup-v2-evaluation.md`, where
# it reads 0.
def _local_observability_config(tmp_path) -> ObservabilityConfig:
return ObservabilityConfig(
enabled=True,
mode="local",
otlp_endpoint="http://127.0.0.1:4318",
signoz_ui_url="http://127.0.0.1:8080",
artifact_root=tmp_path,
)
def _latest_result_prepare_row(store) -> dict:
rows = [r for r in store.all() if r["operation"] == "rag.result_prepare"]
assert rows, "expected at least one rag.result_prepare observation"
return rows[-1]
def test_dedup_v2_reports_nonzero_duplicates_removed_from_pool(isolated_db, tmp_path):
bootstrap.initialize_observability(config=_local_observability_config(tmp_path))
try:
_seed(isolated_db)
isolated_db.query_observed(
"library",
_query_vec(isolated_db),
n_results=5,
where={"project_id": "proj-1"},
pipeline_version="rag-dedup-v2",
)
row = _latest_result_prepare_row(bootstrap.get_state().local_store)
# Pool = fetch_n = requested_n(5) * overfetch_factor(3) = 15, in 5
# groups of 3 near-duplicates each. Dedup keeps the first (highest-
# ranked) member of each group -- 5 survive, 10 are removed -- before
# the survivors are truncated back to requested_n(5).
assert row["attributes"]["duplicates_removed_from_pool"] == 10
finally:
bootstrap.shutdown_observability(timeout_seconds=2.0)
def test_naive_v1_reports_zero_duplicates_removed_from_pool(isolated_db, tmp_path):
"""v1 never over-fetches or dedups, so `duplicates_removed_from_pool` is
always exactly 0 -- by construction (the dedup branch never runs), not
because this fixture's naive top-5 pool happens to be duplicate-free (it
isn't: see test_naive_v1_default_returns_low_diversity_top5's
duplicate_candidate_ratio=3/5 on that same top-5)."""
bootstrap.initialize_observability(config=_local_observability_config(tmp_path))
try:
_seed(isolated_db)
isolated_db.query_observed(
"library", _query_vec(isolated_db), n_results=5, where={"project_id": "proj-1"}
)
row = _latest_result_prepare_row(bootstrap.get_state().local_store)
assert row["attributes"]["duplicates_removed_from_pool"] == 0
finally:
bootstrap.shutdown_observability(timeout_seconds=2.0)