Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions redisvl/schema/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,10 @@ def as_redis_field(self) -> RedisField:
if self.attrs.phonetic_matcher is not None: # type: ignore
kwargs["phonetic_matcher"] = self.attrs.phonetic_matcher # type: ignore

# Add WITHSUFFIXTRIE if enabled
if self.attrs.withsuffixtrie: # type: ignore
kwargs["withsuffixtrie"] = True
Comment on lines +403 to +405
Copy link

Copilot AI Mar 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_normalize_field_modifiers() overwrites field.args_suffix with only the modifiers listed in canonical_order. Since WITHSUFFIXTRIE is not included in that list, enabling withsuffixtrie here is likely to be stripped back out before FT.CREATE is issued. Consider either (a) including WITHSUFFIXTRIE in the canonical order for TEXT fields, or (b) updating _normalize_field_modifiers to preserve non-canonical suffix modifiers while reordering the known ones.

Copilot uses AI. Check for mistakes.

# Add INDEXMISSING if enabled
if self.attrs.index_missing: # type: ignore
kwargs["index_missing"] = True
Expand Down Expand Up @@ -442,6 +446,10 @@ def as_redis_field(self) -> RedisField:
if as_name is not None:
kwargs["as_name"] = as_name

# Add WITHSUFFIXTRIE if enabled
if self.attrs.withsuffixtrie: # type: ignore
kwargs["withsuffixtrie"] = True
Comment on lines +449 to +451
Copy link

Copilot AI Mar 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue as TEXT: after setting kwargs["withsuffixtrie"] = True, _normalize_field_modifiers() will reset args_suffix to only INDEXEMPTY/INDEXMISSING/SORTABLE/NOINDEX. If WITHSUFFIXTRIE is represented as a suffix modifier by redis-py, it will be dropped and the index will be created without suffix trie support. Update the canonical order to include WITHSUFFIXTRIE (or make _normalize_field_modifiers preserve unknown modifiers).

Copilot uses AI. Check for mistakes.

# Add INDEXMISSING if enabled
if self.attrs.index_missing: # type: ignore
kwargs["index_missing"] = True
Expand Down
190 changes: 190 additions & 0 deletions tests/integration/test_withsuffixtrie_integration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
"""
Integration tests for withsuffixtrie attribute on Text and Tag fields.

Tests verify that the WITHSUFFIXTRIE modifier is correctly passed to Redis
when creating indexes, enabling optimized suffix and contains queries.
"""

import pytest

Comment on lines +8 to +9
Copy link

Copilot AI Mar 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pytest is imported but not used in this test module. Consider removing the import to avoid dead code / keep the test file minimal (or use it for an explicit skip/marker if intended).

Suggested change
import pytest

Copilot uses AI. Check for mistakes.
from redisvl.index import SearchIndex
from redisvl.schema import IndexSchema


class TestTextFieldWithSuffixTrie:
"""Integration tests for TextField withsuffixtrie attribute."""

def test_textfield_withsuffixtrie_creates_successfully(
self, client, redis_url, worker_id
):
"""Test TextField with withsuffixtrie creates successfully."""
schema_dict = {
"index": {
"name": f"test_text_suffix_{worker_id}",
"prefix": f"text_suffix_{worker_id}:",
"storage_type": "hash",
},
"fields": [
{
"name": "email",
"type": "text",
"attrs": {"withsuffixtrie": True},
}
],
}

schema = IndexSchema.from_dict(schema_dict)
index = SearchIndex(schema=schema, redis_url=redis_url)
index.create(overwrite=True)

# Verify index was created and has WITHSUFFIXTRIE
info = client.execute_command("FT.INFO", f"test_text_suffix_{worker_id}")

# Find the field attributes in the info response
# FT.INFO returns a flat list, we need to find the attributes section
info_dict = _parse_ft_info(info)
field_attrs = _get_field_attributes(info_dict, "email")

assert (
"WITHSUFFIXTRIE" in field_attrs
), f"WITHSUFFIXTRIE not found in field attributes: {field_attrs}"

# Cleanup
index.delete(drop=True)

