Skip to content

Commit bd084c0

Browse files
committed
fix: (schema) ensure field modifiers follow canonical order for RediSearch parser
Fix field modifier ordering to satisfy RediSearch parser requirements where INDEXEMPTY and INDEXMISSING must appear BEFORE SORTABLE in field definitions. This resolves index creation failures when using index_missing=True with sortable=True.
1 parent 62dc045 commit bd084c0

File tree

3 files changed

+851
-24
lines changed

3 files changed

+851
-24
lines changed

redisvl/schema/fields.py

Lines changed: 73 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
"""
3636

3737
from enum import Enum
38-
from typing import Any, Dict, Literal, Optional, Tuple, Type, Union
38+
from typing import Any, Dict, List, Literal, Optional, Tuple, Type, Union
3939

4040
from pydantic import BaseModel, Field, field_validator, model_validator
4141
from redis.commands.search.field import Field as RedisField
@@ -97,6 +97,51 @@ class CompressionType(str, Enum):
9797
LeanVec8x8 = "LeanVec8x8"
9898

9999

100+
### Helper Functions ###
101+
102+
103+
def _normalize_field_modifiers(
104+
field: RedisField, canonical_order: List[str], want_unf: bool = False
105+
) -> None:
106+
"""Normalize field modifier ordering for RediSearch parser.
107+
108+
RediSearch has a parser limitation (redis/redis#5177) where INDEXEMPTY and
109+
INDEXMISSING must appear BEFORE SORTABLE in field definitions. This function
110+
reorders field.args_suffix to match the canonical order while preserving
111+
unknown modifiers at the start.
112+
113+
Args:
114+
field: Redis field object whose args_suffix will be normalized
115+
canonical_order: List of modifiers in desired canonical order
116+
want_unf: Whether UNF should be added after SORTABLE (default: False)
117+
118+
Time Complexity: O(n + m) where n = len(field.args_suffix), m = len(canonical_order)
119+
Space Complexity: O(n + m)
120+
121+
Example:
122+
>>> field = RedisTextField("title")
123+
>>> field.args_suffix = ["SORTABLE", "INDEXMISSING"]
124+
>>> _normalize_field_modifiers(field, ["INDEXEMPTY", "INDEXMISSING", "SORTABLE"])
125+
>>> field.args_suffix
126+
['INDEXMISSING', 'SORTABLE']
127+
"""
128+
suffix_set = set(field.args_suffix)
129+
known_set = set(canonical_order)
130+
131+
# Preserve unknown modifiers in original order
132+
new_suffix = [t for t in field.args_suffix if t not in known_set]
133+
134+
# Add known modifiers in canonical order
135+
for modifier in canonical_order:
136+
if modifier in suffix_set:
137+
new_suffix.append(modifier)
138+
# Special case: UNF only appears with SORTABLE
139+
if modifier == "SORTABLE" and want_unf and "UNF" not in suffix_set:
140+
new_suffix.append("UNF")
141+
142+
field.args_suffix = new_suffix
143+
144+
100145
### Field Attributes ###
101146

102147

@@ -290,7 +335,7 @@ def validate_svs_params(self):
290335
):
291336
logger.warning(
292337
f"LeanVec compression selected without 'reduce'. "
293-
f"Consider setting reduce={self.dims//2} for better performance"
338+
f"Consider setting reduce={self.dims // 2} for better performance"
294339
)
295340

296341
if self.graph_max_degree and self.graph_max_degree < 32:
@@ -371,16 +416,11 @@ def as_redis_field(self) -> RedisField:
371416

372417
field = RedisTextField(name, **kwargs)
373418

374-
# Add UNF support (only when sortable)
375-
# UNF must come before NOINDEX in the args_suffix
376-
if self.attrs.unf and self.attrs.sortable: # type: ignore
377-
if "NOINDEX" in field.args_suffix:
378-
# Insert UNF before NOINDEX
379-
noindex_idx = field.args_suffix.index("NOINDEX")
380-
field.args_suffix.insert(noindex_idx, "UNF")
381-
else:
382-
# No NOINDEX, append normally
383-
field.args_suffix.append("UNF")
419+
# Normalize suffix ordering to satisfy RediSearch parser expectations.
420+
# Canonical order: [INDEXEMPTY] [INDEXMISSING] [SORTABLE [UNF]] [NOINDEX]
421+
canonical_order = ["INDEXEMPTY", "INDEXMISSING", "SORTABLE", "UNF", "NOINDEX"]
422+
want_unf = self.attrs.unf and self.attrs.sortable # type: ignore
423+
_normalize_field_modifiers(field, canonical_order, want_unf)
384424

385425
return field
386426

@@ -416,7 +456,14 @@ def as_redis_field(self) -> RedisField:
416456
if self.attrs.no_index: # type: ignore
417457
kwargs["no_index"] = True
418458

419-
return RedisTagField(name, **kwargs)
459+
field = RedisTagField(name, **kwargs)
460+
461+
# Normalize suffix ordering to satisfy RediSearch parser expectations.
462+
# Canonical order: [INDEXEMPTY] [INDEXMISSING] [SORTABLE] [NOINDEX]
463+
canonical_order = ["INDEXEMPTY", "INDEXMISSING", "SORTABLE", "NOINDEX"]
464+
_normalize_field_modifiers(field, canonical_order)
465+
466+
return field
420467

421468

422469
class NumericField(BaseField):
@@ -446,16 +493,11 @@ def as_redis_field(self) -> RedisField:
446493

447494
field = RedisNumericField(name, **kwargs)
448495

449-
# Add UNF support (only when sortable)
450-
# UNF must come before NOINDEX in the args_suffix
451-
if self.attrs.unf and self.attrs.sortable: # type: ignore
452-
if "NOINDEX" in field.args_suffix:
453-
# Insert UNF before NOINDEX
454-
noindex_idx = field.args_suffix.index("NOINDEX")
455-
field.args_suffix.insert(noindex_idx, "UNF")
456-
else:
457-
# No NOINDEX, append normally
458-
field.args_suffix.append("UNF")
496+
# Normalize suffix ordering to satisfy RediSearch parser expectations.
497+
# Canonical order: [INDEXEMPTY] [INDEXMISSING] [SORTABLE [UNF]] [NOINDEX]
498+
canonical_order = ["INDEXEMPTY", "INDEXMISSING", "SORTABLE", "UNF", "NOINDEX"]
499+
want_unf = self.attrs.unf and self.attrs.sortable # type: ignore
500+
_normalize_field_modifiers(field, canonical_order, want_unf)
459501

460502
return field
461503

@@ -485,7 +527,14 @@ def as_redis_field(self) -> RedisField:
485527
if self.attrs.no_index: # type: ignore
486528
kwargs["no_index"] = True
487529

488-
return RedisGeoField(name, **kwargs)
530+
field = RedisGeoField(name, **kwargs)
531+
532+
# Normalize suffix ordering to satisfy RediSearch parser expectations.
533+
# Canonical order: [INDEXEMPTY] [INDEXMISSING] [SORTABLE] [NOINDEX]
534+
canonical_order = ["INDEXEMPTY", "INDEXMISSING", "SORTABLE", "NOINDEX"]
535+
_normalize_field_modifiers(field, canonical_order)
536+
537+
return field
489538

490539

491540
class FlatVectorField(BaseField):

0 commit comments

Comments
 (0)