test_unified.py 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. #!/usr/bin/env python3
  2. """
  3. Test unified search vs BM25 baseline on 20 questions.
  4. Runs both engines, compares results, reports quality metrics.
  5. """
  6. import json
  7. import sys
  8. import time
  9. import os
  10. sys.path.insert(0, "/data/knowledge/maritime")
  11. os.environ['OLLAMA_EMBED_URL'] = 'http://127.0.0.1:11435/api/embed'
  12. # Load both engines
  13. from search_engine import search as bm25_search
  14. from search_unified import search as unified_search, _do_classify as classify_query
  15. # 20 test questions covering all 5 types
  16. QUESTIONS = [
  17. # fact (5)
  18. {"q": "2019年全国注册船员总数是多少?", "type": "fact", "expected_keywords": ["1659188", "船员"]},
  19. {"q": "船员适任证书的有效期是多少年?", "type": "fact", "expected_keywords": ["证书", "有效期"]},
  20. {"q": "琼州海峡船舶定线制是什么时候施行的?", "type": "fact", "expected_keywords": ["琼州海峡", "施行"]},
  21. {"q": "船舶进出港报告制度的法律依据是什么?", "type": "fact", "expected_keywords": ["进出港", "报告"]},
  22. {"q": "内河船舶船员培训机构有多少家?", "type": "fact", "expected_keywords": ["培训机构", "内河"]},
  23. # compare (4)
  24. {"q": "海上交通安全法与水污染防治法在船舶管理方面的规定有何异同?", "type": "compare", "expected_keywords": ["海上交通安全法", "水污染防治法"]},
  25. {"q": "对比2019年和2020年中国船员发展报告的主要变化", "type": "compare", "expected_keywords": ["2019", "2020", "船员"]},
  26. {"q": "国内航行船舶和国际航行船舶的检验要求有什么区别?", "type": "compare", "expected_keywords": ["国内", "国际", "检验"]},
  27. {"q": "海事局规范性文件与交通运输部规章有何区别?", "type": "compare", "expected_keywords": ["海事局", "交通运输部"]},
  28. # relation (4)
  29. {"q": "海事局的上级主管部门是哪个?", "type": "relation", "expected_keywords": ["海事局", "交通运输部"]},
  30. {"q": "船员适任证书制度依据哪些国际公约?", "type": "relation", "expected_keywords": ["适任证书", "STCW", "公约"]},
  31. {"q": "防治船舶污染海洋环境管理条例由谁制定?", "type": "relation", "expected_keywords": ["防治", "污染", "条例"]},
  32. {"q": "船舶检验机构的资质由哪个部门认可?", "type": "relation", "expected_keywords": ["检验", "资质", "认可"]},
  33. # synthesis (4)
  34. {"q": "请梳理中国海事法律法规的层级体系", "type": "synthesis", "expected_keywords": ["全国人大", "国务院", "交通运输部", "海事局"]},
  35. {"q": "总结船员管理方面的主要法规和政策", "type": "synthesis", "expected_keywords": ["船员", "管理", "法规"]},
  36. {"q": "概述海洋环境保护相关的法律框架", "type": "synthesis", "expected_keywords": ["海洋", "环境", "保护"]},
  37. {"q": "分析港口管理的法规体系", "type": "synthesis", "expected_keywords": ["港口", "管理"]},
  38. # temporal (3)
  39. {"q": "海上交通安全法最新修订是什么时候?", "type": "temporal", "expected_keywords": ["海上交通安全法", "修订"]},
  40. {"q": "船员培训管理规则的最新版本何时施行?", "type": "temporal", "expected_keywords": ["培训", "施行"]},
  41. {"q": "水污染防治法经历了哪些修订?", "type": "temporal", "expected_keywords": ["水污染防治法", "修订"]},
  42. ]
  43. print("=" * 70)
  44. print(" LambdaRAG Unified Search vs BM25 Baseline Test")
  45. print(" 20 questions × 2 engines")
  46. print("=" * 70)
  47. results = []
  48. for i, q in enumerate(QUESTIONS):
  49. print(f"\n{'─' * 70}")
  50. print(f"Q{i+1} [{q['type']}] {q['q']}")
  51. # Classify
  52. classified = classify_query(q['q'])
  53. classify_correct = classified == q['type']
  54. # BM25 baseline
  55. t0 = time.time()
  56. try:
  57. bm25_results = bm25_search(q['q'], top_k=5)
  58. except Exception as e:
  59. bm25_results = []
  60. print(f" BM25 ERROR: {e}")
  61. bm25_time = time.time() - t0
  62. # Unified
  63. t0 = time.time()
  64. try:
  65. unified_results = unified_search(q['q'], top_k=5)
  66. except Exception as e:
  67. unified_results = []
  68. print(f" UNIFIED ERROR: {e}")
  69. unified_time = time.time() - t0
  70. # Quality check: do results contain expected keywords?
  71. def check_relevance(search_results, keywords):
  72. if not search_results:
  73. return 0, []
  74. all_text = ' '.join([r.get('text', '') + ' ' + r.get('source', '') for r in search_results])
  75. found = [kw for kw in keywords if kw in all_text]
  76. return len(found) / max(len(keywords), 1), found
  77. bm25_score, bm25_found = check_relevance(bm25_results, q['expected_keywords'])
  78. unified_score, unified_found = check_relevance(unified_results, q['expected_keywords'])
  79. # Get unified metadata
  80. u_type = unified_results[0].get('_query_type', '?') if unified_results else '?'
  81. u_strategy = unified_results[0].get('_strategy', '?') if unified_results else '?'
  82. print(f" Classify: predicted={classified} expected={q['type']} {'✓' if classify_correct else '✗'}")
  83. print(f" BM25: {bm25_time:.2f}s | relevance={bm25_score:.0%} | found={bm25_found}")
  84. print(f" Unified: {unified_time:.2f}s | relevance={unified_score:.0%} | found={unified_found}")
  85. print(f" Route: {u_type} → {u_strategy}")
  86. # Check: unified sources different from BM25?
  87. bm25_sources = set(r.get('source', '')[:30] for r in bm25_results[:3])
  88. unified_sources = set(r.get('source', '')[:30] for r in unified_results[:3])
  89. sources_differ = bm25_sources != unified_sources
  90. record = {
  91. 'id': i + 1,
  92. 'question': q['q'],
  93. 'expected_type': q['type'],
  94. 'classified_type': classified,
  95. 'classify_correct': classify_correct,
  96. 'bm25_time': round(bm25_time, 2),
  97. 'unified_time': round(unified_time, 2),
  98. 'bm25_relevance': round(bm25_score, 2),
  99. 'unified_relevance': round(unified_score, 2),
  100. 'bm25_results_count': len(bm25_results),
  101. 'unified_results_count': len(unified_results),
  102. 'sources_differ': sources_differ,
  103. 'unified_route': f"{u_type} → {u_strategy}",
  104. 'winner': 'unified' if unified_score > bm25_score else ('tie' if unified_score == bm25_score else 'bm25'),
  105. }
  106. results.append(record)
  107. # Summary
  108. print(f"\n{'=' * 70}")
  109. print(" SUMMARY")
  110. print(f"{'=' * 70}")
  111. # Classification accuracy
  112. correct = sum(1 for r in results if r['classify_correct'])
  113. print(f"\nClassification accuracy: {correct}/{len(results)} ({correct/len(results)*100:.0f}%)")
  114. # Relevance comparison
  115. bm25_avg = sum(r['bm25_relevance'] for r in results) / len(results)
  116. unified_avg = sum(r['unified_relevance'] for r in results) / len(results)
  117. print(f"\nRelevance (keyword match):")
  118. print(f" BM25 avg: {bm25_avg:.0%}")
  119. print(f" Unified avg: {unified_avg:.0%}")
  120. # Winner counts
  121. wins = {'unified': 0, 'bm25': 0, 'tie': 0}
  122. for r in results:
  123. wins[r['winner']] += 1
  124. print(f"\nWins: Unified={wins['unified']} BM25={wins['bm25']} Tie={wins['tie']}")
  125. # By type
  126. print(f"\nBy question type:")
  127. for qtype in ['fact', 'compare', 'relation', 'synthesis', 'temporal']:
  128. type_results = [r for r in results if r['expected_type'] == qtype]
  129. if not type_results:
  130. continue
  131. b_avg = sum(r['bm25_relevance'] for r in type_results) / len(type_results)
  132. u_avg = sum(r['unified_relevance'] for r in type_results) / len(type_results)
  133. wins_u = sum(1 for r in type_results if r['winner'] == 'unified')
  134. wins_b = sum(1 for r in type_results if r['winner'] == 'bm25')
  135. print(f" {qtype:10s}: BM25={b_avg:.0%} Unified={u_avg:.0%} | U wins={wins_u} B wins={wins_b}")
  136. # Timing
  137. bm25_avg_time = sum(r['bm25_time'] for r in results) / len(results)
  138. unified_avg_time = sum(r['unified_time'] for r in results) / len(results)
  139. print(f"\nAvg search time:")
  140. print(f" BM25: {bm25_avg_time:.2f}s")
  141. print(f" Unified: {unified_avg_time:.2f}s")
  142. # Sources differ
  143. differ_count = sum(1 for r in results if r['sources_differ'])
  144. print(f"\nSources differ in {differ_count}/{len(results)} questions")
  145. # Issues
  146. print(f"\nPotential issues:")
  147. for r in results:
  148. if r['unified_results_count'] == 0:
  149. print(f" Q{r['id']}: Unified returned 0 results!")
  150. if not r['classify_correct']:
  151. print(f" Q{r['id']}: Misclassified as {r['classified_type']} (expected {r['expected_type']})")
  152. if r['unified_relevance'] < r['bm25_relevance']:
  153. print(f" Q{r['id']}: Unified worse than BM25 ({r['unified_relevance']:.0%} vs {r['bm25_relevance']:.0%})")
  154. # Save results
  155. with open('/data/knowledge/maritime/test_unified_results.json', 'w') as f:
  156. json.dump(results, f, ensure_ascii=False, indent=2)
  157. print(f"\nResults saved to test_unified_results.json")