"""计算单个教学问题的检索指标；排序与相关等级均由作者标注。"""
import math

# 2 表示核心证据，1 表示补充证据；未列出的文档等级为 0。
relevance = {"quality": 2, "order": 1}
ranking = ["invoice", "quality", "normal", "order"]
k = 3
# 同一文档重复返回不应增加召回或 DCG，先保留第一次出现。
unique = list(dict.fromkeys(ranking))
top = unique[:k]
relevant = {key for key, grade in relevance.items() if grade > 0}
hits = relevant.intersection(top)
recall = len(hits) / len(relevant) if relevant else None
hit_rate = float(bool(hits))
rr = next((1 / rank for rank, doc in enumerate(top, 1) if doc in relevant), 0.0)

def dcg(grades):
    # 使用指数增益 2^rel-1，排名从 1 开始，对较晚结果打折。
    return sum((2**grade - 1) / math.log2(rank + 1) for rank, grade in enumerate(grades, 1))

actual = dcg([relevance.get(doc, 0) for doc in top])
ideal = dcg(sorted(relevance.values(), reverse=True)[:k])
print({"Hit@3": hit_rate, "Recall@3": recall, "RR@3": rr,
       "nDCG@3": actual / ideal if ideal else None})
# 无答案问题的 IDCG 为 0，应单独评价拒答，不强行纳入这个归一化分数。
