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.
This commit is contained in:
Soby Chacko
2025-01-31 15:46:54 -05:00
committed by GitHub
parent cc730268e7
commit 8162029cc6
2 changed files with 99 additions and 3 deletions

View File

@@ -43,13 +43,12 @@ import io.weaviate.client.v1.graphql.query.builder.GetBuilder;
import io.weaviate.client.v1.graphql.query.builder.GetBuilder.GetBuilderBuilder;
import io.weaviate.client.v1.graphql.query.fields.Field;
import io.weaviate.client.v1.graphql.query.fields.Fields;
import org.apache.commons.logging.LogFactory;
import org.springframework.ai.document.Document;
import org.springframework.ai.document.DocumentMetadata;
import org.springframework.ai.embedding.BatchingStrategy;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.embedding.EmbeddingOptionsBuilder;
import org.springframework.ai.embedding.TokenCountBatchingStrategy;
import org.springframework.ai.model.EmbeddingUtils;
import org.springframework.ai.observation.conventions.VectorStoreProvider;
import org.springframework.ai.vectorstore.AbstractVectorStoreBuilder;
@@ -57,6 +56,7 @@ import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.filter.Filter;
import org.springframework.ai.vectorstore.observation.AbstractObservationVectorStore;
import org.springframework.ai.vectorstore.observation.VectorStoreObservationContext;
import org.springframework.core.log.LogAccessor;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
@@ -93,6 +93,8 @@ import org.springframework.util.StringUtils;
*/
public class WeaviateVectorStore extends AbstractObservationVectorStore {
private static final LogAccessor logger = new LogAccessor(LogFactory.getLog(WeaviateVectorStore.class));
private static final String METADATA_FIELD_PREFIX = "meta_";
private static final String CONTENT_FIELD_NAME = "content";
@@ -296,6 +298,43 @@ public class WeaviateVectorStore extends AbstractObservationVectorStore {
return Optional.of(!result.hasErrors());
}
@Override
protected void doDelete(Filter.Expression filterExpression) {
Assert.notNull(filterExpression, "Filter expression must not be null");
try {
// Use similarity search with empty query to find documents matching the
// filter
SearchRequest searchRequest = SearchRequest.builder()
.query("") // empty query since we only want filter matches
.filterExpression(filterExpression)
.topK(10000) // large enough to get all matches
.similarityThresholdAll()
.build();
List<Document> matchingDocs = similaritySearch(searchRequest);
if (!matchingDocs.isEmpty()) {
List<String> idsToDelete = matchingDocs.stream().map(Document::getId).collect(Collectors.toList());
Optional<Boolean> result = delete(idsToDelete);
if (result.isPresent() && !result.get()) {
throw new IllegalStateException("Failed to delete some documents");
}
logger.debug(() -> "Deleted " + idsToDelete.size() + " documents matching filter expression");
}
else {
logger.debug(() -> "No documents found matching filter expression");
}
}
catch (Exception e) {
logger.error(e, () -> "Failed to delete documents by filter");
throw new IllegalStateException("Failed to delete documents by filter", e);
}
}
@Override
public List<Document> doSimilaritySearch(SearchRequest request) {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2023-2024 the original author or authors.
* 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.
@@ -37,6 +37,7 @@ import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.transformers.TransformersEmbeddingModel;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.ai.vectorstore.filter.Filter;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
@@ -254,6 +255,62 @@ public class WeaviateVectorStoreIT {
});
}
@Test
void deleteByFilter() {
this.contextRunner.run(context -> {
VectorStore vectorStore = context.getBean(VectorStore.class);
var bgDocument = new Document("The World is Big and Salvation Lurks Around the Corner",
Map.of("country", "BG", "year", 2020));
var nlDocument = new Document("The World is Big and Salvation Lurks Around the Corner",
Map.of("country", "NL"));
var bgDocument2 = new Document("The World is Big and Salvation Lurks Around the Corner",
Map.of("country", "BG", "year", 2023));
vectorStore.add(List.of(bgDocument, nlDocument, bgDocument2));
Filter.Expression filterExpression = new Filter.Expression(Filter.ExpressionType.EQ,
new Filter.Key("country"), new Filter.Value("BG"));
vectorStore.delete(filterExpression);
List<Document> results = vectorStore
.similaritySearch(SearchRequest.builder().query("The World").topK(5).similarityThresholdAll().build());
assertThat(results).hasSize(1);
assertThat(results.get(0).getMetadata()).containsEntry("country", "NL");
vectorStore.delete(List.of(nlDocument.getId()));
});
}
@Test
void deleteWithStringFilterExpression() {
this.contextRunner.run(context -> {
VectorStore vectorStore = context.getBean(VectorStore.class);
var bgDocument = new Document("The World is Big and Salvation Lurks Around the Corner",
Map.of("country", "BG", "year", 2020));
var nlDocument = new Document("The World is Big and Salvation Lurks Around the Corner",
Map.of("country", "NL"));
var bgDocument2 = new Document("The World is Big and Salvation Lurks Around the Corner",
Map.of("country", "BG", "year", 2023));
vectorStore.add(List.of(bgDocument, nlDocument, bgDocument2));
vectorStore.delete("country == 'BG'");
List<Document> results = vectorStore
.similaritySearch(SearchRequest.builder().query("The World").topK(5).similarityThresholdAll().build());
assertThat(results).hasSize(1);
assertThat(results.get(0).getMetadata()).containsEntry("country", "NL");
vectorStore.delete(List.of(nlDocument.getId()));
});
}
@SpringBootConfiguration
@EnableAutoConfiguration
public static class TestApplication {