Skip to content

Commit fc1e90c

Browse files
authored
Add filter-based deletion to MariaDB vector store (#2125)
Add string-based filter deletion alongside the Filter.Expression-based deletion for MariaDB vector store, providing consistent deletion capabilities with other vector store implementations. Key changes: - Add delete(Filter.Expression) implementation for MariaDB store - Integrate with existing MariaDBFilterExpressionConverter - Add comprehensive integration tests for filter deletion - Support both simple and complex filter expressions This maintains consistency with other vector store implementations and enables flexible document deletion based on metadata filters.
1 parent c33edc7 commit fc1e90c

File tree

2 files changed

+126
-1
lines changed

2 files changed

+126
-1
lines changed

vector-stores/spring-ai-mariadb-store/src/main/java/org/springframework/ai/vectorstore/mariadb/MariaDBVectorStore.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import org.springframework.ai.util.JacksonUtils;
3939
import org.springframework.ai.vectorstore.AbstractVectorStoreBuilder;
4040
import org.springframework.ai.vectorstore.SearchRequest;
41+
import org.springframework.ai.vectorstore.filter.Filter;
4142
import org.springframework.ai.vectorstore.filter.FilterExpressionConverter;
4243
import org.springframework.ai.vectorstore.observation.AbstractObservationVectorStore;
4344
import org.springframework.ai.vectorstore.observation.VectorStoreObservationContext;
@@ -133,6 +134,7 @@
133134
*
134135
* @author Diego Dupin
135136
* @author Ilayaperumal Gopinathan
137+
* @author Soby Chacko
136138
* @since 1.0.0
137139
*/
138140
public class MariaDBVectorStore extends AbstractObservationVectorStore implements InitializingBean {
@@ -325,6 +327,25 @@ public Optional<Boolean> doDelete(List<String> idList) {
325327
return Optional.of(updateCount == idList.size());
326328
}
327329

330+
@Override
331+
protected void doDelete(Filter.Expression filterExpression) {
332+
Assert.notNull(filterExpression, "Filter expression must not be null");
333+
334+
try {
335+
String nativeFilterExpression = this.filterExpressionConverter.convertExpression(filterExpression);
336+
337+
String sql = String.format("DELETE FROM %s WHERE %s", getFullyQualifiedTableName(), nativeFilterExpression);
338+
339+
logger.debug("Executing delete with filter: {}", sql);
340+
341+
this.jdbcTemplate.update(sql);
342+
}
343+
catch (Exception e) {
344+
logger.error("Failed to delete documents by filter: {}", e.getMessage(), e);
345+
throw new IllegalStateException("Failed to delete documents by filter", e);
346+
}
347+
}
348+
328349
@Override
329350
public List<Document> doSimilaritySearch(SearchRequest request) {
330351

vector-stores/spring-ai-mariadb-store/src/test/java/org/springframework/ai/vectorstore/mariadb/MariaDBStoreIT.java

Lines changed: 105 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.
@@ -23,12 +23,14 @@
2323
import java.util.List;
2424
import java.util.Map;
2525
import java.util.UUID;
26+
import java.util.stream.Collectors;
2627
import java.util.stream.Stream;
2728

2829
import javax.sql.DataSource;
2930

3031
import com.zaxxer.hikari.HikariDataSource;
3132
import org.junit.Assert;
33+
import org.junit.jupiter.api.Test;
3234
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
3335
import org.junit.jupiter.params.ParameterizedTest;
3436
import org.junit.jupiter.params.provider.Arguments;
@@ -44,6 +46,7 @@
4446
import org.springframework.ai.openai.api.OpenAiApi;
4547
import org.springframework.ai.vectorstore.SearchRequest;
4648
import org.springframework.ai.vectorstore.VectorStore;
49+
import org.springframework.ai.vectorstore.filter.Filter;
4750
import org.springframework.ai.vectorstore.filter.FilterExpressionTextParser.FilterExpressionParseException;
4851
import org.springframework.beans.factory.annotation.Value;
4952
import org.springframework.boot.SpringBootConfiguration;
@@ -63,6 +66,7 @@
6366

6467
/**
6568
* @author Diego Dupin
69+
* @author Soby Chacko
6670
*/
6771
@Testcontainers
6872
@EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".+")
@@ -357,6 +361,106 @@ public void searchWithThreshold(String distanceType) {
357361
});
358362
}
359363

