Skip to content

Commit 419f5ca

Browse files
authored
Add separator parameter to Field() for TAG fields (#800)
Fixes #488 - TAG separators are now honored when creating indexes. Users can specify custom separators via Field(separator=',')
1 parent b2d9cea commit 419f5ca

2 files changed

Lines changed: 336 additions & 29 deletions

File tree

aredis_om/model/model.py

Lines changed: 34 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -8,25 +8,10 @@
88
from copy import copy
99
from enum import Enum
1010
from functools import reduce
11-
from typing import (
12-
Any,
13-
Callable,
14-
Dict,
15-
List,
16-
Literal,
17-
Mapping,
18-
Optional,
19-
Sequence,
20-
Set,
21-
Tuple,
22-
Type,
23-
TypeVar,
24-
Union,
25-
)
11+
from typing import (Any, Callable, Dict, List, Literal, Mapping, Optional,
12+
Sequence, Set, Tuple, Type, TypeVar, Union)
2613
from typing import get_args as typing_get_args
27-
from typing import (
28-
no_type_check,
29-
)
14+
from typing import no_type_check
3015

3116
from more_itertools import ichunked
3217
from pydantic import BaseModel
@@ -2142,6 +2127,7 @@ def __init__(self, default: Any = ..., **kwargs: Any) -> None:
21422127
full_text_search = kwargs.pop("full_text_search", None)
21432128
vector_options = kwargs.pop("vector_options", None)
21442129
expire = kwargs.pop("expire", None)
2130+
separator = kwargs.pop("separator", SINGLE_VALUE_TAG_FIELD_SEPARATOR)
21452131
super().__init__(default=default, **kwargs)
21462132
self.primary_key = primary_key
21472133
self.sortable = sortable
@@ -2150,6 +2136,7 @@ def __init__(self, default: Any = ..., **kwargs: Any) -> None:
21502136
self.full_text_search = full_text_search
21512137
self.vector_options = vector_options
21522138
self.expire = expire
2139+
self.separator = separator
21532140

21542141

21552142
class RelationshipInfo(Representation):
@@ -2261,6 +2248,7 @@ def Field(
22612248
full_text_search: Union[bool, UndefinedType] = Undefined,
22622249
vector_options: Optional[VectorFieldOptions] = None,
22632250
expire: Optional[int] = None,
2251+
separator: str = SINGLE_VALUE_TAG_FIELD_SEPARATOR,
22642252
**kwargs: Unpack[_FromFieldInfoInputs],
22652253
) -> Any:
22662254
"""
@@ -2276,6 +2264,8 @@ def Field(
22762264
vector_options: Vector field configuration for similarity search.
22772265
expire: TTL in seconds for this field (HashModel only, requires Redis 7.4+).
22782266
When set, the field will automatically expire after save().
2267+
separator: TAG field separator character for RediSearch indexing.
2268+
Defaults to "|". Use "," for comma-separated multi-value fields.
22792269
**kwargs: Additional Pydantic field options.
22802270
22812271
Returns:
@@ -2291,6 +2281,7 @@ def Field(
22912281
full_text_search=full_text_search,
22922282
vector_options=vector_options,
22932283
expire=expire,
2284+
separator=separator,
22942285
)
22952286
return field_info
22962287

@@ -3286,9 +3277,10 @@ def schema_for_fields(cls):
32863277

32873278
if getattr(field_info, "primary_key", None) is True:
32883279
if issubclass(_type, str):
3289-
redisearch_field = (
3290-
f"{name} TAG SEPARATOR {SINGLE_VALUE_TAG_FIELD_SEPARATOR}"
3280+
separator = getattr(
3281+
field_info, "separator", SINGLE_VALUE_TAG_FIELD_SEPARATOR
32913282
)
3283+
redisearch_field = f"{name} TAG SEPARATOR {separator}"
32923284
else:
32933285
redisearch_field = cls.schema_for_type(name, _type, field_info)
32943286
schema_parts.append(redisearch_field)
@@ -3346,13 +3338,15 @@ def schema_for_type(cls, name, typ: Any, field_info: PydanticFieldInfo):
33463338
else:
33473339
schema = f"{name} NUMERIC"
33483340
elif issubclass(typ, str):
3341+
separator = getattr(
3342+
field_info, "separator", SINGLE_VALUE_TAG_FIELD_SEPARATOR
3343+
)
33493344
if getattr(field_info, "full_text_search", False) is True:
33503345
schema = (
3351-
f"{name} TAG SEPARATOR {SINGLE_VALUE_TAG_FIELD_SEPARATOR} "
3352-
f"{name} AS {name}_fts TEXT"
3346+
f"{name} TAG SEPARATOR {separator} " f"{name} AS {name}_fts TEXT"
33533347
)
33543348
else:
3355-
schema = f"{name} TAG SEPARATOR {SINGLE_VALUE_TAG_FIELD_SEPARATOR}"
3349+
schema = f"{name} TAG SEPARATOR {separator}"
33563350
elif issubclass(typ, RedisModel):
33573351
sub_fields = []
33583352
for embedded_name, field in typ.model_fields.items():
@@ -3363,7 +3357,10 @@ def schema_for_type(cls, name, typ: Any, field_info: PydanticFieldInfo):
33633357
)
33643358
schema = " ".join(sub_fields)
33653359
else:
3366-
schema = f"{name} TAG SEPARATOR {SINGLE_VALUE_TAG_FIELD_SEPARATOR}"
3360+
separator = getattr(
3361+
field_info, "separator", SINGLE_VALUE_TAG_FIELD_SEPARATOR
3362+
)
3363+
schema = f"{name} TAG SEPARATOR {separator}"
33673364
if schema and sortable is True:
33683365
schema += " SORTABLE"
33693366
if schema and case_sensitive is True:
@@ -3627,7 +3624,10 @@ def schema_for_fields(cls):
36273624

36283625
if getattr(field_info, "primary_key", None) is True:
36293626
if issubclass(_type, str):
3630-
redisearch_field = f"$.{name} AS {name} TAG SEPARATOR {SINGLE_VALUE_TAG_FIELD_SEPARATOR}"
3627+
separator = getattr(
3628+
field_info, "separator", SINGLE_VALUE_TAG_FIELD_SEPARATOR
3629+
)
3630+
redisearch_field = f"$.{name} AS {name} TAG SEPARATOR {separator}"
36313631
else:
36323632
redisearch_field = cls.schema_for_type(
36333633
json_path, name, "", _type, field_info
@@ -3781,6 +3781,11 @@ def schema_for_type(
37813781
else typ
37823782
)
37833783

3784+
# Get separator from field_info, defaulting to pipe
3785+
separator = getattr(
3786+
field_info, "separator", SINGLE_VALUE_TAG_FIELD_SEPARATOR
3787+
)
3788+
37843789
if is_vector and vector_options:
37853790
schema = f"{path} AS {index_field_name} {vector_options.schema}"
37863791
elif parent_is_container_type or parent_is_model_in_container:
@@ -3795,7 +3800,7 @@ def schema_for_type(
37953800
f"search. Problem field: {name}. Docs: {ERRORS_URL}#E13"
37963801
)
37973802
# List/tuple fields are indexed as TAG fields and can be sortable
3798-
schema = f"{path} AS {index_field_name} TAG SEPARATOR {SINGLE_VALUE_TAG_FIELD_SEPARATOR}"
3803+
schema = f"{path} AS {index_field_name} TAG SEPARATOR {separator}"
37993804
if sortable is True:
38003805
schema += " SORTABLE"
38013806
if case_sensitive is True:
@@ -3815,7 +3820,7 @@ def schema_for_type(
38153820
elif issubclass(typ, str):
38163821
if full_text_search is True:
38173822
schema = (
3818-
f"{path} AS {index_field_name} TAG SEPARATOR {SINGLE_VALUE_TAG_FIELD_SEPARATOR} "
3823+
f"{path} AS {index_field_name} TAG SEPARATOR {separator} "
38193824
f"{path} AS {index_field_name}_fts TEXT"
38203825
)
38213826
if sortable is True:
@@ -3829,14 +3834,14 @@ def schema_for_type(
38293834
raise RedisModelError("Text fields cannot be case-sensitive.")
38303835
else:
38313836
# String fields are indexed as TAG fields and can be sortable
3832-
schema = f"{path} AS {index_field_name} TAG SEPARATOR {SINGLE_VALUE_TAG_FIELD_SEPARATOR}"
3837+
schema = f"{path} AS {index_field_name} TAG SEPARATOR {separator}"
38333838
if sortable is True:
38343839
schema += " SORTABLE"
38353840
if case_sensitive is True:
38363841
schema += " CASESENSITIVE"
38373842
else:
38383843
# Default to TAG field, which can be sortable
3839-
schema = f"{path} AS {index_field_name} TAG SEPARATOR {SINGLE_VALUE_TAG_FIELD_SEPARATOR}"
3844+
schema = f"{path} AS {index_field_name} TAG SEPARATOR {separator}"
38403845
if sortable is True:
38413846
schema += " SORTABLE"
38423847

0 commit comments

Comments
 (0)