Skip to content

Commit 32c79b0

Browse files
committed
fix: RedisFilterExpressionConverter handling string values for TAG/TEXT filter values
- Replace the existing stringValue() with tagStringValue() and textStringValue(), each applying field-type-appropriate backslash escaping before the value is concatenated into the query string. - escapeTagValue() escapes the characters structurally significant inside a TAG clause (\, $, |, {, }, (, ), [, ], -, '). - Use RediSearchUtil.escapeQuery() from Jedis for TEXT escaping Signed-off-by: Ilayaperumal Gopinathan <ilayaperumal.gopinathan@broadcom.com>
1 parent d97da30 commit 32c79b0

2 files changed

Lines changed: 79 additions & 8 deletions

File tree

vector-stores/spring-ai-redis-store/src/main/java/org/springframework/ai/vectorstore/redis/RedisFilterExpressionConverter.java

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
import java.util.function.Function;
2323
import java.util.stream.Collectors;
2424

25+
import redis.clients.jedis.search.RediSearchUtil;
26+
2527
import org.springframework.ai.vectorstore.filter.Filter.Expression;
2628
import org.springframework.ai.vectorstore.filter.Filter.ExpressionType;
2729
import org.springframework.ai.vectorstore.filter.Filter.Group;
@@ -116,12 +118,12 @@ private void doField(Expression expression, StringBuilder context) {
116118
break;
117119
case TAG:
118120
context.append("{");
119-
context.append(stringValue(expression, value));
121+
context.append(tagStringValue(expression, value));
120122
context.append("}");
121123
break;
122124
case TEXT:
123125
context.append("(");
124-
context.append(stringValue(expression, value));
126+
context.append(textStringValue(expression, value));
125127
context.append(")");
126128
break;
127129
default:
@@ -130,12 +132,41 @@ private void doField(Expression expression, StringBuilder context) {
130132
}
131133
}
132134

133-
private Object stringValue(Expression expression, Value value) {
135+
private String tagStringValue(Expression expression, Value value) {
136+
String delimiter = tagValueDelimiter(expression);
137+
if (value.value() instanceof List<?> list) {
138+
return list.stream().map(String::valueOf).map(this::escapeTagValue).collect(Collectors.joining(delimiter));
139+
}
140+
return escapeTagValue(String.valueOf(value.value()));
141+
}
142+
143+
private String textStringValue(Expression expression, Value value) {
134144
String delimiter = tagValueDelimiter(expression);
135145
if (value.value() instanceof List<?> list) {
136-
return String.join(delimiter, list.stream().map(String::valueOf).toList());
146+
return list.stream()
147+
.map(String::valueOf)
148+
.map(RediSearchUtil::escapeQuery)
149+
.collect(Collectors.joining(delimiter));
150+
}
151+
return RediSearchUtil.escapeQuery(String.valueOf(value.value()));
152+
}
153+
154+
/**
155+
* Escapes characters that have special meaning inside a RediSearch TAG query clause
156+
* ({@code @field:\{value\}}). The following characters are escaped with a backslash:
157+
* {@code $}, {@code \}, {@code |}, {@code {}, {@code }}, {@code (}, {@code )},
158+
* {@code [}, {@code ]}, {@code -}, and {@code '}.
159+
*/
160+
private String escapeTagValue(String value) {
161+
StringBuilder sb = new StringBuilder(value.length());
162+
for (int i = 0; i < value.length(); i++) {
163+
char c = value.charAt(i);
164+
switch (c) {
165+
case '\\', '$', '|', '{', '}', '(', ')', '[', ']', '-', '\'' -> sb.append('\\').append(c);
166+
default -> sb.append(c);
167+
}
137168
}
138-
return value.value();
169+
return sb.toString();
139170
}
140171

141172
private String tagValueDelimiter(Expression expression) {

vector-stores/spring-ai-redis-store/src/test/java/org/springframework/ai/vectorstore/redis/RedisFilterExpressionConverterTests.java

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -132,12 +132,52 @@ void testComplexIdentifiers() {
132132

133133
@Test
134134
void testSpecialCharactersInValues() {
135-
// Test values with Redis special characters that need escaping
136135
String vectorExpr = converter(RedisVectorStore.MetadataField.tag("description"))
137136
.convertExpression(new Expression(EQ, new Key("description"), new Value("test@value{with}special|chars")));
138137

139-
// Should properly escape special Redis characters
140-
assertThat(vectorExpr).isEqualTo("@description:{test@value{with}special|chars}");
138+
assertThat(vectorExpr).isEqualTo("@description:{test@value\\{with\\}special\\|chars}");
139+
}
140+
141+
@Test
142+
void testTagValueWithInjectionPayload() {
143+
String vectorExpr = converter(RedisVectorStore.MetadataField.tag("category")).convertExpression(
144+
new Expression(EQ, new Key("category"), new Value("science} | @access_level:{restricted")));
145+
146+
assertThat(vectorExpr).isEqualTo("@category:{science\\} \\| @access_level:\\{restricted}");
147+
assertThat(vectorExpr).doesNotContain("} | @");
148+
}
149+
150+
@Test
151+
void testTagValueInListWithSpecialChars() {
152+
String vectorExpr = converter(RedisVectorStore.MetadataField.tag("category")).convertExpression(new Expression(
153+
IN, new Key("category"), new Value(List.of("science} | @access_level:{restricted", "normal"))));
154+
155+
assertThat(vectorExpr).isEqualTo("@category:{science\\} \\| @access_level:\\{restricted | normal}");
156+
assertThat(vectorExpr).doesNotContain("} | @");
157+
}
158+
159+
@Test
160+
void testTagValueWithPipe() {
161+
String vectorExpr = converter(RedisVectorStore.MetadataField.tag("status"))
162+
.convertExpression(new Expression(EQ, new Key("status"), new Value("active|inactive")));
163+
164+
assertThat(vectorExpr).isEqualTo("@status:{active\\|inactive}");
165+
}
166+
167+
@Test
168+
void testTagValueWithHyphen() {
169+
String vectorExpr = converter(RedisVectorStore.MetadataField.tag("type"))
170+
.convertExpression(new Expression(EQ, new Key("type"), new Value("non-fiction")));
171+
172+
assertThat(vectorExpr).isEqualTo("@type:{non\\-fiction}");
173+
}
174+
175+
@Test
176+
void testTextValueWithSpecialChars() {
177+
String vectorExpr = converter(RedisVectorStore.MetadataField.text("description"))
178+
.convertExpression(new Expression(EQ, new Key("description"), new Value("hello@world.com")));
179+
180+
assertThat(vectorExpr).isEqualTo("@description:(hello\\@world\\.com)");
141181
}
142182

143183
@Test

0 commit comments

Comments
 (0)