Pin the over-fetch rule, and assert the type a lookup raises

`_fetch_limit` had no test: a text query over-fetches `limit * 10` only with a
reranker, and an image query keeps its vector ranking either way. The sandbox
test asserted `KeyError`, which `UnknownDatabaseError` subclasses, so it could
not tell the contract from a bare one.

`uses_configured_databases` documents a mapping of one as covered; the test
named for it passed no mapping at all. Its `config` parameter is an `AppConfig`.

`test_an_analysis_capability_mounts_the_configured_set` carried a VCR marker and
no cassette, making no HTTP calls.
This commit is contained in:
Yiorgis Gozadinos 2026-08-28 14:03:13 +03:00
parent 688d7e708e
commit 2bfb661c10
No known key found for this signature in database
6 changed files with 98 additions and 9 deletions

View file

@ -22,12 +22,11 @@
### Fixed
- `haiku-rag settings` prints YAML instead of Python dict reprs.
- The chat document filter pages results, searches the database for the rest, and
lists the selected separately, instead of mounting a checkbox per document.
Selection is by document ID.
- `haiku-rag settings` prints YAML.
- The chat document filter pages results and lists the selected separately.
Selection is by document ID, and a typed search applies on enter.
- `haiku-rag list` prints only the fields a document has.
- `haiku-rag` and `haiku-ingester` print the message and exit when the configured embedder does not match the database, instead of raising a traceback.
- `haiku-rag` and `haiku-ingester` exit with a message on an embedder mismatch.
- Capabilities created without a client honor `lancedb.uri`.
- A `lancedb.uri` without a scheme is treated as a local path. `--db PATH`
overrides it.

View file

@ -7,6 +7,7 @@ from datasets import Dataset
from pydantic import BaseModel, model_validator
from pydantic_evals import Case
from pydantic_evals.evaluators import Evaluator
from haiku.rag.config.models import AppConfig
class Turn(BaseModel):
@ -84,7 +85,7 @@ class DatasetSpec:
experiment_metadata: dict[str, Any] | None = None
def uses_configured_databases(
self, config, override_path: Path | None = None
self, config: AppConfig, override_path: Path | None = None
) -> bool:
"""Whether `lancedb.databases` places the databases to evaluate over.

View file

@ -179,7 +179,18 @@ class TestCoversASet:
assert spec.uses_configured_databases(config, _Path("/chosen.lancedb")) is False
def test_one_database_is_not_a_set(self):
def test_a_configured_set_of_one_is_still_configured(self):
"""A mapping of one is a named database like any other."""
from haiku.rag.config.models import AppConfig, LanceDBConfig
from evaluations.datasets import DATASETS
spec = next(iter(DATASETS.values()))
config = AppConfig(lancedb=LanceDBConfig(databases={"a": "/a.lancedb"}))
assert spec.uses_configured_databases(config) is True
def test_naming_no_database_is_not_a_set(self):
from haiku.rag.config.models import AppConfig
from evaluations.datasets import DATASETS

View file

@ -81,7 +81,6 @@ class TestStandaloneCapabilities:
assert "beta document" in formatted
@pytest.mark.asyncio
@pytest.mark.vcr()
async def test_an_analysis_capability_mounts_the_configured_set(self, tmp_path):
from haiku.rag.capabilities.analysis import (
create_capability as create_analysis,

View file

@ -268,6 +268,84 @@ class TestRerankerFusion:
assert stub.attached == {"alpha": b"bytes-alpha", "beta": b"bytes-beta"}
class TestOverFetchingForAReranker:
"""A reranker needs more candidates than it returns. Ranking without one does
not, and an image query keeps its vector ranking either way."""
@staticmethod
def _limits_asked(monkeypatch) -> list[int]:
from haiku.rag.store.repositories.chunk import ChunkRepository
asked: list[int] = []
search = ChunkRepository.search
async def spy(self, *args, **kwargs):
asked.append(kwargs["limit"])
return await search(self, *args, **kwargs)
monkeypatch.setattr(ChunkRepository, "search", spy)
return asked
@pytest.mark.asyncio
async def test_a_text_query_over_fetches_for_a_reranker(
self, tmp_path, monkeypatch
):
config = _config(tmp_path, ["alpha", "beta"])
await _seed(config, "alpha", ["alpha document about cats"])
await _seed(config, "beta", ["beta document about cats"])
monkeypatch.setattr(HaikuRAG, "reranker", property(lambda self: StubReranker()))
asked = self._limits_asked(monkeypatch)
async with HaikuRAG(config=config) as rag:
await rag.search("cats", limit=3, search_type="fts")
per_database = list(asked)
asked.clear()
await rag.search("cats", limit=3, search_type="fts", sources=["alpha"])
assert per_database == [30, 30]
assert asked == [30]
@pytest.mark.asyncio
async def test_a_text_query_without_a_reranker_fetches_what_it_returns(
self, tmp_path, monkeypatch
):
config = _config(tmp_path, ["alpha", "beta"])
await _seed(config, "alpha", ["alpha document about cats"])
await _seed(config, "beta", ["beta document about cats"])
monkeypatch.setattr(HaikuRAG, "reranker", property(lambda self: None))
asked = self._limits_asked(monkeypatch)
async with HaikuRAG(config=config) as rag:
await rag.search("cats", limit=3, search_type="fts")
per_database = list(asked)
asked.clear()
await rag.search("cats", limit=3, search_type="fts", sources=["alpha"])
assert per_database == [3, 3]
assert asked == [3]
@pytest.mark.asyncio
async def test_an_image_query_fetches_what_it_returns(self, tmp_path, monkeypatch):
config = _config(tmp_path, ["alpha", "beta"])
await _seed(config, "alpha", ["alpha document about cats"])
await _seed(config, "beta", ["beta document about cats"])
monkeypatch.setattr(HaikuRAG, "reranker", property(lambda self: StubReranker()))
dim = get_config().embeddings.model.vector_dim
async def embed_image(self, image): # noqa: ARG001
return [0.1] * dim
monkeypatch.setattr(EmbedderWrapper, "supports_images", True)
monkeypatch.setattr(EmbedderWrapper, "embed_image", embed_image)
asked = self._limits_asked(monkeypatch)
async with HaikuRAG(config=config) as rag:
await rag.search(b"\x89PNG\r\n\x1a\n", limit=3)
assert asked == [3, 3]
class TestOneReranker:
@pytest.mark.asyncio
async def test_the_set_builds_one_reranker_for_a_text_query(

View file

@ -5,6 +5,7 @@ import pytest
from haiku.rag.client import HaikuRAG
from haiku.rag.client.scope import DatabaseRef, DatabaseScope
from haiku.rag.sandbox import AnalysisContext, Sandbox
from haiku.rag.store.exceptions import UnknownDatabaseError
from tests.multi_db.helpers import _config, _seed
@ -300,7 +301,7 @@ class TestSelectionOnOneDatabase:
await _seed(config, "alpha", ["alpha document about cats"])
async with HaikuRAG(config=config) as rag:
with pytest.raises(KeyError, match="beta"):
with pytest.raises(UnknownDatabaseError, match="beta"):
await _mounted(rag, sources=["beta"])
@pytest.mark.asyncio