Skip to content

Commit 9c14abe

Browse files
authored
Merge branch 'main' into meili-bot/bump-version
2 parents f548834 + 60b5625 commit 9c14abe

File tree

5 files changed

+132
-27
lines changed

5 files changed

+132
-27
lines changed

meilisearch/errors.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
from __future__ import annotations
22

33
import json
4+
from functools import wraps
5+
from typing import TYPE_CHECKING, Any, Callable
46

57
from requests import Response
68

9+
if TYPE_CHECKING:
10+
from meilisearch.client import Client
11+
from meilisearch.index import Index
12+
from meilisearch.task import TaskHandler
13+
714

815
class MeilisearchError(Exception):
916
"""Generic class for Meilisearch error handling"""
@@ -54,3 +61,15 @@ class MeilisearchTimeoutError(MeilisearchError):
5461

5562
def __str__(self) -> str:
5663
return f"MeilisearchTimeoutError, {self.message}"
64+
65+
66+
def version_error_hint_message(func: Callable) -> Any:
67+
@wraps(func)
68+
def wrapper(*args: Any, **kwargs: Any) -> Any:
69+
try:
70+
return func(*args, **kwargs)
71+
except MeilisearchApiError as exc:
72+
exc.message = f"{exc.message}. Hint: It might not be working because you're not up to date with the Meilisearch version that {func.__name__} call requires."
73+
raise exc
74+
75+
return wrapper

meilisearch/index.py

Lines changed: 46 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
from datetime import datetime
44
from typing import Any, Dict, Generator, List, Optional, Union
55
from urllib import parse
6+
from warnings import warn
67

78
from meilisearch._httprequests import HttpRequests
89
from meilisearch.config import Config
10+
from meilisearch.errors import version_error_hint_message
911
from meilisearch.models.document import Document, DocumentsResults
1012
from meilisearch.models.index import Faceting, IndexStats, Pagination, TypoTolerance
1113
from meilisearch.models.task import Task, TaskInfo, TaskResults
@@ -301,13 +303,15 @@ def get_document(
301303
)
302304
return Document(document)
303305

306+
@version_error_hint_message
304307
def get_documents(self, parameters: Optional[Dict[str, Any]] = None) -> DocumentsResults:
305308
"""Get a set of documents from the index.
306309
307310
Parameters
308311
----------
309312
parameters (optional):
310313
parameters accepted by the get documents route: https://www.meilisearch.com/docs/reference/api/documents#get-documents
314+
Note: The filter parameter is only available in Meilisearch >= 1.2.0.
311315
312316
Returns
313317
-------
@@ -323,13 +327,20 @@ def get_documents(self, parameters: Optional[Dict[str, Any]] = None) -> Document
323327
MeilisearchApiError
324328
An error containing details about why Meilisearch can't process your request. Meilisearch error codes are described here: https://www.meilisearch.com/docs/reference/errors/error_codes#meilisearch-errors
325329
"""
326-
if parameters is None:
327-
parameters = {}
328-
elif "fields" in parameters and isinstance(parameters["fields"], list):
329-
parameters["fields"] = ",".join(parameters["fields"])
330+
if parameters is None or parameters.get("filter") is None:
331+
if parameters is None:
332+
parameters = {}
333+
elif "fields" in parameters and isinstance(parameters["fields"], list):
334+
parameters["fields"] = ",".join(parameters["fields"])
335+
336+
response = self.http.get(
337+
f"{self.config.paths.index}/{self.uid}/{self.config.paths.document}?{parse.urlencode(parameters)}"
338+
)
339+
return DocumentsResults(response)
330340

331-
response = self.http.get(
332-
f"{self.config.paths.index}/{self.uid}/{self.config.paths.document}?{parse.urlencode(parameters)}"
341+
response = self.http.post(
342+
f"{self.config.paths.index}/{self.uid}/{self.config.paths.document}/fetch",
343+
body=parameters,
333344
)
334345
return DocumentsResults(response)
335346

@@ -729,13 +740,24 @@ def delete_document(self, document_id: Union[str, int]) -> TaskInfo:
729740
)
730741
return TaskInfo(**response)
731742

