From da347c3efc665656e78f86d137bb80ef010def52 Mon Sep 17 00:00:00 2001 From: Minu Kim Date: Fri, 16 May 2025 12:31:56 +0900 Subject: [PATCH 1/3] Fix escaping issue in filterExpression for RedisVectorStore file name filtering Signed-off-by: Minu Kim --- .../ai/vectorstore/SearchRequest.java | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/spring-ai-vector-store/src/main/java/org/springframework/ai/vectorstore/SearchRequest.java b/spring-ai-vector-store/src/main/java/org/springframework/ai/vectorstore/SearchRequest.java index 4eae298a86d..2ca98878474 100644 --- a/spring-ai-vector-store/src/main/java/org/springframework/ai/vectorstore/SearchRequest.java +++ b/spring-ai-vector-store/src/main/java/org/springframework/ai/vectorstore/SearchRequest.java @@ -24,6 +24,8 @@ import org.springframework.ai.vectorstore.filter.FilterExpressionTextParser; import org.springframework.lang.Nullable; import org.springframework.util.Assert; +import java.util.regex.Pattern; +import java.util.regex.Matcher; /** * Similarity search request. Use the {@link SearchRequest#builder()} to create the @@ -283,10 +285,22 @@ public Builder filterExpression(@Nullable Filter.Expression expression) { */ public Builder filterExpression(@Nullable String textExpression) { this.searchRequest.filterExpression = (textExpression != null) - ? new FilterExpressionTextParser().parse(textExpression) : null; + ? new FilterExpressionTextParser().parse(escapeTextExpression(textExpression)) : null; return this; } + private String escapeTextExpression(String expression) { + Pattern pattern = Pattern.compile("'([^']*)'"); + Matcher matcher = pattern.matcher(expression); + StringBuffer sb = new StringBuffer(); + while (matcher.find()) { + String content = matcher.group(1).replace("\\", "\\\\").replace(".", "\\."); + matcher.appendReplacement(sb, "'" + content + "'"); + } + matcher.appendTail(sb); + return sb.toString(); + } + public SearchRequest build() { return this.searchRequest; } From 5b55d59197540c512fa2601c0d326874afdfb820 Mon Sep 17 00:00:00 2001 From: Minu Kim Date: Sat, 17 May 2025 00:08:21 +0900 Subject: [PATCH 2/3] Fix escaping issue in filterExpression for RedisVectorStore file name filtering Signed-off-by: Minu Kim --- .../filter/SearchRequestTests.java | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/spring-ai-vector-store/src/test/java/org/springframework/ai/vectorstore/filter/SearchRequestTests.java b/spring-ai-vector-store/src/test/java/org/springframework/ai/vectorstore/filter/SearchRequestTests.java index 44861840fff..e7c2797f168 100644 --- a/spring-ai-vector-store/src/test/java/org/springframework/ai/vectorstore/filter/SearchRequestTests.java +++ b/spring-ai-vector-store/src/test/java/org/springframework/ai/vectorstore/filter/SearchRequestTests.java @@ -138,6 +138,61 @@ public void filterExpression() { } + @Test + public void filterExpressionWithDotAndBackslashIsEscaped() { + var request = SearchRequest.builder() + .query("Test") + .filterExpression("file_name == 'clean.code.pdf' && path == 'C:\\docs\\files'") + .build(); + + var expression = request.getFilterExpression(); + + assertThat(expression.toString()).contains("clean\\.code\\.pdf"); + assertThat(expression.toString()).contains("C:\\\\docs\\\\files"); + } + + @Test + public void filterExpressionWithMultipleLiteralsIsEscapedIndependently() { + var request = SearchRequest.builder() + .query("Test") + .filterExpression("file_name == 'a.b.c' && author == 'me.you'") + .build(); + + var expression = request.getFilterExpression(); + + assertThat(expression.toString()).contains("a\\.b\\.c"); + assertThat(expression.toString()).contains("me\\.you"); + } + + @Test + public void filterExpressionWithVariousFileExtensionsIsEscaped() { + var request = SearchRequest.builder() + .query("Test") + .filterExpression( + "file_name == 'summary.epub' || file_name == 'lecture_notes.md' || file_name == 'slides.pptx'") + .build(); + + String expression = request.getFilterExpression().toString(); + + assertThat(expression).contains("summary\\.epub"); + assertThat(expression).contains("lecture_notes\\.md"); + assertThat(expression).contains("slides\\.pptx"); + } + + @Test + public void filterExpressionWithInListIsEscaped() { + var request = SearchRequest.builder() + .query("Test") + .filterExpression("file_name IN ['a.pdf', 'b.txt', 'final.report.docx']") + .build(); + + String expression = request.getFilterExpression().toString(); + + assertThat(expression).contains("a\\.pdf"); + assertThat(expression).contains("b\\.txt"); + assertThat(expression).contains("final\\.report\\.docx"); + } + private void checkDefaults(SearchRequest request) { assertThat(request.getFilterExpression()).isNull(); assertThat(request.getSimilarityThreshold()).isEqualTo(SearchRequest.SIMILARITY_THRESHOLD_ACCEPT_ALL); From 82cbaa76f3d6c1b2221c3c8d036865f0c892e4ed Mon Sep 17 00:00:00 2001 From: Minu Kim Date: Wed, 21 May 2025 18:39:47 +0900 Subject: [PATCH 3/3] Fix escaping issue in filterExpression for RedisVectorStore file name filtering Signed-off-by: Minu Kim --- .../ai/vectorstore/SearchRequest.java | 30 ++++++++++++------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/spring-ai-vector-store/src/main/java/org/springframework/ai/vectorstore/SearchRequest.java b/spring-ai-vector-store/src/main/java/org/springframework/ai/vectorstore/SearchRequest.java index 2ca98878474..a0ec93e0bd4 100644 --- a/spring-ai-vector-store/src/main/java/org/springframework/ai/vectorstore/SearchRequest.java +++ b/spring-ai-vector-store/src/main/java/org/springframework/ai/vectorstore/SearchRequest.java @@ -17,6 +17,7 @@ package org.springframework.ai.vectorstore; import java.util.Objects; +import java.util.Map; import org.springframework.ai.document.Document; import org.springframework.ai.vectorstore.filter.Filter; @@ -24,8 +25,6 @@ import org.springframework.ai.vectorstore.filter.FilterExpressionTextParser; import org.springframework.lang.Nullable; import org.springframework.util.Assert; -import java.util.regex.Pattern; -import java.util.regex.Matcher; /** * Similarity search request. Use the {@link SearchRequest#builder()} to create the @@ -61,6 +60,8 @@ public class SearchRequest { @Nullable private Filter.Expression filterExpression; + private static final Map ESCAPE_TEXT = Map.of('\\', "\\\\", '.', "\\."); + /** * Copy an existing {@link SearchRequest.Builder} instance. * @param originalSearchRequest {@link SearchRequest} instance to copy. @@ -193,7 +194,6 @@ public Builder similarityThresholdAll() { /** * Retrieves documents by query embedding similarity and matching the filters. * Value of 'null' means that no metadata filters will be applied to the search. - * * For example if the {@link Document#getMetadata()} schema is: * *
{@code
@@ -290,14 +290,24 @@ public Builder filterExpression(@Nullable String textExpression) {
 		}
 
 		private String escapeTextExpression(String expression) {
-			Pattern pattern = Pattern.compile("'([^']*)'");
-			Matcher matcher = pattern.matcher(expression);
-			StringBuffer sb = new StringBuffer();
-			while (matcher.find()) {
-				String content = matcher.group(1).replace("\\", "\\\\").replace(".", "\\.");
-				matcher.appendReplacement(sb, "'" + content + "'");
+			StringBuilder sb = new StringBuilder(expression.length() + 8);
+			boolean inQuote = false;
+
+			for (int i = 0; i < expression.length(); i++) {
+				char ch = expression.charAt(i);
+
+				if (ch == '\'') {
+					inQuote = !inQuote;
+					sb.append(ch);
+				}
+				else if (inQuote) {
+					sb.append(ESCAPE_TEXT.getOrDefault(ch, String.valueOf(ch)));
+				}
+				else {
+					sb.append(ch);
+				}
 			}
-			matcher.appendTail(sb);
+
 			return sb.toString();
 		}