retrieval_stability.py 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. import asyncio
  2. import json
  3. from pathlib import Path
  4. from uuid import UUID, uuid4
  5. from pymilvus import AnnSearchRequest, RRFRanker
  6. from retrieval_evaluation import publish_dataset
  7. from test_after_sales import scenario # noqa: F401
  8. from zhifu.modules.knowledge.vector import connection, stable_candidates
  9. async def test_retrieval_stability(scenario): # noqa: F811
  10. client = scenario["app"].state.knowledge
  11. try:
  12. dataset, identifiers = await publish_dataset(scenario)
  13. originals = await asyncio.to_thread(
  14. client.milvus.query,
  15. client.settings.collection,
  16. filter=f'tenant == "{scenario["tenant"].hex}"',
  17. output_fields=[
  18. "id",
  19. "tenant",
  20. "store",
  21. "document",
  22. "version",
  23. "position",
  24. "text",
  25. "dense",
  26. ],
  27. )
  28. queries = await client.embeddings([item["query"] for item in dataset["queries"]])
  29. generations = []
  30. for generation in range(4):
  31. store = uuid4().hex
  32. keys = {}
  33. rows = []
  34. for original in originals:
  35. identifier = uuid4().hex
  36. keys[identifier] = identifiers[str(UUID(hex=original["document"]))]
  37. rows.append(
  38. {
  39. **original,
  40. "id": f"{scenario['tenant'].hex}:{identifier}:1:0",
  41. "store": store,
  42. "document": identifier,
  43. }
  44. )
  45. await asyncio.to_thread(client.milvus.upsert, client.settings.collection, rows)
  46. expression = f'tenant == "{scenario["tenant"].hex}" and store == "{store}"'
  47. results = []
  48. for question, vector in zip(dataset["queries"], queries, strict=True):
  49. branches = []
  50. for field, metric, data in (
  51. ("dense", "COSINE", vector),
  52. ("sparse", "BM25", question["query"]),
  53. ):
  54. hits = await asyncio.to_thread(
  55. client.milvus.search,
  56. client.settings.collection,
  57. [data],
  58. anns_field=field,
  59. search_params={"metric_type": metric},
  60. filter=expression,
  61. limit=30,
  62. output_fields=["document"],
  63. )
  64. branches.append(
  65. [
  66. {"key": keys[hit["entity"]["document"]], "score": hit["distance"]}
  67. for hit in hits[0]
  68. ]
  69. )
  70. reqs = [
  71. AnnSearchRequest(
  72. [vector], "dense", {"metric_type": "COSINE"}, 30, expr=expression
  73. ),
  74. AnnSearchRequest(
  75. [question["query"]], "sparse", {"metric_type": "BM25"}, 30, expr=expression
  76. ),
  77. ]
  78. repeats, ordered = [], []
  79. for _ in range(3):
  80. hits = await asyncio.to_thread(
  81. client.milvus.hybrid_search,
  82. client.settings.collection,
  83. reqs,
  84. RRFRanker(),
  85. limit=30,
  86. output_fields=["document", "position", "text"],
  87. )
  88. repeats.append(
  89. [
  90. {"key": keys[hit["entity"]["document"]], "score": hit["distance"]}
  91. for hit in hits[0]
  92. ]
  93. )
  94. stable = stable_candidates(hits[0])
  95. ordered.append([keys[hit["entity"]["document"]] for hit in stable])
  96. assert [hit["distance"] for hit in stable] == sorted(
  97. [hit["distance"] for hit in hits[0]], reverse=True
  98. )
  99. results.append(
  100. {
  101. **question,
  102. "dense": branches[0],
  103. "sparse": branches[1],
  104. "repeats": repeats,
  105. "stable": ordered,
  106. }
  107. )
  108. generations.append({"generation": generation, "queries": results})
  109. raw_changes, stable_changes = [], []
  110. for index, question in enumerate(dataset["queries"]):
  111. raw = {run["queries"][index]["repeats"][0][0]["key"] for run in generations}
  112. stable = {run["queries"][index]["stable"][0][0] for run in generations}
  113. if len(raw) > 1:
  114. raw_changes.append(question["query"])
  115. if len(stable) > 1:
  116. stable_changes.append(question["query"])
  117. report = {
  118. "dataset": dataset["version"],
  119. "generations": generations,
  120. "raw_top1_changes": raw_changes,
  121. "stable_top1_changes": stable_changes,
  122. }
  123. await asyncio.to_thread(
  124. Path("docs/verification/retrieval-stability.json").write_text,
  125. json.dumps(report, ensure_ascii=False, indent=2),
  126. encoding="utf-8",
  127. )
  128. assert not stable_changes, stable_changes
  129. assert all(
  130. query["stable"][0] == query["stable"][1] == query["stable"][2]
  131. for run in generations
  132. for query in run["queries"]
  133. )
  134. finally:
  135. with connection() as (milvus, collection):
  136. milvus.delete(collection, filter=f'tenant == "{scenario["tenant"].hex}"')