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.
Files changed (62) hide show
  1. kiarina/lib/redisearch/__init__.py +35 -0
  2. kiarina/lib/redisearch/_async/__init__.py +0 -0
  3. kiarina/lib/redisearch/_async/client.py +181 -0
  4. kiarina/lib/redisearch/_async/registry.py +16 -0
  5. kiarina/lib/redisearch/_core/__init__.py +0 -0
  6. kiarina/lib/redisearch/_core/context.py +69 -0
  7. kiarina/lib/redisearch/_core/operations/__init__.py +0 -0
  8. kiarina/lib/redisearch/_core/operations/count.py +55 -0
  9. kiarina/lib/redisearch/_core/operations/create_index.py +52 -0
  10. kiarina/lib/redisearch/_core/operations/delete.py +43 -0
  11. kiarina/lib/redisearch/_core/operations/drop_index.py +59 -0
  12. kiarina/lib/redisearch/_core/operations/exists_index.py +56 -0
  13. kiarina/lib/redisearch/_core/operations/find.py +105 -0
  14. kiarina/lib/redisearch/_core/operations/get.py +61 -0
  15. kiarina/lib/redisearch/_core/operations/get_info.py +155 -0
  16. kiarina/lib/redisearch/_core/operations/get_key.py +8 -0
  17. kiarina/lib/redisearch/_core/operations/migrate_index.py +160 -0
  18. kiarina/lib/redisearch/_core/operations/reset_index.py +60 -0
  19. kiarina/lib/redisearch/_core/operations/search.py +111 -0
  20. kiarina/lib/redisearch/_core/operations/set.py +65 -0
  21. kiarina/lib/redisearch/_core/utils/__init__.py +0 -0
  22. kiarina/lib/redisearch/_core/utils/calc_score.py +35 -0
  23. kiarina/lib/redisearch/_core/utils/marshal_mappings.py +57 -0
  24. kiarina/lib/redisearch/_core/utils/parse_search_result.py +57 -0
  25. kiarina/lib/redisearch/_core/utils/unmarshal_mappings.py +57 -0
  26. kiarina/lib/redisearch/_core/views/__init__.py +0 -0
  27. kiarina/lib/redisearch/_core/views/document.py +25 -0
  28. kiarina/lib/redisearch/_core/views/info_result.py +24 -0
  29. kiarina/lib/redisearch/_core/views/search_result.py +31 -0
  30. kiarina/lib/redisearch/_sync/__init__.py +0 -0
  31. kiarina/lib/redisearch/_sync/client.py +179 -0
  32. kiarina/lib/redisearch/_sync/registry.py +16 -0
  33. kiarina/lib/redisearch/asyncio.py +33 -0
  34. kiarina/lib/redisearch/filter/__init__.py +61 -0
  35. kiarina/lib/redisearch/filter/_decorators.py +28 -0
  36. kiarina/lib/redisearch/filter/_enums.py +28 -0
  37. kiarina/lib/redisearch/filter/_field/__init__.py +5 -0
  38. kiarina/lib/redisearch/filter/_field/base.py +67 -0
  39. kiarina/lib/redisearch/filter/_field/numeric.py +178 -0
  40. kiarina/lib/redisearch/filter/_field/tag.py +142 -0
  41. kiarina/lib/redisearch/filter/_field/text.py +111 -0
  42. kiarina/lib/redisearch/filter/_model.py +93 -0
  43. kiarina/lib/redisearch/filter/_registry.py +153 -0
  44. kiarina/lib/redisearch/filter/_types.py +32 -0
  45. kiarina/lib/redisearch/filter/_utils.py +18 -0
  46. kiarina/lib/redisearch/py.typed +0 -0
  47. kiarina/lib/redisearch/schema/__init__.py +25 -0
  48. kiarina/lib/redisearch/schema/_field/__init__.py +0 -0
  49. kiarina/lib/redisearch/schema/_field/base.py +20 -0
  50. kiarina/lib/redisearch/schema/_field/numeric.py +33 -0
  51. kiarina/lib/redisearch/schema/_field/tag.py +46 -0
  52. kiarina/lib/redisearch/schema/_field/text.py +44 -0
  53. kiarina/lib/redisearch/schema/_field/vector/__init__.py +0 -0
  54. kiarina/lib/redisearch/schema/_field/vector/base.py +61 -0
  55. kiarina/lib/redisearch/schema/_field/vector/flat.py +40 -0
  56. kiarina/lib/redisearch/schema/_field/vector/hnsw.py +53 -0
  57. kiarina/lib/redisearch/schema/_model.py +98 -0
  58. kiarina/lib/redisearch/schema/_types.py +16 -0
  59. kiarina/lib/redisearch/settings.py +47 -0
  60. kiarina_lib_redisearch-1.0.0.dist-info/METADATA +886 -0
  61. kiarina_lib_redisearch-1.0.0.dist-info/RECORD +62 -0
  62. 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,5 @@
1
+ from .numeric import Numeric
2
+ from .tag import Tag
3
+ from .text import Text
4
+
5
+ __all__ = ["Numeric", "Tag", "Text"]
@@ -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