Skip to content
93 changes: 69 additions & 24 deletions redisvl/schema/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
"""

from enum import Enum
from typing import Any, Dict, Literal, Optional, Tuple, Type, Union
from typing import Any, Dict, List, Literal, Optional, Tuple, Type, Union

from pydantic import BaseModel, Field, field_validator, model_validator
from redis.commands.search.field import Field as RedisField
Expand Down Expand Up @@ -97,6 +97,47 @@ class CompressionType(str, Enum):
LeanVec8x8 = "LeanVec8x8"


### Helper Functions ###


def _normalize_field_modifiers(
field: RedisField, canonical_order: List[str], want_unf: bool = False
) -> None:
"""Normalize field modifier ordering for RediSearch parser.

RediSearch has a parser limitation where INDEXEMPTY and
INDEXMISSING must appear BEFORE SORTABLE in field definitions. This function
reorders field.args_suffix to match the canonical order.

Args:
field: Redis field object whose args_suffix will be normalized
canonical_order: List of modifiers in desired canonical order
want_unf: Whether UNF should be added after SORTABLE (default: False)

Time Complexity: O(n + m) where n = len(field.args_suffix), m = len(canonical_order)
Space Complexity: O(n)

Example:
>>> field = RedisTextField("title")
>>> field.args_suffix = ["SORTABLE", "INDEXMISSING"]
>>> _normalize_field_modifiers(field, ["INDEXEMPTY", "INDEXMISSING", "SORTABLE"])
>>> field.args_suffix
['INDEXMISSING', 'SORTABLE']
"""
suffix_set = set(field.args_suffix)

# Build new suffix with only known modifiers in canonical order
new_suffix = []
for modifier in canonical_order:
if modifier in suffix_set:
new_suffix.append(modifier)
# Special case: UNF only appears with SORTABLE
if modifier == "SORTABLE" and want_unf and "UNF" not in suffix_set:
new_suffix.append("UNF")

field.args_suffix = new_suffix


### Field Attributes ###


Expand Down Expand Up @@ -290,7 +331,7 @@ def validate_svs_params(self):
):
logger.warning(
f"LeanVec compression selected without 'reduce'. "
f"Consider setting reduce={self.dims//2} for better performance"
f"Consider setting reduce={self.dims // 2} for better performance"
)

if self.graph_max_degree and self.graph_max_degree < 32:
Expand Down Expand Up @@ -371,16 +412,11 @@ def as_redis_field(self) -> RedisField:

field = RedisTextField(name, **kwargs)

# Add UNF support (only when sortable)
# UNF must come before NOINDEX in the args_suffix
if self.attrs.unf and self.attrs.sortable: # type: ignore
if "NOINDEX" in field.args_suffix:
# Insert UNF before NOINDEX
noindex_idx = field.args_suffix.index("NOINDEX")
field.args_suffix.insert(noindex_idx, "UNF")
else:
# No NOINDEX, append normally
field.args_suffix.append("UNF")
# Normalize suffix ordering to satisfy RediSearch parser expectations.
# Canonical order: [INDEXEMPTY] [INDEXMISSING] [SORTABLE [UNF]] [NOINDEX]
canonical_order = ["INDEXEMPTY", "INDEXMISSING", "SORTABLE", "UNF", "NOINDEX"]
want_unf = self.attrs.unf and self.attrs.sortable # type: ignore
_normalize_field_modifiers(field, canonical_order, want_unf)

return field

Expand Down Expand Up @@ -416,7 +452,14 @@ def as_redis_field(self) -> RedisField:
if self.attrs.no_index: # type: ignore
kwargs["no_index"] = True

return RedisTagField(name, **kwargs)
field = RedisTagField(name, **kwargs)

# Normalize suffix ordering to satisfy RediSearch parser expectations.
# Canonical order: [INDEXEMPTY] [INDEXMISSING] [SORTABLE] [NOINDEX]
canonical_order = ["INDEXEMPTY", "INDEXMISSING", "SORTABLE", "NOINDEX"]
_normalize_field_modifiers(field, canonical_order)

return field


class NumericField(BaseField):
Expand Down Expand Up @@ -446,16 +489,11 @@ def as_redis_field(self) -> RedisField:

field = RedisNumericField(name, **kwargs)

# Add UNF support (only when sortable)
# UNF must come before NOINDEX in the args_suffix
if self.attrs.unf and self.attrs.sortable: # type: ignore
if "NOINDEX" in field.args_suffix:
# Insert UNF before NOINDEX
noindex_idx = field.args_suffix.index("NOINDEX")
field.args_suffix.insert(noindex_idx, "UNF")
else:
# No NOINDEX, append normally
field.args_suffix.append("UNF")
# Normalize suffix ordering to satisfy RediSearch parser expectations.
# Canonical order: [INDEXEMPTY] [INDEXMISSING] [SORTABLE [UNF]] [NOINDEX]
canonical_order = ["INDEXEMPTY", "INDEXMISSING", "SORTABLE", "UNF", "NOINDEX"]
want_unf = self.attrs.unf and self.attrs.sortable # type: ignore
_normalize_field_modifiers(field, canonical_order, want_unf)

return field

Expand Down Expand Up @@ -485,7 +523,14 @@ def as_redis_field(self) -> RedisField:
if self.attrs.no_index: # type: ignore
kwargs["no_index"] = True

return RedisGeoField(name, **kwargs)
field = RedisGeoField(name, **kwargs)

# Normalize suffix ordering to satisfy RediSearch parser expectations.
# Canonical order: [INDEXEMPTY] [INDEXMISSING] [SORTABLE] [NOINDEX]
canonical_order = ["INDEXEMPTY", "INDEXMISSING", "SORTABLE", "NOINDEX"]
_normalize_field_modifiers(field, canonical_order)

return field


class FlatVectorField(BaseField):
Expand Down
Loading
Loading