|
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,49 @@ 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 where INDEXEMPTY and |
| 109 | + INDEXMISSING must appear BEFORE SORTABLE in field definitions. This function |
| 110 | + reorders field.args_suffix to match the canonical order. |
| 111 | +
|
| 112 | + Args: |
| 113 | + field: Redis field object whose args_suffix will be normalized |
| 114 | + canonical_order: List of modifiers in desired canonical order |
| 115 | + want_unf: Whether UNF should be added after SORTABLE (default: False) |
| 116 | +
|
| 117 | + Time Complexity: O(n + m), where n = len(field.args_suffix), m = len(canonical_order). |
| 118 | + - O(n) to create the set from field.args_suffix |
| 119 | + - O(m) to iterate over canonical_order and perform set lookups (O(1) average case per lookup) |
| 120 | + Space Complexity: O(n) |
| 121 | +
|
| 122 | + Example: |
| 123 | + >>> field = RedisTextField("title") |
| 124 | + >>> field.args_suffix = ["SORTABLE", "INDEXMISSING"] |
| 125 | + >>> _normalize_field_modifiers(field, ["INDEXEMPTY", "INDEXMISSING", "SORTABLE"]) |
| 126 | + >>> field.args_suffix |
| 127 | + ['INDEXMISSING', 'SORTABLE'] |
| 128 | + """ |
| 129 | + suffix_set = set(field.args_suffix) |
| 130 | + |
| 131 | + # Build new suffix with only known modifiers in canonical order |
| 132 | + new_suffix = [] |
| 133 | + for modifier in canonical_order: |
| 134 | + if modifier in suffix_set: |
| 135 | + new_suffix.append(modifier) |
| 136 | + # Special case: UNF only appears with SORTABLE |
| 137 | + if modifier == "SORTABLE" and want_unf and "UNF" not in suffix_set: |
| 138 | + new_suffix.append("UNF") |
| 139 | + |
| 140 | + field.args_suffix = new_suffix |
| 141 | + |
| 142 | + |
100 | 143 | ### Field Attributes ### |
101 | 144 |
|
102 | 145 |
|
@@ -290,7 +333,7 @@ def validate_svs_params(self): |
290 | 333 | ): |
291 | 334 | logger.warning( |
292 | 335 | f"LeanVec compression selected without 'reduce'. " |
293 | | - f"Consider setting reduce={self.dims//2} for better performance" |
| 336 | + f"Consider setting reduce={self.dims // 2} for better performance" |
294 | 337 | ) |
295 | 338 |
|
296 | 339 | if self.graph_max_degree and self.graph_max_degree < 32: |
@@ -371,16 +414,11 @@ def as_redis_field(self) -> RedisField: |
371 | 414 |
|
372 | 415 | field = RedisTextField(name, **kwargs) |
373 | 416 |
|
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") |
| 417 | + # Normalize suffix ordering to satisfy RediSearch parser expectations. |
| 418 | + # Canonical order: [INDEXEMPTY] [INDEXMISSING] [SORTABLE [UNF]] [NOINDEX] |
| 419 | + canonical_order = ["INDEXEMPTY", "INDEXMISSING", "SORTABLE", "UNF", "NOINDEX"] |
| 420 | + want_unf = self.attrs.unf and self.attrs.sortable # type: ignore |
| 421 | + _normalize_field_modifiers(field, canonical_order, want_unf) |
384 | 422 |
|
385 | 423 | return field |
386 | 424 |
|
@@ -416,7 +454,14 @@ def as_redis_field(self) -> RedisField: |
416 | 454 | if self.attrs.no_index: # type: ignore |
417 | 455 | kwargs["no_index"] = True |
418 | 456 |
|
419 | | - return RedisTagField(name, **kwargs) |
| 457 | + field = RedisTagField(name, **kwargs) |
| 458 | + |
| 459 | + # Normalize suffix ordering to satisfy RediSearch parser expectations. |
| 460 | + # Canonical order: [INDEXEMPTY] [INDEXMISSING] [SORTABLE] [NOINDEX] |
| 461 | + canonical_order = ["INDEXEMPTY", "INDEXMISSING", "SORTABLE", "NOINDEX"] |
| 462 | + _normalize_field_modifiers(field, canonical_order) |
| 463 | + |
| 464 | + return field |
420 | 465 |
|
421 | 466 |
|
422 | 467 | class NumericField(BaseField): |
@@ -446,16 +491,12 @@ def as_redis_field(self) -> RedisField: |
446 | 491 |
|
447 | 492 | field = RedisNumericField(name, **kwargs) |
448 | 493 |
|
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") |
| 494 | + # Normalize suffix ordering to satisfy RediSearch parser expectations. |
| 495 | + # Canonical order: [INDEXMISSING] [SORTABLE [UNF]] [NOINDEX] |
| 496 | + # Note: INDEXEMPTY is not supported for NUMERIC fields |
| 497 | + canonical_order = ["INDEXMISSING", "SORTABLE", "UNF", "NOINDEX"] |
| 498 | + want_unf = self.attrs.unf and self.attrs.sortable # type: ignore |
| 499 | + _normalize_field_modifiers(field, canonical_order, want_unf) |
459 | 500 |
|
460 | 501 | return field |
461 | 502 |
|
@@ -485,7 +526,15 @@ def as_redis_field(self) -> RedisField: |
485 | 526 | if self.attrs.no_index: # type: ignore |
486 | 527 | kwargs["no_index"] = True |
487 | 528 |
|
488 | | - return RedisGeoField(name, **kwargs) |
| 529 | + field = RedisGeoField(name, **kwargs) |
| 530 | + |
| 531 | + # Normalize suffix ordering to satisfy RediSearch parser expectations. |
| 532 | + # Canonical order: [INDEXMISSING] [SORTABLE] [NOINDEX] |
| 533 | + # Note: INDEXEMPTY is not supported for GEO fields |
| 534 | + canonical_order = ["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