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
356 lines
13 KiB
Python
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
|