Resolve the search type once per search

An image query has no text to match, so it is vector-only whatever the
caller asked for, and full-text search embeds nothing, so it needs no
agreement on embedders.
This commit is contained in:
Yiorgis Gozadinos 2026-08-24 09:28:26 +03:00
parent f35be76604
commit c284d86885
No known key found for this signature in database
3 changed files with 61 additions and 4 deletions

View file

@ -39,7 +39,7 @@ async def search(
if limit is None:
limit = client._config.search.limit
resolved = search_type or ("hybrid" if isinstance(query, str) else "vector")
resolved = _resolved_search_type(query, search_type)
# One database embeds inside the repository, which returns early for a filter
# that matches nothing, so a text query that finds no documents never embeds.
query_vector = (
@ -86,13 +86,14 @@ async def search_sources(
if not names:
return []
selected = await client.clients_for(names)
client._require_one_embedder(selected)
resolved = _resolved_search_type(query, search_type)
if resolved != "fts":
client._require_one_embedder(selected)
# One over-fetch decision, one query vector, and one reranker, for the whole
# set. The databases in a selection share an embedder, so the vector is the
# same wherever it is computed.
fetch_limit = _fetch_limit(client, query, limit)
resolved = search_type or ("hybrid" if isinstance(query, str) else "vector")
query_vector = await _embed_query(selected[0], query, resolved)
per_source = await asyncio.gather(
*(
@ -198,6 +199,19 @@ def _fetch_limit(
return limit * _RERANK_OVERFETCH if client.reranker else limit
def _resolved_search_type(
query: "str | bytes | PILImage.Image", search_type: SearchType | None
) -> SearchType:
"""The search actually run for this query.
An image query has no text to match against, so it is vector-only whatever
the caller asked for; a text query defaults to hybrid.
"""
if not isinstance(query, str):
return "vector"
return search_type or "hybrid"
async def _embed_query(
client: "HaikuRAG", query: "str | bytes | PILImage.Image", search_type: SearchType
) -> list[float] | None:
@ -205,7 +219,8 @@ async def _embed_query(
Computed by the caller so that searching several databases embeds once: the
databases in a selection share an embedder, and embedding per database costs
a round trip each on a remote endpoint.
a round trip each on a remote endpoint. `search_type` is the resolved one, so
only a text query ever reaches this as full-text.
"""
if search_type == "fts":
return None

View file

@ -384,6 +384,20 @@ class TestOneEmbedderAcrossTheSet:
assert await rag.search("one", sources=["alpha"]) is not None
assert await rag.count_documents(filter=None) is not None
@pytest.mark.asyncio
async def test_full_text_search_needs_no_agreement(self, tmp_path):
"""Full-text search embeds nothing, so which model wrote each database
does not come into it."""
config = _config(tmp_path, ["alpha", "beta"])
await _seed(config, "alpha", ["alpha one"])
await _seed(config, "beta", ["beta one"])
await _restore_embedder(config, "beta", model_name="some-other-model")
async with HaikuRAG(config=config, read_only=True) as rag:
results = await rag.search("one", search_type="fts")
assert {r.source for r in results} == {"alpha", "beta"}
@pytest.mark.asyncio
async def test_agreeing_databases_search_together(self, tmp_path, query_embedding):
"""The databases agree with each other; that they were written by a

View file

@ -58,3 +58,31 @@ async def test_image_fetch_never_builds_the_reranker(
# No over-fetch: nothing will re-rank these.
assert seen["limit"] == 5
@pytest.mark.asyncio
async def test_an_image_query_ignores_a_full_text_search_type(
temp_db_path, monkeypatch
):
"""An image has no text to match, so it searches by vector whatever the
caller asked for."""
async with HaikuRAG(temp_db_path, create=True) as rag:
seen = {}
async def fake_search(query, limit, search_type, filter, query_vector):
seen.update({"search_type": search_type, "query_vector": query_vector})
return []
async def fake_embed_image(self, image):
return [0.1] * 8
monkeypatch.setattr(rag.chunk_repository, "search", fake_search)
monkeypatch.setattr(type(rag.embedder), "embed_image", fake_embed_image)
monkeypatch.setattr(
type(rag.embedder), "supports_images", property(lambda self: True)
)
await rag.search(b"image-bytes", search_type="fts")
assert seen["search_type"] == "vector"
assert seen["query_vector"] == [0.1] * 8