Skip to content

Commit 02ba7b2

Browse files
authored
fix: pass withsuffixtrie attribute to Redis Text and Tag fields (#536)
Related: #535 The withsuffixtrie attribute was defined in TextFieldAttributes and TagFieldAttributes but never passed to the underlying Redis field constructors in as_redis_field() methods. This meant the attribute was silently ignored - indexes were created without suffix trie support even when explicitly specified. Solution - Added withsuffixtrie parameter to both RedisTextField() and RedisTagField() constructor calls in redisvl/schema/fields.py. Changes - TextField.as_redis_field() - now passes withsuffixtrie=True when enabled - TagField.as_redis_field() - now passes withsuffixtrie=True when enabled - Added 4 integration tests verifying WITHSUFFIXTRIE appears in FT.INFO output <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes how Redis search indexes are created by enabling an additional field modifier when configured, which can affect index size/performance and query behavior. Coverage is improved via integration tests, but correctness depends on Redis/redis-py support for the parameter. > > **Overview** > Fixes schema-to-Redis translation so `TextField.as_redis_field()` and `TagField.as_redis_field()` pass `withsuffixtrie=True` to the underlying Redis field constructors when the attribute is enabled. > > Adds integration tests that create indexes with `withsuffixtrie` and assert `WITHSUFFIXTRIE` appears in `FT.INFO`, including combinations with `sortable` and `case_sensitive`. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 56a3d94. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent a2808c4 commit 02ba7b2

2 files changed

Lines changed: 196 additions & 0 deletions

File tree

redisvl/schema/fields.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,10 @@ def as_redis_field(self) -> RedisField:
400400
if self.attrs.phonetic_matcher is not None: # type: ignore
401401
kwargs["phonetic_matcher"] = self.attrs.phonetic_matcher # type: ignore
402402

403+
# Add WITHSUFFIXTRIE if enabled
404+
if self.attrs.withsuffixtrie: # type: ignore
405+
kwargs["withsuffixtrie"] = True
406+
403407
# Add INDEXMISSING if enabled
404408
if self.attrs.index_missing: # type: ignore
405409
kwargs["index_missing"] = True
@@ -442,6 +446,10 @@ def as_redis_field(self) -> RedisField:
442446
if as_name is not None:
443447
kwargs["as_name"] = as_name
444448

449+
# Add WITHSUFFIXTRIE if enabled
450+
if self.attrs.withsuffixtrie: # type: ignore
451+
kwargs["withsuffixtrie"] = True
452+
445453
# Add INDEXMISSING if enabled
446454
if self.attrs.index_missing: # type: ignore
447455
kwargs["index_missing"] = True
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
"""
2+
Integration tests for withsuffixtrie attribute on Text and Tag fields.
3+
4+
Tests verify that the WITHSUFFIXTRIE modifier is correctly passed to Redis
5+
when creating indexes, enabling optimized suffix and contains queries.
6+
"""
7+
8+
from redisvl.index import SearchIndex
9+
from redisvl.schema import IndexSchema
10+
11+
12+
class TestTextFieldWithSuffixTrie:
13+
"""Integration tests for TextField withsuffixtrie attribute."""
14+
15+
def test_textfield_withsuffixtrie_creates_successfully(
16+
self, client, redis_url, worker_id
17+
):
18+
"""Test TextField with withsuffixtrie creates successfully."""
19+
schema_dict = {
20+
"index": {
21+
"name": f"test_text_suffix_{worker_id}",
22+
"prefix": f"text_suffix_{worker_id}:",
23+
"storage_type": "hash",
24+
},
25+
"fields": [
26+
{
27+
"name": "email",
28+
"type": "text",
29+
"attrs": {"withsuffixtrie": True},
30+
}
31+
],
32+
}
33+
34+
schema = IndexSchema.from_dict(schema_dict)
35+
index = SearchIndex(schema=schema, redis_url=redis_url)
36+
index.create(overwrite=True)
37+
38+
# Verify index was created and has WITHSUFFIXTRIE
39+
info = client.execute_command("FT.INFO", f"test_text_suffix_{worker_id}")
40+
41+
# Find the field attributes in the info response
42+
# FT.INFO returns a flat list, we need to find the attributes section
43+
info_dict = _parse_ft_info(info)
44+
field_attrs = _get_field_attributes(info_dict, "email")
45+
46+
assert (
47+
"WITHSUFFIXTRIE" in field_attrs
48+
), f"WITHSUFFIXTRIE not found in field attributes: {field_attrs}"
49+
50+
# Cleanup
51+
index.delete(drop=True)
52+
53+
def test_textfield_withsuffixtrie_and_sortable(self, client, redis_url, worker_id):
54+
"""Test TextField with withsuffixtrie and sortable combined."""
55+
schema_dict = {
56+
"index": {
57+
"name": f"test_text_suffix_sort_{worker_id}",
58+
"prefix": f"text_suffix_sort_{worker_id}:",
59+
"storage_type": "hash",
60+
},
61+
"fields": [
62+
{
63+
"name": "title",
64+
"type": "text",
65+
"attrs": {"withsuffixtrie": True, "sortable": True},
66+
}
67+
],
68+
}
69+
70+
schema = IndexSchema.from_dict(schema_dict)
71+
index = SearchIndex(schema=schema, redis_url=redis_url)
72+
index.create(overwrite=True)
73+
74+
info = client.execute_command("FT.INFO", f"test_text_suffix_sort_{worker_id}")
75+
info_dict = _parse_ft_info(info)
76+
field_attrs = _get_field_attributes(info_dict, "title")
77+
78+
assert "WITHSUFFIXTRIE" in field_attrs
79+
assert "SORTABLE" in field_attrs
80+
81+
index.delete(drop=True)
82+
83+
84+
class TestTagFieldWithSuffixTrie:
85+
"""Integration tests for TagField withsuffixtrie attribute."""
86+
87+
def test_tagfield_withsuffixtrie_creates_successfully(
88+
self, client, redis_url, worker_id
89+
):
90+
"""Test TagField with withsuffixtrie creates successfully."""
91+
schema_dict = {
92+
"index": {
93+
"name": f"test_tag_suffix_{worker_id}",
94+
"prefix": f"tag_suffix_{worker_id}:",
95+
"storage_type": "hash",
96+
},
97+
"fields": [
98+
{
99+
"name": "domain",
100+
"type": "tag",
101+
"attrs": {"withsuffixtrie": True},
102+
}
103+
],
104+
}
105+
106+
schema = IndexSchema.from_dict(schema_dict)
107+
index = SearchIndex(schema=schema, redis_url=redis_url)
108+
index.create(overwrite=True)
109+
110+
info = client.execute_command("FT.INFO", f"test_tag_suffix_{worker_id}")
111+
info_dict = _parse_ft_info(info)
112+
field_attrs = _get_field_attributes(info_dict, "domain")
113+
114+
assert (
115+
"WITHSUFFIXTRIE" in field_attrs
116+
), f"WITHSUFFIXTRIE not found in field attributes: {field_attrs}"
117+
118+
index.delete(drop=True)
119+
120+
def test_tagfield_withsuffixtrie_and_case_sensitive(
121+
self, client, redis_url, worker_id
122+
):
123+
"""Test TagField with withsuffixtrie and case_sensitive combined."""
124+
schema_dict = {
125+
"index": {
126+
"name": f"test_tag_suffix_cs_{worker_id}",
127+
"prefix": f"tag_suffix_cs_{worker_id}:",
128+
"storage_type": "hash",
129+
},
130+
"fields": [
131+
{
132+
"name": "sku",
133+
"type": "tag",
134+
"attrs": {"withsuffixtrie": True, "case_sensitive": True},
135+
}
136+
],
137+
}
138+
139+
schema = IndexSchema.from_dict(schema_dict)
140+
index = SearchIndex(schema=schema, redis_url=redis_url)
141+
index.create(overwrite=True)
142+
143+
info = client.execute_command("FT.INFO", f"test_tag_suffix_cs_{worker_id}")
144+
info_dict = _parse_ft_info(info)
145+
field_attrs = _get_field_attributes(info_dict, "sku")
146+
147+
assert "WITHSUFFIXTRIE" in field_attrs
148+
assert "CASESENSITIVE" in field_attrs
149+
150+
index.delete(drop=True)
151+
152+
153+
# Helper functions to parse FT.INFO response
154+
155+
156+
def _parse_ft_info(info) -> dict:
157+
"""Parse FT.INFO response into a dictionary."""
158+
result = {}
159+
if isinstance(info, list):
160+
i = 0
161+
while i < len(info) - 1:
162+
key = info[i]
163+
value = info[i + 1]
164+
if isinstance(key, bytes):
165+
key = key.decode("utf-8")
166+
result[key] = value
167+
i += 2
168+
return result
169+
170+
171+
def _get_field_attributes(info_dict: dict, field_name: str) -> list:
172+
"""Extract field attributes from parsed FT.INFO for a specific field."""
173+
attributes = info_dict.get("attributes", [])
174+
if isinstance(attributes, list):
175+
for field_info in attributes:
176+
if isinstance(field_info, list):
177+
# Field info is a list like [b'identifier', b'email', b'type', b'TEXT', ...]
178+
# Convert bytes to strings for comparison
179+
field_info_str = [
180+
x.decode("utf-8") if isinstance(x, bytes) else str(x)
181+
for x in field_info
182+
]
183+
# Check if this is the field we're looking for
184+
for i, item in enumerate(field_info_str):
185+
if item == "identifier" and i + 1 < len(field_info_str):
186+
if field_info_str[i + 1] == field_name:
187+
return field_info_str
188+
return []

0 commit comments

Comments
 (0)