Skip to content

Commit ef0bade

Browse files
sobychackomarkpollack
authored andcommitted
Add filter-based deletion to MongoDB Atlas vector store
Add string-based filter deletion alongside the Filter.Expression-based deletion for MongoDB Atlas vector store, providing consistent deletion capabilities with other vector store implementations. Key changes: - Add delete(Filter.Expression) implementation for MongoDB Atlas store - Leverage existing MongoDBAtlasFilterExpressionConverter for filter translation - Use MongoTemplate's native query capabilities for deletion - Add comprehensive integration tests for filter deletion - Support both simple and complex filter expressions This maintains consistency with other vector store implementations while utilizing MongoDB-specific query capabilities for efficient metadata-based deletion.
1 parent c5dd768 commit ef0bade

File tree

2 files changed

+123
-3
lines changed

2 files changed

+123
-3
lines changed

vector-stores/spring-ai-mongodb-atlas-store/src/main/java/org/springframework/ai/vectorstore/mongodb/atlas/MongoDBAtlasVectorStore.java

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,23 +23,25 @@
2323
import java.util.Optional;
2424

2525
import com.mongodb.MongoCommandException;
26+
import com.mongodb.client.result.DeleteResult;
27+
import org.springframework.core.log.LogAccessor;
2628

2729
import org.springframework.ai.document.Document;
2830
import org.springframework.ai.document.DocumentMetadata;
29-
import org.springframework.ai.embedding.BatchingStrategy;
3031
import org.springframework.ai.embedding.EmbeddingModel;
3132
import org.springframework.ai.embedding.EmbeddingOptionsBuilder;
32-
import org.springframework.ai.embedding.TokenCountBatchingStrategy;
3333
import org.springframework.ai.model.EmbeddingUtils;
3434
import org.springframework.ai.observation.conventions.VectorStoreProvider;
3535
import org.springframework.ai.vectorstore.AbstractVectorStoreBuilder;
3636
import org.springframework.ai.vectorstore.SearchRequest;
37+
import org.springframework.ai.vectorstore.filter.Filter;
3738
import org.springframework.ai.vectorstore.observation.AbstractObservationVectorStore;
3839
import org.springframework.ai.vectorstore.observation.VectorStoreObservationContext;
3940
import org.springframework.beans.factory.InitializingBean;
4041
import org.springframework.data.mongodb.UncategorizedMongoDbException;
4142
import org.springframework.data.mongodb.core.MongoTemplate;
4243
import org.springframework.data.mongodb.core.aggregation.Aggregation;
44+
import org.springframework.data.mongodb.core.query.BasicQuery;
4345
import org.springframework.data.mongodb.core.query.Criteria;
4446
import org.springframework.data.mongodb.core.query.Query;
4547
import org.springframework.util.Assert;
@@ -125,6 +127,8 @@
125127
*/
126128
public class MongoDBAtlasVectorStore extends AbstractObservationVectorStore implements InitializingBean {
127129

130+
private static final LogAccessor logger = new LogAccessor(MongoDBAtlasVectorStore.class);
131+
128132
public static final String ID_FIELD_NAME = "_id";
129133

130134
public static final String METADATA_FIELD_NAME = "metadata";
@@ -272,6 +276,22 @@ public Optional<Boolean> doDelete(List<String> idList) {
272276
return Optional.of(deleteCount == idList.size());
273277
}
274278

279+
@Override
280+
protected void doDelete(Filter.Expression filterExpression) {
281+
Assert.notNull(filterExpression, "Filter expression must not be null");
282+
283+
try {
284+
String nativeFilterExpression = this.filterExpressionConverter.convertExpression(filterExpression);
285+
BasicQuery query = new BasicQuery(nativeFilterExpression);
286+
DeleteResult deleteResult = this.mongoTemplate.remove(query, this.collectionName);
287+
288+
logger.debug("Deleted " + deleteResult.getDeletedCount() + " documents matching filter expression");
289+
}
290+
catch (Exception e) {
291+
throw new IllegalStateException("Failed to delete documents by filter", e);
292+
}
293+
}
294+
275295
@Override
276296
public List<Document> similaritySearch(String query) {
277297
return similaritySearch(SearchRequest.builder().query(query).build());

vector-stores/spring-ai-mongodb-atlas-store/src/test/java/org/springframework/ai/vectorstore/mongodb/atlas/MongoDBAtlasVectorStoreIT.java

Lines changed: 101 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.
@@ -40,6 +40,7 @@
4040
import org.springframework.ai.openai.api.OpenAiApi;
4141
import org.springframework.ai.vectorstore.SearchRequest;
4242
import org.springframework.ai.vectorstore.VectorStore;
43+
import org.springframework.ai.vectorstore.filter.Filter;
4344
import org.springframework.boot.SpringBootConfiguration;
4445
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
4546
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
@@ -255,6 +256,105 @@ public void searchWithThreshold() {
255256
});
256257
}
257258

