17
17
package org .springframework .ai .vectorstore ;
18
18
19
19
import java .util .Objects ;
20
+ import java .util .Map ;
20
21
21
22
import org .springframework .ai .document .Document ;
22
23
import org .springframework .ai .vectorstore .filter .Filter ;
23
24
import org .springframework .ai .vectorstore .filter .FilterExpressionBuilder ;
24
25
import org .springframework .ai .vectorstore .filter .FilterExpressionTextParser ;
25
26
import org .springframework .lang .Nullable ;
26
27
import org .springframework .util .Assert ;
27
- import java .util .regex .Pattern ;
28
- import java .util .regex .Matcher ;
29
28
30
29
/**
31
30
* Similarity search request. Use the {@link SearchRequest#builder()} to create the
@@ -61,6 +60,8 @@ public class SearchRequest {
61
60
@ Nullable
62
61
private Filter .Expression filterExpression ;
63
62
63
+ private static final Map <Character , String > ESCAPE_TEXT = Map .of ('\\' , "\\ \\ " , '.' , "\\ ." );
64
+
64
65
/**
65
66
* Copy an existing {@link SearchRequest.Builder} instance.
66
67
* @param originalSearchRequest {@link SearchRequest} instance to copy.
@@ -193,7 +194,7 @@ public Builder similarityThresholdAll() {
193
194
/**
194
195
* Retrieves documents by query embedding similarity and matching the filters.
195
196
* Value of 'null' means that no metadata filters will be applied to the search.
196
- *
197
+ * <p>
197
198
* For example if the {@link Document#getMetadata()} schema is:
198
199
*
199
200
* <pre>{@code
@@ -205,7 +206,7 @@ public Builder similarityThresholdAll() {
205
206
* "isActive": <Boolean>
206
207
* }
207
208
* }</pre>
208
- *
209
+ * <p>
209
210
* you can constrain the search result to only UK countries with isActive=true and
210
211
* year equal or greater 2020. You can build this such metadata filter
211
212
* programmatically like this:
@@ -217,10 +218,10 @@ public Builder similarityThresholdAll() {
217
218
* new Expression(GTE, new Key("year"), new Value(2020)),
218
219
* new Expression(EQ, new Key("isActive"), new Value(true))));
219
220
* }</pre>
220
- *
221
+ * <p>
221
222
* The {@link Filter.Expression} is portable across all vector stores.<br/>
222
- *
223
- *
223
+ * <p>
224
+ * <p>
224
225
* The {@link FilterExpressionBuilder} is a DSL creating expressions
225
226
* programmatically:
226
227
*
@@ -232,7 +233,7 @@ public Builder similarityThresholdAll() {
232
233
* b.gte("year", 2020),
233
234
* b.eq("isActive", true)));
234
235
* }</pre>
235
- *
236
+ * <p>
236
237
* The {@link FilterExpressionTextParser} converts textual, SQL like filter
237
238
* expression language into {@link Filter.Expression}:
238
239
*
@@ -262,21 +263,21 @@ public Builder filterExpression(@Nullable Filter.Expression expression) {
262
263
* "isActive": <Boolean>
263
264
* }
264
265
* }</pre>
265
- *
266
+ * <p>
266
267
* then you can constrain the search result with metadata filter expressions like:
267
268
*
268
269
* <pre>{@code
269
270
* country == 'UK' && year >= 2020 && isActive == true
270
271
* Or
271
272
* country == 'BG' && (city NOT IN ['Sofia', 'Plovdiv'] || price < 134.34)
272
273
* }</pre>
273
- *
274
+ * <p>
274
275
* This ensures that the response contains only embeddings that match the
275
276
* specified filer criteria. <br/>
276
- *
277
+ * <p>
277
278
* The declarative, SQL like, filter syntax is portable across all vector stores
278
279
* supporting the filter search feature.<br/>
279
- *
280
+ * <p>
280
281
* The {@link FilterExpressionTextParser} is used to convert the text filter
281
282
* expression into {@link Filter.Expression}.
282
283
* @param textExpression declarative, portable, SQL like, metadata filter syntax.
@@ -290,14 +291,24 @@ public Builder filterExpression(@Nullable String textExpression) {
290
291
}
291
292
292
293
private String escapeTextExpression (String expression ) {
293
- Pattern pattern = Pattern .compile ("'([^']*)'" );
294
- Matcher matcher = pattern .matcher (expression );
295
- StringBuffer sb = new StringBuffer ();
296
- while (matcher .find ()) {
297
- String content = matcher .group (1 ).replace ("\\ " , "\\ \\ " ).replace ("." , "\\ ." );
298
- matcher .appendReplacement (sb , "'" + content + "'" );
294
+ StringBuilder sb = new StringBuilder (expression .length () + 8 );
295
+ boolean inQuote = false ;
296
+
297
+ for (int i = 0 ; i < expression .length (); i ++) {
298
+ char ch = expression .charAt (i );
299
+
300
+ if (ch == '\'' ) {
301
+ inQuote = !inQuote ;
302
+ sb .append (ch );
303
+ }
304
+ else if (inQuote ) {
305
+ sb .append (ESCAPE_TEXT .getOrDefault (ch , String .valueOf (ch )));
306
+ }
307
+ else {
308
+ sb .append (ch );
309
+ }
299
310
}
300
- matcher . appendTail ( sb );
311
+
301
312
return sb .toString ();
302
313
}
303
314
0 commit comments