| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140 |
- import asyncio
- import json
- from pathlib import Path
- from uuid import UUID, uuid4
- from pymilvus import AnnSearchRequest, RRFRanker
- from retrieval_evaluation import publish_dataset
- from test_after_sales import scenario # noqa: F401
- from zhifu.modules.knowledge.vector import connection, stable_candidates
- async def test_retrieval_stability(scenario): # noqa: F811
- client = scenario["app"].state.knowledge
- try:
- dataset, identifiers = await publish_dataset(scenario)
- originals = await asyncio.to_thread(
- client.milvus.query,
- client.settings.collection,
- filter=f'tenant == "{scenario["tenant"].hex}"',
- output_fields=[
- "id",
- "tenant",
- "store",
- "document",
- "version",
- "position",
- "text",
- "dense",
- ],
- )
- queries = await client.embeddings([item["query"] for item in dataset["queries"]])
- generations = []
- for generation in range(4):
- store = uuid4().hex
- keys = {}
- rows = []
- for original in originals:
- identifier = uuid4().hex
- keys[identifier] = identifiers[str(UUID(hex=original["document"]))]
- rows.append(
- {
- **original,
- "id": f"{scenario['tenant'].hex}:{identifier}:1:0",
- "store": store,
- "document": identifier,
- }
- )
- await asyncio.to_thread(client.milvus.upsert, client.settings.collection, rows)
- expression = f'tenant == "{scenario["tenant"].hex}" and store == "{store}"'
- results = []
- for question, vector in zip(dataset["queries"], queries, strict=True):
- branches = []
- for field, metric, data in (
- ("dense", "COSINE", vector),
- ("sparse", "BM25", question["query"]),
- ):
- hits = await asyncio.to_thread(
- client.milvus.search,
- client.settings.collection,
- [data],
- anns_field=field,
- search_params={"metric_type": metric},
- filter=expression,
- limit=30,
- output_fields=["document"],
- )
- branches.append(
- [
- {"key": keys[hit["entity"]["document"]], "score": hit["distance"]}
- for hit in hits[0]
- ]
- )
- reqs = [
- AnnSearchRequest(
- [vector], "dense", {"metric_type": "COSINE"}, 30, expr=expression
- ),
- AnnSearchRequest(
- [question["query"]], "sparse", {"metric_type": "BM25"}, 30, expr=expression
- ),
- ]
- repeats, ordered = [], []
- for _ in range(3):
- hits = await asyncio.to_thread(
- client.milvus.hybrid_search,
- client.settings.collection,
- reqs,
- RRFRanker(),
- limit=30,
- output_fields=["document", "position", "text"],
- )
- repeats.append(
- [
- {"key": keys[hit["entity"]["document"]], "score": hit["distance"]}
- for hit in hits[0]
- ]
- )
- stable = stable_candidates(hits[0])
- ordered.append([keys[hit["entity"]["document"]] for hit in stable])
- assert [hit["distance"] for hit in stable] == sorted(
- [hit["distance"] for hit in hits[0]], reverse=True
- )
- results.append(
- {
- **question,
- "dense": branches[0],
- "sparse": branches[1],
- "repeats": repeats,
- "stable": ordered,
- }
- )
- generations.append({"generation": generation, "queries": results})
- raw_changes, stable_changes = [], []
- for index, question in enumerate(dataset["queries"]):
- raw = {run["queries"][index]["repeats"][0][0]["key"] for run in generations}
- stable = {run["queries"][index]["stable"][0][0] for run in generations}
- if len(raw) > 1:
- raw_changes.append(question["query"])
- if len(stable) > 1:
- stable_changes.append(question["query"])
- report = {
- "dataset": dataset["version"],
- "generations": generations,
- "raw_top1_changes": raw_changes,
- "stable_top1_changes": stable_changes,
- }
- await asyncio.to_thread(
- Path("docs/verification/retrieval-stability.json").write_text,
- json.dumps(report, ensure_ascii=False, indent=2),
- encoding="utf-8",
- )
- assert not stable_changes, stable_changes
- assert all(
- query["stable"][0] == query["stable"][1] == query["stable"][2]
- for run in generations
- for query in run["queries"]
- )
- finally:
- with connection() as (milvus, collection):
- milvus.delete(collection, filter=f'tenant == "{scenario["tenant"].hex}"')
|