Repository: deepset-ai/haystack

Issue:
MemoryBM25Retriever returns non relevant documents
**Describe the bug**

`MemoryBM25Retriever` currently always returns `top_k` number of `Document` even if they're not relevant.

The [`test_retriever_valid_run`](https://github.com/deepset-ai/haystack/blob/ccc9f010bbdc75e1c5f1d269a43a5f06f36acba1/test/preview/components/retrievers/test_memory_bm25_retriever.py#L119-L129) is a good example of the current behavior.

A simple fix would be to filter out `Document`s with `score==0.0` before scaling the score.

**Expected behavior**

Non relevant `Document`, those with score `0.0`, are not returned.

**Additional context**

@tstadel confirmed that it's unexpected we're returning `Document`s with a score of `0.0`.

**To Reproduce**

```
from haystack.preview.document_stores import MemoryDocumentStore
from haystack.preview.dataclasses import Document
from haystack.preview.components.retrievers import MemoryBM25Retriever

docs = [
    Document(text="Javascript is a popular programming language"),
    Document(text="Java is a popular programming language"),
    Document(text="Python is a popular programming language"),
    Document(text="Ruby is a popular programming language"),
    Document(text="PHP is a popular programming language"),
]

ds = MemoryDocumentStore()
ds.write_documents(docs)

retriever = MemoryBM25Retriever(ds, top_k=5, scale_score=False)
result = retriever.run(query="PHP")

# This is wrong
assert len(result["documents"]) == 5
assert result["documents"][0].text == "PHP is a popular programming language"
assert result["documents"][1].score == 0.0
assert result["documents"][2].score == 0.0
assert result["documents"][3].score == 0.0
assert result["documents"][4].score == 0.0

# This is the expected behaviour
assert len(result["documents"]) == 1
assert result["documents"][0].text == "PHP is a popular programming language"
assert result["documents"][0].score > 0.0
```


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


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

---



|  Totals | [![Coverage Status](https://coveralls.io/builds/64984707/badge)](https://coveralls.io/builds/64984707) |
| :-- | --: |
| Change from base [Build 7472826695](https://coveralls.io/builds/64983794): |  0.008% |
| Covered Lines: | 4217 |
| Relevant Lines: | 4843 |

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


Patch:
diff --git a/haystack/core/component/component.py b/haystack/core/component/component.py
index 4a082873..7d5eb00d 100644
--- a/haystack/core/component/component.py
+++ b/haystack/core/component/component.py
@@ -139,7 +139,10 @@ class ComponentMeta(type):
             instance.__canals_input__ = {}
         run_signature = inspect.signature(getattr(cls, "run"))
         for param in list(run_signature.parameters)[1:]:  # First is 'self' and it doesn't matter.
-            if run_signature.parameters[param].kind == inspect.Parameter.POSITIONAL_OR_KEYWORD:  # ignore `**kwargs`
+            if run_signature.parameters[param].kind not in (
+                inspect.Parameter.VAR_POSITIONAL,
+                inspect.Parameter.VAR_KEYWORD,
+            ):  # ignore variable args
                 socket_kwargs = {"name": param, "type": run_signature.parameters[param].annotation}
                 if run_signature.parameters[param].default != inspect.Parameter.empty:
                     socket_kwargs["default_value"] = run_signature.parameters[param].default
diff --git a/haystack/document_stores/in_memory/document_store.py b/haystack/document_stores/in_memory/document_store.py
index 027e9c4b..44b2f6d5 100644
--- a/haystack/document_stores/in_memory/document_store.py
+++ b/haystack/document_stores/in_memory/document_store.py
@@ -212,8 +212,11 @@ class InMemoryDocumentStore:
         return_documents = []
         for i in top_docs_positions:
             doc = all_documents[i]
+            score = docs_scores[i]
+            if score <= 0.0:
+                continue
             doc_fields = doc.to_dict()
-            doc_fields["score"] = docs_scores[i]
+            doc_fields["score"] = score
             return_document = Document.from_dict(doc_fields)
             return_documents.append(return_document)
         return return_documents
diff --git a/releasenotes/notes/component-kw-only-run-args-eedee8907232d2d4.yaml b/releasenotes/notes/component-kw-only-run-args-eedee8907232d2d4.yaml
new file mode 100644
index 00000000..68ef50fb
--- /dev/null
+++ b/releasenotes/notes/component-kw-only-run-args-eedee8907232d2d4.yaml
@@ -0,0 +1,4 @@
+---
+fixes:
+  - |
+    Fix ComponentMeta ignoring keyword-only parameters in the `run` method. ComponentMeta.__call__ handles the creation of InputSockets for the component's inputs when the latter has not explicitly called _Component.set_input_types(). This logic was not correctly handling keyword-only parameters.
diff --git a/releasenotes/notes/inmemorybm25retriever-zero-score-docs-67406062a76aa7f4.yaml b/releasenotes/notes/inmemorybm25retriever-zero-score-docs-67406062a76aa7f4.yaml
new file mode 100644
index 00000000..3ae44016
--- /dev/null
+++ b/releasenotes/notes/inmemorybm25retriever-zero-score-docs-67406062a76aa7f4.yaml
@@ -0,0 +1,3 @@
+---
+fixes:
+  - Prevent InMemoryBM25Retriever from returning documents with a score of 0.0.


Test patch:
diff --git a/test/components/retrievers/test_in_memory_bm25_retriever.py b/test/components/retrievers/test_in_memory_bm25_retriever.py
index db5e82a3..4c1df2f2 100644
--- a/test/components/retrievers/test_in_memory_bm25_retriever.py
+++ b/test/components/retrievers/test_in_memory_bm25_retriever.py
@@ -113,15 +113,14 @@ class TestMemoryBM25Retriever:
             InMemoryBM25Retriever.from_dict(data)
 
     def test_retriever_valid_run(self, mock_docs):
-        top_k = 5
         ds = InMemoryDocumentStore()
         ds.write_documents(mock_docs)
 
-        retriever = InMemoryBM25Retriever(ds, top_k=top_k)
+        retriever = InMemoryBM25Retriever(ds, top_k=5)
         result = retriever.run(query="PHP")
 
         assert "documents" in result
-        assert len(result["documents"]) == top_k
+        assert len(result["documents"]) == 1
         assert result["documents"][0].content == "PHP is a popular programming language"
 
     def test_invalid_run_wrong_store_type(self):
@@ -174,5 +173,5 @@ class TestMemoryBM25Retriever:
         assert "retriever" in result
         results_docs = result["retriever"]["documents"]
         assert results_docs
-        assert len(results_docs) == top_k
+        assert len(results_docs) == 1
         assert results_docs[0].content == query_result
diff --git a/test/core/component/test_component.py b/test/core/component/test_component.py
index cba6fc42..f2835e62 100644
--- a/test/core/component/test_component.py
+++ b/test/core/component/test_component.py
@@ -176,3 +176,17 @@ def test_input_has_default_value():
     comp = MockComponent()
     assert comp.__canals_input__["value"].default_value == 42
     assert not comp.__canals_input__["value"].is_mandatory
+
+
+def test_keyword_only_args():
+    @component
+    class MockComponent:
+        def __init__(self):
+            component.set_output_types(self, value=int)
+
+        def run(self, *, arg: int):
+            return {"value": arg}
+
+    comp = MockComponent()
+    component_inputs = {name: {"type": socket.type} for name, socket in comp.__canals_input__.items()}
+    assert component_inputs == {"arg": {"type": int}}
diff --git a/test/document_stores/test_in_memory.py b/test/document_stores/test_in_memory.py
index 1d3a3613..9ebcae41 100644
--- a/test/document_stores/test_in_memory.py
+++ b/test/document_stores/test_in_memory.py
@@ -5,8 +5,8 @@ import pandas as pd
 import pytest
 
 from haystack import Document
-from haystack.document_stores.in_memory import InMemoryDocumentStore
 from haystack.document_stores.errors import DocumentStoreError, DuplicateDocumentError
+from haystack.document_stores.in_memory import InMemoryDocumentStore
 from haystack.testing.document_store import DocumentStoreBaseTests
 
 
@@ -17,7 +17,7 @@ class TestMemoryDocumentStore(DocumentStoreBaseTests):  # pylint: disable=R0904
 
     @pytest.fixture
     def document_store(self) -> InMemoryDocumentStore:
-        return InMemoryDocumentStore()
+        return InMemoryDocumentStore(bm25_algorithm="BM25L")
 
     def test_to_dict(self):
         store = InMemoryDocumentStore()
@@ -73,7 +73,6 @@ class TestMemoryDocumentStore(DocumentStoreBaseTests):  # pylint: disable=R0904
             document_store.write_documents(docs)
 
     def test_bm25_retrieval(self, document_store: InMemoryDocumentStore):
-        document_store = InMemoryDocumentStore()
         # Tests if the bm25_retrieval method returns the correct document based on the input query.
         docs = [Document(content="Hello world"), Document(content="Haystack supports multiple languages")]
         document_store.write_documents(docs)
@@ -106,7 +105,7 @@ class TestMemoryDocumentStore(DocumentStoreBaseTests):  # pylint: disable=R0904
         document_store.write_documents(docs)
 
         # top_k = 2
-        results = document_store.bm25_retrieval(query="languages", top_k=2)
+        results = document_store.bm25_retrieval(query="language", top_k=2)
         assert len(results) == 2
 
         # top_k = 3
@@ -141,7 +140,7 @@ class TestMemoryDocumentStore(DocumentStoreBaseTests):  # pylint: disable=R0904
         document_store.write_documents(docs)
 
         results = document_store.bm25_retrieval(query="Python", top_k=1)
-        assert len(results) == 1
+        assert len(results) == 0
 
         document_store.write_documents([Document(content="Python is a popular programming language")])
         results = document_store.bm25_retrieval(query="Python", top_k=1)
@@ -199,10 +198,10 @@ class TestMemoryDocumentStore(DocumentStoreBaseTests):  # pylint: disable=R0904
         docs = [Document(), Document(content="Gardening"), Document(content="Bird watching")]
         document_store.write_documents(docs)
         results = document_store.bm25_retrieval(query="doesn't matter, top_k is 10", top_k=10)
-        assert len(results) == 2
+        assert len(results) == 0
 
     def test_bm25_retrieval_with_filters(self, document_store: InMemoryDocumentStore):
-        selected_document = Document(content="Gardening", meta={"selected": True})
+        selected_document = Document(content="Java is, well...", meta={"selected": True})
         docs = [Document(), selected_document, Document(content="Bird watching")]
         document_store.write_documents(docs)
         results = document_store.bm25_retrieval(query="Java", top_k=10, filters={"selected": True})
@@ -224,10 +223,10 @@ class TestMemoryDocumentStore(DocumentStoreBaseTests):  # pylint: disable=R0904
         assert results[0].id == document.id
 
     def test_bm25_retrieval_with_documents_with_mixed_content(self, document_store: InMemoryDocumentStore):
-        double_document = Document(content="Gardening", embedding=[1.0, 2.0, 3.0])
+        double_document = Document(content="Gardening is a hobby", embedding=[1.0, 2.0, 3.0])
         docs = [Document(embedding=[1.0, 2.0, 3.0]), double_document, Document(content="Bird watching")]
         document_store.write_documents(docs)
-        results = document_store.bm25_retrieval(query="Java", top_k=10, filters={"embedding": {"$not": None}})
+        results = document_store.bm25_retrieval(query="Gardening", top_k=10, filters={"embedding": {"$not": None}})
         assert len(results) == 1
         assert results[0].id == double_document.id