diff --git a/haystack/components/rankers/meta_field_grouping_ranker.py b/haystack/components/rankers/meta_field_grouping_ranker.py index 26634205e60..4c27936303d 100644 --- a/haystack/components/rankers/meta_field_grouping_ranker.py +++ b/haystack/components/rankers/meta_field_grouping_ranker.py @@ -4,9 +4,11 @@ from collections import defaultdict -from haystack import Document, component +from haystack import Document, component, logging from haystack.utils.misc import _deduplicate_documents +logger = logging.getLogger(__name__) + @component class MetaFieldGroupingRanker: @@ -117,9 +119,21 @@ def run(self, documents: list[Document]) -> dict[str, list[Document]]: for docs in subgroups.values(): if sort_field: # Sort by the field value, placing documents with a missing value last. - # The (is_missing, value) tuple ensures that only actual field values are - # compared, making the sort work for numbers, strings, and other types. - docs.sort(key=lambda d: (d.meta.get(sort_field) is None, d.meta.get(sort_field))) + # The (is_missing, value) tuple keeps documents with a missing value out of the + # value comparison, but two present values of mutually non-comparable types + # (e.g. an int and a str) would still raise a TypeError. In that case we keep the + # group's insertion order instead of crashing, mirroring MetaFieldRanker. + try: + docs.sort(key=lambda d: (d.meta.get(sort_field) is None, d.meta.get(sort_field))) + except TypeError as error: + logger.warning( + "Tried to sort Documents with IDs {document_ids}, but got TypeError with the " + "message: {error}\nKeeping the original order of the Documents in this group " + "since sorting by '{sort_field}' is not possible.", + document_ids=",".join([doc.id for doc in docs]), + error=error, + sort_field=sort_field, + ) ordered_docs.extend(docs) ordered_docs.extend(no_group_docs) diff --git a/releasenotes/notes/meta-field-grouping-ranker-mixed-type-sort-161c30a3ec07c5ac.yaml b/releasenotes/notes/meta-field-grouping-ranker-mixed-type-sort-161c30a3ec07c5ac.yaml new file mode 100644 index 00000000000..309d943daa3 --- /dev/null +++ b/releasenotes/notes/meta-field-grouping-ranker-mixed-type-sort-161c30a3ec07c5ac.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - | + Fixed ``MetaFieldGroupingRanker`` crashing with an uncaught ``TypeError`` when a group contained + ``sort_docs_by`` values of mutually non-comparable types (for example an ``int`` and a ``str``). The + sort is now wrapped in a ``try``/``except``, so in that case the group's original insertion order is + kept and a warning is logged, mirroring the behavior of ``MetaFieldRanker``. diff --git a/test/components/rankers/test_meta_field_grouping_ranker.py b/test/components/rankers/test_meta_field_grouping_ranker.py index 748f9d8c776..0a0f6a5979d 100644 --- a/test/components/rankers/test_meta_field_grouping_ranker.py +++ b/test/components/rankers/test_meta_field_grouping_ranker.py @@ -185,6 +185,23 @@ def test_run_metadata_with_different_data_types(self) -> None: assert result["documents"][1].meta["group"] == 42 assert result["documents"][2].meta["group"] is True + def test_run_sort_docs_by_mixed_uncomparable_types(self) -> None: + """ + Test that the ranker does not crash when a group's sort_docs_by values have mutually + non-comparable present types (e.g. int and str), keeping the group's insertion order instead. + """ + docs_with_mixed_sort_values = [ + Document(content="int value", meta={"group": "g1", "split_id": 3}), + Document(content="str value", meta={"group": "g1", "split_id": "10"}), + ] + sample_ranker = MetaFieldGroupingRanker(group_by="group", sort_docs_by="split_id") + result = sample_ranker.run(documents=docs_with_mixed_sort_values) + assert "documents" in result + assert len(result["documents"]) == 2 + # Insertion order is preserved because the values cannot be compared. + assert result["documents"][0].content == "int value" + assert result["documents"][1].content == "str value" + def test_run_deduplicates_documents(self) -> None: """ Test that duplicate documents are removed before grouping.