kiarina-lib-redisearch 1.0.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.
- kiarina/lib/redisearch/__init__.py +35 -0
- kiarina/lib/redisearch/_async/__init__.py +0 -0
- kiarina/lib/redisearch/_async/client.py +181 -0
- kiarina/lib/redisearch/_async/registry.py +16 -0
- kiarina/lib/redisearch/_core/__init__.py +0 -0
- kiarina/lib/redisearch/_core/context.py +69 -0
- kiarina/lib/redisearch/_core/operations/__init__.py +0 -0
- kiarina/lib/redisearch/_core/operations/count.py +55 -0
- kiarina/lib/redisearch/_core/operations/create_index.py +52 -0
- kiarina/lib/redisearch/_core/operations/delete.py +43 -0
- kiarina/lib/redisearch/_core/operations/drop_index.py +59 -0
- kiarina/lib/redisearch/_core/operations/exists_index.py +56 -0
- kiarina/lib/redisearch/_core/operations/find.py +105 -0
- kiarina/lib/redisearch/_core/operations/get.py +61 -0
- kiarina/lib/redisearch/_core/operations/get_info.py +155 -0
- kiarina/lib/redisearch/_core/operations/get_key.py +8 -0
- kiarina/lib/redisearch/_core/operations/migrate_index.py +160 -0
- kiarina/lib/redisearch/_core/operations/reset_index.py +60 -0
- kiarina/lib/redisearch/_core/operations/search.py +111 -0
- kiarina/lib/redisearch/_core/operations/set.py +65 -0
- kiarina/lib/redisearch/_core/utils/__init__.py +0 -0
- kiarina/lib/redisearch/_core/utils/calc_score.py +35 -0
- kiarina/lib/redisearch/_core/utils/marshal_mappings.py +57 -0
- kiarina/lib/redisearch/_core/utils/parse_search_result.py +57 -0
- kiarina/lib/redisearch/_core/utils/unmarshal_mappings.py +57 -0
- kiarina/lib/redisearch/_core/views/__init__.py +0 -0
- kiarina/lib/redisearch/_core/views/document.py +25 -0
- kiarina/lib/redisearch/_core/views/info_result.py +24 -0
- kiarina/lib/redisearch/_core/views/search_result.py +31 -0
- kiarina/lib/redisearch/_sync/__init__.py +0 -0
- kiarina/lib/redisearch/_sync/client.py +179 -0
- kiarina/lib/redisearch/_sync/registry.py +16 -0
- kiarina/lib/redisearch/asyncio.py +33 -0
- kiarina/lib/redisearch/filter/__init__.py +61 -0
- kiarina/lib/redisearch/filter/_decorators.py +28 -0
- kiarina/lib/redisearch/filter/_enums.py +28 -0
- kiarina/lib/redisearch/filter/_field/__init__.py +5 -0
- kiarina/lib/redisearch/filter/_field/base.py +67 -0
- kiarina/lib/redisearch/filter/_field/numeric.py +178 -0
- kiarina/lib/redisearch/filter/_field/tag.py +142 -0
- kiarina/lib/redisearch/filter/_field/text.py +111 -0
- kiarina/lib/redisearch/filter/_model.py +93 -0
- kiarina/lib/redisearch/filter/_registry.py +153 -0
- kiarina/lib/redisearch/filter/_types.py +32 -0
- kiarina/lib/redisearch/filter/_utils.py +18 -0
- kiarina/lib/redisearch/py.typed +0 -0
- kiarina/lib/redisearch/schema/__init__.py +25 -0
- kiarina/lib/redisearch/schema/_field/__init__.py +0 -0
- kiarina/lib/redisearch/schema/_field/base.py +20 -0
- kiarina/lib/redisearch/schema/_field/numeric.py +33 -0
- kiarina/lib/redisearch/schema/_field/tag.py +46 -0
- kiarina/lib/redisearch/schema/_field/text.py +44 -0
- kiarina/lib/redisearch/schema/_field/vector/__init__.py +0 -0
- kiarina/lib/redisearch/schema/_field/vector/base.py +61 -0
- kiarina/lib/redisearch/schema/_field/vector/flat.py +40 -0
- kiarina/lib/redisearch/schema/_field/vector/hnsw.py +53 -0
- kiarina/lib/redisearch/schema/_model.py +98 -0
- kiarina/lib/redisearch/schema/_types.py +16 -0
- kiarina/lib/redisearch/settings.py +47 -0
- kiarina_lib_redisearch-1.0.0.dist-info/METADATA +886 -0
- kiarina_lib_redisearch-1.0.0.dist-info/RECORD +62 -0
- kiarina_lib_redisearch-1.0.0.dist-info/WHEEL +4 -0
@@ -0,0 +1,57 @@
|
|
1
|
+
from typing import Any
|
2
|
+
|
3
|
+
import numpy as np
|
4
|
+
|
5
|
+
from ...schema import RedisearchSchema
|
6
|
+
|
7
|
+
|
8
|
+
def marshal_mappings(
|
9
|
+
*,
|
10
|
+
schema: RedisearchSchema,
|
11
|
+
mapping: dict[str, Any],
|
12
|
+
) -> dict[str, Any]:
|
13
|
+
"""
|
14
|
+
Convert the mapping to the appropriate format based on the schema
|
15
|
+
"""
|
16
|
+
marshaled: dict[str, Any] = {}
|
17
|
+
|
18
|
+
for key, value in mapping.items():
|
19
|
+
field = schema.get_field(key)
|
20
|
+
|
21
|
+
if not field:
|
22
|
+
marshaled[key] = value
|
23
|
+
continue
|
24
|
+
|
25
|
+
if field.type == "tag":
|
26
|
+
if isinstance(value, (list, tuple)):
|
27
|
+
if not field.multiple:
|
28
|
+
raise ValueError(
|
29
|
+
f"Field '{key}' does not allow multiple tags. Got: {value}"
|
30
|
+
)
|
31
|
+
|
32
|
+
marshaled[key] = field.separator.join(str(v) for v in value)
|
33
|
+
else:
|
34
|
+
marshaled[key] = str(value)
|
35
|
+
|
36
|
+
elif field.type == "numeric":
|
37
|
+
try:
|
38
|
+
marshaled[key] = float(value)
|
39
|
+
except (TypeError, ValueError):
|
40
|
+
raise ValueError(
|
41
|
+
f"Field '{key}' requires a numeric value. Got: {value}"
|
42
|
+
)
|
43
|
+
|
44
|
+
elif field.type == "text":
|
45
|
+
marshaled[key] = str(value)
|
46
|
+
|
47
|
+
elif field.type == "vector":
|
48
|
+
if not isinstance(value, list) or not all(
|
49
|
+
isinstance(v, (float, int)) for v in value
|
50
|
+
):
|
51
|
+
raise ValueError(
|
52
|
+
f"Field '{key}' requires a list of floats. Got: {value}"
|
53
|
+
)
|
54
|
+
|
55
|
+
marshaled[key] = np.array(value).astype(field.dtype).tobytes()
|
56
|
+
|
57
|
+
return marshaled
|
@@ -0,0 +1,57 @@
|
|
1
|
+
from redis.commands.search.result import Result
|
2
|
+
|
3
|
+
from ...schema import RedisearchSchema
|
4
|
+
from ..views.document import Document
|
5
|
+
from ..views.search_result import SearchResult
|
6
|
+
from .calc_score import calc_score
|
7
|
+
|
8
|
+
|
9
|
+
def parse_search_result(
|
10
|
+
*,
|
11
|
+
key_prefix: str,
|
12
|
+
schema: RedisearchSchema,
|
13
|
+
return_fields: list[str] | None,
|
14
|
+
result: Result,
|
15
|
+
) -> SearchResult:
|
16
|
+
documents: list[Document] = []
|
17
|
+
|
18
|
+
for doc in result.docs:
|
19
|
+
# key
|
20
|
+
key: str = doc.id
|
21
|
+
|
22
|
+
if not key.startswith(key_prefix):
|
23
|
+
raise ValueError(
|
24
|
+
f"Document ID {doc.id} does not start with key prefix {key_prefix}"
|
25
|
+
)
|
26
|
+
|
27
|
+
# id
|
28
|
+
id = key[len(key_prefix) :]
|
29
|
+
|
30
|
+
# mapping
|
31
|
+
mapping = doc.__dict__
|
32
|
+
|
33
|
+
if "id" in mapping:
|
34
|
+
if "id" in (return_fields or []):
|
35
|
+
mapping["id"] = id
|
36
|
+
else:
|
37
|
+
mapping.pop("id")
|
38
|
+
|
39
|
+
mapping.pop("payload", None)
|
40
|
+
|
41
|
+
# score
|
42
|
+
score = 0.0
|
43
|
+
|
44
|
+
if distance := mapping.pop("distance", None):
|
45
|
+
score = calc_score(
|
46
|
+
float(distance),
|
47
|
+
datatype=schema.vector_field.datatype,
|
48
|
+
distance_metric=schema.vector_field.distance_metric,
|
49
|
+
)
|
50
|
+
|
51
|
+
documents.append(Document(key=key, id=id, score=score, mapping=mapping))
|
52
|
+
|
53
|
+
return SearchResult(
|
54
|
+
total=result.total,
|
55
|
+
duration=result.duration,
|
56
|
+
documents=documents,
|
57
|
+
)
|
@@ -0,0 +1,57 @@
|
|
1
|
+
from typing import Any
|
2
|
+
|
3
|
+
import numpy as np
|
4
|
+
|
5
|
+
from ...schema import RedisearchSchema
|
6
|
+
|
7
|
+
|
8
|
+
def unmarshal_mappings(
|
9
|
+
*,
|
10
|
+
schema: RedisearchSchema,
|
11
|
+
mapping: dict[bytes, bytes],
|
12
|
+
) -> dict[str, Any]:
|
13
|
+
"""
|
14
|
+
Convert the mapping from the appropriate format based on the schema
|
15
|
+
"""
|
16
|
+
unmarshaled: dict[str, Any] = {}
|
17
|
+
|
18
|
+
for bkey, value in mapping.items():
|
19
|
+
key = bkey.decode("utf-8")
|
20
|
+
|
21
|
+
field = schema.get_field(key)
|
22
|
+
|
23
|
+
if not field:
|
24
|
+
try:
|
25
|
+
unmarshaled[key] = _decode_numeric(value)
|
26
|
+
except ValueError:
|
27
|
+
unmarshaled[key] = value.decode("utf-8")
|
28
|
+
|
29
|
+
elif field.type == "tag":
|
30
|
+
if field.multiple:
|
31
|
+
unmarshaled[key] = value.decode("utf-8").split(field.separator)
|
32
|
+
else:
|
33
|
+
unmarshaled[key] = value.decode("utf-8").split(field.separator).pop(0)
|
34
|
+
|
35
|
+
elif field.type == "numeric":
|
36
|
+
unmarshaled[key] = _decode_numeric(value)
|
37
|
+
|
38
|
+
elif field.type == "text":
|
39
|
+
unmarshaled[key] = value.decode("utf-8")
|
40
|
+
|
41
|
+
elif field.type == "vector":
|
42
|
+
unmarshaled[key] = np.frombuffer(value, dtype=field.dtype).tolist()
|
43
|
+
|
44
|
+
return unmarshaled
|
45
|
+
|
46
|
+
|
47
|
+
def _decode_numeric(value: bytes) -> float | int:
|
48
|
+
try:
|
49
|
+
return int(value)
|
50
|
+
except ValueError:
|
51
|
+
pass
|
52
|
+
try:
|
53
|
+
return float(value)
|
54
|
+
except ValueError:
|
55
|
+
pass
|
56
|
+
|
57
|
+
raise ValueError(f"Cannot decode numeric value: {value.decode('utf-8')}")
|
File without changes
|
@@ -0,0 +1,25 @@
|
|
1
|
+
from typing import Any
|
2
|
+
|
3
|
+
from pydantic import BaseModel, Field
|
4
|
+
|
5
|
+
|
6
|
+
class Document(BaseModel):
|
7
|
+
"""
|
8
|
+
Redisearch Document
|
9
|
+
"""
|
10
|
+
|
11
|
+
key: str = ""
|
12
|
+
"""
|
13
|
+
Redis key
|
14
|
+
|
15
|
+
{key_prefix}:{id} is the key
|
16
|
+
"""
|
17
|
+
|
18
|
+
id: str = ""
|
19
|
+
"""Redisearch document ID"""
|
20
|
+
|
21
|
+
score: float = 0.0
|
22
|
+
"""Redisearch document score"""
|
23
|
+
|
24
|
+
mapping: dict[str, Any] = Field(default_factory=dict)
|
25
|
+
"""Redisearch document mapping"""
|
@@ -0,0 +1,24 @@
|
|
1
|
+
from pydantic import BaseModel
|
2
|
+
|
3
|
+
from ...schema import RedisearchSchema
|
4
|
+
|
5
|
+
|
6
|
+
class InfoResult(BaseModel):
|
7
|
+
"""
|
8
|
+
Model representing FT.INFO results
|
9
|
+
"""
|
10
|
+
|
11
|
+
index_name: str
|
12
|
+
"""Index name"""
|
13
|
+
|
14
|
+
num_docs: int
|
15
|
+
"""Number of documents"""
|
16
|
+
|
17
|
+
num_terms: int
|
18
|
+
"""Number of terms"""
|
19
|
+
|
20
|
+
num_records: int
|
21
|
+
"""Number of records"""
|
22
|
+
|
23
|
+
index_schema: RedisearchSchema
|
24
|
+
"""Index schema"""
|
@@ -0,0 +1,31 @@
|
|
1
|
+
from pydantic import BaseModel, Field
|
2
|
+
|
3
|
+
from .document import Document
|
4
|
+
|
5
|
+
|
6
|
+
class SearchResult(BaseModel):
|
7
|
+
"""
|
8
|
+
Model representing FT.SEARCH results
|
9
|
+
"""
|
10
|
+
|
11
|
+
total: int = 0
|
12
|
+
"""
|
13
|
+
Total number of results
|
14
|
+
|
15
|
+
For searches not using KNN (RedisearchClient.find):
|
16
|
+
This indicates the total number of documents matching the query,
|
17
|
+
regardless of the limit parameter.
|
18
|
+
|
19
|
+
For vector search using KNN (RedisearchClient.search):
|
20
|
+
When using KNN, the total in FT.SEARCH depends on the value of k.
|
21
|
+
If limit is not specified,
|
22
|
+
the number of records in the population after filtering will be total.
|
23
|
+
When a limit is specified, k becomes equal to the limit,
|
24
|
+
so the value of total is less than or equal to the limit.
|
25
|
+
"""
|
26
|
+
|
27
|
+
duration: float = 0.0
|
28
|
+
"""The execution time of the query in milliseconds"""
|
29
|
+
|
30
|
+
documents: list[Document] = Field(default_factory=list)
|
31
|
+
"""List of documents"""
|
File without changes
|
@@ -0,0 +1,179 @@
|
|
1
|
+
from typing import Any
|
2
|
+
|
3
|
+
from redis import Redis
|
4
|
+
|
5
|
+
from .._core.context import RedisearchContext
|
6
|
+
from .._core.operations.count import count
|
7
|
+
from .._core.operations.create_index import create_index
|
8
|
+
from .._core.operations.delete import delete
|
9
|
+
from .._core.operations.drop_index import drop_index
|
10
|
+
from .._core.operations.exists_index import exists_index
|
11
|
+
from .._core.operations.find import find
|
12
|
+
from .._core.operations.get import get
|
13
|
+
from .._core.operations.get_info import get_info
|
14
|
+
from .._core.operations.get_key import get_key
|
15
|
+
from .._core.operations.migrate_index import migrate_index
|
16
|
+
from .._core.operations.reset_index import reset_index
|
17
|
+
from .._core.operations.search import search
|
18
|
+
from .._core.operations.set import set
|
19
|
+
from .._core.views.document import Document
|
20
|
+
from .._core.views.info_result import InfoResult
|
21
|
+
from .._core.views.search_result import SearchResult
|
22
|
+
from ..filter import RedisearchFilter, RedisearchFilterConditions
|
23
|
+
from ..settings import RedisearchSettings
|
24
|
+
|
25
|
+
|
26
|
+
class RedisearchClient:
|
27
|
+
"""
|
28
|
+
Redisearch client
|
29
|
+
"""
|
30
|
+
|
31
|
+
def __init__(
|
32
|
+
self,
|
33
|
+
settings: RedisearchSettings,
|
34
|
+
*,
|
35
|
+
redis: Redis,
|
36
|
+
) -> None:
|
37
|
+
"""
|
38
|
+
Initialize the Redisearch client.
|
39
|
+
"""
|
40
|
+
if redis.get_encoder().decode_responses:
|
41
|
+
# As the vector field in Redisearch is expected to be handled as bytes in redis-py,
|
42
|
+
raise ValueError("Redis client must have decode_responses=False")
|
43
|
+
|
44
|
+
self.ctx: RedisearchContext = RedisearchContext(settings=settings, _redis=redis)
|
45
|
+
"""Redisearch context"""
|
46
|
+
|
47
|
+
# --------------------------------------------------
|
48
|
+
# Index operations
|
49
|
+
# --------------------------------------------------
|
50
|
+
|
51
|
+
def exists_index(self) -> bool:
|
52
|
+
"""
|
53
|
+
Check if the index exists.
|
54
|
+
"""
|
55
|
+
return exists_index("sync", self.ctx)
|
56
|
+
|
57
|
+
def create_index(self) -> None:
|
58
|
+
"""
|
59
|
+
Create the search index.
|
60
|
+
"""
|
61
|
+
create_index("sync", self.ctx)
|
62
|
+
|
63
|
+
def drop_index(self, *, delete_documents: bool = False) -> bool:
|
64
|
+
"""
|
65
|
+
Delete the index.
|
66
|
+
"""
|
67
|
+
return drop_index("sync", self.ctx, delete_documents=delete_documents)
|
68
|
+
|
69
|
+
def reset_index(self) -> None:
|
70
|
+
"""
|
71
|
+
Reset the search index.
|
72
|
+
"""
|
73
|
+
reset_index("sync", self.ctx)
|
74
|
+
|
75
|
+
def migrate_index(self) -> None:
|
76
|
+
"""
|
77
|
+
Migrate the search index.
|
78
|
+
"""
|
79
|
+
migrate_index("sync", self.ctx)
|
80
|
+
|
81
|
+
def get_info(self) -> InfoResult:
|
82
|
+
"""
|
83
|
+
Get index information using FT.INFO command.
|
84
|
+
"""
|
85
|
+
return get_info("sync", self.ctx)
|
86
|
+
|
87
|
+
# --------------------------------------------------
|
88
|
+
# Data operations
|
89
|
+
# --------------------------------------------------
|
90
|
+
|
91
|
+
def set(self, mapping: dict[str, Any], *, id: str | None = None) -> None:
|
92
|
+
"""
|
93
|
+
Set a hash value.
|
94
|
+
|
95
|
+
If no id is specified, the mapping must contain an "id" field.
|
96
|
+
"""
|
97
|
+
set("sync", self.ctx, mapping, id=id)
|
98
|
+
|
99
|
+
def delete(self, id: str) -> None:
|
100
|
+
"""
|
101
|
+
Delete a document from the index.
|
102
|
+
"""
|
103
|
+
delete("sync", self.ctx, id)
|
104
|
+
|
105
|
+
def get(self, id: str) -> Document | None:
|
106
|
+
"""
|
107
|
+
Get a document from the index.
|
108
|
+
"""
|
109
|
+
return get("sync", self.ctx, id)
|
110
|
+
|
111
|
+
# --------------------------------------------------
|
112
|
+
# Search operations
|
113
|
+
# --------------------------------------------------
|
114
|
+
|
115
|
+
def count(
|
116
|
+
self,
|
117
|
+
*,
|
118
|
+
filter: RedisearchFilter | RedisearchFilterConditions | None = None,
|
119
|
+
) -> SearchResult:
|
120
|
+
"""
|
121
|
+
Count documents matching the filter.
|
122
|
+
"""
|
123
|
+
return count("sync", self.ctx, filter=filter)
|
124
|
+
|
125
|
+
def find(
|
126
|
+
self,
|
127
|
+
*,
|
128
|
+
filter: RedisearchFilter | RedisearchFilterConditions | None = None,
|
129
|
+
sort_by: str | None = None,
|
130
|
+
sort_desc: bool = False,
|
131
|
+
offset: int | None = None,
|
132
|
+
limit: int | None = None,
|
133
|
+
return_fields: list[str] | None = None,
|
134
|
+
) -> SearchResult:
|
135
|
+
"""
|
136
|
+
Find documents matching the filter criteria.
|
137
|
+
"""
|
138
|
+
return find(
|
139
|
+
"sync",
|
140
|
+
self.ctx,
|
141
|
+
filter=filter,
|
142
|
+
sort_by=sort_by,
|
143
|
+
sort_desc=sort_desc,
|
144
|
+
offset=offset,
|
145
|
+
limit=limit,
|
146
|
+
return_fields=return_fields,
|
147
|
+
)
|
148
|
+
|
149
|
+
def search(
|
150
|
+
self,
|
151
|
+
*,
|
152
|
+
vector: list[float],
|
153
|
+
filter: RedisearchFilter | RedisearchFilterConditions | None = None,
|
154
|
+
offset: int | None = None,
|
155
|
+
limit: int | None = None,
|
156
|
+
return_fields: list[str] | None = None,
|
157
|
+
) -> SearchResult:
|
158
|
+
"""
|
159
|
+
Search documents using vector similarity search.
|
160
|
+
"""
|
161
|
+
return search(
|
162
|
+
"sync",
|
163
|
+
self.ctx,
|
164
|
+
vector=vector,
|
165
|
+
filter=filter,
|
166
|
+
offset=offset,
|
167
|
+
limit=limit,
|
168
|
+
return_fields=return_fields,
|
169
|
+
)
|
170
|
+
|
171
|
+
# --------------------------------------------------
|
172
|
+
# Utilities
|
173
|
+
# --------------------------------------------------
|
174
|
+
|
175
|
+
def get_key(self, id: str) -> str:
|
176
|
+
"""
|
177
|
+
Get the Redis key for a given Redisearch ID.
|
178
|
+
"""
|
179
|
+
return get_key(self.ctx, id)
|
@@ -0,0 +1,16 @@
|
|
1
|
+
import redis
|
2
|
+
|
3
|
+
from ..settings import settings_manager
|
4
|
+
from .client import RedisearchClient
|
5
|
+
|
6
|
+
|
7
|
+
def create_redisearch_client(
|
8
|
+
config_key: str | None = None,
|
9
|
+
*,
|
10
|
+
redis: redis.Redis,
|
11
|
+
) -> RedisearchClient:
|
12
|
+
"""
|
13
|
+
Create a Redisearch client.
|
14
|
+
"""
|
15
|
+
settings = settings_manager.get_settings_by_key(config_key)
|
16
|
+
return RedisearchClient(settings, redis=redis)
|
@@ -0,0 +1,33 @@
|
|
1
|
+
import logging
|
2
|
+
from importlib import import_module
|
3
|
+
from typing import TYPE_CHECKING
|
4
|
+
|
5
|
+
if TYPE_CHECKING:
|
6
|
+
from ._async.client import RedisearchClient
|
7
|
+
from ._async.registry import create_redisearch_client
|
8
|
+
from .settings import RedisearchSettings, settings_manager
|
9
|
+
|
10
|
+
__all__ = [
|
11
|
+
"create_redisearch_client",
|
12
|
+
"RedisearchClient",
|
13
|
+
"RedisearchSettings",
|
14
|
+
"settings_manager",
|
15
|
+
]
|
16
|
+
|
17
|
+
logging.getLogger(__name__).addHandler(logging.NullHandler())
|
18
|
+
|
19
|
+
|
20
|
+
def __getattr__(name: str) -> object:
|
21
|
+
if name not in __all__:
|
22
|
+
raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
|
23
|
+
|
24
|
+
module_map = {
|
25
|
+
"create_redisearch_client": "._async.registry",
|
26
|
+
"RedisearchClient": "._async.client",
|
27
|
+
"RedisearchSettings": ".settings",
|
28
|
+
"settings_manager": ".settings",
|
29
|
+
}
|
30
|
+
|
31
|
+
parent = __name__.rsplit(".", 1)[0]
|
32
|
+
globals()[name] = getattr(import_module(module_map[name], parent), name)
|
33
|
+
return globals()[name]
|
@@ -0,0 +1,61 @@
|
|
1
|
+
"""
|
2
|
+
Module for constructing filter queries for Redisearch
|
3
|
+
|
4
|
+
RedisearchFilter can be combined using & and | operators to create
|
5
|
+
complex logical expressions that are evaluated in the Redis Query language.
|
6
|
+
|
7
|
+
This interface allows users to construct complex queries without needing to know
|
8
|
+
the Redis Query language.
|
9
|
+
|
10
|
+
Filter-based fields are not initialised directly.
|
11
|
+
Instead, they are constructed by combining RedisFilterFields
|
12
|
+
using the & and | operators.
|
13
|
+
|
14
|
+
Examples:
|
15
|
+
>>> import kiarina.lib.redisearch.filter as rf
|
16
|
+
>>> filter = (rf.Tag("color") == "blue") & (rf.Numeric("price") < 100)
|
17
|
+
>>> print(str(filter))
|
18
|
+
(@color:{blue} @price:[-inf (100)])
|
19
|
+
|
20
|
+
All examples:
|
21
|
+
>>> import kiarina.lib.redisearch.filter as rf
|
22
|
+
>>>
|
23
|
+
>>> rf.Tag("color") == "blue"
|
24
|
+
>>> rf.Tag("color") == ["blue", "red"]
|
25
|
+
>>> rf.Tag("color") != "blue"
|
26
|
+
>>> rf.Tag("color") != ["blue", "red"]
|
27
|
+
>>>
|
28
|
+
>>> rf.Numeric("price") == 100
|
29
|
+
>>> rf.Numeric("price") != 100
|
30
|
+
>>> rf.Numeric("price") > 100
|
31
|
+
>>> rf.Numeric("price") < 100
|
32
|
+
>>> rf.Numeric("price") >= 100
|
33
|
+
>>> rf.Numeric("price") <= 100
|
34
|
+
>>>
|
35
|
+
>>> rf.Text("title") == "hello"
|
36
|
+
>>> rf.Text("title") != "hello"
|
37
|
+
>>> rf.Text("title") % "*hello*"
|
38
|
+
>>>
|
39
|
+
>>> (rf.Tag("color") == "blue") & (rf.Numeric("price") < 100)
|
40
|
+
>>> (rf.Tag("color") == "blue") | (rf.Numeric("price")
|
41
|
+
"""
|
42
|
+
|
43
|
+
from ._field.numeric import Numeric
|
44
|
+
from ._field.tag import Tag
|
45
|
+
from ._field.text import Text
|
46
|
+
from ._model import RedisearchFilter
|
47
|
+
from ._registry import create_redisearch_filter
|
48
|
+
from ._types import RedisearchFilterConditions
|
49
|
+
|
50
|
+
__all__ = [
|
51
|
+
# ._field
|
52
|
+
"Numeric",
|
53
|
+
"Tag",
|
54
|
+
"Text",
|
55
|
+
# ._model
|
56
|
+
"RedisearchFilter",
|
57
|
+
# ._registry
|
58
|
+
"create_redisearch_filter",
|
59
|
+
# ._types
|
60
|
+
"RedisearchFilterConditions",
|
61
|
+
]
|
@@ -0,0 +1,28 @@
|
|
1
|
+
from functools import wraps
|
2
|
+
from typing import Any, Callable
|
3
|
+
|
4
|
+
|
5
|
+
def check_operator_misuse(func: Callable[..., Any]) -> Callable[..., Any]:
|
6
|
+
"""
|
7
|
+
Decorator to check misuse of the equality operator
|
8
|
+
"""
|
9
|
+
|
10
|
+
@wraps(func)
|
11
|
+
def wrapper(instance: Any, *args: Any, **kwargs: Any) -> Any:
|
12
|
+
other = kwargs.get("other") if "other" in kwargs else None
|
13
|
+
|
14
|
+
if not other:
|
15
|
+
for arg in args:
|
16
|
+
if isinstance(arg, type(instance)):
|
17
|
+
other = arg
|
18
|
+
break
|
19
|
+
|
20
|
+
if isinstance(other, type(instance)):
|
21
|
+
raise ValueError(
|
22
|
+
"The equality operator is overridden when creating a FilterExpression."
|
23
|
+
"Use .equals() for equality checks."
|
24
|
+
)
|
25
|
+
|
26
|
+
return func(instance, *args, **kwargs)
|
27
|
+
|
28
|
+
return wrapper
|
@@ -0,0 +1,28 @@
|
|
1
|
+
from enum import Enum
|
2
|
+
|
3
|
+
|
4
|
+
class RedisearchFilterOperator(Enum):
|
5
|
+
"""
|
6
|
+
Filter operator enumeration type
|
7
|
+
"""
|
8
|
+
|
9
|
+
EQ = 1
|
10
|
+
"""Equal"""
|
11
|
+
NE = 2
|
12
|
+
"""Not Equal"""
|
13
|
+
LT = 3
|
14
|
+
"""Less Than"""
|
15
|
+
GT = 4
|
16
|
+
"""Greater Than"""
|
17
|
+
LE = 5
|
18
|
+
"""Less Than or Equal"""
|
19
|
+
GE = 6
|
20
|
+
"""Greater Than or Equal"""
|
21
|
+
OR = 7
|
22
|
+
"""Logical OR"""
|
23
|
+
AND = 8
|
24
|
+
"""Logical AND"""
|
25
|
+
LIKE = 9
|
26
|
+
"""LIKE"""
|
27
|
+
IN = 10
|
28
|
+
"""IN"""
|
@@ -0,0 +1,67 @@
|
|
1
|
+
from typing import Any, Self
|
2
|
+
|
3
|
+
from .._enums import RedisearchFilterOperator
|
4
|
+
|
5
|
+
|
6
|
+
class RedisearchFieldFilter:
|
7
|
+
"""
|
8
|
+
Base class for field filters.
|
9
|
+
"""
|
10
|
+
|
11
|
+
OPERATORS: dict[RedisearchFilterOperator, str] = {}
|
12
|
+
"""Supported operators"""
|
13
|
+
|
14
|
+
def __init__(self, field_name: str):
|
15
|
+
"""
|
16
|
+
Initialization
|
17
|
+
"""
|
18
|
+
self._field_name: str = field_name
|
19
|
+
"""Field name"""
|
20
|
+
|
21
|
+
self._operator: RedisearchFilterOperator = RedisearchFilterOperator.EQ
|
22
|
+
"""Filter operator"""
|
23
|
+
|
24
|
+
self._value: Any = None
|
25
|
+
"""Filter value"""
|
26
|
+
|
27
|
+
# --------------------------------------------------
|
28
|
+
# Public Methods
|
29
|
+
# --------------------------------------------------
|
30
|
+
|
31
|
+
def equals(self, other: Self) -> bool:
|
32
|
+
"""
|
33
|
+
Check if another filter field is equal to this one.
|
34
|
+
"""
|
35
|
+
if not isinstance(other, type(self)):
|
36
|
+
return False
|
37
|
+
|
38
|
+
return self._field_name == other._field_name and self._value == other._value
|
39
|
+
|
40
|
+
# --------------------------------------------------
|
41
|
+
# Protected Methods
|
42
|
+
# --------------------------------------------------
|
43
|
+
|
44
|
+
def _set(
|
45
|
+
self,
|
46
|
+
*,
|
47
|
+
operator: RedisearchFilterOperator,
|
48
|
+
value: Any,
|
49
|
+
value_type: tuple[Any],
|
50
|
+
) -> None:
|
51
|
+
# Check if the operator is supported by this class
|
52
|
+
if operator not in self.OPERATORS:
|
53
|
+
raise ValueError(
|
54
|
+
f"Operator {operator} not supported by {self.__class__.__name__}. "
|
55
|
+
f"Supported operators are {self.OPERATORS.values()}."
|
56
|
+
)
|
57
|
+
|
58
|
+
if not isinstance(value, value_type):
|
59
|
+
raise TypeError(
|
60
|
+
f"Right side argument passed to operator {self.OPERATORS[operator]} "
|
61
|
+
f"with left side "
|
62
|
+
f"argument {self.__class__.__name__} must be of type {value_type}, "
|
63
|
+
f"received value {value}"
|
64
|
+
)
|
65
|
+
|
66
|
+
self._operator = operator
|
67
|
+
self._value = value
|