test-int/semantic/test_real_fastembed_reranker.py
Rama main · réplica de lectura del repositorio original.
"""Real-model smoke coverage for the local FastEmbed reranker."""
from __future__ import annotations
import math
from typing import cast
import pytest
from fastembed.rerank.cross_encoder import TextCrossEncoder
from fastembed.rerank.cross_encoder.onnx_text_cross_encoder import OnnxTextCrossEncoder
from basic_memory.config import BasicMemoryConfig, DatabaseBackend
from basic_memory.repository.fastembed_rerank_provider import FastEmbedRerankProvider
from basic_memory.repository.rerank_provider_factory import create_rerank_provider
from basic_memory.repository.sqlite_search_repository import SQLiteSearchRepository
from basic_memory.schemas.search import SearchItemType, SearchQuery, SearchRetrievalMode
from semantic.conftest import SearchCombo, _create_fastembed_provider, create_search_service
from semantic.corpus import RankingDocument, seed_ranking_documents
SMOKE_QUERY = "How should I fill and heat a moka pot to brew coffee?"
SMOKE_DOCUMENTS = (
RankingDocument(
title="Moka Pot Brewing Guide",
permalink="rerank-smoke/moka-pot-brewing",
content=(
"Fill the lower chamber with water just below the safety valve. Add finely ground "
"coffee to the filter basket without tamping, assemble the moka pot, and heat it "
"gently until brewed coffee reaches the upper chamber."
),
),
RankingDocument(
title="Moka Pot Gasket Replacement",
permalink="rerank-smoke/moka-pot-gasket",
content=(
"Replace a worn rubber gasket when a moka pot leaks around the middle seam. Remove "
"the old seal, clean the threads, and press the new gasket into its groove."
),
),
RankingDocument(
title="Cold Brew Storage",
permalink="rerank-smoke/cold-brew-storage",
content=(
"Keep concentrated cold brew coffee refrigerated in a sealed glass bottle and "
"dilute each serving with water or milk."
),
),
RankingDocument(
title="Espresso Grinder Calibration",
permalink="rerank-smoke/espresso-grinder",
content=(
"Adjust an espresso grinder one step at a time and purge retained coffee before "
"timing the next shot."
),
),
RankingDocument(
title="Reset a Locked Account",
permalink="rerank-smoke/account-reset",
content=(
"An administrator can unlock a user account after verifying identity and revoke "
"old login sessions before issuing a password reset link."
),
),
RankingDocument(
title="Apply a Database Migration",
permalink="rerank-smoke/database-migration",
content=(
"Back up the database, apply the schema migration in a transaction, and verify the "
"new index before serving application traffic."
),
),
)
@pytest.mark.asyncio
@pytest.mark.semantic
async def test_real_fastembed_reranker_scores_and_hybrid_search(
sqlite_engine_factory,
tmp_path,
) -> None:
"""The default cross-encoder should deterministically promote the relevant note."""
config = BasicMemoryConfig(
env="test",
projects={"bench-project": str(tmp_path)},
default_project="bench-project",
database_backend=DatabaseBackend.SQLITE,
reranker_enabled=True,
)
provider = create_rerank_provider(config)
assert isinstance(provider, FastEmbedRerankProvider)
rerank_documents = [f"{document.content}\n{document.title}" for document in SMOKE_DOCUMENTS]
first_scores = await provider.rerank(SMOKE_QUERY, rerank_documents)
second_scores = await provider.rerank(SMOKE_QUERY, rerank_documents)
assert first_scores == second_scores
assert all(math.isfinite(score) and 0.0 <= score <= 1.0 for score in first_scores)
assert max(range(len(first_scores)), key=first_scores.__getitem__) == 0
combo = SearchCombo("sqlite-fastembed-rerank", DatabaseBackend.SQLITE, "fastembed", 384)
search_service = await create_search_service(
sqlite_engine_factory,
combo,
tmp_path,
embedding_provider=_create_fastembed_provider(),
reranker_enabled=True,
)
repository = cast(SQLiteSearchRepository, search_service.repository)
assert repository._rerank_provider is provider
await seed_ranking_documents(search_service, SMOKE_DOCUMENTS)
results = await search_service.search(
SearchQuery(
text=SMOKE_QUERY,
retrieval_mode=SearchRetrievalMode.HYBRID,
entity_types=[SearchItemType.ENTITY],
),
limit=len(SMOKE_DOCUMENTS),
)
assert results
assert results[0].permalink == SMOKE_DOCUMENTS[0].permalink
@pytest.mark.asyncio
@pytest.mark.semantic
async def test_real_reranker_small_batches_preserve_all_candidate_scores() -> None:
"""Exercise the actual ONNX options and score a pool larger than one batch."""
provider = FastEmbedRerankProvider(threads=1)
documents = [document.content for document in SMOKE_DOCUMENTS] * 3 + [
document.content for document in SMOKE_DOCUMENTS[:3]
]
scores = await provider.rerank(SMOKE_QUERY, documents)
model = await provider._load_model()
assert isinstance(model.model, OnnxTextCrossEncoder)
assert model.model.model is not None
assert model.model.model.get_session_options().enable_cpu_mem_arena is False
baseline = TextCrossEncoder(model_name=provider.model_name, threads=1)
baseline_logits = list(baseline.rerank(SMOKE_QUERY, documents, batch_size=64))
baseline_scores = [1 / (1 + math.exp(-float(value))) for value in baseline_logits]
assert len(scores) == 21
assert scores == pytest.approx(baseline_scores, abs=1e-5)
assert max(range(len(scores)), key=scores.__getitem__) % len(SMOKE_DOCUMENTS) == 0