732-
def delete_documents(self, ids: List[Union[str, int]]) -> TaskInfo:
733-
"""Delete multiple documents from the index.
743+
@version_error_hint_message
744+
def delete_documents(
745+
self,
746+
ids: Optional[List[Union[str, int]]] = None,
747+
*,
748+
filter: Optional[ # pylint: disable=redefined-builtin
749+
Union[str, List[Union[str, List[str]]]]
750+
] = None,
751+
) -> TaskInfo:
752+
"""Delete multiple documents from the index by id or filter.
734753
735754
Parameters
736755
----------
737-
list:
738-
List of unique identifiers of documents.
756+
ids:
757+
List of unique identifiers of documents. Note: using ids is depreciated and will be
758+
removed in a future version.
759+
filter:
760+
The filter value information.
739761
740762
Returns
741763
-------
@@ -748,10 +770,20 @@ def delete_documents(self, ids: List[Union[str, int]]) -> TaskInfo:
748770
MeilisearchApiError
749771
An error containing details about why Meilisearch can't process your request. Meilisearch error codes are described here: https://www.meilisearch.com/docs/reference/errors/error_codes#meilisearch-errors
750772
"""
751-
response = self.http.post(
752-
f"{self.config.paths.index}/{self.uid}/{self.config.paths.document}/delete-batch",
753-
[str(i) for i in ids],
754-
)
773+
if ids:
774+
warn(
775+
"The use of ids is depreciated and will be removed in the future",
776+
DeprecationWarning,
777+
)
778+
response = self.http.post(
779+
f"{self.config.paths.index}/{self.uid}/{self.config.paths.document}/delete-batch",
780+
[str(i) for i in ids],
781+
)
782+
else:
783+
response = self.http.post(
784+
f"{self.config.paths.index}/{self.uid}/{self.config.paths.document}/delete",
785+
body={"filter": filter},
786+
)
755787
return TaskInfo(**response)
756788

757789
def delete_all_documents(self) -> TaskInfo:

meilisearch/models/document.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
from __future__ import annotations
2-
3-
from typing import Any, Dict, Iterator
1+
from typing import Any, Dict, Iterator, List
42

53

64
class Document:
@@ -22,7 +20,7 @@ def __iter__(self) -> Iterator:
2220

2321
class DocumentsResults:
2422
def __init__(self, resp: Dict[str, Any]) -> None:
25-
self.results: list[Document] = [Document(doc) for doc in resp["results"]]
23+
self.results: List[Document] = [Document(doc) for doc in resp["results"]]
2624
self.offset: int = resp["offset"]
2725
self.limit: int = resp["limit"]
2826
self.total: int = resp["total"]

tests/errors/test_api_error_meilisearch.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import requests
77

88
import meilisearch
9-
from meilisearch.errors import MeilisearchApiError
9+
from meilisearch.errors import MeilisearchApiError, version_error_hint_message
1010
from tests import BASE_URL, MASTER_KEY
1111

1212

@@ -33,3 +33,22 @@ def test_meilisearch_api_error_no_code(mock_post):
3333
with pytest.raises(MeilisearchApiError):
3434
client = meilisearch.Client(BASE_URL, MASTER_KEY + "123")
3535
client.create_index("some_index")
36+
37+
38+
def test_version_error_hint_message():
39+
mock_response = requests.models.Response()
40+
mock_response.status_code = 408
41+
42+
class FakeClass:
43+
@version_error_hint_message
44+
def test_method(self):
45+
raise MeilisearchApiError("This is a test", mock_response)
46+
47+
with pytest.raises(MeilisearchApiError) as e:
48+
fake = FakeClass()
49+
fake.test_method()
50+
51+
assert (
52+
"MeilisearchApiError. This is a test. Hint: It might not be working because you're not up to date with the Meilisearch version that test_method call requires."
53+
== str(e.value)
54+
)

tests/index/test_index_document_meilisearch.py

Lines changed: 45 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# pylint: disable=invalid-name
22

33
from math import ceil
4+
from warnings import catch_warnings
45

56
import pytest
67

@@ -102,6 +103,26 @@ def test_get_documents_offset_optional_params(index_with_documents):
102103
assert response_offset_limit.results[0].title == response.results[1].title
103104

104105

106+
def test_get_documents_filter(index_with_documents):
107+
index = index_with_documents()
108+
response = index.update_filterable_attributes(["genre"])
109+
index.wait_for_task(response.task_uid)
110+
response = index.get_documents({"filter": "genre=action"})
111+
genres = {x.genre for x in response.results}
112+
assert len(genres) == 1
113+
assert next(iter(genres)) == "action"
114+
115+
116+
def test_get_documents_filter_with_fields(index_with_documents):
117+
index = index_with_documents()
118+
response = index.update_filterable_attributes(["genre"])
119+
index.wait_for_task(response.task_uid)
120+
response = index.get_documents({"fields": ["genre"], "filter": "genre=action"})
121+
genres = {x.genre for x in response.results}
122+
assert len(genres) == 1
123+
assert next(iter(genres)) == "action"
124+
125+
105126
def test_update_documents(index_with_documents, small_movies):
106127
"""Tests updating a single document and a set of documents."""
107128
index = index_with_documents()
@@ -160,17 +181,33 @@ def test_delete_document(index_with_documents):
160181
index.get_document("500682")
161182

162183

163-
def test_delete_documents(index_with_documents):
184+
def test_delete_documents_by_id(index_with_documents):
164185
"""Tests deleting a set of documents."""
165-
to_delete = [522681, "450465", 329996]
186+
with catch_warnings(record=True) as w:
187+
to_delete = [522681, "450465", 329996]
188+
index = index_with_documents()
189+
response = index.delete_documents(to_delete)
190+
assert isinstance(response, TaskInfo)
191+
assert response.task_uid is not None
192+
index.wait_for_task(response.task_uid)
193+
for document in to_delete:
194+
with pytest.raises(Exception):
195+
index.get_document(document)
196+
assert "The use of ids is depreciated" in str(w[0].message)
197+
198+
199+
def test_delete_documents(index_with_documents):
166200
index = index_with_documents()
167-
response = index.delete_documents(to_delete)
168-
assert isinstance(response, TaskInfo)
169-
assert response.task_uid is not None
201+
response = index.update_filterable_attributes(["genre"])
202+
index.wait_for_task(response.task_uid)
203+
response = index.get_documents()
204+
assert "action" in ([x.__dict__.get("genre") for x in response.results])
205+
response = index.delete_documents(filter="genre=action")
170206
index.wait_for_task(response.task_uid)
171-
for document in to_delete:
172-
with pytest.raises(Exception):
173-
index.get_document(document)
207+
response = index.get_documents()
208+
genres = [x.__dict__.get("genre") for x in response.results]
209+
assert "action" not in genres
210+
assert "cartoon" in genres
174211

175212

176213
def test_delete_all_documents(index_with_documents):

0 commit comments

Comments
 (0)