We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Controller search_documents function should accept multiple filter values for a single key
Using the rest api I would like to make a request with multiple values for a filter key in the HTTP body to use them as filters on the retrieved results from my document store. The following two lines below in the Question class and search_documents function respectively doesn't seem to handle this case.

https://github.com/deepset-ai/haystack/blob/2b352d6ac4af9fe129d8e8798deb6712d4c044b5/rest_api/controller/request.py#L13
https://github.com/deepset-ai/haystack/blob/2b352d6ac4af9fe129d8e8798deb6712d4c044b5/rest_api/controller/search.py#L181

The **first issue** in the filters property in the [Question class](https://github.com/deepset-ai/haystack/blob/2b352d6ac4af9fe129d8e8798deb6712d4c044b5/rest_api/controller/request.py#L11) restricts the type to be a dictionary of key string and value string or null.  In the case where the HTTP body has a list of values for a key filter name then it fails:

**HTTP Body:**
`{
  "questions": [
    "is this a question?"
  ],
  "filters": {"key-1":["a","b"]},
  "top_k_reader": 6,
  "top_k_retriever": 6
}`
**Response:**
`{
    "detail": [
        {
            "loc": [
                "body",
                "filters",
                "key-1"
            ],
            "msg": "str type expected",
            "type": "type_error.str"
        }
    ]
}`

The **second issue** in the [search_documents](https://github.com/deepset-ai/haystack/blob/2b352d6ac4af9fe129d8e8798deb6712d4c044b5/rest_api/controller/search.py#L176) function is that regardless of filters being type str or List, each key of filters in line 181 needs to have a one dimensional list value.

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `rest_api/controller/search.py`
Content:
```
1 import json
2 import logging
3 import time
4 from datetime import datetime
5 from typing import Any, Dict, List, Optional
6 
7 import elasticapm
8 from fastapi import APIRouter
9 from fastapi import HTTPException
10 
11 from haystack import Finder
12 from rest_api.config import DB_HOST, DB_PORT, DB_USER, DB_PW, DB_INDEX, DEFAULT_TOP_K_READER, ES_CONN_SCHEME, \
13     TEXT_FIELD_NAME, SEARCH_FIELD_NAME, EMBEDDING_DIM, EMBEDDING_FIELD_NAME, EXCLUDE_META_DATA_FIELDS, \
14     RETRIEVER_TYPE, EMBEDDING_MODEL_PATH, USE_GPU, READER_MODEL_PATH, BATCHSIZE, CONTEXT_WINDOW_SIZE, \
15     TOP_K_PER_CANDIDATE, NO_ANS_BOOST, MAX_PROCESSES, MAX_SEQ_LEN, DOC_STRIDE, CONCURRENT_REQUEST_PER_WORKER, \
16     FAQ_QUESTION_FIELD_NAME, EMBEDDING_MODEL_FORMAT, READER_TYPE, READER_TOKENIZER, GPU_NUMBER, NAME_FIELD_NAME, \
17     VECTOR_SIMILARITY_METRIC, CREATE_INDEX, LOG_LEVEL
18 
19 from rest_api.controller.request import Question
20 from rest_api.controller.response import Answers, AnswersToIndividualQuestion
21 
22 from rest_api.controller.utils import RequestLimiter
23 from haystack.document_store.elasticsearch import ElasticsearchDocumentStore
24 from haystack.reader.base import BaseReader
25 from haystack.reader.farm import FARMReader
26 from haystack.reader.transformers import TransformersReader
27 from haystack.retriever.base import BaseRetriever
28 from haystack.retriever.sparse import ElasticsearchRetriever, ElasticsearchFilterOnlyRetriever
29 from haystack.retriever.dense import EmbeddingRetriever
30 
31 logger = logging.getLogger('haystack')
32 logger.setLevel(LOG_LEVEL)
33 
34 router = APIRouter()
35 
36 # Init global components: DocumentStore, Retriever, Reader, Finder
37 document_store = ElasticsearchDocumentStore(
38     host=DB_HOST,
39     port=DB_PORT,
40     username=DB_USER,
41     password=DB_PW,
42     index=DB_INDEX,
43     scheme=ES_CONN_SCHEME,
44     ca_certs=False,
45     verify_certs=False,
46     text_field=TEXT_FIELD_NAME,
47     name_field=NAME_FIELD_NAME,
48     search_fields=SEARCH_FIELD_NAME,
49     embedding_dim=EMBEDDING_DIM,
50     embedding_field=EMBEDDING_FIELD_NAME,
51     excluded_meta_data=EXCLUDE_META_DATA_FIELDS,  # type: ignore
52     faq_question_field=FAQ_QUESTION_FIELD_NAME,
53     create_index=CREATE_INDEX,
54     similarity=VECTOR_SIMILARITY_METRIC
55 )
56 
57 if RETRIEVER_TYPE == "EmbeddingRetriever":
58     retriever = EmbeddingRetriever(
59         document_store=document_store,
60         embedding_model=EMBEDDING_MODEL_PATH,
61         model_format=EMBEDDING_MODEL_FORMAT,
62         use_gpu=USE_GPU
63     )  # type: BaseRetriever
64 elif RETRIEVER_TYPE == "ElasticsearchRetriever":
65     retriever = ElasticsearchRetriever(document_store=document_store)
66 elif RETRIEVER_TYPE is None or RETRIEVER_TYPE == "ElasticsearchFilterOnlyRetriever":
67     retriever = ElasticsearchFilterOnlyRetriever(document_store=document_store)
68 else:
69     raise ValueError(f"Could not load Retriever of type '{RETRIEVER_TYPE}'. "
70                      f"Please adjust RETRIEVER_TYPE to one of: "
71                      f"'EmbeddingRetriever', 'ElasticsearchRetriever', 'ElasticsearchFilterOnlyRetriever', None"
72                      f"OR modify rest_api/search.py to support your retriever"
73                      )
74 
75 if READER_MODEL_PATH:  # for extractive doc-qa
76     if READER_TYPE == "TransformersReader":
77         use_gpu = -1 if not USE_GPU else GPU_NUMBER
78         reader = TransformersReader(
79             model_name_or_path=str(READER_MODEL_PATH),
80             use_gpu=use_gpu,
81             context_window_size=CONTEXT_WINDOW_SIZE,
82             tokenizer=str(READER_TOKENIZER)
83         )  # type: Optional[BaseReader]
84     elif READER_TYPE == "FARMReader":
85         reader = FARMReader(
86             model_name_or_path=str(READER_MODEL_PATH),
87             batch_size=BATCHSIZE,
88             use_gpu=USE_GPU,
89             context_window_size=CONTEXT_WINDOW_SIZE,
90             top_k_per_candidate=TOP_K_PER_CANDIDATE,
91             no_ans_boost=NO_ANS_BOOST,
92             num_processes=MAX_PROCESSES,
93             max_seq_len=MAX_SEQ_LEN,
94             doc_stride=DOC_STRIDE,
95         )  # type: Optional[BaseReader]
96     else:
97         raise ValueError(f"Could not load Reader of type '{READER_TYPE}'. "
98                          f"Please adjust READER_TYPE to one of: "
99                          f"'FARMReader', 'TransformersReader', None"
100                          )
101 else:
102     reader = None  # don't need one for pure FAQ matching
103 
104 FINDERS = {1: Finder(reader=reader, retriever=retriever)}
105 
106 
107 #############################################
108 # Endpoints
109 #############################################
110 doc_qa_limiter = RequestLimiter(CONCURRENT_REQUEST_PER_WORKER)
111 
112 
113 @router.post("/models/{model_id}/doc-qa", response_model=Answers, response_model_exclude_unset=True)
114 def doc_qa(model_id: int, question_request: Question):
115     with doc_qa_limiter.run():
116         start_time = time.time()
117         finder = FINDERS.get(model_id, None)
118         if not finder:
119             raise HTTPException(
120                 status_code=404, detail=f"Couldn't get Finder with ID {model_id}. Available IDs: {list(FINDERS.keys())}"
121             )
122 
123         results = search_documents(finder, question_request, start_time)
124 
125         return {"results": results}
126 
127 
128 @router.post("/models/{model_id}/faq-qa", response_model=Answers, response_model_exclude_unset=True)
129 def faq_qa(model_id: int, request: Question):
130     finder = FINDERS.get(model_id, None)
131     if not finder:
132         raise HTTPException(
133             status_code=404, detail=f"Couldn't get Finder with ID {model_id}. Available IDs: {list(FINDERS.keys())}"
134         )
135 
136     results = []
137     for question in request.questions:
138         if request.filters:
139             # put filter values into a list and remove filters with null value
140             filters = {key: [value] for key, value in request.filters.items() if value is not None}
141             logger.info(f" [{datetime.now()}] Request: {request}")
142         else:
143             filters = {}
144 
145         result = finder.get_answers_via_similar_questions(
146             question=question, top_k_retriever=request.top_k_retriever, filters=filters,
147         )
148         results.append(result)
149 
150     elasticapm.set_custom_context({"results": results})
151     logger.info(json.dumps({"request": request.dict(), "results": results}))
152 
153     return {"results": results}
154 
155 
156 @router.post("/models/{model_id}/query", response_model=Dict[str, Any], response_model_exclude_unset=True)
157 def query(model_id: int, query_request: Dict[str, Any], top_k_reader: int = DEFAULT_TOP_K_READER):
158     with doc_qa_limiter.run():
159         start_time = time.time()
160         finder = FINDERS.get(model_id, None)
161         if not finder:
162             raise HTTPException(
163                 status_code=404, detail=f"Couldn't get Finder with ID {model_id}. Available IDs: {list(FINDERS.keys())}"
164             )
165 
166         question_request = Question.from_elastic_query_dsl(query_request, top_k_reader)
167 
168         answers = search_documents(finder, question_request, start_time)
169         response: Dict[str, Any] = {}
170         if answers and len(answers) > 0:
171             response = AnswersToIndividualQuestion.to_elastic_response_dsl(dict(answers[0]))
172 
173         return response
174 
175 
176 def search_documents(finder, question_request, start_time) -> List[AnswersToIndividualQuestion]:
177     results = []
178     for question in question_request.questions:
179         if question_request.filters:
180             # put filter values into a list and remove filters with null value
181             filters = {key: [value] for key, value in question_request.filters.items() if value is not None}
182             logger.info(f" [{datetime.now()}] Request: {question_request}")
183         else:
184             filters = {}
185 
186         result = finder.get_answers(
187             question=question,
188             top_k_retriever=question_request.top_k_retriever,
189             top_k_reader=question_request.top_k_reader,
190             filters=filters,
191         )
192         results.append(result)
193     elasticapm.set_custom_context({"results": results})
194     end_time = time.time()
195     logger.info(
196         json.dumps({"request": question_request.dict(), "results": results,
197                     "time": f"{(end_time - start_time):.2f}"}))
198     return results
199
```

Path: `rest_api/controller/request.py`
Content:
```
1 import sys
2 from typing import Any, Collection, Dict, List, Optional
3 
4 from pydantic import BaseModel
5 
6 from rest_api.config import DEFAULT_TOP_K_READER, DEFAULT_TOP_K_RETRIEVER
7 
8 MAX_RECURSION_DEPTH = sys.getrecursionlimit() - 1
9 
10 
11 class Question(BaseModel):
12     questions: List[str]
13     filters: Optional[Dict[str, Optional[str]]] = None
14     top_k_reader: int = DEFAULT_TOP_K_READER
15     top_k_retriever: int = DEFAULT_TOP_K_RETRIEVER
16 
17     @classmethod
18     def from_elastic_query_dsl(cls, query_request: Dict[str, Any], top_k_reader: int = DEFAULT_TOP_K_READER):
19 
20         # Refer Query DSL
21         # https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-bool-query.html
22         # Currently do not support query matching with field parameter
23         query_strings: List[str] = []
24         filters: Dict[str, str] = {}
25         top_k_retriever: int = DEFAULT_TOP_K_RETRIEVER if "size" not in query_request else query_request["size"]
26 
27         cls._iterate_dsl_request(query_request, query_strings, filters)
28 
29         if len(query_strings) != 1:
30             raise SyntaxError('Only one valid `query` field required expected, '
31                               'refer https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-query.html')
32 
33         return cls(questions=query_strings, filters=filters if len(filters) else None, top_k_retriever=top_k_retriever,
34                    top_k_reader=top_k_reader)
35 
36     @classmethod
37     def _iterate_dsl_request(cls, query_dsl: Any, query_strings: List[str], filters: Dict[str, str], depth: int = 0):
38         if depth == MAX_RECURSION_DEPTH:
39             raise RecursionError('Parsing incoming DSL reaching current value of the recursion limit')
40 
41         # For question: Only consider values of "query" key for "match" and "multi_match" request.
42         # For filter: Only consider Dict[str, str] value of "term" or "terms" key
43         if isinstance(query_dsl, List):
44             for item in query_dsl:
45                 cls._iterate_dsl_request(item, query_strings, filters, depth + 1)
46         elif isinstance(query_dsl, Dict):
47             for key, value in query_dsl.items():
48                 # "query" value should be "str" type
49                 if key == 'query' and isinstance(value, str):
50                     query_strings.append(value)
51                 elif key in ["filter", "filters"]:
52                     cls._iterate_filters(value, filters, depth + 1)
53                 elif isinstance(value, Collection):
54                     cls._iterate_dsl_request(value, query_strings, filters, depth + 1)
55 
56     @classmethod
57     def _iterate_filters(cls, filter_dsl: Any, filters: Dict[str, str], depth: int = 0):
58         if depth == MAX_RECURSION_DEPTH:
59             raise RecursionError('Parsing incoming DSL reaching current value of the recursion limit')
60 
61         if isinstance(filter_dsl, List):
62             for item in filter_dsl:
63                 cls._iterate_filters(item, filters, depth + 1)
64         elif isinstance(filter_dsl, Dict):
65             for key, value in filter_dsl.items():
66                 if key in ["term", "terms"]:
67                     if isinstance(value, Dict):
68                         for filter_key, filter_value in value.items():
69                             # Currently only accepting Dict[str, str]
70                             if isinstance(filter_value, str):
71                                 filters[filter_key] = filter_value
72                 elif isinstance(value, Collection):
73                     cls._iterate_filters(value, filters, depth + 1)
74
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/rest_api/controller/request.py b/rest_api/controller/request.py
--- a/rest_api/controller/request.py
+++ b/rest_api/controller/request.py
@@ -1,5 +1,5 @@
 import sys
-from typing import Any, Collection, Dict, List, Optional
+from typing import Any, Collection, Dict, List, Optional, Union
 
 from pydantic import BaseModel
 
@@ -10,7 +10,7 @@
 
 class Question(BaseModel):
     questions: List[str]
-    filters: Optional[Dict[str, Optional[str]]] = None
+    filters: Optional[Dict[str, Optional[Union[str, List[str]]]]] = None
     top_k_reader: int = DEFAULT_TOP_K_READER
     top_k_retriever: int = DEFAULT_TOP_K_RETRIEVER
 
diff --git a/rest_api/controller/search.py b/rest_api/controller/search.py
--- a/rest_api/controller/search.py
+++ b/rest_api/controller/search.py
@@ -117,7 +117,7 @@
         finder = FINDERS.get(model_id, None)
         if not finder:
             raise HTTPException(
-                status_code=404, detail=f"Couldn't get Finder with ID {model_id}. Available IDs: {list(FINDERS.keys())}"
+                status_code=404, detail=f"Could not get Finder with ID {model_id}. Available IDs: {list(FINDERS.keys())}"
             )
 
         results = search_documents(finder, question_request, start_time)
@@ -130,14 +130,20 @@
     finder = FINDERS.get(model_id, None)
     if not finder:
         raise HTTPException(
-            status_code=404, detail=f"Couldn't get Finder with ID {model_id}. Available IDs: {list(FINDERS.keys())}"
+            status_code=404, detail=f"Could not get Finder with ID {model_id}. Available IDs: {list(FINDERS.keys())}"
         )
 
     results = []
     for question in request.questions:
         if request.filters:
             # put filter values into a list and remove filters with null value
-            filters = {key: [value] for key, value in request.filters.items() if value is not None}
+            filters = {}
+            for key, values in request.filters.items():
+                if values is None:
+                    continue
+                if not isinstance(values, list):
+                    values = [values]
+                filters[key] = values
             logger.info(f" [{datetime.now()}] Request: {request}")
         else:
             filters = {}
@@ -160,7 +166,7 @@
         finder = FINDERS.get(model_id, None)
         if not finder:
             raise HTTPException(
-                status_code=404, detail=f"Couldn't get Finder with ID {model_id}. Available IDs: {list(FINDERS.keys())}"
+                status_code=404, detail=f"Could not get Finder with ID {model_id}. Available IDs: {list(FINDERS.keys())}"
             )
 
         question_request = Question.from_elastic_query_dsl(query_request, top_k_reader)
@@ -178,7 +184,13 @@
     for question in question_request.questions:
         if question_request.filters:
             # put filter values into a list and remove filters with null value
-            filters = {key: [value] for key, value in question_request.filters.items() if value is not None}
+            filters = {}
+            for key, values in question_request.filters.items():
+                if values is None:
+                    continue
+                if not isinstance(values, list):
+                    values = [values]
+                filters[key] = values
             logger.info(f" [{datetime.now()}] Request: {question_request}")
         else:
             filters = {}

{"golden_diff": "diff --git a/rest_api/controller/request.py b/rest_api/controller/request.py\n--- a/rest_api/controller/request.py\n+++ b/rest_api/controller/request.py\n@@ -1,5 +1,5 @@\n import sys\n-from typing import Any, Collection, Dict, List, Optional\n+from typing import Any, Collection, Dict, List, Optional, Union\n \n from pydantic import BaseModel\n \n@@ -10,7 +10,7 @@\n \n class Question(BaseModel):\n     questions: List[str]\n-    filters: Optional[Dict[str, Optional[str]]] = None\n+    filters: Optional[Dict[str, Optional[Union[str, List[str]]]]] = None\n     top_k_reader: int = DEFAULT_TOP_K_READER\n     top_k_retriever: int = DEFAULT_TOP_K_RETRIEVER\n \ndiff --git a/rest_api/controller/search.py b/rest_api/controller/search.py\n--- a/rest_api/controller/search.py\n+++ b/rest_api/controller/search.py\n@@ -117,7 +117,7 @@\n         finder = FINDERS.get(model_id, None)\n         if not finder:\n             raise HTTPException(\n-                status_code=404, detail=f\"Couldn't get Finder with ID {model_id}. Available IDs: {list(FINDERS.keys())}\"\n+                status_code=404, detail=f\"Could not get Finder with ID {model_id}. Available IDs: {list(FINDERS.keys())}\"\n             )\n \n         results = search_documents(finder, question_request, start_time)\n@@ -130,14 +130,20 @@\n     finder = FINDERS.get(model_id, None)\n     if not finder:\n         raise HTTPException(\n-            status_code=404, detail=f\"Couldn't get Finder with ID {model_id}. Available IDs: {list(FINDERS.keys())}\"\n+            status_code=404, detail=f\"Could not get Finder with ID {model_id}. Available IDs: {list(FINDERS.keys())}\"\n         )\n \n     results = []\n     for question in request.questions:\n         if request.filters:\n             # put filter values into a list and remove filters with null value\n-            filters = {key: [value] for key, value in request.filters.items() if value is not None}\n+            filters = {}\n+            for key, values in request.filters.items():\n+                if values is None:\n+                    continue\n+                if not isinstance(values, list):\n+                    values = [values]\n+                filters[key] = values\n             logger.info(f\" [{datetime.now()}] Request: {request}\")\n         else:\n             filters = {}\n@@ -160,7 +166,7 @@\n         finder = FINDERS.get(model_id, None)\n         if not finder:\n             raise HTTPException(\n-                status_code=404, detail=f\"Couldn't get Finder with ID {model_id}. Available IDs: {list(FINDERS.keys())}\"\n+                status_code=404, detail=f\"Could not get Finder with ID {model_id}. Available IDs: {list(FINDERS.keys())}\"\n             )\n \n         question_request = Question.from_elastic_query_dsl(query_request, top_k_reader)\n@@ -178,7 +184,13 @@\n     for question in question_request.questions:\n         if question_request.filters:\n             # put filter values into a list and remove filters with null value\n-            filters = {key: [value] for key, value in question_request.filters.items() if value is not None}\n+            filters = {}\n+            for key, values in question_request.filters.items():\n+                if values is None:\n+                    continue\n+                if not isinstance(values, list):\n+                    values = [values]\n+                filters[key] = values\n             logger.info(f\" [{datetime.now()}] Request: {question_request}\")\n         else:\n             filters = {}\n", "issue": "Controller search_documents function should accept multiple filter values for a single key\nUsing the rest api I would like to make a request with multiple values for a filter key in the HTTP body to use them as filters on the retrieved results from my document store. The following two lines below in the Question class and search_documents function respectively doesn't seem to handle this case.\r\n\r\nhttps://github.com/deepset-ai/haystack/blob/2b352d6ac4af9fe129d8e8798deb6712d4c044b5/rest_api/controller/request.py#L13\r\nhttps://github.com/deepset-ai/haystack/blob/2b352d6ac4af9fe129d8e8798deb6712d4c044b5/rest_api/controller/search.py#L181\r\n\r\nThe **first issue** in the filters property in the [Question class](https://github.com/deepset-ai/haystack/blob/2b352d6ac4af9fe129d8e8798deb6712d4c044b5/rest_api/controller/request.py#L11) restricts the type to be a dictionary of key string and value string or null.  In the case where the HTTP body has a list of values for a key filter name then it fails:\r\n\r\n**HTTP Body:**\r\n`{\r\n  \"questions\": [\r\n    \"is this a question?\"\r\n  ],\r\n  \"filters\": {\"key-1\":[\"a\",\"b\"]},\r\n  \"top_k_reader\": 6,\r\n  \"top_k_retriever\": 6\r\n}`\r\n**Response:**\r\n`{\r\n    \"detail\": [\r\n        {\r\n            \"loc\": [\r\n                \"body\",\r\n                \"filters\",\r\n                \"key-1\"\r\n            ],\r\n            \"msg\": \"str type expected\",\r\n            \"type\": \"type_error.str\"\r\n        }\r\n    ]\r\n}`\r\n\r\nThe **second issue** in the [search_documents](https://github.com/deepset-ai/haystack/blob/2b352d6ac4af9fe129d8e8798deb6712d4c044b5/rest_api/controller/search.py#L176) function is that regardless of filters being type str or List, each key of filters in line 181 needs to have a one dimensional list value.\n", "before_files": [{"content": "import json\nimport logging\nimport time\nfrom datetime import datetime\nfrom typing import Any, Dict, List, Optional\n\nimport elasticapm\nfrom fastapi import APIRouter\nfrom fastapi import HTTPException\n\nfrom haystack import Finder\nfrom rest_api.config import DB_HOST, DB_PORT, DB_USER, DB_PW, DB_INDEX, DEFAULT_TOP_K_READER, ES_CONN_SCHEME, \\\n    TEXT_FIELD_NAME, SEARCH_FIELD_NAME, EMBEDDING_DIM, EMBEDDING_FIELD_NAME, EXCLUDE_META_DATA_FIELDS, \\\n    RETRIEVER_TYPE, EMBEDDING_MODEL_PATH, USE_GPU, READER_MODEL_PATH, BATCHSIZE, CONTEXT_WINDOW_SIZE, \\\n    TOP_K_PER_CANDIDATE, NO_ANS_BOOST, MAX_PROCESSES, MAX_SEQ_LEN, DOC_STRIDE, CONCURRENT_REQUEST_PER_WORKER, \\\n    FAQ_QUESTION_FIELD_NAME, EMBEDDING_MODEL_FORMAT, READER_TYPE, READER_TOKENIZER, GPU_NUMBER, NAME_FIELD_NAME, \\\n    VECTOR_SIMILARITY_METRIC, CREATE_INDEX, LOG_LEVEL\n\nfrom rest_api.controller.request import Question\nfrom rest_api.controller.response import Answers, AnswersToIndividualQuestion\n\nfrom rest_api.controller.utils import RequestLimiter\nfrom haystack.document_store.elasticsearch import ElasticsearchDocumentStore\nfrom haystack.reader.base import BaseReader\nfrom haystack.reader.farm import FARMReader\nfrom haystack.reader.transformers import TransformersReader\nfrom haystack.retriever.base import BaseRetriever\nfrom haystack.retriever.sparse import ElasticsearchRetriever, ElasticsearchFilterOnlyRetriever\nfrom haystack.retriever.dense import EmbeddingRetriever\n\nlogger = logging.getLogger('haystack')\nlogger.setLevel(LOG_LEVEL)\n\nrouter = APIRouter()\n\n# Init global components: DocumentStore, Retriever, Reader, Finder\ndocument_store = ElasticsearchDocumentStore(\n    host=DB_HOST,\n    port=DB_PORT,\n    username=DB_USER,\n    password=DB_PW,\n    index=DB_INDEX,\n    scheme=ES_CONN_SCHEME,\n    ca_certs=False,\n    verify_certs=False,\n    text_field=TEXT_FIELD_NAME,\n    name_field=NAME_FIELD_NAME,\n    search_fields=SEARCH_FIELD_NAME,\n    embedding_dim=EMBEDDING_DIM,\n    embedding_field=EMBEDDING_FIELD_NAME,\n    excluded_meta_data=EXCLUDE_META_DATA_FIELDS,  # type: ignore\n    faq_question_field=FAQ_QUESTION_FIELD_NAME,\n    create_index=CREATE_INDEX,\n    similarity=VECTOR_SIMILARITY_METRIC\n)\n\nif RETRIEVER_TYPE == \"EmbeddingRetriever\":\n    retriever = EmbeddingRetriever(\n        document_store=document_store,\n        embedding_model=EMBEDDING_MODEL_PATH,\n        model_format=EMBEDDING_MODEL_FORMAT,\n        use_gpu=USE_GPU\n    )  # type: BaseRetriever\nelif RETRIEVER_TYPE == \"ElasticsearchRetriever\":\n    retriever = ElasticsearchRetriever(document_store=document_store)\nelif RETRIEVER_TYPE is None or RETRIEVER_TYPE == \"ElasticsearchFilterOnlyRetriever\":\n    retriever = ElasticsearchFilterOnlyRetriever(document_store=document_store)\nelse:\n    raise ValueError(f\"Could not load Retriever of type '{RETRIEVER_TYPE}'. \"\n                     f\"Please adjust RETRIEVER_TYPE to one of: \"\n                     f\"'EmbeddingRetriever', 'ElasticsearchRetriever', 'ElasticsearchFilterOnlyRetriever', None\"\n                     f\"OR modify rest_api/search.py to support your retriever\"\n                     )\n\nif READER_MODEL_PATH:  # for extractive doc-qa\n    if READER_TYPE == \"TransformersReader\":\n        use_gpu = -1 if not USE_GPU else GPU_NUMBER\n        reader = TransformersReader(\n            model_name_or_path=str(READER_MODEL_PATH),\n            use_gpu=use_gpu,\n            context_window_size=CONTEXT_WINDOW_SIZE,\n            tokenizer=str(READER_TOKENIZER)\n        )  # type: Optional[BaseReader]\n    elif READER_TYPE == \"FARMReader\":\n        reader = FARMReader(\n            model_name_or_path=str(READER_MODEL_PATH),\n            batch_size=BATCHSIZE,\n            use_gpu=USE_GPU,\n            context_window_size=CONTEXT_WINDOW_SIZE,\n            top_k_per_candidate=TOP_K_PER_CANDIDATE,\n            no_ans_boost=NO_ANS_BOOST,\n            num_processes=MAX_PROCESSES,\n            max_seq_len=MAX_SEQ_LEN,\n            doc_stride=DOC_STRIDE,\n        )  # type: Optional[BaseReader]\n    else:\n        raise ValueError(f\"Could not load Reader of type '{READER_TYPE}'. \"\n                         f\"Please adjust READER_TYPE to one of: \"\n                         f\"'FARMReader', 'TransformersReader', None\"\n                         )\nelse:\n    reader = None  # don't need one for pure FAQ matching\n\nFINDERS = {1: Finder(reader=reader, retriever=retriever)}\n\n\n#############################################\n# Endpoints\n#############################################\ndoc_qa_limiter = RequestLimiter(CONCURRENT_REQUEST_PER_WORKER)\n\n\n@router.post(\"/models/{model_id}/doc-qa\", response_model=Answers, response_model_exclude_unset=True)\ndef doc_qa(model_id: int, question_request: Question):\n    with doc_qa_limiter.run():\n        start_time = time.time()\n        finder = FINDERS.get(model_id, None)\n        if not finder:\n            raise HTTPException(\n                status_code=404, detail=f\"Couldn't get Finder with ID {model_id}. Available IDs: {list(FINDERS.keys())}\"\n            )\n\n        results = search_documents(finder, question_request, start_time)\n\n        return {\"results\": results}\n\n\n@router.post(\"/models/{model_id}/faq-qa\", response_model=Answers, response_model_exclude_unset=True)\ndef faq_qa(model_id: int, request: Question):\n    finder = FINDERS.get(model_id, None)\n    if not finder:\n        raise HTTPException(\n            status_code=404, detail=f\"Couldn't get Finder with ID {model_id}. Available IDs: {list(FINDERS.keys())}\"\n        )\n\n    results = []\n    for question in request.questions:\n        if request.filters:\n            # put filter values into a list and remove filters with null value\n            filters = {key: [value] for key, value in request.filters.items() if value is not None}\n            logger.info(f\" [{datetime.now()}] Request: {request}\")\n        else:\n            filters = {}\n\n        result = finder.get_answers_via_similar_questions(\n            question=question, top_k_retriever=request.top_k_retriever, filters=filters,\n        )\n        results.append(result)\n\n    elasticapm.set_custom_context({\"results\": results})\n    logger.info(json.dumps({\"request\": request.dict(), \"results\": results}))\n\n    return {\"results\": results}\n\n\n@router.post(\"/models/{model_id}/query\", response_model=Dict[str, Any], response_model_exclude_unset=True)\ndef query(model_id: int, query_request: Dict[str, Any], top_k_reader: int = DEFAULT_TOP_K_READER):\n    with doc_qa_limiter.run():\n        start_time = time.time()\n        finder = FINDERS.get(model_id, None)\n        if not finder:\n            raise HTTPException(\n                status_code=404, detail=f\"Couldn't get Finder with ID {model_id}. Available IDs: {list(FINDERS.keys())}\"\n            )\n\n        question_request = Question.from_elastic_query_dsl(query_request, top_k_reader)\n\n        answers = search_documents(finder, question_request, start_time)\n        response: Dict[str, Any] = {}\n        if answers and len(answers) > 0:\n            response = AnswersToIndividualQuestion.to_elastic_response_dsl(dict(answers[0]))\n\n        return response\n\n\ndef search_documents(finder, question_request, start_time) -> List[AnswersToIndividualQuestion]:\n    results = []\n    for question in question_request.questions:\n        if question_request.filters:\n            # put filter values into a list and remove filters with null value\n            filters = {key: [value] for key, value in question_request.filters.items() if value is not None}\n            logger.info(f\" [{datetime.now()}] Request: {question_request}\")\n        else:\n            filters = {}\n\n        result = finder.get_answers(\n            question=question,\n            top_k_retriever=question_request.top_k_retriever,\n            top_k_reader=question_request.top_k_reader,\n            filters=filters,\n        )\n        results.append(result)\n    elasticapm.set_custom_context({\"results\": results})\n    end_time = time.time()\n    logger.info(\n        json.dumps({\"request\": question_request.dict(), \"results\": results,\n                    \"time\": f\"{(end_time - start_time):.2f}\"}))\n    return results\n", "path": "rest_api/controller/search.py"}, {"content": "import sys\nfrom typing import Any, Collection, Dict, List, Optional\n\nfrom pydantic import BaseModel\n\nfrom rest_api.config import DEFAULT_TOP_K_READER, DEFAULT_TOP_K_RETRIEVER\n\nMAX_RECURSION_DEPTH = sys.getrecursionlimit() - 1\n\n\nclass Question(BaseModel):\n    questions: List[str]\n    filters: Optional[Dict[str, Optional[str]]] = None\n    top_k_reader: int = DEFAULT_TOP_K_READER\n    top_k_retriever: int = DEFAULT_TOP_K_RETRIEVER\n\n    @classmethod\n    def from_elastic_query_dsl(cls, query_request: Dict[str, Any], top_k_reader: int = DEFAULT_TOP_K_READER):\n\n        # Refer Query DSL\n        # https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-bool-query.html\n        # Currently do not support query matching with field parameter\n        query_strings: List[str] = []\n        filters: Dict[str, str] = {}\n        top_k_retriever: int = DEFAULT_TOP_K_RETRIEVER if \"size\" not in query_request else query_request[\"size\"]\n\n        cls._iterate_dsl_request(query_request, query_strings, filters)\n\n        if len(query_strings) != 1:\n            raise SyntaxError('Only one valid `query` field required expected, '\n                              'refer https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-query.html')\n\n        return cls(questions=query_strings, filters=filters if len(filters) else None, top_k_retriever=top_k_retriever,\n                   top_k_reader=top_k_reader)\n\n    @classmethod\n    def _iterate_dsl_request(cls, query_dsl: Any, query_strings: List[str], filters: Dict[str, str], depth: int = 0):\n        if depth == MAX_RECURSION_DEPTH:\n            raise RecursionError('Parsing incoming DSL reaching current value of the recursion limit')\n\n        # For question: Only consider values of \"query\" key for \"match\" and \"multi_match\" request.\n        # For filter: Only consider Dict[str, str] value of \"term\" or \"terms\" key\n        if isinstance(query_dsl, List):\n            for item in query_dsl:\n                cls._iterate_dsl_request(item, query_strings, filters, depth + 1)\n        elif isinstance(query_dsl, Dict):\n            for key, value in query_dsl.items():\n                # \"query\" value should be \"str\" type\n                if key == 'query' and isinstance(value, str):\n                    query_strings.append(value)\n                elif key in [\"filter\", \"filters\"]:\n                    cls._iterate_filters(value, filters, depth + 1)\n                elif isinstance(value, Collection):\n                    cls._iterate_dsl_request(value, query_strings, filters, depth + 1)\n\n    @classmethod\n    def _iterate_filters(cls, filter_dsl: Any, filters: Dict[str, str], depth: int = 0):\n        if depth == MAX_RECURSION_DEPTH:\n            raise RecursionError('Parsing incoming DSL reaching current value of the recursion limit')\n\n        if isinstance(filter_dsl, List):\n            for item in filter_dsl:\n                cls._iterate_filters(item, filters, depth + 1)\n        elif isinstance(filter_dsl, Dict):\n            for key, value in filter_dsl.items():\n                if key in [\"term\", \"terms\"]:\n                    if isinstance(value, Dict):\n                        for filter_key, filter_value in value.items():\n                            # Currently only accepting Dict[str, str]\n                            if isinstance(filter_value, str):\n                                filters[filter_key] = filter_value\n                elif isinstance(value, Collection):\n                    cls._iterate_filters(value, filters, depth + 1)\n", "path": "rest_api/controller/request.py"}], "after_files": [{"content": "import json\nimport logging\nimport time\nfrom datetime import datetime\nfrom typing import Any, Dict, List, Optional\n\nimport elasticapm\nfrom fastapi import APIRouter\nfrom fastapi import HTTPException\n\nfrom haystack import Finder\nfrom rest_api.config import DB_HOST, DB_PORT, DB_USER, DB_PW, DB_INDEX, DEFAULT_TOP_K_READER, ES_CONN_SCHEME, \\\n    TEXT_FIELD_NAME, SEARCH_FIELD_NAME, EMBEDDING_DIM, EMBEDDING_FIELD_NAME, EXCLUDE_META_DATA_FIELDS, \\\n    RETRIEVER_TYPE, EMBEDDING_MODEL_PATH, USE_GPU, READER_MODEL_PATH, BATCHSIZE, CONTEXT_WINDOW_SIZE, \\\n    TOP_K_PER_CANDIDATE, NO_ANS_BOOST, MAX_PROCESSES, MAX_SEQ_LEN, DOC_STRIDE, CONCURRENT_REQUEST_PER_WORKER, \\\n    FAQ_QUESTION_FIELD_NAME, EMBEDDING_MODEL_FORMAT, READER_TYPE, READER_TOKENIZER, GPU_NUMBER, NAME_FIELD_NAME, \\\n    VECTOR_SIMILARITY_METRIC, CREATE_INDEX, LOG_LEVEL\n\nfrom rest_api.controller.request import Question\nfrom rest_api.controller.response import Answers, AnswersToIndividualQuestion\n\nfrom rest_api.controller.utils import RequestLimiter\nfrom haystack.document_store.elasticsearch import ElasticsearchDocumentStore\nfrom haystack.reader.base import BaseReader\nfrom haystack.reader.farm import FARMReader\nfrom haystack.reader.transformers import TransformersReader\nfrom haystack.retriever.base import BaseRetriever\nfrom haystack.retriever.sparse import ElasticsearchRetriever, ElasticsearchFilterOnlyRetriever\nfrom haystack.retriever.dense import EmbeddingRetriever\n\nlogger = logging.getLogger('haystack')\nlogger.setLevel(LOG_LEVEL)\n\nrouter = APIRouter()\n\n# Init global components: DocumentStore, Retriever, Reader, Finder\ndocument_store = ElasticsearchDocumentStore(\n    host=DB_HOST,\n    port=DB_PORT,\n    username=DB_USER,\n    password=DB_PW,\n    index=DB_INDEX,\n    scheme=ES_CONN_SCHEME,\n    ca_certs=False,\n    verify_certs=False,\n    text_field=TEXT_FIELD_NAME,\n    name_field=NAME_FIELD_NAME,\n    search_fields=SEARCH_FIELD_NAME,\n    embedding_dim=EMBEDDING_DIM,\n    embedding_field=EMBEDDING_FIELD_NAME,\n    excluded_meta_data=EXCLUDE_META_DATA_FIELDS,  # type: ignore\n    faq_question_field=FAQ_QUESTION_FIELD_NAME,\n    create_index=CREATE_INDEX,\n    similarity=VECTOR_SIMILARITY_METRIC\n)\n\nif RETRIEVER_TYPE == \"EmbeddingRetriever\":\n    retriever = EmbeddingRetriever(\n        document_store=document_store,\n        embedding_model=EMBEDDING_MODEL_PATH,\n        model_format=EMBEDDING_MODEL_FORMAT,\n        use_gpu=USE_GPU\n    )  # type: BaseRetriever\nelif RETRIEVER_TYPE == \"ElasticsearchRetriever\":\n    retriever = ElasticsearchRetriever(document_store=document_store)\nelif RETRIEVER_TYPE is None or RETRIEVER_TYPE == \"ElasticsearchFilterOnlyRetriever\":\n    retriever = ElasticsearchFilterOnlyRetriever(document_store=document_store)\nelse:\n    raise ValueError(f\"Could not load Retriever of type '{RETRIEVER_TYPE}'. \"\n                     f\"Please adjust RETRIEVER_TYPE to one of: \"\n                     f\"'EmbeddingRetriever', 'ElasticsearchRetriever', 'ElasticsearchFilterOnlyRetriever', None\"\n                     f\"OR modify rest_api/search.py to support your retriever\"\n                     )\n\nif READER_MODEL_PATH:  # for extractive doc-qa\n    if READER_TYPE == \"TransformersReader\":\n        use_gpu = -1 if not USE_GPU else GPU_NUMBER\n        reader = TransformersReader(\n            model_name_or_path=str(READER_MODEL_PATH),\n            use_gpu=use_gpu,\n            context_window_size=CONTEXT_WINDOW_SIZE,\n            tokenizer=str(READER_TOKENIZER)\n        )  # type: Optional[BaseReader]\n    elif READER_TYPE == \"FARMReader\":\n        reader = FARMReader(\n            model_name_or_path=str(READER_MODEL_PATH),\n            batch_size=BATCHSIZE,\n            use_gpu=USE_GPU,\n            context_window_size=CONTEXT_WINDOW_SIZE,\n            top_k_per_candidate=TOP_K_PER_CANDIDATE,\n            no_ans_boost=NO_ANS_BOOST,\n            num_processes=MAX_PROCESSES,\n            max_seq_len=MAX_SEQ_LEN,\n            doc_stride=DOC_STRIDE,\n        )  # type: Optional[BaseReader]\n    else:\n        raise ValueError(f\"Could not load Reader of type '{READER_TYPE}'. \"\n                         f\"Please adjust READER_TYPE to one of: \"\n                         f\"'FARMReader', 'TransformersReader', None\"\n                         )\nelse:\n    reader = None  # don't need one for pure FAQ matching\n\nFINDERS = {1: Finder(reader=reader, retriever=retriever)}\n\n\n#############################################\n# Endpoints\n#############################################\ndoc_qa_limiter = RequestLimiter(CONCURRENT_REQUEST_PER_WORKER)\n\n\n@router.post(\"/models/{model_id}/doc-qa\", response_model=Answers, response_model_exclude_unset=True)\ndef doc_qa(model_id: int, question_request: Question):\n    with doc_qa_limiter.run():\n        start_time = time.time()\n        finder = FINDERS.get(model_id, None)\n        if not finder:\n            raise HTTPException(\n                status_code=404, detail=f\"Could not get Finder with ID {model_id}. Available IDs: {list(FINDERS.keys())}\"\n            )\n\n        results = search_documents(finder, question_request, start_time)\n\n        return {\"results\": results}\n\n\n@router.post(\"/models/{model_id}/faq-qa\", response_model=Answers, response_model_exclude_unset=True)\ndef faq_qa(model_id: int, request: Question):\n    finder = FINDERS.get(model_id, None)\n    if not finder:\n        raise HTTPException(\n            status_code=404, detail=f\"Could not get Finder with ID {model_id}. Available IDs: {list(FINDERS.keys())}\"\n        )\n\n    results = []\n    for question in request.questions:\n        if request.filters:\n            # put filter values into a list and remove filters with null value\n            filters = {}\n            for key, values in request.filters.items():\n                if values is None:\n                    continue\n                if not isinstance(values, list):\n                    values = [values]\n                filters[key] = values\n            logger.info(f\" [{datetime.now()}] Request: {request}\")\n        else:\n            filters = {}\n\n        result = finder.get_answers_via_similar_questions(\n            question=question, top_k_retriever=request.top_k_retriever, filters=filters,\n        )\n        results.append(result)\n\n    elasticapm.set_custom_context({\"results\": results})\n    logger.info(json.dumps({\"request\": request.dict(), \"results\": results}))\n\n    return {\"results\": results}\n\n\n@router.post(\"/models/{model_id}/query\", response_model=Dict[str, Any], response_model_exclude_unset=True)\ndef query(model_id: int, query_request: Dict[str, Any], top_k_reader: int = DEFAULT_TOP_K_READER):\n    with doc_qa_limiter.run():\n        start_time = time.time()\n        finder = FINDERS.get(model_id, None)\n        if not finder:\n            raise HTTPException(\n                status_code=404, detail=f\"Could not get Finder with ID {model_id}. Available IDs: {list(FINDERS.keys())}\"\n            )\n\n        question_request = Question.from_elastic_query_dsl(query_request, top_k_reader)\n\n        answers = search_documents(finder, question_request, start_time)\n        response: Dict[str, Any] = {}\n        if answers and len(answers) > 0:\n            response = AnswersToIndividualQuestion.to_elastic_response_dsl(dict(answers[0]))\n\n        return response\n\n\ndef search_documents(finder, question_request, start_time) -> List[AnswersToIndividualQuestion]:\n    results = []\n    for question in question_request.questions:\n        if question_request.filters:\n            # put filter values into a list and remove filters with null value\n            filters = {}\n            for key, values in question_request.filters.items():\n                if values is None:\n                    continue\n                if not isinstance(values, list):\n                    values = [values]\n                filters[key] = values\n            logger.info(f\" [{datetime.now()}] Request: {question_request}\")\n        else:\n            filters = {}\n\n        result = finder.get_answers(\n            question=question,\n            top_k_retriever=question_request.top_k_retriever,\n            top_k_reader=question_request.top_k_reader,\n            filters=filters,\n        )\n        results.append(result)\n    elasticapm.set_custom_context({\"results\": results})\n    end_time = time.time()\n    logger.info(\n        json.dumps({\"request\": question_request.dict(), \"results\": results,\n                    \"time\": f\"{(end_time - start_time):.2f}\"}))\n    return results\n", "path": "rest_api/controller/search.py"}, {"content": "import sys\nfrom typing import Any, Collection, Dict, List, Optional, Union\n\nfrom pydantic import BaseModel\n\nfrom rest_api.config import DEFAULT_TOP_K_READER, DEFAULT_TOP_K_RETRIEVER\n\nMAX_RECURSION_DEPTH = sys.getrecursionlimit() - 1\n\n\nclass Question(BaseModel):\n    questions: List[str]\n    filters: Optional[Dict[str, Optional[Union[str, List[str]]]]] = None\n    top_k_reader: int = DEFAULT_TOP_K_READER\n    top_k_retriever: int = DEFAULT_TOP_K_RETRIEVER\n\n    @classmethod\n    def from_elastic_query_dsl(cls, query_request: Dict[str, Any], top_k_reader: int = DEFAULT_TOP_K_READER):\n\n        # Refer Query DSL\n        # https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-bool-query.html\n        # Currently do not support query matching with field parameter\n        query_strings: List[str] = []\n        filters: Dict[str, str] = {}\n        top_k_retriever: int = DEFAULT_TOP_K_RETRIEVER if \"size\" not in query_request else query_request[\"size\"]\n\n        cls._iterate_dsl_request(query_request, query_strings, filters)\n\n        if len(query_strings) != 1:\n            raise SyntaxError('Only one valid `query` field required expected, '\n                              'refer https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-query.html')\n\n        return cls(questions=query_strings, filters=filters if len(filters) else None, top_k_retriever=top_k_retriever,\n                   top_k_reader=top_k_reader)\n\n    @classmethod\n    def _iterate_dsl_request(cls, query_dsl: Any, query_strings: List[str], filters: Dict[str, str], depth: int = 0):\n        if depth == MAX_RECURSION_DEPTH:\n            raise RecursionError('Parsing incoming DSL reaching current value of the recursion limit')\n\n        # For question: Only consider values of \"query\" key for \"match\" and \"multi_match\" request.\n        # For filter: Only consider Dict[str, str] value of \"term\" or \"terms\" key\n        if isinstance(query_dsl, List):\n            for item in query_dsl:\n                cls._iterate_dsl_request(item, query_strings, filters, depth + 1)\n        elif isinstance(query_dsl, Dict):\n            for key, value in query_dsl.items():\n                # \"query\" value should be \"str\" type\n                if key == 'query' and isinstance(value, str):\n                    query_strings.append(value)\n                elif key in [\"filter\", \"filters\"]:\n                    cls._iterate_filters(value, filters, depth + 1)\n                elif isinstance(value, Collection):\n                    cls._iterate_dsl_request(value, query_strings, filters, depth + 1)\n\n    @classmethod\n    def _iterate_filters(cls, filter_dsl: Any, filters: Dict[str, str], depth: int = 0):\n        if depth == MAX_RECURSION_DEPTH:\n            raise RecursionError('Parsing incoming DSL reaching current value of the recursion limit')\n\n        if isinstance(filter_dsl, List):\n            for item in filter_dsl:\n                cls._iterate_filters(item, filters, depth + 1)\n        elif isinstance(filter_dsl, Dict):\n            for key, value in filter_dsl.items():\n                if key in [\"term\", \"terms\"]:\n                    if isinstance(value, Dict):\n                        for filter_key, filter_value in value.items():\n                            # Currently only accepting Dict[str, str]\n                            if isinstance(filter_value, str):\n                                filters[filter_key] = filter_value\n                elif isinstance(value, Collection):\n                    cls._iterate_filters(value, filters, depth + 1)\n", "path": "rest_api/controller/request.py"}]}