Skip to content

Commit 830ee82

Browse files
committed
feat(multi-search): add async support for multi-search operations
- add AsyncMultiSearch class for async multi-search operations - add async tests for multi-search functionality - add async fixtures for testing async multi-search operations
1 parent a9bcc4e commit 830ee82

File tree

3 files changed

+302
-5
lines changed

3 files changed

+302
-5
lines changed
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
"""
2+
This module provides async functionality for performing multi-search operations in the Typesense API.
3+
4+
It contains the AsyncMultiSearch class, which allows for executing multiple search queries
5+
asynchronously in a single API call.
6+
7+
Classes:
8+
AsyncMultiSearch: Manages async multi-search operations in the Typesense API.
9+
10+
Dependencies:
11+
- typesense.async_api_call: Provides the AsyncApiCall class for making async API requests.
12+
- typesense.preprocess: Provides the stringify_search_params function for parameter processing.
13+
- typesense.types.document: Provides the MultiSearchCommonParameters type.
14+
- typesense.types.multi_search: Provides MultiSearchRequestSchema and MultiSearchResponse types.
15+
16+
Note: This module uses conditional imports to support both Python 3.11+ and earlier versions.
17+
"""
18+
19+
import sys
20+
21+
from typesense.async_api_call import AsyncApiCall
22+
from typesense.preprocess import stringify_search_params
23+
from typesense.types.document import MultiSearchCommonParameters
24+
from typesense.types.multi_search import MultiSearchRequestSchema, MultiSearchResponse
25+
26+
if sys.version_info >= (3, 11):
27+
import typing
28+
else:
29+
import typing_extensions as typing
30+
31+
32+
class AsyncMultiSearch:
33+
"""
34+
Manages async multi-search operations in the Typesense API.
35+
36+
This class provides async methods to perform multiple search queries in a single API call.
37+
38+
Attributes:
39+
resource_path (str): The API endpoint path for multi-search operations.
40+
api_call (AsyncApiCall): The AsyncApiCall instance for making async API requests.
41+
"""
42+
43+
resource_path: typing.Final[str] = "/multi_search"
44+
45+
def __init__(self, api_call: AsyncApiCall) -> None:
46+
"""
47+
Initialize the AsyncMultiSearch instance.
48+
49+
Args:
50+
api_call (AsyncApiCall): The AsyncApiCall instance for making async API requests.
51+
"""
52+
self.api_call = api_call
53+
54+
async def perform(
55+
self,
56+
search_queries: MultiSearchRequestSchema,
57+
common_params: typing.Union[MultiSearchCommonParameters, None] = None,
58+
) -> MultiSearchResponse:
59+
"""
60+
Perform a multi-search operation.
61+
62+
This method allows executing multiple search queries in a single API call.
63+
It processes the search parameters, sends the request to the Typesense API,
64+
and returns the multi-search response.
65+
66+
Args:
67+
search_queries (MultiSearchRequestSchema):
68+
A dictionary containing the list of search queries to perform.
69+
The dictionary should have a 'searches' key with a list of search
70+
parameter dictionaries.
71+
common_params (Union[MultiSearchCommonParameters, None], optional):
72+
Common parameters to apply to all search queries. Defaults to None.
73+
74+
Returns:
75+
MultiSearchResponse:
76+
The response from the multi-search operation, containing
77+
the results of all search queries.
78+
79+
Example:
80+
>>> multi_search = AsyncMultiSearch(async_api_call)
81+
>>> response = await multi_search.perform(
82+
... {
83+
... "searches": [
84+
... {
85+
... "q": "com",
86+
... "query_by": "company_name",
87+
... "collection": "companies",
88+
... },
89+
... ],
90+
... }
91+
... )
92+
"""
93+
stringified_search_params = [
94+
stringify_search_params(search_params)
95+
for search_params in search_queries.get("searches")
96+
]
97+
search_body = {
98+
"searches": stringified_search_params,
99+
"union": search_queries.get("union", False),
100+
}
101+
response: MultiSearchResponse = await self.api_call.post(
102+
AsyncMultiSearch.resource_path,
103+
body=search_body,
104+
params=common_params,
105+
as_json=True,
106+
entity_type=MultiSearchResponse,
107+
)
108+
return response