364+
@Test
365+
public void deleteByFilter() {
366+
this.contextRunner.withPropertyValues("test.spring.ai.vectorstore.mariadb.distanceType=COSINE").run(context -> {
367+
VectorStore vectorStore = context.getBean(VectorStore.class);
368+
369+
var bgDocument = new Document("The World is Big and Salvation Lurks Around the Corner",
370+
Map.of("country", "BG", "year", 2020));
371+
var nlDocument = new Document("The World is Big and Salvation Lurks Around the Corner",
372+
Map.of("country", "NL", "year", 2021));
373+
var bgDocument2 = new Document("The World is Big and Salvation Lurks Around the Corner",
374+
Map.of("country", "BG", "year", 2023));
375+
376+
vectorStore.add(List.of(bgDocument, nlDocument, bgDocument2));
377+
378+
SearchRequest searchRequest = SearchRequest.builder()
379+
.query("The World")
380+
.topK(5)
381+
.similarityThresholdAll()
382+
.build();
383+
384+
List<Document> results = vectorStore.similaritySearch(searchRequest);
385+
assertThat(results).hasSize(3);
386+
387+
Filter.Expression filterExpression = new Filter.Expression(Filter.ExpressionType.EQ,
388+
new Filter.Key("country"), new Filter.Value("BG"));
389+
390+
vectorStore.delete(filterExpression);
391+
392+
// Verify deletion - should only have NL document remaining
393+
results = vectorStore.similaritySearch(searchRequest);
394+
assertThat(results).hasSize(1);
395+
assertThat(results.get(0).getMetadata()).containsEntry("country", "NL");
396+
397+
dropTable(context);
398+
});
399+
}
400+
401+
@Test
402+
public void deleteWithStringFilterExpression() {
403+
this.contextRunner.withPropertyValues("test.spring.ai.vectorstore.mariadb.distanceType=COSINE").run(context -> {
404+
VectorStore vectorStore = context.getBean(VectorStore.class);
405+
406+
var bgDocument = new Document("The World is Big and Salvation Lurks Around the Corner",
407+
Map.of("country", "BG", "year", 2020));
408+
var nlDocument = new Document("The World is Big and Salvation Lurks Around the Corner",
409+
Map.of("country", "NL", "year", 2021));
410+
var bgDocument2 = new Document("The World is Big and Salvation Lurks Around the Corner",
411+
Map.of("country", "BG", "year", 2023));
412+
413+
vectorStore.add(List.of(bgDocument, nlDocument, bgDocument2));
414+
415+
var searchRequest = SearchRequest.builder().query("The World").topK(5).similarityThresholdAll().build();
416+
417+
List<Document> results = vectorStore.similaritySearch(searchRequest);
418+
assertThat(results).hasSize(3);
419+
420+
vectorStore.delete("country == 'BG'");
421+
422+
results = vectorStore.similaritySearch(searchRequest);
423+
assertThat(results).hasSize(1);
424+
assertThat(results.get(0).getMetadata()).containsEntry("country", "NL");
425+
426+
dropTable(context);
427+
});
428+
}
429+
430+
@Test
431+
public void deleteWithComplexFilterExpression() {
432+
this.contextRunner.withPropertyValues("test.spring.ai.vectorstore.mariadb.distanceType=COSINE").run(context -> {
433+
VectorStore vectorStore = context.getBean(VectorStore.class);
434+
435+
var doc1 = new Document("Content 1", Map.of("type", "A", "priority", 1));
436+
var doc2 = new Document("Content 2", Map.of("type", "A", "priority", 2));
437+
var doc3 = new Document("Content 3", Map.of("type", "B", "priority", 1));
438+
439+
vectorStore.add(List.of(doc1, doc2, doc3));
440+
441+
// Complex filter expression: (type == 'A' AND priority > 1)
442+
Filter.Expression priorityFilter = new Filter.Expression(Filter.ExpressionType.GT,
443+
new Filter.Key("priority"), new Filter.Value(1));
444+
Filter.Expression typeFilter = new Filter.Expression(Filter.ExpressionType.EQ, new Filter.Key("type"),
445+
new Filter.Value("A"));
446+
Filter.Expression complexFilter = new Filter.Expression(Filter.ExpressionType.AND, typeFilter,
447+
priorityFilter);
448+
449+
vectorStore.delete(complexFilter);
450+
451+
var results = vectorStore
452+
.similaritySearch(SearchRequest.builder().query("Content").topK(5).similarityThresholdAll().build());
453+
454+
assertThat(results).hasSize(2);
455+
assertThat(results.stream().map(doc -> doc.getMetadata().get("type")).collect(Collectors.toList()))
456+
.containsExactlyInAnyOrder("A", "B");
457+
assertThat(results.stream().map(doc -> doc.getMetadata().get("priority")).collect(Collectors.toList()))
458+
.containsExactlyInAnyOrder(1, 1);
459+
460+
dropTable(context);
461+
});
462+
}
463+
360464
@SpringBootConfiguration
361465
@EnableAutoConfiguration(exclude = { DataSourceAutoConfiguration.class })
362466
public static class TestApplication {

0 commit comments

Comments
 (0)