meilisearch-python-sdk 5.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- meilisearch_python_sdk/__init__.py +8 -0
- meilisearch_python_sdk/_batch.py +166 -0
- meilisearch_python_sdk/_client.py +2468 -0
- meilisearch_python_sdk/_http_requests.py +197 -0
- meilisearch_python_sdk/_task.py +368 -0
- meilisearch_python_sdk/_utils.py +58 -0
- meilisearch_python_sdk/_version.py +1 -0
- meilisearch_python_sdk/decorators.py +242 -0
- meilisearch_python_sdk/errors.py +75 -0
- meilisearch_python_sdk/index/__init__.py +4 -0
- meilisearch_python_sdk/index/_common.py +296 -0
- meilisearch_python_sdk/index/async_index.py +4891 -0
- meilisearch_python_sdk/index/index.py +3839 -0
- meilisearch_python_sdk/json_handler.py +74 -0
- meilisearch_python_sdk/models/__init__.py +0 -0
- meilisearch_python_sdk/models/batch.py +58 -0
- meilisearch_python_sdk/models/client.py +97 -0
- meilisearch_python_sdk/models/documents.py +12 -0
- meilisearch_python_sdk/models/health.py +5 -0
- meilisearch_python_sdk/models/index.py +46 -0
- meilisearch_python_sdk/models/search.py +126 -0
- meilisearch_python_sdk/models/settings.py +197 -0
- meilisearch_python_sdk/models/task.py +77 -0
- meilisearch_python_sdk/models/version.py +9 -0
- meilisearch_python_sdk/models/webhook.py +24 -0
- meilisearch_python_sdk/plugins.py +124 -0
- meilisearch_python_sdk/py.typed +0 -0
- meilisearch_python_sdk/types.py +8 -0
- meilisearch_python_sdk-5.5.0.dist-info/METADATA +279 -0
- meilisearch_python_sdk-5.5.0.dist-info/RECORD +32 -0
- meilisearch_python_sdk-5.5.0.dist-info/WHEEL +4 -0
- meilisearch_python_sdk-5.5.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from functools import wraps
|
|
6
|
+
from typing import Any, NamedTuple
|
|
7
|
+
|
|
8
|
+
from meilisearch_python_sdk import AsyncClient, Client
|
|
9
|
+
from meilisearch_python_sdk._utils import use_task_groups
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ConnectionInfo(NamedTuple):
|
|
13
|
+
"""Information on how to connect to Meilisearch.
|
|
14
|
+
|
|
15
|
+
url: URL for the Meilisearch server.
|
|
16
|
+
api_key: The API key for the server.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
url: str
|
|
20
|
+
api_key: str
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def async_add_documents(
|
|
24
|
+
*,
|
|
25
|
+
index_name: str,
|
|
26
|
+
connection_info: AsyncClient | ConnectionInfo,
|
|
27
|
+
batch_size: int | None = None,
|
|
28
|
+
primary_key: str | None = None,
|
|
29
|
+
wait_for_task: bool = False,
|
|
30
|
+
verify: bool = True,
|
|
31
|
+
) -> Callable:
|
|
32
|
+
"""Decorator that takes the returned documents from a function and asynchronously adds them to Meilisearch.
|
|
33
|
+
|
|
34
|
+
It is required that either an async_client or url is provided.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
index_name: The name of the index to which the documents should be added.
|
|
38
|
+
connection_info: Either an AsyncClient instance ConnectionInfo with information on how to
|
|
39
|
+
connect to Meilisearch.
|
|
40
|
+
batch_size: If provided the documents will be sent in batches of the specified size.
|
|
41
|
+
Otherwise all documents are sent at once. Default = None.
|
|
42
|
+
primary_key: The primary key of the documents. This will be ignored if already set.
|
|
43
|
+
Defaults to None.
|
|
44
|
+
wait_for_task: If set to `True` the decorator will wait for the document addition to finish
|
|
45
|
+
indexing before returning, otherwise it will return right away. Default = False.
|
|
46
|
+
verify: If set to `False` the decorator will not verify the SSL certificate of the server.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
The list of documents provided by the decorated function.
|
|
50
|
+
|
|
51
|
+
Raises:
|
|
52
|
+
MeilisearchCommunicationError: If there was an error communicating with the server.
|
|
53
|
+
MeilisearchApiError: If the Meilisearch API returned an error.
|
|
54
|
+
ValueError: If neither an async_client nor an url is provided.
|
|
55
|
+
|
|
56
|
+
Examples
|
|
57
|
+
>>> from meilisearch_python_sdk import AsyncClient
|
|
58
|
+
>>> from meilisearch_python_sdk.decorators import async_add_documents, ConnectionInfo
|
|
59
|
+
>>>
|
|
60
|
+
>>>
|
|
61
|
+
>>> # with `AsyncClient`
|
|
62
|
+
>>> client = AsyncClient(url="http://localhost:7700", api_key="masterKey")
|
|
63
|
+
>>> @async_add_documents(index_name="movies", connection_info=client)
|
|
64
|
+
>>> async def my_function() -> list[dict[str, Any]]:
|
|
65
|
+
>>> return [{"id": 1, "title": "Test 1"}, {"id": 2, "title": "Test 2"}]
|
|
66
|
+
>>>
|
|
67
|
+
>>> # with `ConnectionInfo`
|
|
68
|
+
>>> @async_add_documents(
|
|
69
|
+
index_name="movies",
|
|
70
|
+
connection_info=ConnectionInfo(url="http://localhost:7700", api_key="masterKey"),
|
|
71
|
+
)
|
|
72
|
+
>>> async def my_function() -> list[dict[str, Any]]:
|
|
73
|
+
>>> return [{"id": 1, "title": "Test 1"}, {"id": 2, "title": "Test 2"}]
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
def decorator(func: Callable) -> Callable:
|
|
77
|
+
@wraps(func)
|
|
78
|
+
async def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
79
|
+
result = await func(*args, **kwargs)
|
|
80
|
+
if isinstance(connection_info, AsyncClient):
|
|
81
|
+
await _async_add_documents(
|
|
82
|
+
connection_info,
|
|
83
|
+
index_name,
|
|
84
|
+
result,
|
|
85
|
+
batch_size,
|
|
86
|
+
primary_key,
|
|
87
|
+
wait_for_task,
|
|
88
|
+
)
|
|
89
|
+
return result
|
|
90
|
+
|
|
91
|
+
async with AsyncClient(
|
|
92
|
+
connection_info.url, connection_info.api_key, verify=verify
|
|
93
|
+
) as client:
|
|
94
|
+
await _async_add_documents(
|
|
95
|
+
client, index_name, result, batch_size, primary_key, wait_for_task
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
return result
|
|
99
|
+
|
|
100
|
+
return wrapper
|
|
101
|
+
|
|
102
|
+
return decorator
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def add_documents(
|
|
106
|
+
*,
|
|
107
|
+
index_name: str,
|
|
108
|
+
connection_info: Client | ConnectionInfo,
|
|
109
|
+
batch_size: int | None = None,
|
|
110
|
+
primary_key: str | None = None,
|
|
111
|
+
wait_for_task: bool = False,
|
|
112
|
+
verify: bool = True,
|
|
113
|
+
) -> Callable:
|
|
114
|
+
"""Decorator that takes the returned documents from a function and adds them to Meilisearch.
|
|
115
|
+
|
|
116
|
+
It is required that either an client or url is provided.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
index_name: The name of the index to which the documents should be added.
|
|
120
|
+
connection_info: Either an Client instance ConnectionInfo with information on how to
|
|
121
|
+
connect to Meilisearch.
|
|
122
|
+
batch_size: If provided the documents will be sent in batches of the specified size.
|
|
123
|
+
Otherwise all documents are sent at once. Default = None.
|
|
124
|
+
primary_key: The primary key of the documents. This will be ignored if already set.
|
|
125
|
+
Defaults to None.
|
|
126
|
+
wait_for_task: If set to `True` the decorator will wait for the document addition to finish
|
|
127
|
+
indexing before returning, otherwise it will return right away. Default = False.
|
|
128
|
+
verify: If set to `False` the decorator will not verify the SSL certificate of the server.
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
The list of documents provided by the decorated function.
|
|
132
|
+
|
|
133
|
+
Raises:
|
|
134
|
+
MeilisearchCommunicationError: If there was an error communicating with the server.
|
|
135
|
+
MeilisearchApiError: If the Meilisearch API returned an error.
|
|
136
|
+
ValueError: If neither an async_client nor an url is provided.
|
|
137
|
+
|
|
138
|
+
Examples
|
|
139
|
+
>>> from meilisearch_python_sdk import Client
|
|
140
|
+
>>> from meilisearch_python_sdk.decorators import add_documents, ConnectionInfo
|
|
141
|
+
>>>
|
|
142
|
+
>>>
|
|
143
|
+
>>> # With `Client`
|
|
144
|
+
>>> client = Client(url="http://localhost:7700", api_key="masterKey")
|
|
145
|
+
>>> @add_documents(index_name="movies", connection_info=client)
|
|
146
|
+
>>> def my_function() -> list[dict[str, Any]]:
|
|
147
|
+
>>> return [{"id": 1, "title": "Test 1"}, {"id": 2, "title": "Test 2"}]
|
|
148
|
+
>>>
|
|
149
|
+
>>> # With `ConnectionInfo`
|
|
150
|
+
>>> @add_documents(
|
|
151
|
+
index_name="movies",
|
|
152
|
+
connection_info=ConnectionInfo(url="http://localhost:7700", api_key="masterKey"),
|
|
153
|
+
)
|
|
154
|
+
>>> def my_function() -> list[dict[str, Any]]:
|
|
155
|
+
>>> return [{"id": 1, "title": "Test 1"}, {"id": 2, "title": "Test 2"}]
|
|
156
|
+
"""
|
|
157
|
+
|
|
158
|
+
def decorator(func: Callable) -> Callable:
|
|
159
|
+
@wraps(func)
|
|
160
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
161
|
+
result = func(*args, **kwargs)
|
|
162
|
+
if isinstance(connection_info, Client):
|
|
163
|
+
_add_documents(
|
|
164
|
+
connection_info,
|
|
165
|
+
index_name,
|
|
166
|
+
result,
|
|
167
|
+
batch_size,
|
|
168
|
+
primary_key,
|
|
169
|
+
wait_for_task,
|
|
170
|
+
)
|
|
171
|
+
return result
|
|
172
|
+
|
|
173
|
+
decorator_client = Client(
|
|
174
|
+
url=connection_info.url, api_key=connection_info.api_key, verify=verify
|
|
175
|
+
)
|
|
176
|
+
_add_documents(
|
|
177
|
+
decorator_client,
|
|
178
|
+
index_name,
|
|
179
|
+
result,
|
|
180
|
+
batch_size,
|
|
181
|
+
primary_key,
|
|
182
|
+
wait_for_task,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
return result
|
|
186
|
+
|
|
187
|
+
return wrapper
|
|
188
|
+
|
|
189
|
+
return decorator
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
async def _async_add_documents(
|
|
193
|
+
async_client: AsyncClient,
|
|
194
|
+
index_name: str,
|
|
195
|
+
documents: Any,
|
|
196
|
+
batch_size: int | None,
|
|
197
|
+
primary_key: str | None,
|
|
198
|
+
wait_for_task: bool,
|
|
199
|
+
) -> None:
|
|
200
|
+
index = async_client.index(index_name)
|
|
201
|
+
if not batch_size:
|
|
202
|
+
task = await index.add_documents(documents, primary_key)
|
|
203
|
+
if wait_for_task:
|
|
204
|
+
await async_client.wait_for_task(task.task_uid, timeout_in_ms=None)
|
|
205
|
+
return
|
|
206
|
+
|
|
207
|
+
tasks = await index.add_documents_in_batches(
|
|
208
|
+
documents, batch_size=batch_size, primary_key=primary_key
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
if wait_for_task:
|
|
212
|
+
if not use_task_groups():
|
|
213
|
+
waits = [async_client.wait_for_task(x.task_uid) for x in tasks]
|
|
214
|
+
await asyncio.gather(*waits)
|
|
215
|
+
return
|
|
216
|
+
|
|
217
|
+
async with asyncio.TaskGroup() as tg: # type: ignore[attr-defined]
|
|
218
|
+
[tg.create_task(async_client.wait_for_task(x.task_uid)) for x in tasks]
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _add_documents(
|
|
222
|
+
client: Client,
|
|
223
|
+
index_name: str,
|
|
224
|
+
documents: Any,
|
|
225
|
+
batch_size: int | None,
|
|
226
|
+
primary_key: str | None,
|
|
227
|
+
wait_for_task: bool,
|
|
228
|
+
) -> None:
|
|
229
|
+
index = client.index(index_name)
|
|
230
|
+
if not batch_size:
|
|
231
|
+
task = index.add_documents(documents, primary_key)
|
|
232
|
+
if wait_for_task:
|
|
233
|
+
client.wait_for_task(task.task_uid, timeout_in_ms=None)
|
|
234
|
+
return
|
|
235
|
+
|
|
236
|
+
tasks = index.add_documents_in_batches(
|
|
237
|
+
documents, batch_size=batch_size, primary_key=primary_key
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
if wait_for_task:
|
|
241
|
+
for task in tasks:
|
|
242
|
+
client.wait_for_task(task.task_uid)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from httpx import Response
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class BatchNotFoundError(Exception):
|
|
5
|
+
pass
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class InvalidDocumentError(Exception):
|
|
9
|
+
"""Error for documents that are not in a valid format for Meilisearch."""
|
|
10
|
+
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class InvalidRestriction(Exception):
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class MeilisearchError(Exception):
|
|
19
|
+
"""Generic class for Meilisearch error handling."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, message: str) -> None:
|
|
22
|
+
self.message = message
|
|
23
|
+
super().__init__(self.message)
|
|
24
|
+
|
|
25
|
+
def __str__(self) -> str:
|
|
26
|
+
return f"MeilisearchError. Error message: {self.message}."
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class MeilisearchApiError(MeilisearchError):
|
|
30
|
+
"""Error sent by Meilisearch API."""
|
|
31
|
+
|
|
32
|
+
def __init__(self, error: str, response: Response) -> None:
|
|
33
|
+
self.status_code = response.status_code
|
|
34
|
+
self.code = ""
|
|
35
|
+
self.message = ""
|
|
36
|
+
self.link = ""
|
|
37
|
+
self.error_type = ""
|
|
38
|
+
if response.content:
|
|
39
|
+
self.message = f"Error message: {response.json().get('message') or ''}"
|
|
40
|
+
self.code = f"{response.json().get('code') or ''}"
|
|
41
|
+
self.error_type = f"{response.json().get('type') or ''}"
|
|
42
|
+
self.link = f"Error documentation: {response.json().get('link') or ''}"
|
|
43
|
+
else:
|
|
44
|
+
self.message = error
|
|
45
|
+
super().__init__(self.message)
|
|
46
|
+
|
|
47
|
+
def __str__(self) -> str:
|
|
48
|
+
return f"MeilisearchApiError.{self.code} {self.message} {self.error_type} {self.link}"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class MeilisearchCommunicationError(MeilisearchError):
|
|
52
|
+
"""Error when connecting to Meilisearch."""
|
|
53
|
+
|
|
54
|
+
def __str__(self) -> str:
|
|
55
|
+
return f"MeilisearchCommunicationError, {self.message}"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class MeilisearchTaskFailedError(MeilisearchError):
|
|
59
|
+
"""Error when a task is in the failed status."""
|
|
60
|
+
|
|
61
|
+
def __str__(self) -> str:
|
|
62
|
+
return f"MeilisearchTaskFailedError, {self.message}"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class MeilisearchTimeoutError(MeilisearchError):
|
|
66
|
+
"""Error when Meilisearch operation takes longer than expected."""
|
|
67
|
+
|
|
68
|
+
def __str__(self) -> str:
|
|
69
|
+
return f"MeilisearchTimeoutError, {self.message}"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class PayloadTooLarge(Exception):
|
|
73
|
+
"""Error when the payload is larger than the allowed payload size."""
|
|
74
|
+
|
|
75
|
+
pass
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Generator, MutableMapping, Sequence
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
7
|
+
from urllib.parse import urlencode
|
|
8
|
+
|
|
9
|
+
from meilisearch_python_sdk._utils import iso_to_date_time
|
|
10
|
+
from meilisearch_python_sdk.errors import MeilisearchError
|
|
11
|
+
from meilisearch_python_sdk.json_handler import BuiltinHandler, OrjsonHandler, UjsonHandler
|
|
12
|
+
from meilisearch_python_sdk.models.search import (
|
|
13
|
+
Hybrid,
|
|
14
|
+
)
|
|
15
|
+
from meilisearch_python_sdk.models.settings import (
|
|
16
|
+
CompositeEmbedder,
|
|
17
|
+
Embedders,
|
|
18
|
+
HuggingFaceEmbedder,
|
|
19
|
+
OllamaEmbedder,
|
|
20
|
+
OpenAiEmbedder,
|
|
21
|
+
RestEmbedder,
|
|
22
|
+
UserProvidedEmbedder,
|
|
23
|
+
)
|
|
24
|
+
from meilisearch_python_sdk.plugins import (
|
|
25
|
+
AsyncDocumentPlugin,
|
|
26
|
+
AsyncPlugin,
|
|
27
|
+
AsyncPostSearchPlugin,
|
|
28
|
+
DocumentPlugin,
|
|
29
|
+
Plugin,
|
|
30
|
+
PostSearchPlugin,
|
|
31
|
+
)
|
|
32
|
+
from meilisearch_python_sdk.types import JsonDict
|
|
33
|
+
|
|
34
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
35
|
+
import sys
|
|
36
|
+
|
|
37
|
+
from meilisearch_python_sdk.types import Filter, JsonMapping
|
|
38
|
+
|
|
39
|
+
if sys.version_info >= (3, 11):
|
|
40
|
+
pass
|
|
41
|
+
else:
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class BaseIndex:
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
uid: str,
|
|
49
|
+
primary_key: str | None = None,
|
|
50
|
+
created_at: str | datetime | None = None,
|
|
51
|
+
updated_at: str | datetime | None = None,
|
|
52
|
+
json_handler: BuiltinHandler | OrjsonHandler | UjsonHandler | None = None,
|
|
53
|
+
hits_type: Any = JsonDict,
|
|
54
|
+
):
|
|
55
|
+
self.uid = uid
|
|
56
|
+
self.primary_key = primary_key
|
|
57
|
+
self.created_at: datetime | None = iso_to_date_time(created_at)
|
|
58
|
+
self.updated_at: datetime | None = iso_to_date_time(updated_at)
|
|
59
|
+
self.hits_type = hits_type
|
|
60
|
+
self._base_url = "indexes/"
|
|
61
|
+
self._base_url_with_uid = f"{self._base_url}{self.uid}"
|
|
62
|
+
self._documents_url = f"{self._base_url_with_uid}/documents"
|
|
63
|
+
self._stats_url = f"{self._base_url_with_uid}/stats"
|
|
64
|
+
self._settings_url = f"{self._base_url_with_uid}/settings"
|
|
65
|
+
self._json_handler = json_handler if json_handler else BuiltinHandler()
|
|
66
|
+
|
|
67
|
+
def __str__(self) -> str:
|
|
68
|
+
return f"{type(self).__name__}(uid={self.uid}, primary_key={self.primary_key}, created_at={self.created_at}, updated_at={self.updated_at})"
|
|
69
|
+
|
|
70
|
+
def __repr__(self) -> str:
|
|
71
|
+
return f"{type(self).__name__}(uid={self.uid!r}, primary_key={self.primary_key!r}, created_at={self.created_at!r}, updated_at={self.updated_at!r})"
|
|
72
|
+
|
|
73
|
+
def _set_fetch_info(
|
|
74
|
+
self, primary_key: str, created_at_iso_str: str, updated_at_iso_str: str
|
|
75
|
+
) -> None:
|
|
76
|
+
self.primary_key = primary_key
|
|
77
|
+
self.created_at = iso_to_date_time(created_at_iso_str)
|
|
78
|
+
self.updated_at = iso_to_date_time(updated_at_iso_str)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def batch(
|
|
82
|
+
documents: Sequence[MutableMapping], batch_size: int
|
|
83
|
+
) -> Generator[Sequence[MutableMapping], None, None]:
|
|
84
|
+
total_len = len(documents)
|
|
85
|
+
for i in range(0, total_len, batch_size):
|
|
86
|
+
yield documents[i : i + batch_size]
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def combine_documents(documents: list[list[Any]]) -> list[Any]:
|
|
90
|
+
return [x for y in documents for x in y]
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def plugin_has_method(
|
|
94
|
+
plugin: AsyncPlugin
|
|
95
|
+
| AsyncDocumentPlugin
|
|
96
|
+
| AsyncPostSearchPlugin
|
|
97
|
+
| Plugin
|
|
98
|
+
| DocumentPlugin
|
|
99
|
+
| PostSearchPlugin,
|
|
100
|
+
method: str,
|
|
101
|
+
) -> bool:
|
|
102
|
+
check = getattr(plugin, method, None)
|
|
103
|
+
if callable(check):
|
|
104
|
+
return True
|
|
105
|
+
|
|
106
|
+
return False
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def raise_on_no_documents(
|
|
110
|
+
documents: list[Any], document_type: str, directory_path: str | Path
|
|
111
|
+
) -> None:
|
|
112
|
+
if not documents:
|
|
113
|
+
raise MeilisearchError(f"No {document_type} files found in {directory_path}")
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def process_search_parameters(
|
|
117
|
+
*,
|
|
118
|
+
q: str | None = None,
|
|
119
|
+
facet_name: str | None = None,
|
|
120
|
+
facet_query: str | None = None,
|
|
121
|
+
offset: int = 0,
|
|
122
|
+
limit: int = 20,
|
|
123
|
+
filter: Filter | None = None,
|
|
124
|
+
facets: list[str] | None = None,
|
|
125
|
+
attributes_to_retrieve: list[str] | None = None,
|
|
126
|
+
attributes_to_crop: list[str] | None = None,
|
|
127
|
+
crop_length: int = 200,
|
|
128
|
+
attributes_to_highlight: list[str] | None = None,
|
|
129
|
+
sort: list[str] | None = None,
|
|
130
|
+
show_matches_position: bool = False,
|
|
131
|
+
highlight_pre_tag: str = "<em>",
|
|
132
|
+
highlight_post_tag: str = "</em>",
|
|
133
|
+
crop_marker: str = "...",
|
|
134
|
+
matching_strategy: Literal["all", "last", "frequency"] = "last",
|
|
135
|
+
hits_per_page: int | None = None,
|
|
136
|
+
page: int | None = None,
|
|
137
|
+
attributes_to_search_on: list[str] | None = None,
|
|
138
|
+
distinct: str | None = None,
|
|
139
|
+
show_ranking_score: bool = False,
|
|
140
|
+
show_ranking_score_details: bool = False,
|
|
141
|
+
ranking_score_threshold: float | None = None,
|
|
142
|
+
vector: list[float] | None = None,
|
|
143
|
+
hybrid: Hybrid | None = None,
|
|
144
|
+
locales: list[str] | None = None,
|
|
145
|
+
retrieve_vectors: bool | None = None,
|
|
146
|
+
exhaustive_facet_count: bool | None = None,
|
|
147
|
+
media: JsonMapping | None = None,
|
|
148
|
+
) -> JsonDict:
|
|
149
|
+
if attributes_to_retrieve is None:
|
|
150
|
+
attributes_to_retrieve = ["*"]
|
|
151
|
+
|
|
152
|
+
body: JsonDict = {
|
|
153
|
+
"q": q,
|
|
154
|
+
"offset": offset,
|
|
155
|
+
"limit": limit,
|
|
156
|
+
"filter": filter,
|
|
157
|
+
"facets": facets,
|
|
158
|
+
"attributesToRetrieve": attributes_to_retrieve,
|
|
159
|
+
"attributesToCrop": attributes_to_crop,
|
|
160
|
+
"cropLength": crop_length,
|
|
161
|
+
"attributesToHighlight": attributes_to_highlight,
|
|
162
|
+
"sort": sort,
|
|
163
|
+
"showMatchesPosition": show_matches_position,
|
|
164
|
+
"highlightPreTag": highlight_pre_tag,
|
|
165
|
+
"highlightPostTag": highlight_post_tag,
|
|
166
|
+
"cropMarker": crop_marker,
|
|
167
|
+
"matchingStrategy": matching_strategy,
|
|
168
|
+
"hitsPerPage": hits_per_page,
|
|
169
|
+
"page": page,
|
|
170
|
+
"attributesToSearchOn": attributes_to_search_on,
|
|
171
|
+
"showRankingScore": show_ranking_score,
|
|
172
|
+
"rankingScoreThreshold": ranking_score_threshold,
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if facet_name:
|
|
176
|
+
body["facetName"] = facet_name
|
|
177
|
+
|
|
178
|
+
if facet_query:
|
|
179
|
+
body["facetQuery"] = facet_query
|
|
180
|
+
|
|
181
|
+
if distinct:
|
|
182
|
+
body["distinct"] = distinct
|
|
183
|
+
|
|
184
|
+
if show_ranking_score_details:
|
|
185
|
+
body["showRankingScoreDetails"] = show_ranking_score_details
|
|
186
|
+
|
|
187
|
+
if vector:
|
|
188
|
+
body["vector"] = vector
|
|
189
|
+
|
|
190
|
+
if hybrid:
|
|
191
|
+
body["hybrid"] = hybrid.model_dump(by_alias=True)
|
|
192
|
+
|
|
193
|
+
if locales:
|
|
194
|
+
body["locales"] = locales
|
|
195
|
+
|
|
196
|
+
if retrieve_vectors is not None:
|
|
197
|
+
body["retrieveVectors"] = retrieve_vectors
|
|
198
|
+
|
|
199
|
+
if exhaustive_facet_count is not None:
|
|
200
|
+
body["exhaustivefacetCount"] = exhaustive_facet_count
|
|
201
|
+
|
|
202
|
+
if media is not None:
|
|
203
|
+
body["media"] = media
|
|
204
|
+
|
|
205
|
+
return body
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def build_encoded_url(base_url: str, params: JsonMapping) -> str:
|
|
209
|
+
return f"{base_url}?{urlencode(params)}"
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
# TODO: Add back after embedder setting issue fixed https://github.com/meilisearch/meilisearch/issues/4585
|
|
213
|
+
def embedder_json_to_embedders_model( # pragma: no cover
|
|
214
|
+
embedder_json: JsonDict | None,
|
|
215
|
+
) -> Embedders | None:
|
|
216
|
+
if not embedder_json: # pragma: no cover
|
|
217
|
+
return None
|
|
218
|
+
|
|
219
|
+
embedders: dict[
|
|
220
|
+
str,
|
|
221
|
+
OpenAiEmbedder
|
|
222
|
+
| HuggingFaceEmbedder
|
|
223
|
+
| OllamaEmbedder
|
|
224
|
+
| RestEmbedder
|
|
225
|
+
| UserProvidedEmbedder
|
|
226
|
+
| CompositeEmbedder,
|
|
227
|
+
] = {}
|
|
228
|
+
for k, v in embedder_json.items():
|
|
229
|
+
if v.get("source") == "openAi":
|
|
230
|
+
embedders[k] = OpenAiEmbedder(**v)
|
|
231
|
+
elif v.get("source") == "huggingFace":
|
|
232
|
+
embedders[k] = HuggingFaceEmbedder(**v)
|
|
233
|
+
elif v.get("source") == "ollama":
|
|
234
|
+
embedders[k] = OllamaEmbedder(**v)
|
|
235
|
+
elif v.get("source") == "rest":
|
|
236
|
+
embedders[k] = RestEmbedder(**v)
|
|
237
|
+
elif v.get("source") == "composit":
|
|
238
|
+
embedders[k] = CompositeEmbedder(**v)
|
|
239
|
+
else:
|
|
240
|
+
embedders[k] = UserProvidedEmbedder(**v)
|
|
241
|
+
|
|
242
|
+
return Embedders(embedders=embedders)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
# TODO: Add back after embedder setting issue fixed https://github.com/meilisearch/meilisearch/issues/4585
|
|
246
|
+
def embedder_json_to_settings_model( # pragma: no cover
|
|
247
|
+
embedder_json: JsonDict | None,
|
|
248
|
+
) -> (
|
|
249
|
+
dict[
|
|
250
|
+
str,
|
|
251
|
+
OpenAiEmbedder
|
|
252
|
+
| HuggingFaceEmbedder
|
|
253
|
+
| OllamaEmbedder
|
|
254
|
+
| RestEmbedder
|
|
255
|
+
| UserProvidedEmbedder
|
|
256
|
+
| CompositeEmbedder,
|
|
257
|
+
]
|
|
258
|
+
| None
|
|
259
|
+
):
|
|
260
|
+
if not embedder_json: # pragma: no cover
|
|
261
|
+
return None
|
|
262
|
+
|
|
263
|
+
embedders: dict[
|
|
264
|
+
str,
|
|
265
|
+
OpenAiEmbedder
|
|
266
|
+
| HuggingFaceEmbedder
|
|
267
|
+
| OllamaEmbedder
|
|
268
|
+
| RestEmbedder
|
|
269
|
+
| UserProvidedEmbedder
|
|
270
|
+
| CompositeEmbedder,
|
|
271
|
+
] = {}
|
|
272
|
+
for k, v in embedder_json.items():
|
|
273
|
+
if v.get("source") == "openAi":
|
|
274
|
+
embedders[k] = OpenAiEmbedder(**v)
|
|
275
|
+
elif v.get("source") == "huggingFace":
|
|
276
|
+
embedders[k] = HuggingFaceEmbedder(**v)
|
|
277
|
+
elif v.get("source") == "ollama":
|
|
278
|
+
embedders[k] = OllamaEmbedder(**v)
|
|
279
|
+
elif v.get("source") == "rest":
|
|
280
|
+
embedders[k] = RestEmbedder(**v)
|
|
281
|
+
elif v.get("source") == "composit":
|
|
282
|
+
embedders[k] = CompositeEmbedder(**v)
|
|
283
|
+
else:
|
|
284
|
+
embedders[k] = UserProvidedEmbedder(**v)
|
|
285
|
+
|
|
286
|
+
return embedders
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def validate_file_type(file_path: Path) -> None:
|
|
290
|
+
if file_path.suffix not in (".json", ".csv", ".ndjson"):
|
|
291
|
+
raise MeilisearchError("File must be a json, ndjson, or csv file")
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def validate_ranking_score_threshold(ranking_score_threshold: float) -> None:
|
|
295
|
+
if not 0.0 <= ranking_score_threshold <= 1.0:
|
|
296
|
+
raise MeilisearchError("ranking_score_threshold must be between 0.0 and 1.0")
|