tests/fixtures/multi_search_fixtures.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,28 @@
33
import pytest
44

55
from typesense.api_call import ApiCall
6+
from typesense.async_api_call import AsyncApiCall
7+
from typesense.async_multi_search import AsyncMultiSearch
68
from typesense.multi_search import MultiSearch
79

810

911
@pytest.fixture(scope="function", name="actual_multi_search")
1012
def actual_multi_search_fixture(actual_api_call: ApiCall) -> MultiSearch:
1113
"""Return a MultiSearch object using a real API."""
1214
return MultiSearch(actual_api_call)
15+
16+
17+
@pytest.fixture(scope="function", name="actual_async_multi_search")
18+
def actual_async_multi_search_fixture(
19+
actual_async_api_call: AsyncApiCall,
20+
) -> AsyncMultiSearch:
21+
"""Return a AsyncMultiSearch object using a real API."""
22+
return AsyncMultiSearch(actual_async_api_call)
23+
24+
25+
@pytest.fixture(scope="function", name="fake_async_multi_search")
26+
def fake_async_multi_search_fixture(
27+
fake_async_api_call: AsyncApiCall,
28+
) -> AsyncMultiSearch:
29+
"""Return a AsyncMultiSearch object with test values."""
30+
return AsyncMultiSearch(fake_async_api_call)

tests/multi_search_test.py

Lines changed: 176 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,25 +10,42 @@
1010
)
1111
from typesense import exceptions
1212
from typesense.api_call import ApiCall
13+
from typesense.async_api_call import AsyncApiCall
14+
from typesense.async_multi_search import AsyncMultiSearch
1315
from typesense.multi_search import MultiSearch
1416
from typesense.types.multi_search import MultiSearchRequestSchema
1517

1618

1719
def test_init(fake_api_call: ApiCall) -> None:
18-
"""Test that the Document object is initialized correctly."""
19-
documents = MultiSearch(fake_api_call)
20+
"""Test that the MultiSearch object is initialized correctly."""
21+
multi_search = MultiSearch(fake_api_call)
2022

21-
assert_match_object(documents.api_call, fake_api_call)
23+
assert_match_object(multi_search.api_call, fake_api_call)
2224
assert_object_lists_match(
23-
documents.api_call.node_manager.nodes,
25+
multi_search.api_call.node_manager.nodes,
2426
fake_api_call.node_manager.nodes,
2527
)
2628
assert_match_object(
27-
documents.api_call.config.nearest_node,
29+
multi_search.api_call.config.nearest_node,
2830
fake_api_call.config.nearest_node,
2931
)
3032

3133

