Skip to content

Commit 8162029

Browse files
authored
Add filter-based deletion to Weaviate vector store (#2150)
Implement filter-based deletion for WeaviateVectorStore by leveraging the existing ID-based deletion mechanism. Key changes: - Add doDelete(Filter.Expression) implementation that delegates to the existing ID-based deletion method - Use similarity search with filter to find matching documents first - Delete matched documents by their IDs - Add integration tests for filter deletion scenarios: * Deleting by simple equality filter * Deleting by string-based filter expression This maintains consistency with other vector store implementations while working around Weaviate's limitations in direct filtered deletion.
1 parent cc73026 commit 8162029

File tree

2 files changed

+99
-3
lines changed

2 files changed

+99
-3
lines changed

vector-stores/spring-ai-weaviate-store/src/main/java/org/springframework/ai/vectorstore/weaviate/WeaviateVectorStore.java

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,20 +43,20 @@
4343
import io.weaviate.client.v1.graphql.query.builder.GetBuilder.GetBuilderBuilder;
4444
import io.weaviate.client.v1.graphql.query.fields.Field;
4545
import io.weaviate.client.v1.graphql.query.fields.Fields;
46+
import org.apache.commons.logging.LogFactory;
4647

4748
import org.springframework.ai.document.Document;
4849
import org.springframework.ai.document.DocumentMetadata;
49-
import org.springframework.ai.embedding.BatchingStrategy;
5050
import org.springframework.ai.embedding.EmbeddingModel;
5151
import org.springframework.ai.embedding.EmbeddingOptionsBuilder;
52-
import org.springframework.ai.embedding.TokenCountBatchingStrategy;
5352
import org.springframework.ai.model.EmbeddingUtils;
5453
import org.springframework.ai.observation.conventions.VectorStoreProvider;
5554
import org.springframework.ai.vectorstore.AbstractVectorStoreBuilder;
5655
import org.springframework.ai.vectorstore.SearchRequest;
5756
import org.springframework.ai.vectorstore.filter.Filter;
5857
import org.springframework.ai.vectorstore.observation.AbstractObservationVectorStore;
5958
import org.springframework.ai.vectorstore.observation.VectorStoreObservationContext;
59+
import org.springframework.core.log.LogAccessor;
6060
import org.springframework.util.Assert;
6161
import org.springframework.util.CollectionUtils;
6262
import org.springframework.util.StringUtils;
@@ -93,6 +93,8 @@
9393
*/
9494
public class WeaviateVectorStore extends AbstractObservationVectorStore {
9595

96+
private static final LogAccessor logger = new LogAccessor(LogFactory.getLog(WeaviateVectorStore.class));
97+
9698
private static final String METADATA_FIELD_PREFIX = "meta_";
9799

98100
private static final String CONTENT_FIELD_NAME = "content";
@@ -296,6 +298,43 @@ public Optional<Boolean> doDelete(List<String> documentIds) {
296298
return Optional.of(!result.hasErrors());
297299
}
298300

301+
@Override
302+
protected void doDelete(Filter.Expression filterExpression) {
303+
Assert.notNull(filterExpression, "Filter expression must not be null");
304+
305+
try {
306+
// Use similarity search with empty query to find documents matching the
307+
// filter
308+
SearchRequest searchRequest = SearchRequest.builder()
309+
.query("") // empty query since we only want filter matches
310+
.filterExpression(filterExpression)
311+
.topK(10000) // large enough to get all matches
312+
.similarityThresholdAll()
313+
.build();
314+
315+
List<Document> matchingDocs = similaritySearch(searchRequest);
316+
317+
if (!matchingDocs.isEmpty()) {
318+
List<String> idsToDelete = matchingDocs.stream().map(Document::getId).collect(Collectors.toList());
319+
320+
Optional<Boolean> result = delete(idsToDelete);
321+
322+
if (result.isPresent() && !result.get()) {
323+
throw new IllegalStateException("Failed to delete some documents");
324+
}
325+
326+
logger.debug(() -> "Deleted " + idsToDelete.size() + " documents matching filter expression");
327+
}
328+
else {
329+
logger.debug(() -> "No documents found matching filter expression");
330+
}
331+
}
332+
catch (Exception e) {
333+
logger.error(e, () -> "Failed to delete documents by filter");
334+
throw new IllegalStateException("Failed to delete documents by filter", e);
335+
}
336+
}
337+
299338
@Override
300339
public List<Document> doSimilaritySearch(SearchRequest request) {
301340

vector-stores/spring-ai-weaviate-store/src/test/java/org/springframework/ai/vectorstore/weaviate/WeaviateVectorStoreIT.java

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2023-2024 the original author or authors.
2+
* Copyright 2023-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -37,6 +37,7 @@
3737
import org.springframework.ai.transformers.TransformersEmbeddingModel;
3838
import org.springframework.ai.vectorstore.SearchRequest;
3939
import org.springframework.ai.vectorstore.VectorStore;
40+
import org.springframework.ai.vectorstore.filter.Filter;
4041
import org.springframework.boot.SpringBootConfiguration;
4142
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
4243
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
@@ -254,6 +255,62 @@ public void searchWithThreshold() {
254255
});
255256
}
256257

258+
@Test
259+
void deleteByFilter() {
260+
this.contextRunner.run(context -> {
261+
VectorStore vectorStore = context.getBean(VectorStore.class);
262+
263+
var bgDocument = new Document("The World is Big and Salvation Lurks Around the Corner",
264+
Map.of("country", "BG", "year", 2020));
265+
var nlDocument = new Document("The World is Big and Salvation Lurks Around the Corner",
266+
Map.of("country", "NL"));
267+
var bgDocument2 = new Document("The World is Big and Salvation Lurks Around the Corner",
268+
Map.of("country", "BG", "year", 2023));
269+
270+
vectorStore.add(List.of(bgDocument, nlDocument, bgDocument2));
271+
272+
Filter.Expression filterExpression = new Filter.Expression(Filter.ExpressionType.EQ,
273+
new Filter.Key("country"), new Filter.Value("BG"));
274+
275+
vectorStore.delete(filterExpression);
276+
277+
List<Document> results = vectorStore
278+
.similaritySearch(SearchRequest.builder().query("The World").topK(5).similarityThresholdAll().build());
279+
280+
assertThat(results).hasSize(1);
281+
assertThat(results.get(0).getMetadata()).containsEntry("country", "NL");
282+
283+
vectorStore.delete(List.of(nlDocument.getId()));
284+
285+
});
286+
}
287+
288+
@Test
289+
void deleteWithStringFilterExpression() {
290+
this.contextRunner.run(context -> {
291+
VectorStore vectorStore = context.getBean(VectorStore.class);
292+
293+
var bgDocument = new Document("The World is Big and Salvation Lurks Around the Corner",
294+
Map.of("country", "BG", "year", 2020));
295+
var nlDocument = new Document("The World is Big and Salvation Lurks Around the Corner",
296+
Map.of("country", "NL"));
297+
var bgDocument2 = new Document("The World is Big and Salvation Lurks Around the Corner",
298+
Map.of("country", "BG", "year", 2023));
299+
300+
vectorStore.add(List.of(bgDocument, nlDocument, bgDocument2));
301+
302+
vectorStore.delete("country == 'BG'");
303+
304+
List<Document> results = vectorStore
305+
.similaritySearch(SearchRequest.builder().query("The World").topK(5).similarityThresholdAll().build());
306+
307+
assertThat(results).hasSize(1);
308+
assertThat(results.get(0).getMetadata()).containsEntry("country", "NL");
309+
310+
vectorStore.delete(List.of(nlDocument.getId()));
311+
});
312+
}
313+
257314
@SpringBootConfiguration
258315
@EnableAutoConfiguration
259316
public static class TestApplication {

0 commit comments

Comments
 (0)