259+
@Test
260+
void deleteByFilter() {
261+
this.contextRunner.run(context -> {
262+
VectorStore vectorStore = context.getBean(VectorStore.class);
263+
264+
var bgDocument = new Document("The World is Big and Salvation Lurks Around the Corner",
265+
Map.of("country", "BG", "year", 2020));
266+
var nlDocument = new Document("The World is Big and Salvation Lurks Around the Corner",
267+
Map.of("country", "NL", "year", 2021));
268+
var bgDocument2 = new Document("The World is Big and Salvation Lurks Around the Corner",
269+
Map.of("country", "BG", "year", 2023));
270+
271+
vectorStore.add(List.of(bgDocument, nlDocument, bgDocument2));
272+
Thread.sleep(5000); // Wait for indexing
273+
274+
SearchRequest searchRequest = SearchRequest.builder()
275+
.query("The World")
276+
.topK(5)
277+
.similarityThresholdAll()
278+
.build();
279+
280+
List<Document> results = vectorStore.similaritySearch(searchRequest);
281+
assertThat(results).hasSize(3);
282+
283+
Filter.Expression filterExpression = new Filter.Expression(Filter.ExpressionType.EQ,
284+
new Filter.Key("country"), new Filter.Value("BG"));
285+
286+
vectorStore.delete(filterExpression);
287+
Thread.sleep(1000); // Wait for deletion to be processed
288+
289+
results = vectorStore.similaritySearch(searchRequest);
290+
assertThat(results).hasSize(1);
291+
assertThat(results.get(0).getMetadata()).containsEntry("country", "NL");
292+
});
293+
}
294+
295+
@Test
296+
void deleteWithStringFilterExpression() {
297+
this.contextRunner.run(context -> {
298+
VectorStore vectorStore = context.getBean(VectorStore.class);
299+
300+
var bgDocument = new Document("The World is Big and Salvation Lurks Around the Corner",
301+
Map.of("country", "BG", "year", 2020));
302+
var nlDocument = new Document("The World is Big and Salvation Lurks Around the Corner",
303+
Map.of("country", "NL", "year", 2021));
304+
var bgDocument2 = new Document("The World is Big and Salvation Lurks Around the Corner",
305+
Map.of("country", "BG", "year", 2023));
306+
307+
vectorStore.add(List.of(bgDocument, nlDocument, bgDocument2));
308+
Thread.sleep(5000); // Wait for indexing
309+
310+
var searchRequest = SearchRequest.builder().query("The World").topK(5).similarityThresholdAll().build();
311+
312+
List<Document> results = vectorStore.similaritySearch(searchRequest);
313+
assertThat(results).hasSize(3);
314+
315+
vectorStore.delete("country == 'BG'");
316+
Thread.sleep(1000); // Wait for deletion to be processed
317+
318+
results = vectorStore.similaritySearch(searchRequest);
319+
assertThat(results).hasSize(1);
320+
assertThat(results.get(0).getMetadata()).containsEntry("country", "NL");
321+
});
322+
}
323+
324+
@Test
325+
void deleteWithComplexFilterExpression() {
326+
this.contextRunner.run(context -> {
327+
VectorStore vectorStore = context.getBean(VectorStore.class);
328+
329+
var doc1 = new Document("Content 1", Map.of("type", "A", "priority", 1));
330+
var doc2 = new Document("Content 2", Map.of("type", "A", "priority", 2));
331+
var doc3 = new Document("Content 3", Map.of("type", "B", "priority", 1));
332+
333+
vectorStore.add(List.of(doc1, doc2, doc3));
334+
Thread.sleep(5000); // Wait for indexing
335+
336+
// Complex filter expression: (type == 'A' AND priority > 1)
337+
Filter.Expression priorityFilter = new Filter.Expression(Filter.ExpressionType.GT,
338+
new Filter.Key("priority"), new Filter.Value(1));
339+
Filter.Expression typeFilter = new Filter.Expression(Filter.ExpressionType.EQ, new Filter.Key("type"),
340+
new Filter.Value("A"));
341+
Filter.Expression complexFilter = new Filter.Expression(Filter.ExpressionType.AND, typeFilter,
342+
priorityFilter);
343+
344+
vectorStore.delete(complexFilter);
345+
Thread.sleep(1000); // Wait for deletion to be processed
346+
347+
var results = vectorStore
348+
.similaritySearch(SearchRequest.builder().query("Content").topK(5).similarityThresholdAll().build());
349+
350+
assertThat(results).hasSize(2);
351+
assertThat(results.stream().map(doc -> doc.getMetadata().get("type")).collect(Collectors.toList()))
352+
.containsExactlyInAnyOrder("A", "B");
353+
assertThat(results.stream().map(doc -> doc.getMetadata().get("priority")).collect(Collectors.toList()))
354+
.containsExactlyInAnyOrder(1, 1);
355+
});
356+
}
357+
258358
public static String getText(String uri) {
259359
var resource = new DefaultResourceLoader().getResource(uri);
260360
try {

0 commit comments

Comments
 (0)