34+
def test_init_async(fake_async_api_call: AsyncApiCall) -> None:
35+
"""Test that the AsyncMultiSearch object is initialized correctly."""
36+
multi_search = AsyncMultiSearch(fake_async_api_call)
37+
38+
assert_match_object(multi_search.api_call, fake_async_api_call)
39+
assert_object_lists_match(
40+
multi_search.api_call.node_manager.nodes,
41+
fake_async_api_call.node_manager.nodes,
42+
)
43+
assert_match_object(
44+
multi_search.api_call.config.nearest_node,
45+
fake_async_api_call.config.nearest_node,
46+
)
47+
48+
3249
def test_multi_search_single_search(
3350
actual_multi_search: MultiSearch,
3451
actual_api_call: ApiCall,
@@ -220,3 +237,157 @@ def test_search_invalid_parameters(
220237
],
221238
},
222239
)
240+
241+
242+
async def test_multi_search_single_search_async(
243+
actual_async_multi_search: AsyncMultiSearch,
244+
delete_all: None,
245+
create_collection: None,
246+
create_document: None,
247+
) -> None:
248+
"""Test that the AsyncMultiSearch object can perform a single search."""
249+
request_params: MultiSearchRequestSchema = {
250+
"searches": [
251+
{"q": "com", "query_by": "company_name", "collection": "companies"},
252+
],
253+
}
254+
response = await actual_async_multi_search.perform(
255+
search_queries=request_params,
256+
)
257+
258+
assert len(response.get("results")) == 1
259+
assert_to_contain_keys(
260+
response.get("results")[0],
261+
[
262+
"facet_counts",
263+
"found",
264+
"hits",
265+
"page",
266+
"out_of",
267+
"request_params",
268+
"search_time_ms",
269+
"search_cutoff",
270+
],
271+
)
272+
273+
assert_to_contain_keys(
274+
response.get("results")[0].get("hits")[0],
275+
["document", "highlights", "highlight", "text_match", "text_match_info"],
276+
)
277+
278+
279+
async def test_multi_search_multiple_searches_async(
280+
actual_async_multi_search: AsyncMultiSearch,
281+
delete_all: None,
282+
create_collection: None,
283+
create_document: None,
284+
) -> None:
285+
"""Test that the AsyncMultiSearch object can perform multiple searches."""
286+
request_params: MultiSearchRequestSchema = {
287+
"searches": [
288+
{"q": "com", "query_by": "company_name", "collection": "companies"},
289+
{"q": "company", "query_by": "company_name", "collection": "companies"},
290+
],
291+
}
292+
293+
response = await actual_async_multi_search.perform(search_queries=request_params)
294+
295+
assert len(response.get("results")) == len(request_params.get("searches"))
296+
for search_results in response.get("results"):
297+
assert_to_contain_keys(
298+
search_results,
299+
[
300+
"facet_counts",
301+
"found",
302+
"hits",
303+
"page",
304+
"out_of",
305+
"request_params",
306+
"search_time_ms",
307+
"search_cutoff",
308+
],
309+
)
310+
311+
assert_to_contain_keys(
312+
search_results.get("hits")[0],
313+
["document", "highlights", "highlight", "text_match", "text_match_info"],
314+
)
315+
316+
317+
async def test_multi_search_union_async(
318+
actual_async_multi_search: AsyncMultiSearch,
319+
delete_all: None,
320+
create_collection: None,
321+
create_document: None,
322+
) -> None:
323+
"""Test that the AsyncMultiSearch object can perform multiple searches with union."""
324+
request_params: MultiSearchRequestSchema = {
325+
"union": True,
326+
"searches": [
327+
{"q": "com", "query_by": "company_name", "collection": "companies"},
328+
{"q": "company", "query_by": "company_name", "collection": "companies"},
329+
],
330+
}
331+
332+
response = await actual_async_multi_search.perform(search_queries=request_params)
333+
334+
assert_to_contain_keys(
335+
response,
336+
[
337+
"found",
338+
"hits",
339+
"page",
340+
"out_of",
341+
"union_request_params",
342+
"search_time_ms",
343+
"search_cutoff",
344+
],
345+
)
346+
347+
assert_to_contain_keys(
348+
response.get("hits")[0],
349+
[
350+
"collection",
351+
"document",
352+
"highlights",
353+
"highlight",
354+
"text_match",
355+
"text_match_info",
356+
"search_index",
357+
],
358+
)
359+
360+
361+
async def test_multi_search_array_async(
362+
actual_async_multi_search: AsyncMultiSearch,
363+
delete_all: None,
364+
create_collection: None,
365+
create_document: None,
366+
) -> None:
367+
"""Test that the AsyncMultiSearch object can perform a search with an array query_by."""
368+
request_params: MultiSearchRequestSchema = {
369+
"searches": [
370+
{"q": "com", "query_by": ["company_name"], "collection": "companies"},
371+
],
372+
}
373+
response = await actual_async_multi_search.perform(search_queries=request_params)
374+
375+
assert len(response.get("results")) == 1
376+
assert_to_contain_keys(
377+
response.get("results")[0],
378+
[
379+
"facet_counts",
380+
"found",
381+
"hits",
382+
"page",
383+
"out_of",
384+
"request_params",
385+
"search_time_ms",
386+
"search_cutoff",
387+
],
388+
)
389+
390+
assert_to_contain_keys(
391+
response.get("results")[0].get("hits")[0],
392+
["document", "highlights", "highlight", "text_match", "text_match_info"],
393+
)

0 commit comments

Comments
 (0)