Files
QMDSearch/tests/test_eval_metrics.py
T
kplam 51dc8dc4f6 Initial commit: QMDSearch 分层信息检索服务
- FastAPI + Qdrant + Redis + Ollama 技术栈
- L1→L2→L3→chunk 四层分层检索(dense + sparse RRF 融合)
- 文档三级总结与 2.5 级回退
- query 解析路由与分类
- /admin 管理页面
2026-07-29 21:24:40 +08:00

95 lines
3.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""评测指标纯函数单元测试(不依赖 Qdrant / Ollama"""
import pytest
from scripts.eval.metrics import (
aggregate,
entity_recall,
precision_at_k,
pruning_loss,
recall_at_k,
routing_f1,
)
class TestEntityRecall:
def test_all_entities_kept(self) -> None:
source = "使用 Qdrant 存储 1536 维向量,模型 bge-m3,见《部署手册》第三条。"
summary = "基于 Qdrant 与 bge-m3 的 1536 维向量检索,详见《部署手册》第三条。"
assert entity_recall(source, summary) == pytest.approx(1.0)
def test_partial_entities_kept(self) -> None:
source = "支持 text-embedding-3-small 和 bge-m3 两个模型。"
summary = "支持 bge-m3 模型。"
score = entity_recall(source, summary)
assert 0.0 < score < 1.0
def test_no_entities_returns_full_score(self) -> None:
assert entity_recall("这是一段没有任何实体的中文普通句子。", "很短") == 1.0
def test_empty_summary_drops_all(self) -> None:
source = "版本 v1.2.0 修复了 3 个问题。"
assert entity_recall(source, "") == pytest.approx(0.0)
class TestRoutingF1:
def test_perfect_routing(self) -> None:
golden = [{"d1"}, {"d2"}]
routed = [{"d1"}, {"d2"}]
result = routing_f1(golden, routed)
assert result == {"precision": 1.0, "recall": 1.0, "f1": 1.0}
def test_micro_average(self) -> None:
# query1: tp=1 fp=1query2: tp=0 fn=1 → micro p=1/2 r=1/2 f1=1/2
golden = [{"d1"}, {"d2"}]
routed = [{"d1", "d3"}, set()]
result = routing_f1(golden, routed)
assert result["precision"] == pytest.approx(0.5)
assert result["recall"] == pytest.approx(0.5)
assert result["f1"] == pytest.approx(0.5)
def test_empty_routed(self) -> None:
result = routing_f1([{"d1"}], [set()])
assert result["precision"] == 0.0
assert result["recall"] == 0.0
class TestPruningLoss:
def test_mixed_cases(self) -> None:
assert pruning_loss([False, True, False, False]) == pytest.approx(0.25)
def test_empty(self) -> None:
assert pruning_loss([]) == 0.0
class TestPrecisionRecallAtK:
def test_precision_at_k(self) -> None:
hits = ["d1", "d2", "d3", "d4", "d5"]
assert precision_at_k(hits, {"d1", "d3"}, 5) == pytest.approx(0.4)
def test_precision_at_k_empty_hits(self) -> None:
assert precision_at_k([], {"d1"}, 5) == 0.0
def test_recall_at_k_dedup(self) -> None:
hits = ["d1", "d1", "d2"]
assert recall_at_k(hits, {"d1", "d2", "d3"}, 3) == pytest.approx(2 / 3)
def test_recall_at_k_empty_golden(self) -> None:
assert recall_at_k(["d1"], set(), 5) == 0.0
class TestAggregate:
def test_mean_per_key(self) -> None:
records = [{"a": 1.0, "b": 0.5}, {"a": 0.0, "b": 1.0}]
result = aggregate(records)
assert result["a"] == pytest.approx(0.5)
assert result["b"] == pytest.approx(0.75)
def test_partial_keys(self) -> None:
result = aggregate([{"a": 1.0}, {"a": 0.0, "b": 1.0}])
assert result["a"] == pytest.approx(0.5)
assert result["b"] == pytest.approx(1.0)
def test_empty(self) -> None:
assert aggregate([]) == {}