Skip to content

Commit 49f2320

Browse files
authored
Add filter-based deletion to Typesense vector store (#2140)
Add string-based filter deletion alongside the Filter.Expression-based deletion for Typesense vector store, providing consistent deletion capabilities with other vector store implementations. Key changes: - Add delete(Filter.Expression) implementation using Typesense filter expressions - Leverage existing TypesenseFilterExpressionConverter for filter translation - Use Typesense's DeleteDocumentsParameters for filtered 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 Typesense's native filtering capabilities for efficient metadata-based deletion. Signed-off-by: Soby Chacko <[email protected]>
1 parent 8cf33fe commit 49f2320

File tree

2 files changed

+121
-1
lines changed

2 files changed

+121
-1
lines changed

vector-stores/spring-ai-typesense-store/src/main/java/org/springframework/ai/vectorstore/typesense/TypesenseVectorStore.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
import org.springframework.ai.observation.conventions.VectorStoreSimilarityMetric;
4444
import org.springframework.ai.vectorstore.AbstractVectorStoreBuilder;
4545
import org.springframework.ai.vectorstore.SearchRequest;
46+
import org.springframework.ai.vectorstore.filter.Filter;
4647
import org.springframework.ai.vectorstore.filter.FilterExpressionConverter;
4748
import org.springframework.ai.vectorstore.observation.AbstractObservationVectorStore;
4849
import org.springframework.ai.vectorstore.observation.VectorStoreObservationContext;
@@ -69,6 +70,7 @@
6970
* @author Christian Tzolov
7071
* @author Eddú Meléndez
7172
* @author Mark Pollack
73+
* @author Soby Chacko
7274
* @see org.springframework.ai.vectorstore.VectorStore
7375
* @see org.springframework.ai.embedding.EmbeddingModel
7476
*/
@@ -187,6 +189,33 @@ public Optional<Boolean> doDelete(List<String> idList) {
187189
}
188190
}
189191

192+
@Override
193+
protected void doDelete(Filter.Expression filterExpression) {
194+
Assert.notNull(filterExpression, "Filter expression must not be null");
195+
196+
try {
197+
String filterStr = this.filterExpressionConverter.convertExpression(filterExpression);
198+
DeleteDocumentsParameters deleteDocumentsParameters = new DeleteDocumentsParameters();
199+
deleteDocumentsParameters.filterBy(filterStr);
200+
201+
Map<String, Object> response = this.client.collections(this.collectionName)
202+
.documents()
203+
.delete(deleteDocumentsParameters);
204+
205+
int deletedDocs = (Integer) response.getOrDefault("num_deleted", 0);
206+
if (deletedDocs == 0) {
207+
logger.warn(() -> "No documents were deleted matching filter expression");
208+
}
209+
else {
210+
logger.debug(() -> "Deleted " + deletedDocs + " documents matching filter expression");
211+
}
212+
}
213+
catch (Exception e) {
214+
logger.error(e, () -> "Failed to delete documents by filter");
215+
throw new IllegalStateException("Failed to delete documents by filter", e);
216+
}
217+
}
218+
190219
@Override
191220
public List<Document> doSimilaritySearch(SearchRequest request) {
192221
Assert.notNull(request.getQuery(), "Query string must not be null");

vector-stores/spring-ai-typesense-store/src/test/java/org/springframework/ai/vectorstore/typesense/TypesenseVectorStoreIT.java

Lines changed: 92 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.
@@ -24,6 +24,7 @@
2424
import java.util.List;
2525
import java.util.Map;
2626
import java.util.UUID;
27+
import java.util.stream.Collectors;
2728

2829
import org.junit.jupiter.api.Test;
2930

@@ -40,6 +41,7 @@
4041
import org.springframework.ai.transformers.TransformersEmbeddingModel;
4142
import org.springframework.ai.vectorstore.SearchRequest;
4243
import org.springframework.ai.vectorstore.VectorStore;
44+
import org.springframework.ai.vectorstore.filter.Filter;
4345
import org.springframework.boot.SpringBootConfiguration;
4446
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
4547
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
@@ -244,6 +246,95 @@ void searchWithThreshold() {
244246
});
245247
}
246248

