diff --git a/vector-stores/spring-ai-gemfire-store/src/main/java/org/springframework/ai/vectorstore/GemFireAiSearchFilterExpressionConverter.java b/vector-stores/spring-ai-gemfire-store/src/main/java/org/springframework/ai/vectorstore/GemFireAiSearchFilterExpressionConverter.java new file mode 100644 index 00000000000..f800e1cbeee --- /dev/null +++ b/vector-stores/spring-ai-gemfire-store/src/main/java/org/springframework/ai/vectorstore/GemFireAiSearchFilterExpressionConverter.java @@ -0,0 +1,160 @@ +/* + * Copyright 2023 - 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.ai.vectorstore; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; +import java.util.TimeZone; +import java.util.regex.Pattern; + +import org.springframework.ai.vectorstore.filter.Filter; +import org.springframework.ai.vectorstore.filter.Filter.Expression; +import org.springframework.ai.vectorstore.filter.Filter.Key; +import org.springframework.ai.vectorstore.filter.converter.AbstractFilterExpressionConverter; + +/** + * GemFireAiSearchFilterExpressionConverter is a class that converts Filter.Expression + * objects into GemFire VectorDB query string representation. It extends the + * AbstractFilter ExpressionConverter class. + * + * @author Jason Huynh + */ +public class GemFireAiSearchFilterExpressionConverter extends AbstractFilterExpressionConverter { + + private static final Pattern DATE_FORMAT_PATTERN = Pattern.compile("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z"); + + private final SimpleDateFormat dateFormat; + + public GemFireAiSearchFilterExpressionConverter() { + this.dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); + this.dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); + } + + @Override + protected void doExpression(Expression expression, StringBuilder context) { + if (expression.type() == Filter.ExpressionType.IN || expression.type() == Filter.ExpressionType.NIN) { + context.append(getOperationSymbol(expression)); + this.convertOperand(expression.left(), context); + context.append("("); + this.convertOperand(expression.right(), context); + context.append(")"); + } + else if (expression.type() == Filter.ExpressionType.GT || expression.type() == Filter.ExpressionType.GTE) { + this.convertOperand(expression.left(), context); + context.append(getOperationSymbol(expression)); + this.convertOperand(expression.right(), context); + context.append(" TO *]"); + } + else if (expression.type() == Filter.ExpressionType.LT || expression.type() == Filter.ExpressionType.LTE) { + this.convertOperand(expression.left(), context); + context.append("[* TO "); + this.convertOperand(expression.right(), context); + context.append(getOperationSymbol(expression)); + } + else { + this.convertOperand(expression.left(), context); + context.append(getOperationSymbol(expression)); + this.convertOperand(expression.right(), context); + } + } + + @Override + protected void doStartValueRange(Filter.Value listValue, StringBuilder context) { + } + + @Override + protected void doEndValueRange(Filter.Value listValue, StringBuilder context) { + } + + @Override + protected void doAddValueRangeSpitter(Filter.Value listValue, StringBuilder context) { + context.append(" OR "); + } + + private String getOperationSymbol(Expression exp) { + return switch (exp.type()) { + case AND -> " AND "; + case OR -> " OR "; + case EQ, IN -> ""; + case NE -> " NOT "; + case LT -> "}"; + case LTE -> "]"; + case GT -> "{"; + case GTE -> "["; + case NIN -> "NOT "; + default -> throw new RuntimeException("Not supported expression type: " + exp.type()); + }; + } + + @Override + public void doKey(Key key, StringBuilder context) { + var identifier = hasOuterQuotes(key.key()) ? removeOuterQuotes(key.key()) : key.key(); + context.append(identifier.trim()).append(":"); + } + + @Override + protected void doValue(Filter.Value filterValue, StringBuilder context) { + if (filterValue.value() instanceof List list) { + int c = 0; + for (Object v : list) { + context.append(v); + if (c++ < list.size() - 1) { + this.doAddValueRangeSpitter(filterValue, context); + } + } + } + else { + this.doSingleValue(filterValue.value(), context); + } + } + + @Override + protected void doSingleValue(Object value, StringBuilder context) { + if (value instanceof Date date) { + context.append(this.dateFormat.format(date)); + } + else if (value instanceof String text) { + if (DATE_FORMAT_PATTERN.matcher(text).matches()) { + try { + Date date = this.dateFormat.parse(text); + context.append(this.dateFormat.format(date)); + } + catch (ParseException e) { + throw new IllegalArgumentException("Invalid date type:" + text, e); + } + } + else { + context.append(text); + } + } + else { + context.append(value); + } + } + + @Override + public void doStartGroup(Filter.Group group, StringBuilder context) { + context.append("("); + } + + @Override + public void doEndGroup(Filter.Group group, StringBuilder context) { + context.append(")"); + } + +} diff --git a/vector-stores/spring-ai-gemfire-store/src/main/java/org/springframework/ai/vectorstore/gemfire/GemFireVectorStore.java b/vector-stores/spring-ai-gemfire-store/src/main/java/org/springframework/ai/vectorstore/gemfire/GemFireVectorStore.java index 6d90043fde4..0521cd0a8ef 100644 --- a/vector-stores/spring-ai-gemfire-store/src/main/java/org/springframework/ai/vectorstore/gemfire/GemFireVectorStore.java +++ b/vector-stores/spring-ai-gemfire-store/src/main/java/org/springframework/ai/vectorstore/gemfire/GemFireVectorStore.java @@ -16,10 +16,6 @@ package org.springframework.ai.vectorstore.gemfire; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; @@ -28,7 +24,6 @@ import com.fasterxml.jackson.databind.json.JsonMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import org.springframework.ai.document.Document; import org.springframework.ai.document.DocumentMetadata; import org.springframework.ai.embedding.EmbeddingModel; @@ -36,7 +31,9 @@ import org.springframework.ai.observation.conventions.VectorStoreProvider; import org.springframework.ai.util.JacksonUtils; import org.springframework.ai.vectorstore.AbstractVectorStoreBuilder; +import org.springframework.ai.vectorstore.GemFireAiSearchFilterExpressionConverter; import org.springframework.ai.vectorstore.SearchRequest; +import org.springframework.ai.vectorstore.filter.FilterExpressionConverter; import org.springframework.ai.vectorstore.observation.AbstractObservationVectorStore; import org.springframework.ai.vectorstore.observation.VectorStoreObservationContext; import org.springframework.beans.factory.InitializingBean; @@ -50,6 +47,10 @@ import org.springframework.web.reactive.function.client.WebClientResponseException; import org.springframework.web.util.UriComponentsBuilder; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + /** * A VectorStore implementation backed by GemFire. This store supports creating, updating, * deleting, and similarity searching of documents in a GemFire index. @@ -114,6 +115,8 @@ public class GemFireVectorStore extends AbstractObservationVectorStore implement private final String[] fields; + private final FilterExpressionConverter filterExpressionConverter; + /** * Protected constructor that accepts a builder instance. This is the preferred way to * create new GemFireVectorStore instances. @@ -134,6 +137,7 @@ protected GemFireVectorStore(Builder builder) { .build(builder.sslEnabled ? "s" : "", builder.host, builder.port) .toString(); this.client = WebClient.create(base); + this.filterExpressionConverter = new GemFireAiSearchFilterExpressionConverter(); this.objectMapper = JsonMapper.builder().addModules(JacksonUtils.instantiateAvailableModules()).build(); } @@ -245,15 +249,16 @@ public void doDelete(List idList) { @Override @Nullable public List doSimilaritySearch(SearchRequest request) { + String filterQuery = null; if (request.hasFilterExpression()) { - throw new UnsupportedOperationException("GemFire currently does not support metadata filter expressions."); + filterQuery = filterExpressionConverter.convertExpression(request.getFilterExpression()); } float[] floatVector = this.embeddingModel.embed(request.getQuery()); return this.client.post() .uri("/" + this.indexName + QUERY) .contentType(MediaType.APPLICATION_JSON) .bodyValue(new QueryRequest(floatVector, request.getTopK(), request.getTopK(), // TopKPerBucket - true)) + true, filterQuery)) .retrieve() .bodyToFlux(QueryResponse.class) .filter(r -> r.score >= request.getSimilarityThreshold()) @@ -474,11 +479,20 @@ private static final class QueryRequest { @JsonProperty("include-metadata") private final boolean includeMetadata; + @JsonProperty("filter-query") + @JsonInclude(JsonInclude.Include.NON_NULL) + private final String filterQuery; + QueryRequest(float[] vector, int k, int kPerBucket, boolean includeMetadata) { + this(vector, k, kPerBucket, includeMetadata, null); + } + + QueryRequest(float[] vector, int k, int kPerBucket, boolean includeMetadata, String filterQuery) { this.vector = vector; this.k = k; this.kPerBucket = kPerBucket; this.includeMetadata = includeMetadata; + this.filterQuery = filterQuery; } public float[] getVector() { @@ -497,6 +511,10 @@ public boolean isIncludeMetadata() { return this.includeMetadata; } + public String getFilterQuery() { + return filterQuery; + } + } private static final class QueryResponse { diff --git a/vector-stores/spring-ai-gemfire-store/src/test/java/org/springframework/ai/vectorstore/GemFireAiSearchFilterExpressionConverterTest.java b/vector-stores/spring-ai-gemfire-store/src/test/java/org/springframework/ai/vectorstore/GemFireAiSearchFilterExpressionConverterTest.java new file mode 100644 index 00000000000..e7d9428b562 --- /dev/null +++ b/vector-stores/spring-ai-gemfire-store/src/test/java/org/springframework/ai/vectorstore/GemFireAiSearchFilterExpressionConverterTest.java @@ -0,0 +1,134 @@ +/* + * Copyright 2023 - 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.ai.vectorstore; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.ai.vectorstore.filter.Filter.ExpressionType.AND; +import static org.springframework.ai.vectorstore.filter.Filter.ExpressionType.EQ; +import static org.springframework.ai.vectorstore.filter.Filter.ExpressionType.GT; +import static org.springframework.ai.vectorstore.filter.Filter.ExpressionType.GTE; +import static org.springframework.ai.vectorstore.filter.Filter.ExpressionType.IN; +import static org.springframework.ai.vectorstore.filter.Filter.ExpressionType.LTE; +import static org.springframework.ai.vectorstore.filter.Filter.ExpressionType.NE; +import static org.springframework.ai.vectorstore.filter.Filter.ExpressionType.NIN; +import static org.springframework.ai.vectorstore.filter.Filter.ExpressionType.OR; + +import java.util.Date; +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.springframework.ai.vectorstore.filter.Filter; +import org.springframework.ai.vectorstore.filter.FilterExpressionConverter; + +/** + * @author Jason Huynh + */ +class GemFireAiSearchFilterExpressionConverterTest { + + final FilterExpressionConverter converter = new GemFireAiSearchFilterExpressionConverter(); + + @Test + public void testDate() { + String vectorExpr = converter.convertExpression(new Filter.Expression(EQ, new Filter.Key("activationDate"), + new Filter.Value(new Date(1704637752148L)))); + assertThat(vectorExpr).isEqualTo("activationDate:2024-01-07T14:29:12Z"); + + vectorExpr = converter.convertExpression( + new Filter.Expression(EQ, new Filter.Key("activationDate"), new Filter.Value("1970-01-01T00:00:02Z"))); + assertThat(vectorExpr).isEqualTo("activationDate:1970-01-01T00:00:02Z"); + } + + @Test + public void testEQ() { + String vectorExpr = converter + .convertExpression(new Filter.Expression(EQ, new Filter.Key("country"), new Filter.Value("BG"))); + assertThat(vectorExpr).isEqualTo("country:BG"); + } + + @Test + public void testEqAndGte() { + String vectorExpr = converter.convertExpression(new Filter.Expression(AND, + new Filter.Expression(EQ, new Filter.Key("genre"), new Filter.Value("drama")), + new Filter.Expression(GTE, new Filter.Key("year"), new Filter.Value(2020)))); + assertThat(vectorExpr).isEqualTo("genre:drama AND year:[2020 TO *]"); + } + + @Test + public void testEqAndGe() { + String vectorExpr = converter.convertExpression(new Filter.Expression(AND, + new Filter.Expression(EQ, new Filter.Key("genre"), new Filter.Value("drama")), + new Filter.Expression(GT, new Filter.Key("year"), new Filter.Value(2020)))); + assertThat(vectorExpr).isEqualTo("genre:drama AND year:{2020 TO *]"); + } + + @Test + public void testIn() { + String vectorExpr = converter.convertExpression(new Filter.Expression(IN, new Filter.Key("genre"), + new Filter.Value(List.of("comedy", "documentary", "drama")))); + assertThat(vectorExpr).isEqualTo("genre:(comedy OR documentary OR drama)"); + } + + @Test + public void testNe() { + String vectorExpr = converter.convertExpression( + new Filter.Expression(OR, new Filter.Expression(GTE, new Filter.Key("year"), new Filter.Value(2020)), + new Filter.Expression(AND, + new Filter.Expression(EQ, new Filter.Key("country"), new Filter.Value("BG")), + new Filter.Expression(NE, new Filter.Key("city"), new Filter.Value("Sofia"))))); + assertThat(vectorExpr).isEqualTo("year:[2020 TO *] OR country:BG AND city: NOT Sofia"); + } + + @Test + public void testGroup() { + String vectorExpr = converter.convertExpression(new Filter.Expression(AND, + new Filter.Group(new Filter.Expression(OR, + new Filter.Expression(GTE, new Filter.Key("year"), new Filter.Value(2020)), + new Filter.Expression(EQ, new Filter.Key("country"), new Filter.Value("BG")))), + new Filter.Expression(NIN, new Filter.Key("city"), new Filter.Value(List.of("Sofia", "Plovdiv"))))); + assertThat(vectorExpr).isEqualTo("(year:[2020 TO *] OR country:BG) AND NOT city:(Sofia OR Plovdiv)"); + } + + @Test + public void testBoolean() { + String vectorExpr = converter.convertExpression(new Filter.Expression(AND, + new Filter.Expression(AND, new Filter.Expression(EQ, new Filter.Key("isOpen"), new Filter.Value(true)), + new Filter.Expression(GTE, new Filter.Key("year"), new Filter.Value(2020))), + new Filter.Expression(IN, new Filter.Key("country"), new Filter.Value(List.of("BG", "NL", "US"))))); + + assertThat(vectorExpr).isEqualTo("isOpen:true AND year:[2020 TO *] AND country:(BG OR NL OR US)"); + } + + @Test + public void testDecimal() { + String vectorExpr = converter.convertExpression(new Filter.Expression(AND, + new Filter.Expression(GTE, new Filter.Key("temperature"), new Filter.Value(-15.6)), + new Filter.Expression(LTE, new Filter.Key("temperature"), new Filter.Value(20.13)))); + + assertThat(vectorExpr).isEqualTo("temperature:[-15.6 TO *] AND temperature:[* TO 20.13]"); + } + + @Test + public void testComplexIdentifiers() { + String vectorExpr = converter + .convertExpression(new Filter.Expression(EQ, new Filter.Key("\"country 1 2 3\""), new Filter.Value("BG"))); + assertThat(vectorExpr).isEqualTo("country 1 2 3:BG"); + + vectorExpr = converter + .convertExpression(new Filter.Expression(EQ, new Filter.Key("'country 1 2 3'"), new Filter.Value("BG"))); + assertThat(vectorExpr).isEqualTo("country 1 2 3:BG"); + } + +} diff --git a/vector-stores/spring-ai-gemfire-store/src/test/java/org/springframework/ai/vectorstore/gemfire/GemFireVectorStoreIT.java b/vector-stores/spring-ai-gemfire-store/src/test/java/org/springframework/ai/vectorstore/gemfire/GemFireVectorStoreIT.java index 9bbecf30cb0..b30a76a5d2a 100644 --- a/vector-stores/spring-ai-gemfire-store/src/test/java/org/springframework/ai/vectorstore/gemfire/GemFireVectorStoreIT.java +++ b/vector-stores/spring-ai-gemfire-store/src/test/java/org/springframework/ai/vectorstore/gemfire/GemFireVectorStoreIT.java @@ -16,13 +16,6 @@ package org.springframework.ai.vectorstore.gemfire; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.UUID; - import com.github.dockerjava.api.model.ExposedPort; import com.github.dockerjava.api.model.PortBinding; import com.github.dockerjava.api.model.Ports; @@ -32,7 +25,6 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; - import org.springframework.ai.document.Document; import org.springframework.ai.document.DocumentMetadata; import org.springframework.ai.embedding.EmbeddingModel; @@ -45,6 +37,15 @@ import org.springframework.context.annotation.Bean; import org.springframework.core.io.DefaultResourceLoader; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.time.ZonedDateTime; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.UUID; + import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.hasSize; @@ -52,6 +53,7 @@ * @author Geet Rawat * @author Soby Chacko * @author Thomas Vitale + * @author Jason Huynh * @since 1.0.0 */ @Disabled @@ -216,6 +218,168 @@ public void searchThresholdTest() { }); } + @Test + public void searchWithFilters() { + contextRunner.run(context -> { + VectorStore vectorStore = context.getBean(VectorStore.class); + var bgDocument = new Document("1", "The World is Big and Salvation Lurks Around the Corner", + Map.of("country", "BG", "year", "2020", "activationDate", + String.valueOf(new Date(1000).toInstant().toEpochMilli()))); + var nlDocument = new Document("2", "The World is Big and Salvation Lurks Around the Corner", Map + .of("country", "NL", "activationDate", String.valueOf(new Date(2000).toInstant().toEpochMilli()))); + var bgDocument2 = new Document("3", "The World is Big and Salvation Lurks Around the Corner", + Map.of("country", "BG", "year", "2023", "activationDate", + String.valueOf(new Date(3000).toInstant().toEpochMilli()))); + var usDocument = new Document("4", "The World is Big and Salvation Lurks Around the Corner", + Map.of("country", "US", "year", "2025", "activationDate", + String.valueOf(new Date(4000).toInstant().toEpochMilli()))); + + List filterDocuments = List.of(bgDocument, nlDocument, bgDocument2, usDocument); + + vectorStore.add(filterDocuments); + + Awaitility.await() + .until(() -> vectorStore.similaritySearch( + SearchRequest.builder().query("The World").topK(5).similarityThresholdAll().build()), + hasSize(4)); + + List tresults = vectorStore + .similaritySearch(SearchRequest.builder().query("Great Depression").topK(5).build()); + Document tresultDoc = tresults.get(0); + assertThat(tresultDoc.getFormattedContent()).contains("The World"); + + List results = vectorStore.similaritySearch(SearchRequest.builder() + .query("The World") + .topK(5) + .similarityThresholdAll() + .filterExpression("country == 'NL'") + .build()); + assertThat(results).hasSize(1); + assertThat(results.get(0).getId()).isEqualTo(nlDocument.getId()); + + results = vectorStore.similaritySearch(SearchRequest.builder() + .query("The World") + .topK(5) + .similarityThresholdAll() + .filterExpression("country == 'BG'") + .build()); + assertThat(results).hasSize(2); + assertThat(results.get(0).getId()).isIn(bgDocument.getId(), bgDocument2.getId()); + assertThat(results.get(1).getId()).isIn(bgDocument.getId(), bgDocument2.getId()); + + results = vectorStore.similaritySearch(SearchRequest.builder() + .query("The World") + .topK(5) + .similarityThresholdAll() + .filterExpression("country == 'BG' && year == '2020'") + .build()); + assertThat(results).hasSize(1); + assertThat(results.get(0).getId()).isEqualTo(bgDocument.getId()); + + results = vectorStore.similaritySearch(SearchRequest.builder() + .query("The World") + .topK(5) + .similarityThresholdAll() + .filterExpression("country == 'BG' AND year == '2020'") + .build()); + assertThat(results).hasSize(1); + assertThat(results.get(0).getId()).isEqualTo(bgDocument.getId()); + + results = vectorStore.similaritySearch(SearchRequest.builder() + .query("The World") + .topK(5) + .similarityThresholdAll() + .filterExpression("country == 'BG' || year == '2025'") + .build()); + assertThat(results).hasSize(3); + assertThat(results.get(0).getId()).isIn(bgDocument.getId(), bgDocument2.getId(), usDocument.getId()); + + results = vectorStore.similaritySearch(SearchRequest.builder() + .query("The World") + .topK(5) + .similarityThresholdAll() + .filterExpression("country == 'BG' OR year == '2025'") + .build()); + assertThat(results).hasSize(3); + assertThat(results.get(0).getId()).isIn(bgDocument.getId(), bgDocument2.getId(), usDocument.getId()); + + results = vectorStore.similaritySearch(SearchRequest.builder() + .query("The World") + .topK(5) + .similarityThresholdAll() + .filterExpression("country in ['BG']") + .build()); + assertThat(results).hasSize(2); + assertThat(results.get(0).getId()).isIn(bgDocument.getId(), bgDocument2.getId()); + assertThat(results.get(1).getId()).isIn(bgDocument.getId(), bgDocument2.getId()); + + results = vectorStore.similaritySearch(SearchRequest.builder() + .query("The World") + .topK(5) + .similarityThresholdAll() + .filterExpression("country in ['BG','NL']") + .build()); + assertThat(results).hasSize(3); + + results = vectorStore.similaritySearch(SearchRequest.builder() + .query("The World") + .topK(5) + .similarityThresholdAll() + .filterExpression("country in ['*'] AND country not in ['BG']") + .build()); + assertThat(results).hasSize(2); + assertThat(results.get(0).getId()).isIn(nlDocument.getId(), usDocument.getId()); + + results = vectorStore.similaritySearch(SearchRequest.builder() + .query("The World") + .topK(5) + .similarityThresholdAll() + .filterExpression("NOT(country not in ['BG'])") + .build()); + assertThat(results).hasSize(2); + assertThat(results.get(0).getId()).isIn(bgDocument.getId(), bgDocument2.getId()); + assertThat(results.get(1).getId()).isIn(bgDocument.getId(), bgDocument2.getId()); + + results = vectorStore.similaritySearch(SearchRequest.builder() + .query("The World") + .topK(5) + .similarityThresholdAll() + .filterExpression( + "activationDate > " + ZonedDateTime.parse("1970-01-01T00:00:02Z").toInstant().toEpochMilli()) + .build()); + assertThat(results).hasSize(2); + assertThat(results.get(0).getId()).isIn(bgDocument2.getId(), usDocument.getId()); + assertThat(results.get(1).getId()).isIn(bgDocument2.getId(), usDocument.getId()); + + results = vectorStore.similaritySearch(SearchRequest.builder() + .query("The World") + .topK(5) + .similarityThresholdAll() + .filterExpression( + "activationDate <= " + ZonedDateTime.parse("1970-01-01T00:00:02Z").toInstant().toEpochMilli()) + .build()); + assertThat(results).hasSize(2); + assertThat(results.get(0).getId()).isIn(nlDocument.getId(), bgDocument.getId()); + assertThat(results.get(1).getId()).isIn(nlDocument.getId(), bgDocument.getId()); + + results = vectorStore.similaritySearch(SearchRequest.builder() + .query("The World") + .topK(5) + .similarityThresholdAll() + .filterExpression("activationDate < " + new Date(3000).toInstant().toEpochMilli() + + " AND activationDate > " + new Date(1000).toInstant().toEpochMilli()) + .build()); + assertThat(results).hasSize(1); + assertThat(results.get(0).getId()).isIn(nlDocument.getId()); + + // Remove all documents from the store + vectorStore.delete(filterDocuments.stream().map(Document::getId).toList()); + Awaitility.await() + .until(() -> vectorStore.similaritySearch(SearchRequest.builder().query("The World").topK(1).build()), + hasSize(0)); + }); + } + @SpringBootConfiguration @EnableAutoConfiguration public static class TestApplication { @@ -226,6 +390,7 @@ public GemFireVectorStore vectorStore(EmbeddingModel embeddingModel) { .host("localhost") .port(HTTP_SERVICE_PORT) .indexName(INDEX_NAME) + .fields(new String[] { "year", "country", "activationDate" }) .initializeSchema(true) .build(); }