From f650433f9772ba4bf03e921fdb7ac3ccf688c28c Mon Sep 17 00:00:00 2001 From: Soby Chacko Date: Mon, 10 Feb 2025 17:25:06 -0500 Subject: [PATCH 1/3] Add missing integration tests for delete by ID API in vector store implementations. Signed-off-by: Soby Chacko --- .../cassandra/CassandraVectorStoreIT.java | 32 +++++++++++++++++ .../vectorstore/ChromaVectorStoreIT.java | 35 ++++++++++++++++++ .../vectorstore/mariadb/MariaDBStoreIT.java | 34 ++++++++++++++++++ .../milvus/MilvusVectorStoreIT.java | 34 ++++++++++++++++++ .../atlas/MongoDBAtlasVectorStoreIT.java | 33 +++++++++++++++++ .../vectorstore/neo4j/Neo4jVectorStoreIT.java | 31 ++++++++++++++++ .../opensearch/OpenSearchVectorStoreIT.java | 32 +++++++++++++++++ .../oracle/OracleVectorStoreIT.java | 36 +++++++++++++++++++ .../qdrant/QdrantVectorStoreIT.java | 28 +++++++++++++++ .../vectorstore/redis/RedisVectorStoreIT.java | 28 +++++++++++++++ .../typesense/TypesenseVectorStoreIT.java | 28 +++++++++++++++ .../weaviate/WeaviateVectorStoreIT.java | 28 +++++++++++++++ 12 files changed, 379 insertions(+) diff --git a/vector-stores/spring-ai-cassandra-store/src/test/java/org/springframework/ai/vectorstore/cassandra/CassandraVectorStoreIT.java b/vector-stores/spring-ai-cassandra-store/src/test/java/org/springframework/ai/vectorstore/cassandra/CassandraVectorStoreIT.java index e30ba900d44..7a2329fe61e 100644 --- a/vector-stores/spring-ai-cassandra-store/src/test/java/org/springframework/ai/vectorstore/cassandra/CassandraVectorStoreIT.java +++ b/vector-stores/spring-ai-cassandra-store/src/test/java/org/springframework/ai/vectorstore/cassandra/CassandraVectorStoreIT.java @@ -421,6 +421,38 @@ void searchWithThreshold() { }); } + @Test + void deleteById() { + this.contextRunner.run(context -> { + try (CassandraVectorStore store = createTestStore(context, + new SchemaColumn("country", DataTypes.TEXT, SchemaColumnTags.INDEXED), + new SchemaColumn("year", DataTypes.SMALLINT, SchemaColumnTags.INDEXED))) { + + var bgDocument = new Document("The World is Big and Salvation Lurks Around the Corner", + Map.of("country", "BG", "year", (short) 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", (short) 2023)); + + store.add(List.of(bgDocument, nlDocument, bgDocument2)); + + // Verify initial state + List results = store + .similaritySearch(SearchRequest.builder().query("The World").topK(5).build()); + assertThat(results).hasSize(3); + + store.delete(List.of(bgDocument.getId(), bgDocument2.getId())); + + results = store.similaritySearch( + SearchRequest.builder().query("The World").topK(5).similarityThresholdAll().build()); + + assertThat(results).hasSize(1); + assertThat(results.get(0).getMetadata()).containsEntry("country", "NL"); + } + }); + } + @Test void deleteByFilter() { this.contextRunner.run(context -> { diff --git a/vector-stores/spring-ai-chroma-store/src/test/java/org/springframework/ai/chroma/vectorstore/ChromaVectorStoreIT.java b/vector-stores/spring-ai-chroma-store/src/test/java/org/springframework/ai/chroma/vectorstore/ChromaVectorStoreIT.java index 765e3b5633f..d874c169042 100644 --- a/vector-stores/spring-ai-chroma-store/src/test/java/org/springframework/ai/chroma/vectorstore/ChromaVectorStoreIT.java +++ b/vector-stores/spring-ai-chroma-store/src/test/java/org/springframework/ai/chroma/vectorstore/ChromaVectorStoreIT.java @@ -168,6 +168,41 @@ public void addAndSearchWithFilters() { }); } + @Test + public void deleteById() { + this.contextRunner.withPropertyValues("test.spring.ai.vectorstore.mariadb.distanceType=COSINE").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", "year", 2021)); + 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)); + + SearchRequest searchRequest = SearchRequest.builder() + .query("The World") + .topK(5) + .similarityThresholdAll() + .build(); + + List results = vectorStore.similaritySearch(searchRequest); + assertThat(results).hasSize(3); + + Filter.Expression filterExpression = new Filter.Expression(Filter.ExpressionType.EQ, + new Filter.Key("country"), new Filter.Value("BG")); + + vectorStore.delete(List.of(bgDocument.getId(), bgDocument2.getId())); + + // Verify deletion - should only have NL document remaining + results = vectorStore.similaritySearch(searchRequest); + assertThat(results).hasSize(1); + assertThat(results.get(0).getMetadata()).containsEntry("country", "NL"); + }); + } + @Test public void deleteWithFilterExpression() { this.contextRunner.run(context -> { diff --git a/vector-stores/spring-ai-mariadb-store/src/test/java/org/springframework/ai/vectorstore/mariadb/MariaDBStoreIT.java b/vector-stores/spring-ai-mariadb-store/src/test/java/org/springframework/ai/vectorstore/mariadb/MariaDBStoreIT.java index 315254e4b4d..25693a2d970 100644 --- a/vector-stores/spring-ai-mariadb-store/src/test/java/org/springframework/ai/vectorstore/mariadb/MariaDBStoreIT.java +++ b/vector-stores/spring-ai-mariadb-store/src/test/java/org/springframework/ai/vectorstore/mariadb/MariaDBStoreIT.java @@ -362,6 +362,40 @@ public void searchWithThreshold(String distanceType) { }); } + @Test + public void deleteById() { + this.contextRunner.withPropertyValues("test.spring.ai.vectorstore.mariadb.distanceType=COSINE").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", "year", 2021)); + 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)); + + SearchRequest searchRequest = SearchRequest.builder() + .query("The World") + .topK(5) + .similarityThresholdAll() + .build(); + + List results = vectorStore.similaritySearch(searchRequest); + assertThat(results).hasSize(3); + + vectorStore.delete(List.of(bgDocument.getId(), bgDocument2.getId())); + + // Verify deletion - should only have NL document remaining + results = vectorStore.similaritySearch(searchRequest); + assertThat(results).hasSize(1); + assertThat(results.get(0).getMetadata()).containsEntry("country", "NL"); + + dropTable(context); + }); + } + @Test public void deleteByFilter() { this.contextRunner.withPropertyValues("test.spring.ai.vectorstore.mariadb.distanceType=COSINE").run(context -> { diff --git a/vector-stores/spring-ai-milvus-store/src/test/java/org/springframework/ai/vectorstore/milvus/MilvusVectorStoreIT.java b/vector-stores/spring-ai-milvus-store/src/test/java/org/springframework/ai/vectorstore/milvus/MilvusVectorStoreIT.java index 956e13819c5..63348f1bb74 100644 --- a/vector-stores/spring-ai-milvus-store/src/test/java/org/springframework/ai/vectorstore/milvus/MilvusVectorStoreIT.java +++ b/vector-stores/spring-ai-milvus-store/src/test/java/org/springframework/ai/vectorstore/milvus/MilvusVectorStoreIT.java @@ -278,6 +278,40 @@ public void searchWithThreshold(String metricType) { }); } + @Test + public void deleteById() { + this.contextRunner.withPropertyValues("test.spring.ai.vectorstore.milvus.metricType=COSINE").run(context -> { + VectorStore vectorStore = context.getBean(VectorStore.class); + + resetCollection(vectorStore); + + 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", "year", 2021)); + 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)); + + SearchRequest searchRequest = SearchRequest.builder() + .query("The World") + .topK(5) + .similarityThresholdAll() + .build(); + + List results = vectorStore.similaritySearch(searchRequest); + assertThat(results).hasSize(3); + + vectorStore.delete(List.of(bgDocument.getId(), bgDocument2.getId())); + + // Verify deletion - should only have NL document remaining + results = vectorStore.similaritySearch(searchRequest); + assertThat(results).hasSize(1); + assertThat(results.get(0).getMetadata()).containsEntry("country", "NL"); + }); + } + @Test public void deleteByFilter() { this.contextRunner.withPropertyValues("test.spring.ai.vectorstore.milvus.metricType=COSINE").run(context -> { diff --git a/vector-stores/spring-ai-mongodb-atlas-store/src/test/java/org/springframework/ai/vectorstore/mongodb/atlas/MongoDBAtlasVectorStoreIT.java b/vector-stores/spring-ai-mongodb-atlas-store/src/test/java/org/springframework/ai/vectorstore/mongodb/atlas/MongoDBAtlasVectorStoreIT.java index 3e98bae372d..d4231be0bdd 100644 --- a/vector-stores/spring-ai-mongodb-atlas-store/src/test/java/org/springframework/ai/vectorstore/mongodb/atlas/MongoDBAtlasVectorStoreIT.java +++ b/vector-stores/spring-ai-mongodb-atlas-store/src/test/java/org/springframework/ai/vectorstore/mongodb/atlas/MongoDBAtlasVectorStoreIT.java @@ -257,6 +257,39 @@ public void searchWithThreshold() { }); } + @Test + void deleteById() { + 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", "year", 2021)); + 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)); + Thread.sleep(5000); // Wait for indexing + + SearchRequest searchRequest = SearchRequest.builder() + .query("The World") + .topK(5) + .similarityThresholdAll() + .build(); + + List results = vectorStore.similaritySearch(searchRequest); + assertThat(results).hasSize(3); + + vectorStore.delete(List.of(bgDocument.getId(), bgDocument2.getId())); + Thread.sleep(1000); // Wait for deletion to be processed + + results = vectorStore.similaritySearch(searchRequest); + assertThat(results).hasSize(1); + assertThat(results.get(0).getMetadata()).containsEntry("country", "NL"); + }); + } + @Test void deleteByFilter() { this.contextRunner.run(context -> { diff --git a/vector-stores/spring-ai-neo4j-store/src/test/java/org/springframework/ai/vectorstore/neo4j/Neo4jVectorStoreIT.java b/vector-stores/spring-ai-neo4j-store/src/test/java/org/springframework/ai/vectorstore/neo4j/Neo4jVectorStoreIT.java index 29974c8009d..ffdd0e0682d 100644 --- a/vector-stores/spring-ai-neo4j-store/src/test/java/org/springframework/ai/vectorstore/neo4j/Neo4jVectorStoreIT.java +++ b/vector-stores/spring-ai-neo4j-store/src/test/java/org/springframework/ai/vectorstore/neo4j/Neo4jVectorStoreIT.java @@ -305,6 +305,37 @@ void ensureIdIndexGetsCreated() { .isTrue()); } + @Test + void deleteById() { + 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", "year", 2021)); + 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)); + + SearchRequest searchRequest = SearchRequest.builder() + .query("The World") + .topK(5) + .similarityThresholdAll() + .build(); + + List results = vectorStore.similaritySearch(searchRequest); + assertThat(results).hasSize(3); + + vectorStore.delete(List.of(bgDocument.getId(), bgDocument2.getId())); + + results = vectorStore.similaritySearch(searchRequest); + assertThat(results).hasSize(1); + assertThat(results.get(0).getMetadata()).containsEntry("country", "NL"); + }); + } + @Test void deleteByFilter() { this.contextRunner.run(context -> { diff --git a/vector-stores/spring-ai-opensearch-store/src/test/java/org/springframework/ai/vectorstore/opensearch/OpenSearchVectorStoreIT.java b/vector-stores/spring-ai-opensearch-store/src/test/java/org/springframework/ai/vectorstore/opensearch/OpenSearchVectorStoreIT.java index 8eeae052b4f..b401983a092 100644 --- a/vector-stores/spring-ai-opensearch-store/src/test/java/org/springframework/ai/vectorstore/opensearch/OpenSearchVectorStoreIT.java +++ b/vector-stores/spring-ai-opensearch-store/src/test/java/org/springframework/ai/vectorstore/opensearch/OpenSearchVectorStoreIT.java @@ -416,6 +416,38 @@ public void searchDocumentsInTwoIndicesTest() { }); } + @Test + void deleteById() { + getContextRunner().run(context -> { + OpenSearchVectorStore vectorStore = context.getBean("vectorStore", OpenSearchVectorStore.class); + + var bgDocument = new Document("1", "The World is Big and Salvation Lurks Around the Corner", + Map.of("country", "BG", "year", 2020, "activationDate", new Date(1000))); + var nlDocument = new Document("2", "The World is Big and Salvation Lurks Around the Corner", + Map.of("country", "NL", "activationDate", new Date(2000))); + var bgDocument2 = new Document("3", "The World is Big and Salvation Lurks Around the Corner", + Map.of("country", "BG", "year", 2023, "activationDate", new Date(3000))); + + vectorStore.add(List.of(bgDocument, nlDocument, bgDocument2)); + + Awaitility.await() + .until(() -> vectorStore.similaritySearch(SearchRequest.builder().query("The World").topK(5).build()), + hasSize(3)); + + vectorStore.delete(List.of(bgDocument.getId(), bgDocument2.getId())); + + Awaitility.await() + .until(() -> vectorStore.similaritySearch(SearchRequest.builder().query("The World").topK(5).build()), + hasSize(1)); + + List results = vectorStore + .similaritySearch(SearchRequest.builder().query("The World").topK(5).similarityThresholdAll().build()); + + assertThat(results).hasSize(1); + assertThat(results.get(0).getMetadata()).containsEntry("country", "NL"); + }); + } + @Test void deleteByFilter() { getContextRunner().run(context -> { diff --git a/vector-stores/spring-ai-oracle-store/src/test/java/org/springframework/ai/vectorstore/oracle/OracleVectorStoreIT.java b/vector-stores/spring-ai-oracle-store/src/test/java/org/springframework/ai/vectorstore/oracle/OracleVectorStoreIT.java index aaa7fb9a98d..1ef260808d3 100644 --- a/vector-stores/spring-ai-oracle-store/src/test/java/org/springframework/ai/vectorstore/oracle/OracleVectorStoreIT.java +++ b/vector-stores/spring-ai-oracle-store/src/test/java/org/springframework/ai/vectorstore/oracle/OracleVectorStoreIT.java @@ -317,6 +317,42 @@ public void searchWithThreshold(String distanceType) { }); } + @Test + void deleteById() { + this.contextRunner + .withPropertyValues("test.spring.ai.vectorstore.oracle.distanceType=COSINE", + "test.spring.ai.vectorstore.oracle.searchAccuracy=" + OracleVectorStore.DEFAULT_SEARCH_ACCURACY) + .run(context -> { + VectorStore vectorStore = context.getBean(VectorStore.class); + + var doc1 = new Document("The World is Big and Salvation Lurks Around the Corner", + Map.of("country", "BG", "year", 2020)); + var doc2 = new Document("The World is Big and Salvation Lurks Around the Corner", + Map.of("country", "NL")); + var doc3 = new Document("The World is Big and Salvation Lurks Around the Corner", + Map.of("country", "BG", "year", 2023)); + + vectorStore.add(List.of(doc1, doc2, doc3)); + + // Delete first two documents + vectorStore.delete(List.of(doc1.getId(), doc2.getId())); + + List results = vectorStore.similaritySearch( + SearchRequest.builder().query("The World").topK(5).similarityThresholdAll().build()); + + assertThat(results).hasSize(1); + assertThat(results.get(0).getId()).isEqualTo(doc3.getId()); + assertThat(results).hasSize(1); + assertThat(results.get(0).getId()).isEqualTo(doc3.getId()); + assertThat(results.get(0).getMetadata()) + .hasEntrySatisfying("country", value -> assertThat(value.toString()).isEqualTo("\"BG\"")) + .hasEntrySatisfying("year", value -> assertThat(value.toString()).isEqualTo("2023")); + + // Clean up remaining document + vectorStore.delete(List.of(doc3.getId())); + }); + } + @Test void deleteByFilter() { this.contextRunner diff --git a/vector-stores/spring-ai-qdrant-store/src/test/java/org/springframework/ai/vectorstore/qdrant/QdrantVectorStoreIT.java b/vector-stores/spring-ai-qdrant-store/src/test/java/org/springframework/ai/vectorstore/qdrant/QdrantVectorStoreIT.java index cf73709b329..9eef3556c36 100644 --- a/vector-stores/spring-ai-qdrant-store/src/test/java/org/springframework/ai/vectorstore/qdrant/QdrantVectorStoreIT.java +++ b/vector-stores/spring-ai-qdrant-store/src/test/java/org/springframework/ai/vectorstore/qdrant/QdrantVectorStoreIT.java @@ -260,6 +260,34 @@ public void searchThresholdTest() { }); } + @Test + void deleteById() { + this.contextRunner.run(context -> { + VectorStore vectorStore = context.getBean(VectorStore.class); + + var doc1 = new Document("The World is Big and Salvation Lurks Around the Corner", + Map.of("country", "BG", "year", 2020)); + var doc2 = new Document("The World is Big and Salvation Lurks Around the Corner", Map.of("country", "NL")); + var doc3 = new Document("The World is Big and Salvation Lurks Around the Corner", + Map.of("country", "BG", "year", 2023)); + + vectorStore.add(List.of(doc1, doc2, doc3)); + + // Delete first two documents + vectorStore.delete(List.of(doc1.getId(), doc2.getId())); + + List results = vectorStore + .similaritySearch(SearchRequest.builder().query("The World").topK(5).similarityThresholdAll().build()); + + assertThat(results).hasSize(1); + assertThat(results.get(0).getId()).isEqualTo(doc3.getId()); + assertThat(results.get(0).getMetadata()).containsEntry("country", "BG").containsEntry("year", 2023L); + + // Clean up remaining document + vectorStore.delete(List.of(doc3.getId())); + }); + } + @Test void deleteByFilter() { this.contextRunner.run(context -> { diff --git a/vector-stores/spring-ai-redis-store/src/test/java/org/springframework/ai/vectorstore/redis/RedisVectorStoreIT.java b/vector-stores/spring-ai-redis-store/src/test/java/org/springframework/ai/vectorstore/redis/RedisVectorStoreIT.java index a2e7dd53efe..601d5c52801 100644 --- a/vector-stores/spring-ai-redis-store/src/test/java/org/springframework/ai/vectorstore/redis/RedisVectorStoreIT.java +++ b/vector-stores/spring-ai-redis-store/src/test/java/org/springframework/ai/vectorstore/redis/RedisVectorStoreIT.java @@ -264,6 +264,34 @@ void searchWithThreshold() { }); } + @Test + void deleteById() { + this.contextRunner.run(context -> { + VectorStore vectorStore = context.getBean(VectorStore.class); + + var doc1 = new Document("The World is Big and Salvation Lurks Around the Corner", + Map.of("country", "BG", "year", 2020)); + var doc2 = new Document("The World is Big and Salvation Lurks Around the Corner", Map.of("country", "NL")); + var doc3 = new Document("The World is Big and Salvation Lurks Around the Corner", + Map.of("country", "BG", "year", 2023)); + + vectorStore.add(List.of(doc1, doc2, doc3)); + + // Delete first two documents + vectorStore.delete(List.of(doc1.getId(), doc2.getId())); + + List results = vectorStore + .similaritySearch(SearchRequest.builder().query("The World").topK(5).similarityThresholdAll().build()); + + assertThat(results).hasSize(1); + assertThat(results.get(0).getId()).isEqualTo(doc3.getId()); + assertThat(results.get(0).getMetadata()).containsEntry("country", "BG").containsEntry("year", "2023"); + + // Clean up remaining document + vectorStore.delete(List.of(doc3.getId())); + }); + } + @Test void deleteByFilter() { this.contextRunner.run(context -> { diff --git a/vector-stores/spring-ai-typesense-store/src/test/java/org/springframework/ai/vectorstore/typesense/TypesenseVectorStoreIT.java b/vector-stores/spring-ai-typesense-store/src/test/java/org/springframework/ai/vectorstore/typesense/TypesenseVectorStoreIT.java index 0ed2fdecc43..a6ba72af5b9 100644 --- a/vector-stores/spring-ai-typesense-store/src/test/java/org/springframework/ai/vectorstore/typesense/TypesenseVectorStoreIT.java +++ b/vector-stores/spring-ai-typesense-store/src/test/java/org/springframework/ai/vectorstore/typesense/TypesenseVectorStoreIT.java @@ -246,6 +246,34 @@ void searchWithThreshold() { }); } + @Test + void deleteById() { + this.contextRunner.run(context -> { + VectorStore vectorStore = context.getBean(VectorStore.class); + + var doc1 = new Document("The World is Big and Salvation Lurks Around the Corner", + Map.of("country", "BG", "year", 2020)); + var doc2 = new Document("The World is Big and Salvation Lurks Around the Corner", Map.of("country", "NL")); + var doc3 = new Document("The World is Big and Salvation Lurks Around the Corner", + Map.of("country", "BG", "year", 2023)); + + vectorStore.add(List.of(doc1, doc2, doc3)); + + // Delete first two documents + vectorStore.delete(List.of(doc1.getId(), doc2.getId())); + + List results = vectorStore + .similaritySearch(SearchRequest.builder().query("The World").topK(5).similarityThresholdAll().build()); + + assertThat(results).hasSize(1); + assertThat(results.get(0).getId()).isEqualTo(doc3.getId()); + assertThat(results.get(0).getMetadata()).containsEntry("country", "BG").containsEntry("year", 2023); + + // Clean up remaining document + vectorStore.delete(List.of(doc3.getId())); + }); + } + @Test void deleteByFilter() { this.contextRunner.run(context -> { diff --git a/vector-stores/spring-ai-weaviate-store/src/test/java/org/springframework/ai/vectorstore/weaviate/WeaviateVectorStoreIT.java b/vector-stores/spring-ai-weaviate-store/src/test/java/org/springframework/ai/vectorstore/weaviate/WeaviateVectorStoreIT.java index 459d48ed8d4..d5fcc1cce76 100644 --- a/vector-stores/spring-ai-weaviate-store/src/test/java/org/springframework/ai/vectorstore/weaviate/WeaviateVectorStoreIT.java +++ b/vector-stores/spring-ai-weaviate-store/src/test/java/org/springframework/ai/vectorstore/weaviate/WeaviateVectorStoreIT.java @@ -256,6 +256,34 @@ public void searchWithThreshold() { }); } + @Test + void deleteById() { + this.contextRunner.run(context -> { + VectorStore vectorStore = context.getBean(VectorStore.class); + + var doc1 = new Document("The World is Big and Salvation Lurks Around the Corner", + Map.of("country", "BG", "year", 2020)); + var doc2 = new Document("The World is Big and Salvation Lurks Around the Corner", Map.of("country", "NL")); + var doc3 = new Document("The World is Big and Salvation Lurks Around the Corner", + Map.of("country", "BG", "year", 2023)); + + vectorStore.add(List.of(doc1, doc2, doc3)); + + // Delete first two documents + vectorStore.delete(List.of(doc1.getId(), doc2.getId())); + + List results = vectorStore + .similaritySearch(SearchRequest.builder().query("The World").topK(5).similarityThresholdAll().build()); + + assertThat(results).hasSize(1); + assertThat(results.get(0).getId()).isEqualTo(doc3.getId()); + assertThat(results.get(0).getMetadata()).containsEntry("country", "BG").containsEntry("year", 2023); + + // Clean up remaining document + vectorStore.delete(List.of(doc3.getId())); + }); + } + @Test void deleteByFilter() { this.contextRunner.run(context -> { From 92cc3b715b8e50f37563b8167f37ba9d9160e9d3 Mon Sep 17 00:00:00 2001 From: Soby Chacko Date: Tue, 11 Feb 2025 20:17:02 -0500 Subject: [PATCH 2/3] Extract common vector store delete tests to base class This commit extracts shared delete operation tests into a reusable BaseVectorStoreTests class. This reduces code duplication and provides a consistent test suite for delete operations across different vector store implementations. The base class includes tests for: Deleting by ID Deleting by filter expressions Deleting by string filter expressions Most of the vector store implementation now extends this base class and inherits these common tests while maintaining the ability to add vector store specific tests. Signed-off-by: Soby Chacko --- .../vectorstore/BaseVectorStoreTests.java | 164 ++++++++++++++++++ .../cassandra/CassandraVectorStoreIT.java | 60 +++---- .../vectorstore/ChromaVectorStoreIT.java | 110 ++---------- .../ElasticsearchVectorStoreIT.java | 95 ++-------- .../vectorstore/mariadb/MariaDBStoreIT.java | 112 ++---------- .../milvus/MilvusVectorStoreIT.java | 114 ++---------- .../atlas/MongoDBAtlasVectorStoreIT.java | 110 ++---------- .../vectorstore/neo4j/Neo4jVectorStoreIT.java | 104 ++--------- .../oracle/OracleVectorStoreIT.java | 116 ++----------- .../vectorstore/pgvector/PgVectorStoreIT.java | 125 ++----------- .../pinecone/PineconeVectorStoreIT.java | 65 ++----- .../qdrant/QdrantVectorStoreIT.java | 91 ++-------- .../vectorstore/redis/RedisVectorStoreIT.java | 93 ++-------- .../typesense/TypesenseVectorStoreIT.java | 96 ++-------- .../spring-ai-weaviate-store/pom.xml | 2 +- .../weaviate/WeaviateVectorStoreIT.java | 98 ++--------- 16 files changed, 341 insertions(+), 1214 deletions(-) create mode 100644 spring-ai-test/src/main/java/org/springframework/ai/test/vectorstore/BaseVectorStoreTests.java diff --git a/spring-ai-test/src/main/java/org/springframework/ai/test/vectorstore/BaseVectorStoreTests.java b/spring-ai-test/src/main/java/org/springframework/ai/test/vectorstore/BaseVectorStoreTests.java new file mode 100644 index 00000000000..3c0fd671f15 --- /dev/null +++ b/spring-ai-test/src/main/java/org/springframework/ai/test/vectorstore/BaseVectorStoreTests.java @@ -0,0 +1,164 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.ai.test.vectorstore; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +import java.time.Duration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +import org.springframework.ai.document.Document; +import org.springframework.ai.vectorstore.SearchRequest; +import org.springframework.ai.vectorstore.VectorStore; +import org.springframework.ai.vectorstore.filter.Filter; + +/** + * Base test class for VectorStore implementations. Provides common test scenarios for + * delete operations. + * + * @author Soby Chacko + */ +public abstract class BaseVectorStoreTests { + + protected abstract void executeTest(Consumer testFunction); + + protected Document createDocument(String country, Integer year) { + Map metadata = new HashMap<>(); + metadata.put("country", country); + if (year != null) { + metadata.put("year", year); + } + return new Document("The World is Big and Salvation Lurks Around the Corner", metadata); + } + + protected List setupTestDocuments(VectorStore vectorStore) { + var doc1 = createDocument("BG", 2020); + var doc2 = createDocument("NL", null); + var doc3 = createDocument("BG", 2023); + + List documents = List.of(doc1, doc2, doc3); + vectorStore.add(documents); + + return documents; + } + + private String normalizeValue(Object value) { + return value.toString().replaceAll("^\"|\"$", "").trim(); + } + + private void verifyDocumentsExist(VectorStore vectorStore, List documents) { + await().atMost(5, TimeUnit.SECONDS).pollInterval(Duration.ofMillis(500)).untilAsserted(() -> { + List results = vectorStore.similaritySearch( + SearchRequest.builder().query("The World").topK(documents.size()).similarityThresholdAll().build()); + assertThat(results).hasSize(documents.size()); + }); + } + + private void verifyDocumentsDeleted(VectorStore vectorStore, List deletedIds) { + await().atMost(5, TimeUnit.SECONDS).pollInterval(Duration.ofMillis(500)).untilAsserted(() -> { + List results = vectorStore + .similaritySearch(SearchRequest.builder().query("The World").topK(10).similarityThresholdAll().build()); + + List foundIds = results.stream().map(Document::getId).collect(Collectors.toList()); + + assertThat(foundIds).doesNotContainAnyElementsOf(deletedIds); + }); + } + + @Test + protected void deleteById() { + executeTest(vectorStore -> { + List documents = setupTestDocuments(vectorStore); + verifyDocumentsExist(vectorStore, documents); + + List idsToDelete = List.of(documents.get(0).getId(), documents.get(1).getId()); + vectorStore.delete(idsToDelete); + verifyDocumentsDeleted(vectorStore, idsToDelete); + + List results = vectorStore + .similaritySearch(SearchRequest.builder().query("The World").topK(5).similarityThresholdAll().build()); + + assertThat(results).hasSize(1); + assertThat(results.get(0).getId()).isEqualTo(documents.get(2).getId()); + Map metadata = results.get(0).getMetadata(); + assertThat(normalizeValue(metadata.get("country"))).isEqualTo("BG"); + assertThat(normalizeValue(metadata.get("year"))).isEqualTo("2023"); + + vectorStore.delete(List.of(documents.get(2).getId())); + }); + } + + @Test + protected void deleteWithStringFilterExpression() { + executeTest(vectorStore -> { + List documents = setupTestDocuments(vectorStore); + verifyDocumentsExist(vectorStore, documents); + + List bgDocIds = documents.stream() + .filter(d -> "BG".equals(d.getMetadata().get("country"))) + .map(Document::getId) + .collect(Collectors.toList()); + + vectorStore.delete("country == 'BG'"); + verifyDocumentsDeleted(vectorStore, bgDocIds); + + List results = vectorStore + .similaritySearch(SearchRequest.builder().query("The World").topK(5).similarityThresholdAll().build()); + + assertThat(results).hasSize(1); + assertThat(normalizeValue(results.get(0).getMetadata().get("country"))).isEqualTo("NL"); + + vectorStore.delete(List.of(documents.get(1).getId())); + }); + } + + @Test + protected void deleteByFilter() { + executeTest(vectorStore -> { + List documents = setupTestDocuments(vectorStore); + verifyDocumentsExist(vectorStore, documents); + + List bgDocIds = documents.stream() + .filter(d -> "BG".equals(d.getMetadata().get("country"))) + .map(Document::getId) + .collect(Collectors.toList()); + + Filter.Expression filterExpression = new Filter.Expression(Filter.ExpressionType.EQ, + new Filter.Key("country"), new Filter.Value("BG")); + + vectorStore.delete(filterExpression); + verifyDocumentsDeleted(vectorStore, bgDocIds); + + List results = vectorStore + .similaritySearch(SearchRequest.builder().query("The World").topK(5).similarityThresholdAll().build()); + + assertThat(results).hasSize(1); + assertThat(normalizeValue(results.get(0).getMetadata().get("country"))).isEqualTo("NL"); + + vectorStore.delete(List.of(documents.get(1).getId())); + }); + } + +} diff --git a/vector-stores/spring-ai-cassandra-store/src/test/java/org/springframework/ai/vectorstore/cassandra/CassandraVectorStoreIT.java b/vector-stores/spring-ai-cassandra-store/src/test/java/org/springframework/ai/vectorstore/cassandra/CassandraVectorStoreIT.java index 7a2329fe61e..7453350b249 100644 --- a/vector-stores/spring-ai-cassandra-store/src/test/java/org/springframework/ai/vectorstore/cassandra/CassandraVectorStoreIT.java +++ b/vector-stores/spring-ai-cassandra-store/src/test/java/org/springframework/ai/vectorstore/cassandra/CassandraVectorStoreIT.java @@ -19,10 +19,12 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.UUID; +import java.util.function.Consumer; import java.util.stream.Collectors; import com.datastax.oss.driver.api.core.CqlSession; @@ -40,8 +42,10 @@ import org.springframework.ai.document.Document; import org.springframework.ai.document.DocumentMetadata; import org.springframework.ai.embedding.EmbeddingModel; +import org.springframework.ai.test.vectorstore.BaseVectorStoreTests; import org.springframework.ai.transformers.TransformersEmbeddingModel; import org.springframework.ai.vectorstore.SearchRequest; +import org.springframework.ai.vectorstore.VectorStore; import org.springframework.ai.vectorstore.cassandra.CassandraVectorStore.SchemaColumn; import org.springframework.ai.vectorstore.cassandra.CassandraVectorStore.SchemaColumnTags; import org.springframework.ai.vectorstore.filter.Filter; @@ -64,7 +68,7 @@ * @since 1.0.0 */ @Testcontainers -class CassandraVectorStoreIT { +class CassandraVectorStoreIT extends BaseVectorStoreTests { @Container static CassandraContainer cassandraContainer = new CassandraContainer<>(CassandraImage.DEFAULT_IMAGE); @@ -110,6 +114,24 @@ private static CassandraVectorStore createTestStore(ApplicationContext context, return store; } + @Override + protected void executeTest(Consumer testFunction) { + contextRunner.run(context -> { + VectorStore vectorStore = context.getBean(VectorStore.class); + testFunction.accept(vectorStore); + }); + } + + @Override + protected Document createDocument(String country, Integer year) { + Map metadata = new HashMap<>(); + metadata.put("country", country); + if (year != null) { + metadata.put("year", year.shortValue()); + } + return new Document("The World is Big and Salvation Lurks Around the Corner", metadata); + } + @Test void ensureBeanGetsCreated() { this.contextRunner.run(context -> { @@ -422,39 +444,7 @@ void searchWithThreshold() { } @Test - void deleteById() { - this.contextRunner.run(context -> { - try (CassandraVectorStore store = createTestStore(context, - new SchemaColumn("country", DataTypes.TEXT, SchemaColumnTags.INDEXED), - new SchemaColumn("year", DataTypes.SMALLINT, SchemaColumnTags.INDEXED))) { - - var bgDocument = new Document("The World is Big and Salvation Lurks Around the Corner", - Map.of("country", "BG", "year", (short) 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", (short) 2023)); - - store.add(List.of(bgDocument, nlDocument, bgDocument2)); - - // Verify initial state - List results = store - .similaritySearch(SearchRequest.builder().query("The World").topK(5).build()); - assertThat(results).hasSize(3); - - store.delete(List.of(bgDocument.getId(), bgDocument2.getId())); - - results = store.similaritySearch( - SearchRequest.builder().query("The World").topK(5).similarityThresholdAll().build()); - - assertThat(results).hasSize(1); - assertThat(results.get(0).getMetadata()).containsEntry("country", "NL"); - } - }); - } - - @Test - void deleteByFilter() { + protected void deleteByFilter() { this.contextRunner.run(context -> { try (CassandraVectorStore store = createTestStore(context, new SchemaColumn("country", DataTypes.TEXT, SchemaColumnTags.INDEXED), @@ -490,7 +480,7 @@ void deleteByFilter() { } @Test - void deleteWithStringFilterExpression() { + protected void deleteWithStringFilterExpression() { this.contextRunner.run(context -> { try (CassandraVectorStore store = createTestStore(context, new SchemaColumn("country", DataTypes.TEXT, SchemaColumnTags.INDEXED), diff --git a/vector-stores/spring-ai-chroma-store/src/test/java/org/springframework/ai/chroma/vectorstore/ChromaVectorStoreIT.java b/vector-stores/spring-ai-chroma-store/src/test/java/org/springframework/ai/chroma/vectorstore/ChromaVectorStoreIT.java index d874c169042..12d4c2e0040 100644 --- a/vector-stores/spring-ai-chroma-store/src/test/java/org/springframework/ai/chroma/vectorstore/ChromaVectorStoreIT.java +++ b/vector-stores/spring-ai-chroma-store/src/test/java/org/springframework/ai/chroma/vectorstore/ChromaVectorStoreIT.java @@ -20,6 +20,7 @@ import java.util.List; import java.util.Map; import java.util.UUID; +import java.util.function.Consumer; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; @@ -33,6 +34,7 @@ import org.springframework.ai.embedding.EmbeddingModel; import org.springframework.ai.openai.OpenAiEmbeddingModel; import org.springframework.ai.openai.api.OpenAiApi; +import org.springframework.ai.test.vectorstore.BaseVectorStoreTests; import org.springframework.ai.vectorstore.SearchRequest; import org.springframework.ai.vectorstore.VectorStore; import org.springframework.ai.vectorstore.filter.Filter; @@ -51,7 +53,7 @@ */ @Testcontainers @EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".+") -public class ChromaVectorStoreIT { +public class ChromaVectorStoreIT extends BaseVectorStoreTests { @Container static ChromaDBContainer chromaContainer = new ChromaDBContainer(ChromaImage.DEFAULT_IMAGE); @@ -68,6 +70,14 @@ public class ChromaVectorStoreIT { "Great Depression Great Depression Great Depression Great Depression Great Depression Great Depression", Collections.singletonMap("meta2", "meta2"))); + @Override + protected void executeTest(Consumer testFunction) { + contextRunner.run(context -> { + VectorStore vectorStore = context.getBean(VectorStore.class); + testFunction.accept(vectorStore); + }); + } + @Test public void addAndSearch() { this.contextRunner.run(context -> { @@ -168,104 +178,6 @@ public void addAndSearchWithFilters() { }); } - @Test - public void deleteById() { - this.contextRunner.withPropertyValues("test.spring.ai.vectorstore.mariadb.distanceType=COSINE").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", "year", 2021)); - 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)); - - SearchRequest searchRequest = SearchRequest.builder() - .query("The World") - .topK(5) - .similarityThresholdAll() - .build(); - - List results = vectorStore.similaritySearch(searchRequest); - assertThat(results).hasSize(3); - - Filter.Expression filterExpression = new Filter.Expression(Filter.ExpressionType.EQ, - new Filter.Key("country"), new Filter.Value("BG")); - - vectorStore.delete(List.of(bgDocument.getId(), bgDocument2.getId())); - - // Verify deletion - should only have NL document remaining - results = vectorStore.similaritySearch(searchRequest); - assertThat(results).hasSize(1); - assertThat(results.get(0).getMetadata()).containsEntry("country", "NL"); - }); - } - - @Test - public void deleteWithFilterExpression() { - this.contextRunner.run(context -> { - VectorStore vectorStore = context.getBean(VectorStore.class); - - // Create test documents with different metadata - var bgDocument = new Document("The World is Big and Salvation Lurks Around the Corner", - Map.of("country", "Bulgaria")); - var nlDocument = new Document("The World is Big and Salvation Lurks Around the Corner", - Map.of("country", "Netherlands")); - - // Add documents to the store - vectorStore.add(List.of(bgDocument, nlDocument)); - - // Verify initial state - var request = SearchRequest.builder().query("The World").topK(5).build(); - List results = vectorStore.similaritySearch(request); - assertThat(results).hasSize(2); - - // Delete document with country = Bulgaria - Filter.Expression filterExpression = new Filter.Expression(Filter.ExpressionType.EQ, - new Filter.Key("country"), new Filter.Value("Bulgaria")); - - vectorStore.delete(filterExpression); - - // Verify Bulgaria document was deleted - results = vectorStore - .similaritySearch(SearchRequest.from(request).filterExpression("country == 'Bulgaria'").build()); - assertThat(results).isEmpty(); - - // Verify Netherlands document still exists - results = vectorStore - .similaritySearch(SearchRequest.from(request).filterExpression("country == 'Netherlands'").build()); - assertThat(results).hasSize(1); - assertThat(results.get(0).getMetadata().get("country")).isEqualTo("Netherlands"); - - // Clean up - vectorStore.delete(List.of(nlDocument.getId())); - }); - } - - @Test - public void deleteWithStringFilterExpression() { - this.contextRunner.run(context -> { - VectorStore vectorStore = context.getBean(VectorStore.class); - - var bgDocument = new Document("The World is Big", Map.of("country", "Bulgaria")); - var nlDocument = new Document("The World is Big", Map.of("country", "Netherlands")); - vectorStore.add(List.of(bgDocument, nlDocument)); - - var request = SearchRequest.builder().query("World").topK(5).build(); - assertThat(vectorStore.similaritySearch(request)).hasSize(2); - - vectorStore.delete("country == 'Bulgaria'"); - - var results = vectorStore.similaritySearch(request); - assertThat(results).hasSize(1); - assertThat(results.get(0).getMetadata().get("country")).isEqualTo("Netherlands"); - - vectorStore.delete(List.of(nlDocument.getId())); - }); - } - @Test public void documentUpdateTest() { diff --git a/vector-stores/spring-ai-elasticsearch-store/src/test/java/org/springframework/ai/vectorstore/elasticsearch/ElasticsearchVectorStoreIT.java b/vector-stores/spring-ai-elasticsearch-store/src/test/java/org/springframework/ai/vectorstore/elasticsearch/ElasticsearchVectorStoreIT.java index 3acf5176875..c6b3c0bb76b 100644 --- a/vector-stores/spring-ai-elasticsearch-store/src/test/java/org/springframework/ai/vectorstore/elasticsearch/ElasticsearchVectorStoreIT.java +++ b/vector-stores/spring-ai-elasticsearch-store/src/test/java/org/springframework/ai/vectorstore/elasticsearch/ElasticsearchVectorStoreIT.java @@ -27,6 +27,7 @@ import java.util.Optional; import java.util.UUID; import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; import co.elastic.clients.elasticsearch.ElasticsearchClient; import co.elastic.clients.elasticsearch.cat.indices.IndicesRecord; @@ -53,7 +54,9 @@ import org.springframework.ai.embedding.EmbeddingModel; import org.springframework.ai.openai.OpenAiEmbeddingModel; import org.springframework.ai.openai.api.OpenAiApi; +import org.springframework.ai.test.vectorstore.BaseVectorStoreTests; import org.springframework.ai.vectorstore.SearchRequest; +import org.springframework.ai.vectorstore.VectorStore; import org.springframework.ai.vectorstore.filter.Filter.Expression; import org.springframework.ai.vectorstore.filter.Filter.ExpressionType; import org.springframework.ai.vectorstore.filter.Filter.Key; @@ -71,7 +74,7 @@ @Testcontainers @EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".+") -class ElasticsearchVectorStoreIT { +class ElasticsearchVectorStoreIT extends BaseVectorStoreTests { @Container private static final ElasticsearchContainer elasticsearchContainer = new ElasticsearchContainer( @@ -116,40 +119,16 @@ void cleanDatabase() { }); } - @Test - public void addAndDeleteDocumentsTest() { + @Override + protected void executeTest(Consumer testFunction) { getContextRunner().run(context -> { - ElasticsearchVectorStore vectorStore = context.getBean("vectorStore_cosine", - ElasticsearchVectorStore.class); - ElasticsearchClient elasticsearchClient = context.getBean(ElasticsearchClient.class); - - IndicesStats stats = elasticsearchClient.indices() - .stats(s -> s.index("spring-ai-document-index")) - .indices() - .get("spring-ai-document-index"); - - assertThat(stats.total().docs().count()).isEqualTo(0L); - - vectorStore.add(this.documents); - elasticsearchClient.indices().refresh(); - stats = elasticsearchClient.indices() - .stats(s -> s.index("spring-ai-document-index")) - .indices() - .get("spring-ai-document-index"); - assertThat(stats.total().docs().count()).isEqualTo(3L); - - vectorStore.doDelete(List.of("1", "2", "3")); - elasticsearchClient.indices().refresh(); - stats = elasticsearchClient.indices() - .stats(s -> s.index("spring-ai-document-index")) - .indices() - .get("spring-ai-document-index"); - assertThat(stats.total().docs().count()).isEqualTo(0L); + VectorStore vectorStore = context.getBean("vectorStore_cosine", VectorStore.class); + testFunction.accept(vectorStore); }); } @Test - public void deleteDocumentsByFilterExpressionTest() { + public void addAndDeleteDocumentsTest() { getContextRunner().run(context -> { ElasticsearchVectorStore vectorStore = context.getBean("vectorStore_cosine", ElasticsearchVectorStore.class); @@ -162,37 +141,16 @@ public void deleteDocumentsByFilterExpressionTest() { assertThat(stats.total().docs().count()).isEqualTo(0L); - // Add documents with metadata - List documents = List.of( - new Document("1", getText("classpath:/test/data/spring.ai.txt"), Map.of("meta1", "meta1")), - new Document("2", getText("classpath:/test/data/time.shelter.txt"), Map.of()), - new Document("3", getText("classpath:/test/data/great.depression.txt"), Map.of("meta2", "meta2"))); - - vectorStore.add(documents); + vectorStore.add(this.documents); elasticsearchClient.indices().refresh(); - stats = elasticsearchClient.indices() .stats(s -> s.index("spring-ai-document-index")) .indices() .get("spring-ai-document-index"); assertThat(stats.total().docs().count()).isEqualTo(3L); - // Delete documents with meta1 using filter expression - Expression filterExpression = new Expression(ExpressionType.EQ, new Key("meta1"), new Value("meta1")); - - vectorStore.delete(filterExpression); - elasticsearchClient.indices().refresh(); - - stats = elasticsearchClient.indices() - .stats(s -> s.index("spring-ai-document-index")) - .indices() - .get("spring-ai-document-index"); - assertThat(stats.total().docs().count()).isEqualTo(2L); - - // Clean up remaining documents - vectorStore.delete(List.of("2", "3")); + vectorStore.doDelete(List.of("1", "2", "3")); elasticsearchClient.indices().refresh(); - stats = elasticsearchClient.indices() .stats(s -> s.index("spring-ai-document-index")) .indices() @@ -201,37 +159,6 @@ public void deleteDocumentsByFilterExpressionTest() { }); } - @Test - public void deleteWithStringFilterExpressionTest() { - getContextRunner().run(context -> { - ElasticsearchVectorStore vectorStore = context.getBean("vectorStore_cosine", - ElasticsearchVectorStore.class); - ElasticsearchClient elasticsearchClient = context.getBean(ElasticsearchClient.class); - - List documents = List.of( - new Document("1", getText("classpath:/test/data/spring.ai.txt"), Map.of("meta1", "meta1")), - new Document("2", getText("classpath:/test/data/time.shelter.txt"), Map.of()), - new Document("3", getText("classpath:/test/data/great.depression.txt"), Map.of("meta2", "meta2"))); - - vectorStore.add(documents); - elasticsearchClient.indices().refresh(); - - // Delete documents with meta1 using string filter - vectorStore.delete("meta1 == 'meta1'"); - elasticsearchClient.indices().refresh(); - - IndicesStats stats = elasticsearchClient.indices() - .stats(s -> s.index("spring-ai-document-index")) - .indices() - .get("spring-ai-document-index"); - assertThat(stats.total().docs().count()).isEqualTo(2L); - - // Clean up - vectorStore.delete(List.of("2", "3")); - elasticsearchClient.indices().refresh(); - }); - } - @ParameterizedTest(name = "{0} : {displayName} ") @ValueSource(strings = { "cosine", "l2_norm", "dot_product" }) public void addAndSearchTest(String similarityFunction) { diff --git a/vector-stores/spring-ai-mariadb-store/src/test/java/org/springframework/ai/vectorstore/mariadb/MariaDBStoreIT.java b/vector-stores/spring-ai-mariadb-store/src/test/java/org/springframework/ai/vectorstore/mariadb/MariaDBStoreIT.java index 25693a2d970..6ad67bf695a 100644 --- a/vector-stores/spring-ai-mariadb-store/src/test/java/org/springframework/ai/vectorstore/mariadb/MariaDBStoreIT.java +++ b/vector-stores/spring-ai-mariadb-store/src/test/java/org/springframework/ai/vectorstore/mariadb/MariaDBStoreIT.java @@ -24,6 +24,7 @@ import java.util.Map; import java.util.Optional; import java.util.UUID; +import java.util.function.Consumer; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -45,6 +46,7 @@ import org.springframework.ai.embedding.EmbeddingModel; import org.springframework.ai.openai.OpenAiEmbeddingModel; import org.springframework.ai.openai.api.OpenAiApi; +import org.springframework.ai.test.vectorstore.BaseVectorStoreTests; import org.springframework.ai.vectorstore.SearchRequest; import org.springframework.ai.vectorstore.VectorStore; import org.springframework.ai.vectorstore.filter.Filter; @@ -71,7 +73,7 @@ */ @Testcontainers @EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".+") -public class MariaDBStoreIT { +public class MariaDBStoreIT extends BaseVectorStoreTests { private static String schemaName = "testdb"; @@ -141,6 +143,14 @@ private static boolean isSortedByDistance(List docs) { return true; } + @Override + protected void executeTest(Consumer testFunction) { + contextRunner.run(context -> { + VectorStore vectorStore = context.getBean(VectorStore.class); + testFunction.accept(vectorStore); + }); + } + @ParameterizedTest(name = "{0} : {displayName} ") @ValueSource(strings = { "COSINE", "EUCLIDEAN" }) public void addAndSearch(String distanceType) { @@ -362,106 +372,6 @@ public void searchWithThreshold(String distanceType) { }); } - @Test - public void deleteById() { - this.contextRunner.withPropertyValues("test.spring.ai.vectorstore.mariadb.distanceType=COSINE").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", "year", 2021)); - 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)); - - SearchRequest searchRequest = SearchRequest.builder() - .query("The World") - .topK(5) - .similarityThresholdAll() - .build(); - - List results = vectorStore.similaritySearch(searchRequest); - assertThat(results).hasSize(3); - - vectorStore.delete(List.of(bgDocument.getId(), bgDocument2.getId())); - - // Verify deletion - should only have NL document remaining - results = vectorStore.similaritySearch(searchRequest); - assertThat(results).hasSize(1); - assertThat(results.get(0).getMetadata()).containsEntry("country", "NL"); - - dropTable(context); - }); - } - - @Test - public void deleteByFilter() { - this.contextRunner.withPropertyValues("test.spring.ai.vectorstore.mariadb.distanceType=COSINE").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", "year", 2021)); - 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)); - - SearchRequest searchRequest = SearchRequest.builder() - .query("The World") - .topK(5) - .similarityThresholdAll() - .build(); - - List results = vectorStore.similaritySearch(searchRequest); - assertThat(results).hasSize(3); - - Filter.Expression filterExpression = new Filter.Expression(Filter.ExpressionType.EQ, - new Filter.Key("country"), new Filter.Value("BG")); - - vectorStore.delete(filterExpression); - - // Verify deletion - should only have NL document remaining - results = vectorStore.similaritySearch(searchRequest); - assertThat(results).hasSize(1); - assertThat(results.get(0).getMetadata()).containsEntry("country", "NL"); - - dropTable(context); - }); - } - - @Test - public void deleteWithStringFilterExpression() { - this.contextRunner.withPropertyValues("test.spring.ai.vectorstore.mariadb.distanceType=COSINE").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", "year", 2021)); - 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)); - - var searchRequest = SearchRequest.builder().query("The World").topK(5).similarityThresholdAll().build(); - - List results = vectorStore.similaritySearch(searchRequest); - assertThat(results).hasSize(3); - - vectorStore.delete("country == 'BG'"); - - results = vectorStore.similaritySearch(searchRequest); - assertThat(results).hasSize(1); - assertThat(results.get(0).getMetadata()).containsEntry("country", "NL"); - - dropTable(context); - }); - } - @Test public void deleteWithComplexFilterExpression() { this.contextRunner.withPropertyValues("test.spring.ai.vectorstore.mariadb.distanceType=COSINE").run(context -> { diff --git a/vector-stores/spring-ai-milvus-store/src/test/java/org/springframework/ai/vectorstore/milvus/MilvusVectorStoreIT.java b/vector-stores/spring-ai-milvus-store/src/test/java/org/springframework/ai/vectorstore/milvus/MilvusVectorStoreIT.java index 63348f1bb74..76e81684d62 100644 --- a/vector-stores/spring-ai-milvus-store/src/test/java/org/springframework/ai/vectorstore/milvus/MilvusVectorStoreIT.java +++ b/vector-stores/spring-ai-milvus-store/src/test/java/org/springframework/ai/vectorstore/milvus/MilvusVectorStoreIT.java @@ -23,6 +23,7 @@ import java.util.Map; import java.util.Optional; import java.util.UUID; +import java.util.function.Consumer; import java.util.stream.Collectors; import io.milvus.client.MilvusServiceClient; @@ -43,6 +44,7 @@ import org.springframework.ai.embedding.TokenCountBatchingStrategy; import org.springframework.ai.openai.OpenAiEmbeddingModel; import org.springframework.ai.openai.api.OpenAiApi; +import org.springframework.ai.test.vectorstore.BaseVectorStoreTests; import org.springframework.ai.vectorstore.SearchRequest; import org.springframework.ai.vectorstore.VectorStore; import org.springframework.ai.vectorstore.filter.Filter; @@ -64,7 +66,7 @@ */ @Testcontainers @EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".+") -public class MilvusVectorStoreIT { +public class MilvusVectorStoreIT extends BaseVectorStoreTests { @Container private static MilvusContainer milvusContainer = new MilvusContainer(MilvusImage.DEFAULT_IMAGE); @@ -92,6 +94,15 @@ private void resetCollection(VectorStore vectorStore) { ((MilvusVectorStore) vectorStore).createCollection(); } + @Override + protected void executeTest(Consumer testFunction) { + this.contextRunner.withPropertyValues("test.spring.ai.vectorstore.milvus.metricType=" + "COSINE") + .run(context -> { + VectorStore vectorStore = context.getBean(VectorStore.class); + testFunction.accept(vectorStore); + }); + } + @ParameterizedTest(name = "{0} : {displayName} ") @ValueSource(strings = { "COSINE", "L2", "IP" }) public void addAndSearch(String metricType) { @@ -278,107 +289,6 @@ public void searchWithThreshold(String metricType) { }); } - @Test - public void deleteById() { - this.contextRunner.withPropertyValues("test.spring.ai.vectorstore.milvus.metricType=COSINE").run(context -> { - VectorStore vectorStore = context.getBean(VectorStore.class); - - resetCollection(vectorStore); - - 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", "year", 2021)); - 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)); - - SearchRequest searchRequest = SearchRequest.builder() - .query("The World") - .topK(5) - .similarityThresholdAll() - .build(); - - List results = vectorStore.similaritySearch(searchRequest); - assertThat(results).hasSize(3); - - vectorStore.delete(List.of(bgDocument.getId(), bgDocument2.getId())); - - // Verify deletion - should only have NL document remaining - results = vectorStore.similaritySearch(searchRequest); - assertThat(results).hasSize(1); - assertThat(results.get(0).getMetadata()).containsEntry("country", "NL"); - }); - } - - @Test - public void deleteByFilter() { - this.contextRunner.withPropertyValues("test.spring.ai.vectorstore.milvus.metricType=COSINE").run(context -> { - VectorStore vectorStore = context.getBean(VectorStore.class); - - resetCollection(vectorStore); - - 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", "year", 2021)); - 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)); - - SearchRequest searchRequest = SearchRequest.builder() - .query("The World") - .topK(5) - .similarityThresholdAll() - .build(); - - List results = vectorStore.similaritySearch(searchRequest); - assertThat(results).hasSize(3); - - Filter.Expression filterExpression = new Filter.Expression(Filter.ExpressionType.EQ, - new Filter.Key("country"), new Filter.Value("BG")); - - vectorStore.delete(filterExpression); - - // Verify deletion - should only have NL document remaining - results = vectorStore.similaritySearch(searchRequest); - assertThat(results).hasSize(1); - assertThat(results.get(0).getMetadata()).containsEntry("country", "NL"); - }); - } - - @Test - public void deleteWithStringFilterExpression() { - this.contextRunner.withPropertyValues("test.spring.ai.vectorstore.milvus.metricType=COSINE").run(context -> { - VectorStore vectorStore = context.getBean(VectorStore.class); - - resetCollection(vectorStore); - - 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", "year", 2021)); - 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)); - - var searchRequest = SearchRequest.builder().query("The World").topK(5).similarityThresholdAll().build(); - - List results = vectorStore.similaritySearch(searchRequest); - assertThat(results).hasSize(3); - - // Delete using string filter expression - vectorStore.delete("country == 'BG'"); - - results = vectorStore.similaritySearch(searchRequest); - assertThat(results).hasSize(1); - assertThat(results.get(0).getMetadata()).containsEntry("country", "NL"); - }); - } - @Test public void deleteWithComplexFilterExpression() { this.contextRunner.withPropertyValues("test.spring.ai.vectorstore.milvus.metricType=COSINE").run(context -> { diff --git a/vector-stores/spring-ai-mongodb-atlas-store/src/test/java/org/springframework/ai/vectorstore/mongodb/atlas/MongoDBAtlasVectorStoreIT.java b/vector-stores/spring-ai-mongodb-atlas-store/src/test/java/org/springframework/ai/vectorstore/mongodb/atlas/MongoDBAtlasVectorStoreIT.java index d4231be0bdd..b598e4df4bd 100644 --- a/vector-stores/spring-ai-mongodb-atlas-store/src/test/java/org/springframework/ai/vectorstore/mongodb/atlas/MongoDBAtlasVectorStoreIT.java +++ b/vector-stores/spring-ai-mongodb-atlas-store/src/test/java/org/springframework/ai/vectorstore/mongodb/atlas/MongoDBAtlasVectorStoreIT.java @@ -24,6 +24,7 @@ import java.util.Map; import java.util.Optional; import java.util.UUID; +import java.util.function.Consumer; import java.util.stream.Collectors; import com.mongodb.client.MongoClient; @@ -39,6 +40,7 @@ import org.springframework.ai.embedding.EmbeddingModel; import org.springframework.ai.openai.OpenAiEmbeddingModel; import org.springframework.ai.openai.api.OpenAiApi; +import org.springframework.ai.test.vectorstore.BaseVectorStoreTests; import org.springframework.ai.vectorstore.SearchRequest; import org.springframework.ai.vectorstore.VectorStore; import org.springframework.ai.vectorstore.filter.Filter; @@ -64,7 +66,7 @@ */ @Testcontainers @EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".+") -class MongoDBAtlasVectorStoreIT { +class MongoDBAtlasVectorStoreIT extends BaseVectorStoreTests { @Container private static MongoDBAtlasLocalContainer container = new MongoDBAtlasLocalContainer(MongoDbImage.DEFAULT_IMAGE); @@ -82,6 +84,14 @@ public void beforeEach() { }); } + @Override + protected void executeTest(Consumer testFunction) { + contextRunner.run(context -> { + VectorStore vectorStore = context.getBean(VectorStore.class); + testFunction.accept(vectorStore); + }); + } + @Test void vectorStoreTest() { this.contextRunner.run(context -> { @@ -257,104 +267,6 @@ public void searchWithThreshold() { }); } - @Test - void deleteById() { - 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", "year", 2021)); - 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)); - Thread.sleep(5000); // Wait for indexing - - SearchRequest searchRequest = SearchRequest.builder() - .query("The World") - .topK(5) - .similarityThresholdAll() - .build(); - - List results = vectorStore.similaritySearch(searchRequest); - assertThat(results).hasSize(3); - - vectorStore.delete(List.of(bgDocument.getId(), bgDocument2.getId())); - Thread.sleep(1000); // Wait for deletion to be processed - - results = vectorStore.similaritySearch(searchRequest); - assertThat(results).hasSize(1); - assertThat(results.get(0).getMetadata()).containsEntry("country", "NL"); - }); - } - - @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", "year", 2021)); - 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)); - Thread.sleep(5000); // Wait for indexing - - SearchRequest searchRequest = SearchRequest.builder() - .query("The World") - .topK(5) - .similarityThresholdAll() - .build(); - - List results = vectorStore.similaritySearch(searchRequest); - assertThat(results).hasSize(3); - - Filter.Expression filterExpression = new Filter.Expression(Filter.ExpressionType.EQ, - new Filter.Key("country"), new Filter.Value("BG")); - - vectorStore.delete(filterExpression); - Thread.sleep(1000); // Wait for deletion to be processed - - results = vectorStore.similaritySearch(searchRequest); - assertThat(results).hasSize(1); - assertThat(results.get(0).getMetadata()).containsEntry("country", "NL"); - }); - } - - @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", "year", 2021)); - 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)); - Thread.sleep(5000); // Wait for indexing - - var searchRequest = SearchRequest.builder().query("The World").topK(5).similarityThresholdAll().build(); - - List results = vectorStore.similaritySearch(searchRequest); - assertThat(results).hasSize(3); - - vectorStore.delete("country == 'BG'"); - Thread.sleep(1000); // Wait for deletion to be processed - - results = vectorStore.similaritySearch(searchRequest); - assertThat(results).hasSize(1); - assertThat(results.get(0).getMetadata()).containsEntry("country", "NL"); - }); - } - @Test void deleteWithComplexFilterExpression() { this.contextRunner.run(context -> { diff --git a/vector-stores/spring-ai-neo4j-store/src/test/java/org/springframework/ai/vectorstore/neo4j/Neo4jVectorStoreIT.java b/vector-stores/spring-ai-neo4j-store/src/test/java/org/springframework/ai/vectorstore/neo4j/Neo4jVectorStoreIT.java index ffdd0e0682d..fbd86579727 100644 --- a/vector-stores/spring-ai-neo4j-store/src/test/java/org/springframework/ai/vectorstore/neo4j/Neo4jVectorStoreIT.java +++ b/vector-stores/spring-ai-neo4j-store/src/test/java/org/springframework/ai/vectorstore/neo4j/Neo4jVectorStoreIT.java @@ -21,6 +21,7 @@ import java.util.Map; import java.util.Optional; import java.util.UUID; +import java.util.function.Consumer; import java.util.stream.Collectors; import org.junit.Assert; @@ -39,6 +40,7 @@ import org.springframework.ai.embedding.EmbeddingModel; import org.springframework.ai.openai.OpenAiEmbeddingModel; import org.springframework.ai.openai.api.OpenAiApi; +import org.springframework.ai.test.vectorstore.BaseVectorStoreTests; import org.springframework.ai.vectorstore.SearchRequest; import org.springframework.ai.vectorstore.VectorStore; import org.springframework.ai.vectorstore.filter.Filter; @@ -60,7 +62,7 @@ */ @Testcontainers @EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".+") -class Neo4jVectorStoreIT { +class Neo4jVectorStoreIT extends BaseVectorStoreTests { @Container static Neo4jContainer neo4jContainer = new Neo4jContainer<>(Neo4jImage.DEFAULT_IMAGE).withRandomPassword(); @@ -82,6 +84,14 @@ void cleanDatabase() { .run(context -> context.getBean(Driver.class).executableQuery("MATCH (n) DETACH DELETE n").execute()); } + @Override + protected void executeTest(Consumer testFunction) { + contextRunner.run(context -> { + VectorStore vectorStore = context.getBean(VectorStore.class); + testFunction.accept(vectorStore); + }); + } + @Test void addAndSearchTest() { this.contextRunner.run(context -> { @@ -305,98 +315,6 @@ void ensureIdIndexGetsCreated() { .isTrue()); } - @Test - void deleteById() { - 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", "year", 2021)); - 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)); - - SearchRequest searchRequest = SearchRequest.builder() - .query("The World") - .topK(5) - .similarityThresholdAll() - .build(); - - List results = vectorStore.similaritySearch(searchRequest); - assertThat(results).hasSize(3); - - vectorStore.delete(List.of(bgDocument.getId(), bgDocument2.getId())); - - results = vectorStore.similaritySearch(searchRequest); - assertThat(results).hasSize(1); - assertThat(results.get(0).getMetadata()).containsEntry("country", "NL"); - }); - } - - @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", "year", 2021)); - 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)); - - SearchRequest searchRequest = SearchRequest.builder() - .query("The World") - .topK(5) - .similarityThresholdAll() - .build(); - - List results = vectorStore.similaritySearch(searchRequest); - assertThat(results).hasSize(3); - - Filter.Expression filterExpression = new Filter.Expression(Filter.ExpressionType.EQ, - new Filter.Key("country"), new Filter.Value("BG")); - - vectorStore.delete(filterExpression); - - results = vectorStore.similaritySearch(searchRequest); - assertThat(results).hasSize(1); - assertThat(results.get(0).getMetadata()).containsEntry("country", "NL"); - }); - } - - @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", "year", 2021)); - 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)); - - var searchRequest = SearchRequest.builder().query("The World").topK(5).similarityThresholdAll().build(); - - List results = vectorStore.similaritySearch(searchRequest); - assertThat(results).hasSize(3); - - vectorStore.delete("country == 'BG'"); - - results = vectorStore.similaritySearch(searchRequest); - assertThat(results).hasSize(1); - assertThat(results.get(0).getMetadata()).containsEntry("country", "NL"); - }); - } - @Test void deleteWithComplexFilterExpression() { this.contextRunner.run(context -> { diff --git a/vector-stores/spring-ai-oracle-store/src/test/java/org/springframework/ai/vectorstore/oracle/OracleVectorStoreIT.java b/vector-stores/spring-ai-oracle-store/src/test/java/org/springframework/ai/vectorstore/oracle/OracleVectorStoreIT.java index 1ef260808d3..854b309e742 100644 --- a/vector-stores/spring-ai-oracle-store/src/test/java/org/springframework/ai/vectorstore/oracle/OracleVectorStoreIT.java +++ b/vector-stores/spring-ai-oracle-store/src/test/java/org/springframework/ai/vectorstore/oracle/OracleVectorStoreIT.java @@ -24,6 +24,7 @@ import java.util.Map; import java.util.Optional; import java.util.UUID; +import java.util.function.Consumer; import java.util.stream.Collectors; import javax.sql.DataSource; @@ -43,6 +44,7 @@ import org.springframework.ai.document.Document; import org.springframework.ai.document.DocumentMetadata; import org.springframework.ai.embedding.EmbeddingModel; +import org.springframework.ai.test.vectorstore.BaseVectorStoreTests; import org.springframework.ai.transformers.TransformersEmbeddingModel; import org.springframework.ai.vectorstore.SearchRequest; import org.springframework.ai.vectorstore.VectorStore; @@ -66,7 +68,7 @@ @Testcontainers @Disabled("Oracle image is 2GB") -public class OracleVectorStoreIT { +public class OracleVectorStoreIT extends BaseVectorStoreTests { @Container static OracleContainer oracle23aiContainer = new OracleContainer(OracleImage.DEFAULT_IMAGE).withCopyFileToContainer( @@ -121,6 +123,17 @@ private static boolean isSortedBySimilarity(final List documents) { return true; } + @Override + protected void executeTest(Consumer testFunction) { + this.contextRunner + .withPropertyValues("test.spring.ai.vectorstore.oracle.distanceType=COSINE", + "test.spring.ai.vectorstore.oracle.searchAccuracy=" + OracleVectorStore.DEFAULT_SEARCH_ACCURACY) + .run(context -> { + VectorStore vectorStore = context.getBean(VectorStore.class); + testFunction.accept(vectorStore); + }); + } + @ParameterizedTest(name = "{0} : {displayName} ") @ValueSource(strings = { "COSINE", "DOT", "EUCLIDEAN", "EUCLIDEAN_SQUARED", "MANHATTAN" }) public void addAndSearch(String distanceType) { @@ -317,107 +330,6 @@ public void searchWithThreshold(String distanceType) { }); } - @Test - void deleteById() { - this.contextRunner - .withPropertyValues("test.spring.ai.vectorstore.oracle.distanceType=COSINE", - "test.spring.ai.vectorstore.oracle.searchAccuracy=" + OracleVectorStore.DEFAULT_SEARCH_ACCURACY) - .run(context -> { - VectorStore vectorStore = context.getBean(VectorStore.class); - - var doc1 = new Document("The World is Big and Salvation Lurks Around the Corner", - Map.of("country", "BG", "year", 2020)); - var doc2 = new Document("The World is Big and Salvation Lurks Around the Corner", - Map.of("country", "NL")); - var doc3 = new Document("The World is Big and Salvation Lurks Around the Corner", - Map.of("country", "BG", "year", 2023)); - - vectorStore.add(List.of(doc1, doc2, doc3)); - - // Delete first two documents - vectorStore.delete(List.of(doc1.getId(), doc2.getId())); - - List results = vectorStore.similaritySearch( - SearchRequest.builder().query("The World").topK(5).similarityThresholdAll().build()); - - assertThat(results).hasSize(1); - assertThat(results.get(0).getId()).isEqualTo(doc3.getId()); - assertThat(results).hasSize(1); - assertThat(results.get(0).getId()).isEqualTo(doc3.getId()); - assertThat(results.get(0).getMetadata()) - .hasEntrySatisfying("country", value -> assertThat(value.toString()).isEqualTo("\"BG\"")) - .hasEntrySatisfying("year", value -> assertThat(value.toString()).isEqualTo("2023")); - - // Clean up remaining document - vectorStore.delete(List.of(doc3.getId())); - }); - } - - @Test - void deleteByFilter() { - this.contextRunner - .withPropertyValues("test.spring.ai.vectorstore.oracle.distanceType=COSINE", - "test.spring.ai.vectorstore.oracle.searchAccuracy=" + OracleVectorStore.DEFAULT_SEARCH_ACCURACY) - .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 results = vectorStore.similaritySearch( - SearchRequest.builder().query("The World").topK(5).similarityThresholdAll().build()); - - assertThat(results).hasSize(1); - assertThat(results.get(0).getMetadata()).containsKey("country") - .hasEntrySatisfying("country", - value -> assertThat(value.toString().replace("\"", "")).isEqualTo("NL")); - - dropTable(context, ((OracleVectorStore) vectorStore).getTableName()); - }); - } - - @Test - void deleteWithStringFilterExpression() { - this.contextRunner - .withPropertyValues("test.spring.ai.vectorstore.oracle.distanceType=COSINE", - "test.spring.ai.vectorstore.oracle.searchAccuracy=" + OracleVectorStore.DEFAULT_SEARCH_ACCURACY) - .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 results = vectorStore.similaritySearch( - SearchRequest.builder().query("The World").topK(5).similarityThresholdAll().build()); - - assertThat(results).hasSize(1); - assertThat(results.get(0).getMetadata()).containsKey("country") - .hasEntrySatisfying("country", - value -> assertThat(value.toString().replace("\"", "")).isEqualTo("NL")); - - dropTable(context, ((OracleVectorStore) vectorStore).getTableName()); - }); - } - @Test void deleteWithComplexFilterExpression() { this.contextRunner diff --git a/vector-stores/spring-ai-pgvector-store/src/test/java/org/springframework/ai/vectorstore/pgvector/PgVectorStoreIT.java b/vector-stores/spring-ai-pgvector-store/src/test/java/org/springframework/ai/vectorstore/pgvector/PgVectorStoreIT.java index c80329823f0..112f53454c6 100644 --- a/vector-stores/spring-ai-pgvector-store/src/test/java/org/springframework/ai/vectorstore/pgvector/PgVectorStoreIT.java +++ b/vector-stores/spring-ai-pgvector-store/src/test/java/org/springframework/ai/vectorstore/pgvector/PgVectorStoreIT.java @@ -25,6 +25,7 @@ import java.util.Map; import java.util.Optional; import java.util.UUID; +import java.util.function.Consumer; import java.util.stream.Stream; import javax.sql.DataSource; @@ -47,6 +48,7 @@ import org.springframework.ai.embedding.EmbeddingModel; import org.springframework.ai.openai.OpenAiEmbeddingModel; import org.springframework.ai.openai.api.OpenAiApi; +import org.springframework.ai.test.vectorstore.BaseVectorStoreTests; import org.springframework.ai.vectorstore.SearchRequest; import org.springframework.ai.vectorstore.VectorStore; import org.springframework.ai.vectorstore.filter.Filter; @@ -77,7 +79,7 @@ */ @Testcontainers @EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".+") -public class PgVectorStoreIT { +public class PgVectorStoreIT extends BaseVectorStoreTests { @Container @SuppressWarnings("resource") @@ -165,6 +167,14 @@ private static boolean isSortedBySimilarity(List docs) { return true; } + @Override + protected void executeTest(Consumer testFunction) { + contextRunner.run(context -> { + VectorStore vectorStore = context.getBean(VectorStore.class); + testFunction.accept(vectorStore); + }); + } + @ParameterizedTest(name = "{0} : {displayName} ") @ValueSource(strings = { "COSINE_DISTANCE", "EUCLIDEAN_DISTANCE", "NEGATIVE_INNER_PRODUCT" }) public void addAndSearch(String distanceType) { @@ -421,119 +431,6 @@ public void searchWithThreshold(String distanceType) { }); } - @Test - public void deleteByIds() { - this.contextRunner.withPropertyValues("test.spring.ai.vectorstore.pgvector.distanceType=COSINE_DISTANCE") - .run(context -> { - VectorStore vectorStore = context.getBean(VectorStore.class); - - // Create test documents - 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", "year", 2021)); - var bgDocument2 = new Document("The World is Big and Salvation Lurks Around the Corner", - Map.of("country", "BG", "year", 2023)); - - // Add documents to store - vectorStore.add(List.of(bgDocument, nlDocument, bgDocument2)); - - // Verify initial state - SearchRequest searchRequest = SearchRequest.builder() - .query("The World") - .topK(5) - .similarityThresholdAll() - .build(); - - List results = vectorStore.similaritySearch(searchRequest); - assertThat(results).hasSize(3); - - // Delete two documents by ID - vectorStore.delete(List.of(bgDocument.getId(), nlDocument.getId())); - - // Verify deletion - results = vectorStore.similaritySearch(searchRequest); - assertThat(results).hasSize(1); - assertThat(results.get(0).getId()).isEqualTo(bgDocument2.getId()); - - // Remove all documents from the store - dropTable(context); - }); - } - - @Test - public void deleteByFilter() { - this.contextRunner.withPropertyValues("test.spring.ai.vectorstore.pgvector.distanceType=COSINE_DISTANCE") - .run(context -> { - VectorStore vectorStore = context.getBean(VectorStore.class); - - // Create test documents - 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", "year", 2021)); - var bgDocument2 = new Document("The World is Big and Salvation Lurks Around the Corner", - Map.of("country", "BG", "year", 2023)); - - // Add documents to store - vectorStore.add(List.of(bgDocument, nlDocument, bgDocument2)); - - // Verify initial state - SearchRequest searchRequest = SearchRequest.builder() - .query("The World") - .topK(5) - .similarityThresholdAll() - .build(); - - List results = vectorStore.similaritySearch(searchRequest); - assertThat(results).hasSize(3); - - // Create filter to delete all documents with country=BG - Filter.Expression filterExpression = new Filter.Expression(Filter.ExpressionType.EQ, - new Filter.Key("country"), new Filter.Value("BG")); - - // Delete documents using filter - vectorStore.delete(filterExpression); - - // Verify deletion - should only have NL document remaining - results = vectorStore.similaritySearch(searchRequest); - assertThat(results).hasSize(1); - assertThat(results.get(0).getMetadata()).containsEntry("country", "NL"); - - // Remove all documents from the store - dropTable(context); - }); - } - - @Test - public 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", "year", 2021)); - 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)); - - var searchRequest = SearchRequest.builder().query("The World").topK(5).similarityThresholdAll().build(); - - List results = vectorStore.similaritySearch(searchRequest); - assertThat(results).hasSize(3); - - vectorStore.delete("country == 'BG'"); - - results = vectorStore.similaritySearch(searchRequest); - assertThat(results).hasSize(1); - assertThat(results.get(0).getMetadata()).containsEntry("country", "NL"); - - vectorStore.delete(List.of(nlDocument.getId())); - }); - } - @Test void getNativeClientTest() { this.contextRunner.run(context -> { diff --git a/vector-stores/spring-ai-pinecone-store/src/test/java/org/springframework/ai/vectorstore/pinecone/PineconeVectorStoreIT.java b/vector-stores/spring-ai-pinecone-store/src/test/java/org/springframework/ai/vectorstore/pinecone/PineconeVectorStoreIT.java index 1e183ecc417..02adc9a7c09 100644 --- a/vector-stores/spring-ai-pinecone-store/src/test/java/org/springframework/ai/vectorstore/pinecone/PineconeVectorStoreIT.java +++ b/vector-stores/spring-ai-pinecone-store/src/test/java/org/springframework/ai/vectorstore/pinecone/PineconeVectorStoreIT.java @@ -24,6 +24,7 @@ import java.util.Optional; import java.util.UUID; import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; import java.util.stream.Collectors; import io.pinecone.PineconeConnection; @@ -36,6 +37,7 @@ import org.springframework.ai.document.Document; import org.springframework.ai.document.DocumentMetadata; import org.springframework.ai.embedding.EmbeddingModel; +import org.springframework.ai.test.vectorstore.BaseVectorStoreTests; import org.springframework.ai.transformers.TransformersEmbeddingModel; import org.springframework.ai.vectorstore.SearchRequest; import org.springframework.ai.vectorstore.VectorStore; @@ -56,7 +58,7 @@ * @author Soby Chacko */ @EnabledIfEnvironmentVariable(named = "PINECONE_API_KEY", matches = ".+") -public class PineconeVectorStoreIT { +public class PineconeVectorStoreIT extends BaseVectorStoreTests { // Replace the PINECONE_ENVIRONMENT, PINECONE_PROJECT_ID, PINECONE_INDEX_NAME and // PINECONE_API_KEY with your pinecone credentials. @@ -98,6 +100,14 @@ public static void beforeAll() { Awaitility.setDefaultTimeout(Duration.ONE_MINUTE); } + @Override + protected void executeTest(Consumer testFunction) { + contextRunner.run(context -> { + VectorStore vectorStore = context.getBean(VectorStore.class); + testFunction.accept(vectorStore); + }); + } + @Test public void addAndSearchTest() { @@ -290,59 +300,6 @@ public void searchThresholdTest() { }); } - @Test - void deleteByFilter() { - this.contextRunner.run(context -> { - VectorStore vectorStore = context.getBean(VectorStore.class); - - cleanupExistingDocuments(vectorStore, "The World"); - - var documents = createWorldDocuments(); - vectorStore.add(documents); - - awaitDocumentsCount(vectorStore, "The World", 3); - - Filter.Expression filterExpression = new Filter.Expression(Filter.ExpressionType.EQ, - new Filter.Key("country"), new Filter.Value("BG")); - - vectorStore.delete(filterExpression); - - awaitDocumentsCount(vectorStore, "The World", 1); - - List results = searchDocuments(vectorStore, "The World", 5); - assertThat(results).hasSize(1); - assertThat(results.get(0).getMetadata()).containsEntry("country", "NL"); - - vectorStore.delete(List.of(documents.get(1).getId())); // nlDocument - awaitDocumentsCount(vectorStore, "The World", 0); - }); - } - - @Test - void deleteWithStringFilterExpression() { - this.contextRunner.run(context -> { - VectorStore vectorStore = context.getBean(VectorStore.class); - - cleanupExistingDocuments(vectorStore, "The World"); - - var documents = createWorldDocuments(); - vectorStore.add(documents); - - awaitDocumentsCount(vectorStore, "The World", 3); - - vectorStore.delete("country == 'BG'"); - - awaitDocumentsCount(vectorStore, "The World", 1); - - List results = searchDocuments(vectorStore, "The World", 5); - assertThat(results).hasSize(1); - assertThat(results.get(0).getMetadata()).containsEntry("country", "NL"); - - vectorStore.delete(List.of(documents.get(1).getId())); // nlDocument - awaitDocumentsCount(vectorStore, "The World", 0); - }); - } - @Test void deleteWithComplexFilterExpression() { this.contextRunner.run(context -> { diff --git a/vector-stores/spring-ai-qdrant-store/src/test/java/org/springframework/ai/vectorstore/qdrant/QdrantVectorStoreIT.java b/vector-stores/spring-ai-qdrant-store/src/test/java/org/springframework/ai/vectorstore/qdrant/QdrantVectorStoreIT.java index 9eef3556c36..2f45ae0cc9f 100644 --- a/vector-stores/spring-ai-qdrant-store/src/test/java/org/springframework/ai/vectorstore/qdrant/QdrantVectorStoreIT.java +++ b/vector-stores/spring-ai-qdrant-store/src/test/java/org/springframework/ai/vectorstore/qdrant/QdrantVectorStoreIT.java @@ -22,6 +22,7 @@ import java.util.Optional; import java.util.UUID; import java.util.concurrent.ExecutionException; +import java.util.function.Consumer; import java.util.stream.Collectors; import io.qdrant.client.QdrantClient; @@ -41,6 +42,7 @@ import org.springframework.ai.embedding.EmbeddingModel; import org.springframework.ai.mistralai.MistralAiEmbeddingModel; import org.springframework.ai.mistralai.api.MistralAiApi; +import org.springframework.ai.test.vectorstore.BaseVectorStoreTests; import org.springframework.ai.vectorstore.SearchRequest; import org.springframework.ai.vectorstore.VectorStore; import org.springframework.ai.vectorstore.filter.Filter; @@ -61,7 +63,7 @@ @Testcontainers @EnabledIfEnvironmentVariables({ @EnabledIfEnvironmentVariable(named = "MISTRAL_AI_API_KEY", matches = ".+"), @EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".+") }) -public class QdrantVectorStoreIT { +public class QdrantVectorStoreIT extends BaseVectorStoreTests { private static final String COLLECTION_NAME = "test_collection"; @@ -97,6 +99,14 @@ static void setup() throws InterruptedException, ExecutionException { client.close(); } + @Override + protected void executeTest(Consumer testFunction) { + contextRunner.run(context -> { + VectorStore vectorStore = context.getBean(VectorStore.class); + testFunction.accept(vectorStore); + }); + } + @Test public void addAndSearch() { this.contextRunner.run(context -> { @@ -260,85 +270,6 @@ public void searchThresholdTest() { }); } - @Test - void deleteById() { - this.contextRunner.run(context -> { - VectorStore vectorStore = context.getBean(VectorStore.class); - - var doc1 = new Document("The World is Big and Salvation Lurks Around the Corner", - Map.of("country", "BG", "year", 2020)); - var doc2 = new Document("The World is Big and Salvation Lurks Around the Corner", Map.of("country", "NL")); - var doc3 = new Document("The World is Big and Salvation Lurks Around the Corner", - Map.of("country", "BG", "year", 2023)); - - vectorStore.add(List.of(doc1, doc2, doc3)); - - // Delete first two documents - vectorStore.delete(List.of(doc1.getId(), doc2.getId())); - - List results = vectorStore - .similaritySearch(SearchRequest.builder().query("The World").topK(5).similarityThresholdAll().build()); - - assertThat(results).hasSize(1); - assertThat(results.get(0).getId()).isEqualTo(doc3.getId()); - assertThat(results.get(0).getMetadata()).containsEntry("country", "BG").containsEntry("year", 2023L); - - // Clean up remaining document - vectorStore.delete(List.of(doc3.getId())); - }); - } - - @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", "Bulgaria", "number", 3)); - var nlDocument = new Document("The World is Big and Salvation Lurks Around the Corner", - Map.of("country", "Netherlands", "number", 90)); - - vectorStore.add(List.of(bgDocument, nlDocument)); - - Filter.Expression filterExpression = new Filter.Expression(Filter.ExpressionType.EQ, - new Filter.Key("country"), new Filter.Value("Bulgaria")); - - vectorStore.delete(filterExpression); - - List results = vectorStore - .similaritySearch(SearchRequest.builder().query("The World").topK(5).similarityThresholdAll().build()); - - assertThat(results).hasSize(1); - assertThat(results.get(0).getMetadata()).containsEntry("country", "Netherlands"); - - 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", "Bulgaria", "number", 3)); - var nlDocument = new Document("The World is Big and Salvation Lurks Around the Corner", - Map.of("country", "Netherlands", "number", 90)); - - vectorStore.add(List.of(bgDocument, nlDocument)); - - vectorStore.delete("number > 50"); - - List results = vectorStore - .similaritySearch(SearchRequest.builder().query("The World").topK(5).similarityThresholdAll().build()); - - assertThat(results).hasSize(1); - assertThat(results.get(0).getMetadata()).containsEntry("country", "Bulgaria"); - - vectorStore.delete(List.of(bgDocument.getId())); - }); - } - @Test void deleteWithComplexFilterExpression() { this.contextRunner.run(context -> { diff --git a/vector-stores/spring-ai-redis-store/src/test/java/org/springframework/ai/vectorstore/redis/RedisVectorStoreIT.java b/vector-stores/spring-ai-redis-store/src/test/java/org/springframework/ai/vectorstore/redis/RedisVectorStoreIT.java index 601d5c52801..a5ddf65e0db 100644 --- a/vector-stores/spring-ai-redis-store/src/test/java/org/springframework/ai/vectorstore/redis/RedisVectorStoreIT.java +++ b/vector-stores/spring-ai-redis-store/src/test/java/org/springframework/ai/vectorstore/redis/RedisVectorStoreIT.java @@ -23,6 +23,8 @@ import java.util.Map; import java.util.Optional; import java.util.UUID; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; import java.util.stream.Collectors; import com.redis.testcontainers.RedisStackContainer; @@ -35,6 +37,7 @@ import org.springframework.ai.document.Document; import org.springframework.ai.document.DocumentMetadata; import org.springframework.ai.embedding.EmbeddingModel; +import org.springframework.ai.test.vectorstore.BaseVectorStoreTests; import org.springframework.ai.transformers.TransformersEmbeddingModel; import org.springframework.ai.vectorstore.SearchRequest; import org.springframework.ai.vectorstore.VectorStore; @@ -48,6 +51,7 @@ import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.annotation.Bean; import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.data.redis.connection.RedisStandaloneConfiguration; import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; import static org.assertj.core.api.Assertions.assertThat; @@ -59,7 +63,7 @@ * @author Soby Chacko */ @Testcontainers -class RedisVectorStoreIT { +class RedisVectorStoreIT extends BaseVectorStoreTests { @Container static RedisStackContainer redisContainer = new RedisStackContainer( @@ -90,6 +94,14 @@ void cleanDatabase() { this.contextRunner.run(context -> context.getBean(RedisVectorStore.class).getJedis().flushAll()); } + @Override + protected void executeTest(Consumer testFunction) { + contextRunner.run(context -> { + VectorStore vectorStore = context.getBean(VectorStore.class); + testFunction.accept(vectorStore); + }); + } + @Test void ensureIndexGetsCreated() { this.contextRunner.run(context -> assertThat(context.getBean(RedisVectorStore.class).getJedis().ftList()) @@ -264,85 +276,6 @@ void searchWithThreshold() { }); } - @Test - void deleteById() { - this.contextRunner.run(context -> { - VectorStore vectorStore = context.getBean(VectorStore.class); - - var doc1 = new Document("The World is Big and Salvation Lurks Around the Corner", - Map.of("country", "BG", "year", 2020)); - var doc2 = new Document("The World is Big and Salvation Lurks Around the Corner", Map.of("country", "NL")); - var doc3 = new Document("The World is Big and Salvation Lurks Around the Corner", - Map.of("country", "BG", "year", 2023)); - - vectorStore.add(List.of(doc1, doc2, doc3)); - - // Delete first two documents - vectorStore.delete(List.of(doc1.getId(), doc2.getId())); - - List results = vectorStore - .similaritySearch(SearchRequest.builder().query("The World").topK(5).similarityThresholdAll().build()); - - assertThat(results).hasSize(1); - assertThat(results.get(0).getId()).isEqualTo(doc3.getId()); - assertThat(results.get(0).getMetadata()).containsEntry("country", "BG").containsEntry("year", "2023"); - - // Clean up remaining document - vectorStore.delete(List.of(doc3.getId())); - }); - } - - @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 results = vectorStore - .similaritySearch(SearchRequest.builder().query("The World").topK(5).similarityThresholdAll().build()); - - assertThat(results).hasSize(1); - assertThat(results.get(0).getMetadata()).containsEntry("country", "NL"); - }); - } - - @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 results = vectorStore - .similaritySearch(SearchRequest.builder().query("The World").topK(5).similarityThresholdAll().build()); - - assertThat(results).hasSize(1); - assertThat(results.get(0).getMetadata()).containsEntry("country", "NL"); - }); - } - @Test void deleteWithComplexFilterExpression() { this.contextRunner.run(context -> { diff --git a/vector-stores/spring-ai-typesense-store/src/test/java/org/springframework/ai/vectorstore/typesense/TypesenseVectorStoreIT.java b/vector-stores/spring-ai-typesense-store/src/test/java/org/springframework/ai/vectorstore/typesense/TypesenseVectorStoreIT.java index a6ba72af5b9..a2c0f0348c1 100644 --- a/vector-stores/spring-ai-typesense-store/src/test/java/org/springframework/ai/vectorstore/typesense/TypesenseVectorStoreIT.java +++ b/vector-stores/spring-ai-typesense-store/src/test/java/org/springframework/ai/vectorstore/typesense/TypesenseVectorStoreIT.java @@ -25,6 +25,8 @@ import java.util.Map; import java.util.Optional; import java.util.UUID; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; import java.util.stream.Collectors; import org.junit.jupiter.api.Test; @@ -38,6 +40,7 @@ import org.springframework.ai.document.Document; import org.springframework.ai.document.DocumentMetadata; import org.springframework.ai.embedding.EmbeddingModel; +import org.springframework.ai.test.vectorstore.BaseVectorStoreTests; import org.springframework.ai.transformers.TransformersEmbeddingModel; import org.springframework.ai.vectorstore.SearchRequest; import org.springframework.ai.vectorstore.VectorStore; @@ -58,7 +61,7 @@ * @author Thomas Vitale */ @Testcontainers -public class TypesenseVectorStoreIT { +public class TypesenseVectorStoreIT extends BaseVectorStoreTests { @Container private static TypesenseContainer typesense = new TypesenseContainer(TypesenseImage.DEFAULT_IMAGE); @@ -81,6 +84,14 @@ public static String getText(String uri) { } } + @Override + protected void executeTest(Consumer testFunction) { + contextRunner.run(context -> { + VectorStore vectorStore = context.getBean(VectorStore.class); + testFunction.accept(vectorStore); + }); + } + @Test void documentUpdate() { this.contextRunner.run(context -> { @@ -246,89 +257,6 @@ void searchWithThreshold() { }); } - @Test - void deleteById() { - this.contextRunner.run(context -> { - VectorStore vectorStore = context.getBean(VectorStore.class); - - var doc1 = new Document("The World is Big and Salvation Lurks Around the Corner", - Map.of("country", "BG", "year", 2020)); - var doc2 = new Document("The World is Big and Salvation Lurks Around the Corner", Map.of("country", "NL")); - var doc3 = new Document("The World is Big and Salvation Lurks Around the Corner", - Map.of("country", "BG", "year", 2023)); - - vectorStore.add(List.of(doc1, doc2, doc3)); - - // Delete first two documents - vectorStore.delete(List.of(doc1.getId(), doc2.getId())); - - List results = vectorStore - .similaritySearch(SearchRequest.builder().query("The World").topK(5).similarityThresholdAll().build()); - - assertThat(results).hasSize(1); - assertThat(results.get(0).getId()).isEqualTo(doc3.getId()); - assertThat(results.get(0).getMetadata()).containsEntry("country", "BG").containsEntry("year", 2023); - - // Clean up remaining document - vectorStore.delete(List.of(doc3.getId())); - }); - } - - @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 results = vectorStore - .similaritySearch(SearchRequest.builder().query("The World").topK(5).similarityThresholdAll().build()); - - assertThat(results).hasSize(1); - assertThat(results.get(0).getMetadata()).containsEntry("country", "NL"); - - ((TypesenseVectorStore) vectorStore).dropCollection(); - }); - } - - @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 results = vectorStore - .similaritySearch(SearchRequest.builder().query("The World").topK(5).similarityThresholdAll().build()); - - assertThat(results).hasSize(1); - assertThat(results.get(0).getMetadata()).containsEntry("country", "NL"); - - ((TypesenseVectorStore) vectorStore).dropCollection(); - }); - } - @Test void deleteWithComplexFilterExpression() { this.contextRunner.run(context -> { diff --git a/vector-stores/spring-ai-weaviate-store/pom.xml b/vector-stores/spring-ai-weaviate-store/pom.xml index 2ba8d3fa2ef..d603c8a0fdf 100644 --- a/vector-stores/spring-ai-weaviate-store/pom.xml +++ b/vector-stores/spring-ai-weaviate-store/pom.xml @@ -78,7 +78,7 @@ test - + org.springframework.boot spring-boot-starter-test test diff --git a/vector-stores/spring-ai-weaviate-store/src/test/java/org/springframework/ai/vectorstore/weaviate/WeaviateVectorStoreIT.java b/vector-stores/spring-ai-weaviate-store/src/test/java/org/springframework/ai/vectorstore/weaviate/WeaviateVectorStoreIT.java index d5fcc1cce76..100d427083c 100644 --- a/vector-stores/spring-ai-weaviate-store/src/test/java/org/springframework/ai/vectorstore/weaviate/WeaviateVectorStoreIT.java +++ b/vector-stores/spring-ai-weaviate-store/src/test/java/org/springframework/ai/vectorstore/weaviate/WeaviateVectorStoreIT.java @@ -23,6 +23,8 @@ import java.util.Map; import java.util.Optional; import java.util.UUID; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; import io.weaviate.client.Config; import io.weaviate.client.WeaviateClient; @@ -35,10 +37,10 @@ import org.springframework.ai.document.Document; import org.springframework.ai.document.DocumentMetadata; import org.springframework.ai.embedding.EmbeddingModel; +import org.springframework.ai.test.vectorstore.BaseVectorStoreTests; 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; @@ -54,7 +56,7 @@ * @author Thomas Vitale */ @Testcontainers -public class WeaviateVectorStoreIT { +public class WeaviateVectorStoreIT extends BaseVectorStoreTests { @Container static WeaviateContainer weaviateContainer = new WeaviateContainer(WeaviateImage.DEFAULT_IMAGE) @@ -85,6 +87,14 @@ private void resetCollection(VectorStore vectorStore) { vectorStore.delete(this.documents.stream().map(Document::getId).toList()); } + @Override + protected void executeTest(Consumer testFunction) { + contextRunner.run(context -> { + VectorStore vectorStore = context.getBean(VectorStore.class); + testFunction.accept(vectorStore); + }); + } + @Test public void addAndSearch() { @@ -256,90 +266,6 @@ public void searchWithThreshold() { }); } - @Test - void deleteById() { - this.contextRunner.run(context -> { - VectorStore vectorStore = context.getBean(VectorStore.class); - - var doc1 = new Document("The World is Big and Salvation Lurks Around the Corner", - Map.of("country", "BG", "year", 2020)); - var doc2 = new Document("The World is Big and Salvation Lurks Around the Corner", Map.of("country", "NL")); - var doc3 = new Document("The World is Big and Salvation Lurks Around the Corner", - Map.of("country", "BG", "year", 2023)); - - vectorStore.add(List.of(doc1, doc2, doc3)); - - // Delete first two documents - vectorStore.delete(List.of(doc1.getId(), doc2.getId())); - - List results = vectorStore - .similaritySearch(SearchRequest.builder().query("The World").topK(5).similarityThresholdAll().build()); - - assertThat(results).hasSize(1); - assertThat(results.get(0).getId()).isEqualTo(doc3.getId()); - assertThat(results.get(0).getMetadata()).containsEntry("country", "BG").containsEntry("year", 2023); - - // Clean up remaining document - vectorStore.delete(List.of(doc3.getId())); - }); - } - - @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 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 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 getNativeClientTest() { this.contextRunner.run(context -> { From 1c1ed151e5206372442797585c838cf96d3717bd Mon Sep 17 00:00:00 2001 From: Soby Chacko Date: Wed, 12 Feb 2025 14:52:57 -0500 Subject: [PATCH 3/3] Adding javadoc Signed-off-by: Soby Chacko --- .../ai/test/vectorstore/BaseVectorStoreTests.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/spring-ai-test/src/main/java/org/springframework/ai/test/vectorstore/BaseVectorStoreTests.java b/spring-ai-test/src/main/java/org/springframework/ai/test/vectorstore/BaseVectorStoreTests.java index 3c0fd671f15..e7b7e5d2378 100644 --- a/spring-ai-test/src/main/java/org/springframework/ai/test/vectorstore/BaseVectorStoreTests.java +++ b/spring-ai-test/src/main/java/org/springframework/ai/test/vectorstore/BaseVectorStoreTests.java @@ -42,6 +42,12 @@ */ public abstract class BaseVectorStoreTests { + /** + * Execute a test function with a configured VectorStore instance. This method is + * responsible for providing a properly initialized VectorStore within the appropriate + * Spring application context for testing. + * @param testFunction the consumer that executes test operations on the VectorStore + */ protected abstract void executeTest(Consumer testFunction); protected Document createDocument(String country, Integer year) {