249+
@Test
250+
void deleteByFilter() {
251+
this.contextRunner.run(context -> {
252+
VectorStore vectorStore = context.getBean(VectorStore.class);
253+
254+
var bgDocument = new Document("The World is Big and Salvation Lurks Around the Corner",
255+
Map.of("country", "BG", "year", 2020));
256+
var nlDocument = new Document("The World is Big and Salvation Lurks Around the Corner",
257+
Map.of("country", "NL"));
258+
var bgDocument2 = new Document("The World is Big and Salvation Lurks Around the Corner",
259+
Map.of("country", "BG", "year", 2023));
260+
261+
vectorStore.add(List.of(bgDocument, nlDocument, bgDocument2));
262+
263+
Filter.Expression filterExpression = new Filter.Expression(Filter.ExpressionType.EQ,
264+
new Filter.Key("country"), new Filter.Value("BG"));
265+
266+
vectorStore.delete(filterExpression);
267+
268+
List<Document> results = vectorStore
269+
.similaritySearch(SearchRequest.builder().query("The World").topK(5).similarityThresholdAll().build());
270+
271+
assertThat(results).hasSize(1);
272+
assertThat(results.get(0).getMetadata()).containsEntry("country", "NL");
273+
274+
((TypesenseVectorStore) vectorStore).dropCollection();
275+
});
276+
}
277+
278+
@Test
279+
void deleteWithStringFilterExpression() {
280+
this.contextRunner.run(context -> {
281+
VectorStore vectorStore = context.getBean(VectorStore.class);
282+
283+
var bgDocument = new Document("The World is Big and Salvation Lurks Around the Corner",
284+
Map.of("country", "BG", "year", 2020));
285+
var nlDocument = new Document("The World is Big and Salvation Lurks Around the Corner",
286+
Map.of("country", "NL"));
287+
var bgDocument2 = new Document("The World is Big and Salvation Lurks Around the Corner",
288+
Map.of("country", "BG", "year", 2023));
289+
290+
vectorStore.add(List.of(bgDocument, nlDocument, bgDocument2));
291+
292+
vectorStore.delete("country == 'BG'");
293+
294+
List<Document> results = vectorStore
295+
.similaritySearch(SearchRequest.builder().query("The World").topK(5).similarityThresholdAll().build());
296+
297+
assertThat(results).hasSize(1);
298+
assertThat(results.get(0).getMetadata()).containsEntry("country", "NL");
299+
300+
((TypesenseVectorStore) vectorStore).dropCollection();
301+
});
302+
}
303+
304+
@Test
305+
void deleteWithComplexFilterExpression() {
306+
this.contextRunner.run(context -> {
307+
VectorStore vectorStore = context.getBean(VectorStore.class);
308+
309+
var doc1 = new Document("Content 1", Map.of("type", "A", "priority", 1));
310+
var doc2 = new Document("Content 2", Map.of("type", "A", "priority", 2));
311+
var doc3 = new Document("Content 3", Map.of("type", "B", "priority", 1));
312+
313+
vectorStore.add(List.of(doc1, doc2, doc3));
314+
315+
// Complex filter expression: (type == 'A' AND priority > 1)
316+
Filter.Expression priorityFilter = new Filter.Expression(Filter.ExpressionType.GT,
317+
new Filter.Key("priority"), new Filter.Value(1));
318+
Filter.Expression typeFilter = new Filter.Expression(Filter.ExpressionType.EQ, new Filter.Key("type"),
319+
new Filter.Value("A"));
320+
Filter.Expression complexFilter = new Filter.Expression(Filter.ExpressionType.AND, typeFilter,
321+
priorityFilter);
322+
323+
vectorStore.delete(complexFilter);
324+
325+
var results = vectorStore
326+
.similaritySearch(SearchRequest.builder().query("Content").topK(5).similarityThresholdAll().build());
327+
328+
assertThat(results).hasSize(2);
329+
assertThat(results.stream().map(doc -> doc.getMetadata().get("type")).collect(Collectors.toList()))
330+
.containsExactlyInAnyOrder("A", "B");
331+
assertThat(results.stream().map(doc -> doc.getMetadata().get("priority")).collect(Collectors.toList()))
332+
.containsExactlyInAnyOrder(1, 1);
333+
334+
((TypesenseVectorStore) vectorStore).dropCollection();
335+
});
336+
}
337+
247338
@SpringBootConfiguration
248339
@EnableAutoConfiguration(exclude = { DataSourceAutoConfiguration.class })
249340
public static class TestApplication {

0 commit comments

Comments
 (0)