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:
@@ -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) {
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user