Files
wehub-resource-sync c56bef871b
Sync docs with Docusaurus / sync (push) Waiting to run
Tests / Check if changed (push) Waiting to run
Tests / format (push) Blocked by required conditions
Tests / check-imports (push) Blocked by required conditions
Tests / Unit / macos-latest (push) Blocked by required conditions
Tests / Unit / ubuntu-latest (push) Blocked by required conditions
Tests / Unit / windows-latest (push) Blocked by required conditions
Tests / mypy (push) Blocked by required conditions
Tests / Integration / ubuntu-latest (push) Blocked by required conditions
Tests / Integration / macos-latest (push) Blocked by required conditions
Tests / Integration / windows-latest (push) Blocked by required conditions
Tests / notify-slack-on-failure (push) Blocked by required conditions
Tests / Mark tests as completed (push) Blocked by required conditions
Docker image release / Build base image (push) Waiting to run
CodeQL / Analyze (python) (push) Has been cancelled
Update Platform Components Table / update (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:22:28 +08:00

356 lines
13 KiB
Python

# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
#
# SPDX-License-Identifier: Apache-2.0
import pytest
from haystack import Document, default_from_dict
from haystack.components.evaluators.document_ndcg import DocumentNDCGEvaluator
def test_run_with_scores():
evaluator = DocumentNDCGEvaluator()
result = evaluator.run(
ground_truth_documents=[
[
Document(content="doc1", score=3),
Document(content="doc2", score=2),
Document(content="doc3", score=3),
Document(content="doc6", score=2),
Document(content="doc7", score=3),
Document(content="doc8", score=2),
]
],
retrieved_documents=[
[
Document(content="doc1"),
Document(content="doc2"),
Document(content="doc3"),
Document(content="doc4"),
Document(content="doc5"),
]
],
)
assert result["individual_scores"][0] == pytest.approx(0.6592, abs=1e-4)
assert result["score"] == pytest.approx(0.6592, abs=1e-4)
def test_run_without_scores():
evaluator = DocumentNDCGEvaluator()
result = evaluator.run(
ground_truth_documents=[[Document(content="France"), Document(content="Paris")]],
retrieved_documents=[[Document(content="France"), Document(content="Germany"), Document(content="Paris")]],
)
assert result["individual_scores"][0] == pytest.approx(0.9197, abs=1e-4)
assert result["score"] == pytest.approx(0.9197, abs=1e-4)
def test_run_with_multiple_lists_of_docs():
evaluator = DocumentNDCGEvaluator()
result = evaluator.run(
ground_truth_documents=[
[Document(content="France"), Document(content="Paris")],
[
Document(content="doc1", score=3),
Document(content="doc2", score=2),
Document(content="doc3", score=3),
Document(content="doc6", score=2),
Document(content="doc7", score=3),
Document(content="doc8", score=2),
],
],
retrieved_documents=[
[Document(content="France"), Document(content="Germany"), Document(content="Paris")],
[
Document(content="doc1"),
Document(content="doc2"),
Document(content="doc3"),
Document(content="doc4"),
Document(content="doc5"),
],
],
)
assert result["individual_scores"][0] == pytest.approx(0.9197, abs=1e-4)
assert result["individual_scores"][1] == pytest.approx(0.6592, abs=1e-4)
assert result["score"] == pytest.approx(0.7895, abs=1e-4)
def test_run_with_different_lengths():
evaluator = DocumentNDCGEvaluator()
with pytest.raises(ValueError):
evaluator.run(
ground_truth_documents=[[Document(content="Berlin")]],
retrieved_documents=[[Document(content="Berlin")], [Document(content="London")]],
)
with pytest.raises(ValueError):
evaluator.run(
ground_truth_documents=[[Document(content="Berlin")], [Document(content="Paris")]],
retrieved_documents=[[Document(content="Berlin")]],
)
def test_run_with_mixed_documents_with_and_without_scores():
evaluator = DocumentNDCGEvaluator()
with pytest.raises(ValueError):
evaluator.run(
ground_truth_documents=[[Document(content="France", score=3), Document(content="Paris")]],
retrieved_documents=[[Document(content="France"), Document(content="Germany"), Document(content="Paris")]],
)
def test_run_empty_retrieved():
evaluator = DocumentNDCGEvaluator()
result = evaluator.run(ground_truth_documents=[[Document(content="France")]], retrieved_documents=[[]])
assert result["individual_scores"] == [0.0]
assert result["score"] == 0.0
def test_run_empty_ground_truth():
evaluator = DocumentNDCGEvaluator()
result = evaluator.run(ground_truth_documents=[[]], retrieved_documents=[[Document(content="France")]])
assert result["individual_scores"] == [0.0]
assert result["score"] == 0.0
def test_run_empty_retrieved_and_empty_ground_truth():
evaluator = DocumentNDCGEvaluator()
result = evaluator.run(ground_truth_documents=[[]], retrieved_documents=[[]])
assert result["individual_scores"] == [0.0]
assert result["score"] == 0.0
def test_run_no_retrieved():
evaluator = DocumentNDCGEvaluator()
with pytest.raises(ValueError):
_ = evaluator.run(ground_truth_documents=[[Document(content="France")]], retrieved_documents=[])
def test_run_no_ground_truth():
evaluator = DocumentNDCGEvaluator()
with pytest.raises(ValueError):
evaluator.run(ground_truth_documents=[], retrieved_documents=[[Document(content="France")]])
def test_run_no_retrieved_and_no_ground_truth():
evaluator = DocumentNDCGEvaluator()
with pytest.raises(ValueError):
evaluator.run(ground_truth_documents=[], retrieved_documents=[])
def test_calculate_dcg_with_scores():
evaluator = DocumentNDCGEvaluator()
gt_docs = [
Document(content="doc1", score=3),
Document(content="doc2", score=2),
Document(content="doc3", score=3),
Document(content="doc4", score=0),
Document(content="doc5", score=1),
Document(content="doc6", score=2),
]
ret_docs = [
Document(content="doc1"),
Document(content="doc2"),
Document(content="doc3"),
Document(content="doc4"),
Document(content="doc5"),
Document(content="doc6"),
]
dcg = evaluator.calculate_dcg(gt_docs, ret_docs)
assert dcg == pytest.approx(6.8611, abs=1e-4)
def test_calculate_dcg_without_scores():
evaluator = DocumentNDCGEvaluator()
gt_docs = [Document(content="doc1"), Document(content="doc2")]
ret_docs = [Document(content="doc2"), Document(content="doc3"), Document(content="doc1")]
dcg = evaluator.calculate_dcg(gt_docs, ret_docs)
assert dcg == pytest.approx(1.5, abs=1e-4)
def test_calculate_dcg_empty():
evaluator = DocumentNDCGEvaluator()
gt_docs = [Document(content="doc1")]
ret_docs = []
dcg = evaluator.calculate_dcg(gt_docs, ret_docs)
assert dcg == 0
def test_calculate_idcg_with_scores():
evaluator = DocumentNDCGEvaluator()
gt_docs = [
Document(content="doc1", score=3),
Document(content="doc2", score=3),
Document(content="doc3", score=2),
Document(content="doc4", score=3),
Document(content="doc5", score=2),
Document(content="doc6", score=2),
]
idcg = evaluator.calculate_idcg(gt_docs)
assert idcg == pytest.approx(8.7403, abs=1e-4)
def test_calculate_idcg_without_scores():
evaluator = DocumentNDCGEvaluator()
gt_docs = [Document(content="doc1"), Document(content="doc2"), Document(content="doc3")]
idcg = evaluator.calculate_idcg(gt_docs)
assert idcg == pytest.approx(2.1309, abs=1e-4)
def test_calculate_idcg_empty():
evaluator = DocumentNDCGEvaluator()
gt_docs = []
idcg = evaluator.calculate_idcg(gt_docs)
assert idcg == 0
def test_to_dict_default():
evaluator = DocumentNDCGEvaluator()
data = evaluator.to_dict()
assert data == {
"type": "haystack.components.evaluators.document_ndcg.DocumentNDCGEvaluator",
"init_parameters": {"document_comparison_field": "content"},
}
def test_to_dict_custom_field():
evaluator = DocumentNDCGEvaluator(document_comparison_field="id")
data = evaluator.to_dict()
assert data == {
"type": "haystack.components.evaluators.document_ndcg.DocumentNDCGEvaluator",
"init_parameters": {"document_comparison_field": "id"},
}
def test_from_dict():
data = {
"type": "haystack.components.evaluators.document_ndcg.DocumentNDCGEvaluator",
"init_parameters": {"document_comparison_field": "id"},
}
evaluator = default_from_dict(DocumentNDCGEvaluator, data)
assert evaluator.document_comparison_field == "id"
def test_run_with_id_comparison():
# Documents with same content but different IDs — id comparison
# must match on id, not content
evaluator = DocumentNDCGEvaluator(document_comparison_field="id")
result = evaluator.run(
ground_truth_documents=[[Document(id="doc1", content="France"), Document(id="doc2", content="Paris")]],
retrieved_documents=[
[
Document(id="doc1", content="different text"),
Document(id="doc3", content="Germany"),
Document(id="doc2", content="also different"),
]
],
)
assert result["individual_scores"][0] == pytest.approx(0.9197, abs=1e-4)
assert result["score"] == pytest.approx(0.9197, abs=1e-4)
def test_run_with_id_comparison_no_match():
evaluator = DocumentNDCGEvaluator(document_comparison_field="id")
result = evaluator.run(
ground_truth_documents=[[Document(id="doc1", content="France")]],
retrieved_documents=[[Document(id="doc99", content="France")]],
)
# Same content, different ID — should NOT match when comparing by id
assert result["individual_scores"] == [0.0]
assert result["score"] == 0.0
def test_run_with_meta_comparison():
evaluator = DocumentNDCGEvaluator(document_comparison_field="meta.file_id")
result = evaluator.run(
ground_truth_documents=[
[Document(content="France", meta={"file_id": "f1"}), Document(content="Paris", meta={"file_id": "f2"})]
],
retrieved_documents=[
[
Document(content="different", meta={"file_id": "f1"}),
Document(content="irrelevant", meta={"file_id": "f99"}),
Document(content="also different", meta={"file_id": "f2"}),
]
],
)
assert result["individual_scores"][0] == pytest.approx(0.9197, abs=1e-4)
assert result["score"] == pytest.approx(0.9197, abs=1e-4)
def test_run_with_nested_meta_comparison():
evaluator = DocumentNDCGEvaluator(document_comparison_field="meta.source.url")
result = evaluator.run(
ground_truth_documents=[[Document(content="x", meta={"source": {"url": "https://a.com"}})]],
retrieved_documents=[[Document(content="z", meta={"source": {"url": "https://a.com"}})]],
)
assert result["individual_scores"] == [1.0]
assert result["score"] == 1.0
def test_run_with_meta_missing_key_treated_as_no_match():
# Documents missing the meta key should not match anything
evaluator = DocumentNDCGEvaluator(document_comparison_field="meta.file_id")
result = evaluator.run(
ground_truth_documents=[[Document(content="France", meta={"file_id": "f1"})]],
retrieved_documents=[[Document(content="France", meta={})]],
)
assert result["individual_scores"] == [0.0]
assert result["score"] == 0.0
def test_run_with_id_comparison_with_scores():
# Verify that relevance scores are honoured when comparing by id
evaluator = DocumentNDCGEvaluator(document_comparison_field="id")
result = evaluator.run(
ground_truth_documents=[
[
Document(id="doc1", content="foo", score=3),
Document(id="doc2", content="bar", score=2),
Document(id="doc3", content="baz", score=3),
Document(id="doc6", content="qux", score=2),
Document(id="doc7", content="quux", score=3),
Document(id="doc8", content="corge", score=2),
]
],
retrieved_documents=[
[
Document(id="doc1", content="x"),
Document(id="doc2", content="y"),
Document(id="doc3", content="z"),
Document(id="doc4", content="w"),
Document(id="doc5", content="v"),
]
],
)
assert result["individual_scores"][0] == pytest.approx(0.6592, abs=1e-4)
assert result["score"] == pytest.approx(0.6592, abs=1e-4)
def test_unsupported_comparison_field_raises():
evaluator = DocumentNDCGEvaluator(document_comparison_field="embedding")
with pytest.raises(ValueError, match="Unsupported document_comparison_field"):
evaluator.run(
ground_truth_documents=[[Document(content="France")]], retrieved_documents=[[Document(content="France")]]
)
def test_run_with_meta_missing_key_can_still_reach_perfect_ndcg():
"""
Regression test for the IDCG/DCG inflation bug: ground truth documents that
cannot be matched (missing the configured meta key) must be excluded from
IDCG too, otherwise NDCG can never reach 1.0 even for a perfect retrieval.
"""
evaluator = DocumentNDCGEvaluator(document_comparison_field="meta.file_id")
result = evaluator.run(
ground_truth_documents=[
[
Document(content="France", meta={"file_id": "f1"}),
Document(content="unmatchable", meta={}), # no file_id -> cannot be matched
]
],
retrieved_documents=[[Document(content="France", meta={"file_id": "f1"})]],
)
# Perfect retrieval of the one matchable document should yield NDCG of exactly 1.0
assert result["individual_scores"] == [1.0]
assert result["score"] == 1.0