def test_textfield_withsuffixtrie_and_sortable(self, client, redis_url, worker_id):
"""Test TextField with withsuffixtrie and sortable combined."""
schema_dict = {
"index": {
"name": f"test_text_suffix_sort_{worker_id}",
"prefix": f"text_suffix_sort_{worker_id}:",
"storage_type": "hash",
},
"fields": [
{
"name": "title",
"type": "text",
"attrs": {"withsuffixtrie": True, "sortable": True},
}
],
}

schema = IndexSchema.from_dict(schema_dict)
index = SearchIndex(schema=schema, redis_url=redis_url)
index.create(overwrite=True)

info = client.execute_command("FT.INFO", f"test_text_suffix_sort_{worker_id}")
info_dict = _parse_ft_info(info)
field_attrs = _get_field_attributes(info_dict, "title")

assert "WITHSUFFIXTRIE" in field_attrs
assert "SORTABLE" in field_attrs

index.delete(drop=True)


class TestTagFieldWithSuffixTrie:
"""Integration tests for TagField withsuffixtrie attribute."""

def test_tagfield_withsuffixtrie_creates_successfully(
self, client, redis_url, worker_id
):
"""Test TagField with withsuffixtrie creates successfully."""
schema_dict = {
"index": {
"name": f"test_tag_suffix_{worker_id}",
"prefix": f"tag_suffix_{worker_id}:",
"storage_type": "hash",
},
"fields": [
{
"name": "domain",
"type": "tag",
"attrs": {"withsuffixtrie": True},
}
],
}

schema = IndexSchema.from_dict(schema_dict)
index = SearchIndex(schema=schema, redis_url=redis_url)
index.create(overwrite=True)

info = client.execute_command("FT.INFO", f"test_tag_suffix_{worker_id}")
info_dict = _parse_ft_info(info)
field_attrs = _get_field_attributes(info_dict, "domain")

assert (
"WITHSUFFIXTRIE" in field_attrs
), f"WITHSUFFIXTRIE not found in field attributes: {field_attrs}"

index.delete(drop=True)

def test_tagfield_withsuffixtrie_and_case_sensitive(
self, client, redis_url, worker_id
):
"""Test TagField with withsuffixtrie and case_sensitive combined."""
schema_dict = {
"index": {
"name": f"test_tag_suffix_cs_{worker_id}",
"prefix": f"tag_suffix_cs_{worker_id}:",
"storage_type": "hash",
},
"fields": [
{
"name": "sku",
"type": "tag",
"attrs": {"withsuffixtrie": True, "case_sensitive": True},
}
],
}

schema = IndexSchema.from_dict(schema_dict)
index = SearchIndex(schema=schema, redis_url=redis_url)
index.create(overwrite=True)

info = client.execute_command("FT.INFO", f"test_tag_suffix_cs_{worker_id}")
info_dict = _parse_ft_info(info)
field_attrs = _get_field_attributes(info_dict, "sku")

assert "WITHSUFFIXTRIE" in field_attrs
assert "CASESENSITIVE" in field_attrs

index.delete(drop=True)


# Helper functions to parse FT.INFO response


def _parse_ft_info(info) -> dict:
"""Parse FT.INFO response into a dictionary."""
result = {}
if isinstance(info, list):
i = 0
while i < len(info) - 1:
key = info[i]
value = info[i + 1]
if isinstance(key, bytes):
key = key.decode("utf-8")
result[key] = value
i += 2
return result


def _get_field_attributes(info_dict: dict, field_name: str) -> list:
"""Extract field attributes from parsed FT.INFO for a specific field."""
attributes = info_dict.get("attributes", [])
if isinstance(attributes, list):
for field_info in attributes:
if isinstance(field_info, list):
# Field info is a list like [b'identifier', b'email', b'type', b'TEXT', ...]
# Convert bytes to strings for comparison
field_info_str = [
x.decode("utf-8") if isinstance(x, bytes) else str(x)
for x in field_info
]
# Check if this is the field we're looking for
for i, item in enumerate(field_info_str):
if item == "identifier" and i + 1 < len(field_info_str):
if field_info_str[i + 1] == field_name:
return field_info_str
return []
Loading