Repository: deepset-ai/haystack

Issue:
BaseRetriever checks `query` is passed using `not query` instead of `query is not None`
I am using the [FilterRetriever](https://github.com/deepset-ai/haystack/blob/9582a423a2f4826a4a15529b72fea45da611b81c/haystack/nodes/retriever/sparse.py#L344) node which doesn't use the query that the user has to pass at all, just the filters to get filtered documents from the document store.

So I was following the advice in the [docstring of the class](https://github.com/deepset-ai/haystack/blob/9582a423a2f4826a4a15529b72fea45da611b81c/haystack/nodes/retriever/sparse.py#L363) where it says: `:param query: Has no effect, can pass in empty string`.

So I passed an empty string and I got the following error:

```shell
Traceback (most recent call last):
  File "test.py", line 44, in <module>
    docs = retriever.run(
  File ".../python3.8/site-packages/haystack/nodes/retriever/base.py", line 274, in run
    raise HaystackError(
haystack.errors.HaystackError: Must provide a 'query' parameter for retrievers in pipelines where Query is the root node.
``` 

This error happens because, in the [BaseRetriever](https://github.com/deepset-ai/haystack/blob/9582a423a2f4826a4a15529b72fea45da611b81c/haystack/nodes/retriever/base.py#L49) class, the parameter `query` is checked with the following statement:

https://github.com/deepset-ai/haystack/blob/9582a423a2f4826a4a15529b72fea45da611b81c/haystack/nodes/retriever/base.py#L271

That is a bad way of checking if a parameter is passed because **that statement will also be true for an empty string**. The default value is None, so if the parameter is not passed then it should be checked with:

```python
if query is None:
```

As a **workaround**, a string with a space in it could be added:

```python
query = " "
```

### This happens in other classes with other variables too (as far as I know), so I would strongly recommend checking other classes as well


Discussion:
Hi @ugm2 thanks for bringing this to our attention! Would you be willing to open a PR for this issue?
@sjrl yes, of course! I will create one as soon as possible

Resolving patch:
diff --git a/haystack/nodes/retriever/base.py b/haystack/nodes/retriever/base.py
--- a/haystack/nodes/retriever/base.py
+++ b/haystack/nodes/retriever/base.py
@@ -268,10 +268,15 @@ def run(  # type: ignore
         scale_score: bool = None,
     ):
         if root_node == "Query":
-            if not query:
+            if query is None:
                 raise HaystackError(
                     "Must provide a 'query' parameter for retrievers in pipelines where Query is the root node."
                 )
+            if not isinstance(query, str):
+                logger.error(
+                    "The retriever received an unusual query: '%s' This query is likely to produce garbage output.",
+                    query,
+                )
             self.query_count += 1
             run_query_timed = self.timing(self.run_query, "query_time")
             output, stream = run_query_timed(
@@ -296,10 +301,15 @@ def run_batch(  # type: ignore
         headers: Optional[Dict[str, str]] = None,
     ):
         if root_node == "Query":
-            if not queries:
+            if queries is None:
                 raise HaystackError(
                     "Must provide a 'queries' parameter for retrievers in pipelines where Query is the root node."
                 )
+            if not all(isinstance(query, str) for query in queries):
+                logger.error(
+                    "The retriever received an unusual list of queries: '%s' Some of these queries are likely to produce garbage output.",
+                    queries,
+                )
             self.query_count += len(queries) if isinstance(queries, list) else 1
             run_query_batch_timed = self.timing(self.run_query_batch, "query_time")
             output, stream = run_query_batch_timed(
diff --git a/haystack/pipelines/base.py b/haystack/pipelines/base.py
--- a/haystack/pipelines/base.py
+++ b/haystack/pipelines/base.py
@@ -484,7 +484,7 @@ def run(  # type: ignore
         queue: Dict[str, Any] = {
             root_node: {"root_node": root_node, "params": params}
         }  # ordered dict with "node_id" -> "input" mapping that acts as a FIFO queue
-        if query:
+        if query is not None:
             queue[root_node]["query"] = query
         if file_paths:
             queue[root_node]["file_paths"] = file_paths


Test patch:
diff --git a/test/nodes/test_retriever.py b/test/nodes/test_retriever.py
--- a/test/nodes/test_retriever.py
+++ b/test/nodes/test_retriever.py
@@ -1,6 +1,7 @@
 import logging
 import os
 from math import isclose
+from typing import Dict, List, Optional, Union
 
 import numpy as np
 import pandas as pd
@@ -20,7 +21,7 @@
 from haystack.nodes.retriever.sparse import BM25Retriever, FilterRetriever, TfidfRetriever
 from transformers import DPRContextEncoderTokenizerFast, DPRQuestionEncoderTokenizerFast
 
-from ..conftest import SAMPLES_PATH
+from ..conftest import SAMPLES_PATH, MockRetriever
 
 
 # TODO check if we this works with only "memory" arg
@@ -83,6 +84,46 @@ def test_retrieval(retriever_with_docs: BaseRetriever, document_store_with_docs:
         assert len(result) == 0
 
 
+class MockBaseRetriever(MockRetriever):
+    def __init__(self, document_store: BaseDocumentStore, mock_document: Document):
+        self.document_store = document_store
+        self.mock_document = mock_document
+
+    def retrieve(
+        self,
+        query: str,
+        filters: dict,
+        top_k: Optional[int],
+        index: str,
+        headers: Optional[Dict[str, str]],
+        scale_score: bool,
+    ):
+        return [self.mock_document]
+
+    def retrieve_batch(
+        self,
+        queries: List[str],
+        filters: Optional[Dict[str, Union[Dict, List, str, int, float, bool]]] = None,
+        top_k: Optional[int] = None,
+        index: str = None,
+        headers: Optional[Dict[str, str]] = None,
+        batch_size: Optional[int] = None,
+        scale_score: bool = None,
+    ):
+        return [[self.mock_document] for _ in range(len(queries))]
+
+
+def test_retrieval_empty_query(document_store: BaseDocumentStore):
+    # test with empty query using the run() method
+    mock_document = Document(id="0", content="test")
+    retriever = MockBaseRetriever(document_store=document_store, mock_document=mock_document)
+    result = retriever.run(root_node="Query", query="", filters={})
+    assert result[0]["documents"][0] == mock_document
+
+    result = retriever.run_batch(root_node="Query", queries=[""], filters={})
+    assert result[0]["documents"][0][0] == mock_document
+
+
 def test_batch_retrieval_single_query(retriever_with_docs, document_store_with_docs):
     if not isinstance(retriever_with_docs, (BM25Retriever, FilterRetriever, TfidfRetriever)):
         document_store_with_docs.update_embeddings(retriever_with_docs)