Repository: deepset-ai/haystack

Issue:
`JoinDocuments` now fails if any document is without a score
**Describe the bug**
Since the latest release (more specifically this commit: https://github.com/deepset-ai/haystack/commit/32e87d37c153cc8e8c5de5c72e5f939b2efd1022) the `JoinDocument` node will error out if any of the documents joined has not score, even if the `JoinDocuments` is used with `sort_by_score=False`

This break the documented functionality of the `sort_by_score` param, which allows (well, allowed before the above commit) the use of JoinDocuments with documents without scores:
https://github.com/deepset-ai/haystack/blob/main/haystack/nodes/other/join_docs.py#L40

Also this likely means that there is no unit test to verify that `sort_by_score=False` actually works, otherwise that should have caught this bug when it got introduced.

**Error message**
The error you will be seeing if you have some documents without scores even if you have `sort_by_score=False` set :
```
  File ".../lib/python3.10/site-packages/haystack/nodes/base.py", line 317, in _adispatch_run_general
    output, stream = await run_method(**run_inputs, **run_params)
                           │            │             └ {}
                           │            └ {'inputs': [{'documents': [<Document: {'content': "Zoltan Fedor is the Director, AI Platform ...
                           └ <bound method JoinNode.run of <haystack.nodes.other.join_docs.JoinDocuments object at 0x7f7beae0df30>>
  File ".../lib/python3.10/site-packages/haystack/nodes/other/join.py", line 24, in run
    results = self.run_accumulated(inputs, top_k_join=top_k_join)
              │    │               │                  └ None
              │    │               └ [{'documents': [<Document: {'content': "Zoltan Fedor is the Director, AI Platform ...
              │    └ <function JoinDocuments.run_accumulated at 0x7f7af5d160e0>
              └ <haystack.nodes.other.join_docs.JoinDocuments object at 0x7f7beae0df30>
  File ".../lib/python3.10/site-packages/haystack/nodes/other/join_docs.py", line 66, in run_accumulated
    scores_map = self._concatenate_results(results, document_map)
                 │    │                    │        └ {'8a51d965-082b-055f-f486-a372fc7254e6': <Document: {'content': "Zoltan Fedor is the Director, AI Platform ...
                 │    │                    └ [[<Document: {'content': "Zoltan Fedor is the Director, AI Platform ...
                 │    └ <function JoinDocuments._concatenate_results at 0x7f7af5d164d0>
                 └ <haystack.nodes.other.join_docs.JoinDocuments object at 0x7f7beae0df30>
  File ".../lib/python3.10/site-packages/haystack/nodes/other/join_docs.py", line 133, in _concatenate_results
    item_best_score = max(tmp, key=lambda x: x.score)
                          └ [<Document: {'content': "Zoltan Fedor is the Director, AI Platform ...

TypeError: '>' not supported between instances of 'NoneType' and 'float'
```

**Expected behavior**
The document functionality of `sort_by_score=False` of `JoinDocuments` should not get broken by new functionality being introduced.

**Additional context**
As mentioned above this bug could have not been introduced it there would have been a unit test for the `sort_by_score=False` functionality - so I would recommend adding one to avoid this happening again in the future.

**To Reproduce**
Create a document set with some documents with no score and use `JoinDocuments` on them.

**FAQ Check**
- [ X ] Have you had a look at [our new FAQ page](https://docs.haystack.deepset.ai/docs/faq)?

**System:**
 - OS: all
 - GPU/CPU: all
 - Haystack version (commit or version number): 1.22.0
 - DocumentStore: Any
 - Reader: Any
 - Retriever: Any that produces some documents with no score


Discussion:
coveralls: ## Pull Request Test Coverage Report for [Build 6811802313](https://coveralls.io/builds/63856478)


* **0** of **0**   changed or added relevant lines in **0** files are covered.
* No unchanged relevant lines lost coverage.
* Overall coverage increased (+**0.004%**) to **40.328%**

---



|  Totals | [![Coverage Status](https://coveralls.io/builds/63856478/badge)](https://coveralls.io/builds/63856478) |
| :-- | --: |
| Change from base [Build 6811076012](https://coveralls.io/builds/63854758): |  0.004% |
| Covered Lines: | 10499 |
| Relevant Lines: | 26034 |

---
##### 💛  - [Coveralls](https://coveralls.io)


Patch:
diff --git a/haystack/nodes/other/join_docs.py b/haystack/nodes/other/join_docs.py
index 7ce0de81..274e90a3 100644
--- a/haystack/nodes/other/join_docs.py
+++ b/haystack/nodes/other/join_docs.py
@@ -14,8 +14,8 @@ class JoinDocuments(JoinNode):
     A node to join documents outputted by multiple retriever nodes.
 
     The node allows multiple join modes:
-    * concatenate: combine the documents from multiple nodes. Any duplicate documents are discarded.
-                   The score is only determined by the last node that outputs the document.
+    * concatenate: combine the documents from multiple nodes.
+                   In case of duplicate documents, the one with the highest score is kept.
     * merge: merge scores of documents from multiple nodes. Optionally, each input score can be given a different
              `weight` & a `top_k` limit can be set. This mode can also be used for "reranking" retrieved documents.
     * reciprocal_rank_fusion: combines the documents based on their rank in multiple nodes.
@@ -130,7 +130,7 @@ class JoinDocuments(JoinNode):
                 for doc in result:
                     if doc.id == idx:
                         tmp.append(doc)
-            item_best_score = max(tmp, key=lambda x: x.score)
+            item_best_score = max(tmp, key=lambda x: x.score if x.score is not None else -inf)
             scores_map.update({idx: item_best_score.score})
         return scores_map
 
diff --git a/releasenotes/notes/fix-join-docs-null-score-746c392a87adffcc.yaml b/releasenotes/notes/fix-join-docs-null-score-746c392a87adffcc.yaml
new file mode 100644
index 00000000..33d2e931
--- /dev/null
+++ b/releasenotes/notes/fix-join-docs-null-score-746c392a87adffcc.yaml
@@ -0,0 +1,7 @@
+---
+fixes:
+  - |
+    When using `JoinDocuments` with `join_mode=concatenate` (default) and
+    passing duplicate documents, including some with a null score, this
+    node raised an exception.
+    Now this case is handled correctly and the documents are joined as expected.


Test patch:
diff --git a/test/nodes/test_join_documents.py b/test/nodes/test_join_documents.py
index aa303e26..463aeaa5 100644
--- a/test/nodes/test_join_documents.py
+++ b/test/nodes/test_join_documents.py
@@ -78,3 +78,38 @@ def test_joindocuments_concatenate_keep_only_highest_ranking_duplicate():
     result, _ = join_docs.run(inputs)
     assert len(result["documents"]) == 2
     assert result["documents"] == expected_outputs["documents"]
+
+
+@pytest.mark.unit
+def test_joindocuments_concatenate_duplicate_docs_null_score():
+    """
+    Test that the concatenate method correctly handles duplicate documents,
+    when one has a null score.
+    """
+    inputs = [
+        {
+            "documents": [
+                Document(content="text document 1", content_type="text", score=0.2),
+                Document(content="text document 2", content_type="text", score=0.3),
+                Document(content="text document 3", content_type="text", score=None),
+            ]
+        },
+        {
+            "documents": [
+                Document(content="text document 2", content_type="text", score=0.7),
+                Document(content="text document 1", content_type="text", score=None),
+            ]
+        },
+    ]
+    expected_outputs = {
+        "documents": [
+            Document(content="text document 2", content_type="text", score=0.7),
+            Document(content="text document 1", content_type="text", score=0.2),
+            Document(content="text document 3", content_type="text", score=None),
+        ]
+    }
+
+    join_docs = JoinDocuments(join_mode="concatenate")
+    result, _ = join_docs.run(inputs)
+    assert len(result["documents"]) == 3
+    assert result["documents"] == expected_outputs["documents"]