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:
parent
f35be76604
commit
c284d86885
3 changed files with 61 additions and 4 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue