|
35 | 35 | """ |
36 | 36 |
|
37 | 37 | 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 |
39 | 39 |
|
40 | 40 | from pydantic import BaseModel, Field, field_validator, model_validator |
41 | 41 | from redis.commands.search.field import Field as RedisField |
@@ -97,6 +97,51 @@ class CompressionType(str, Enum): |
97 | 97 | LeanVec8x8 = "LeanVec8x8" |
98 | 98 |
|
99 | 99 |
|
| 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 | + |
100 | 145 | ### Field Attributes ### |
101 | 146 |
|
102 | 147 |
|
@@ -290,7 +335,7 @@ def validate_svs_params(self): |
290 | 335 | ): |
291 | 336 | logger.warning( |
292 | 337 | 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" |
294 | 339 | ) |
295 | 340 |
|
296 | 341 | if self.graph_max_degree and self.graph_max_degree < 32: |
@@ -371,16 +416,11 @@ def as_redis_field(self) -> RedisField: |
371 | 416 |
|
372 | 417 | field = RedisTextField(name, **kwargs) |
373 | 418 |
|
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) |
384 | 424 |
|
385 | 425 | return field |
386 | 426 |
|
@@ -416,7 +456,14 @@ def as_redis_field(self) -> RedisField: |
416 | 456 | if self.attrs.no_index: # type: ignore |
417 | 457 | kwargs["no_index"] = True |
418 | 458 |
|
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 |
420 | 467 |
|
421 | 468 |
|
422 | 469 | class NumericField(BaseField): |
@@ -446,16 +493,11 @@ def as_redis_field(self) -> RedisField: |
446 | 493 |
|
447 | 494 | field = RedisNumericField(name, **kwargs) |
448 | 495 |
|
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) |
459 | 501 |
|
460 | 502 | return field |
461 | 503 |
|
@@ -485,7 +527,14 @@ def as_redis_field(self) -> RedisField: |
485 | 527 | if self.attrs.no_index: # type: ignore |
486 | 528 | kwargs["no_index"] = True |
487 | 529 |
|
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 |
489 | 538 |
|
490 | 539 |
|
491 | 540 | class FlatVectorField(BaseField): |
|
0 commit comments