diff --git a/README.adoc b/README.adoc index 43d26ddbd..1bc7ccc48 100644 --- a/README.adoc +++ b/README.adoc @@ -8,9 +8,11 @@ We have separate folders for the samples of individual modules: == Spring Data for Apache Cassandra +* `aot-optimization` - Use Ahead-Of-Time Repositories with Spring Data for Apache Cassandra. * `example` - Shows core Spring Data support for Apache Cassandra. * `kotlin` - Example for using Cassandra with Kotlin. * `reactive` - Example project to show reactive template and repository support. +* `vector-search` - Example how to do vector search with a Spring Data Cassandra repository. == Spring Data Elasticsearch @@ -22,6 +24,7 @@ Local Elasticsearch instance must be running to run the tests. == Spring Data JDBC +* `aot-optimization` - Use Ahead-Of-Time Repositories with Spring Data JDBC. * `basic` - Basic usage of Spring Data JDBC. * `graalvm-native` - This example compiles a basic Spring Data JDBC application into a GraalVM native image. * `howto` - A collection of projects to go with the https://spring.io/blog/2021/09/09/spring-data-jdbc-how-to-use-custom-id-generation[Spring Data JDBC - How to blog posts]. @@ -34,6 +37,7 @@ Local Elasticsearch instance must be running to run the tests. == Spring Data JPA +* `aot-optimization` - Use Ahead-Of-Time Repositories with Spring Data JPA. * `eclipselink` - Sample project to show how to use Spring Data JPA with Spring Boot and https://www.eclipse.org/eclipselink/[Eclipselink]. * `example` - Probably the project you want to have a look at first. Contains a variety of sample packages, showcasing the different levels at which you can use Spring Data JPA. @@ -46,6 +50,7 @@ Contains also examples running on Virtual Threads. * `security` - Example of how to integrate Spring Data JPA Repositories with Spring Security. * `showcase` - Refactoring show case of how to improve a plain-JPA-based persistence layer by using Spring Data JPA (read: removing close to all of the implementation code).Follow the `demo.txt` file for detailed instructions. * `vavr` - Shows the support of https://www.vavr.io[Vavr] collection types as return types for query methods. +* `vector-search` - Example how to do vector search with a Spring Data JPA repository and `hibernate-vector`. == Spring Data LDAP @@ -53,6 +58,7 @@ Contains also examples running on Virtual Threads. == Spring Data MongoDB +* `aot-optimization` - Use Ahead-Of-Time Repositories with Spring Data MongoDB. * `aggregation` - Example project to showcase the MongoDB aggregation framework support. * `example` - Example project for general repository functionality (including geo-spatial functionality), Querydsl integration and advanced topics. * `fluent-api` - Example project to show the new fluent API (`MongoTemplate`-alternative) to interact with MongoDB. @@ -68,6 +74,7 @@ Contains also examples running on Virtual Threads. * `security` - Example project showing usage of Spring Security with MongoDB. * `text-search` - Example project showing usage of MongoDB text search feature. * `transactions` - Example project for imperative and reactive MongoDB 4.0 transaction support. +* `vector-search` - Example how to do vector search with a Spring Data MongoDB repository. == Spring Data Neo4j diff --git a/cassandra/pom.xml b/cassandra/pom.xml index ca44265bb..cfdb158a0 100644 --- a/cassandra/pom.xml +++ b/cassandra/pom.xml @@ -22,6 +22,7 @@ example kotlin reactive + vector-search diff --git a/cassandra/util/src/main/java/example/springdata/cassandra/util/CassandraExtension.java b/cassandra/util/src/main/java/example/springdata/cassandra/util/CassandraExtension.java index 8cb4f0ff7..aaea57e5c 100644 --- a/cassandra/util/src/main/java/example/springdata/cassandra/util/CassandraExtension.java +++ b/cassandra/util/src/main/java/example/springdata/cassandra/util/CassandraExtension.java @@ -54,6 +54,7 @@ public void beforeAll(ExtensionContext context) { CassandraContainer container = runTestcontainer(); System.setProperty("spring.cassandra.port", "" + container.getMappedPort(9042)); System.setProperty("spring.cassandra.contact-points", "" + container.getHost()); + System.setProperty("spring.cassandra.local-datacenter", container.getLocalDatacenter()); return new CassandraServer(container.getHost(), container.getMappedPort(9042), CassandraServer.RuntimeMode.EMBEDDED_IF_NOT_RUNNING); @@ -109,6 +110,6 @@ private CassandraContainer runTestcontainer() { private String getCassandraDockerImageName() { return String.format("cassandra:%s", - Optional.ofNullable(System.getenv("CASSANDRA_VERSION")).filter(StringUtils::hasText).orElse("3.11.10")); + Optional.ofNullable(System.getenv("CASSANDRA_VERSION")).filter(StringUtils::hasText).orElse("5.0.4")); } } diff --git a/cassandra/vector-search/README.md b/cassandra/vector-search/README.md new file mode 100644 index 000000000..d47924fdc --- /dev/null +++ b/cassandra/vector-search/README.md @@ -0,0 +1,37 @@ +# Spring Data for Apache Cassandra - Vector Search Example + +This project +contains [Vector Search](https://docs.spring.io/spring-data/cassandra/reference/5.0/cassandra/repositories/vector-search.html) +with Spring Data for Apache Cassandra. + +## Vector Support + +The Spring Data `Vector` type can be used in repository query methods. +Domain type properties of managed domain types are required to use a numeric array representation for embeddings. + +```java + +@Table +public class Comment { + + @Id + private String id; + + private String country; + private String description; + + @SaiIndexed + @VectorType(dimensions = 5) + private Vector embedding; + + // ... +} + + +public interface CommentRepository extends Repository { + + SearchResults searchTop10ByEmbeddingNear(Vector embedding, ScoringFunction function); +} +``` + +This example contains a test class to illustrate vector search with a Repository in `CassandraVectorSearchTest`. diff --git a/cassandra/vector-search/pom.xml b/cassandra/vector-search/pom.xml new file mode 100644 index 000000000..5418c0121 --- /dev/null +++ b/cassandra/vector-search/pom.xml @@ -0,0 +1,55 @@ + + + 4.0.0 + + org.springframework.data.examples + spring-data-cassandra-examples + 2.0.0.BUILD-SNAPSHOT + + + org.example + spring-data-cassandra-vector-search + Spring Data Cassandra - Vector Search + + + 2025.1.0-M6 + 7.0.0-M9 + + + + + + org.springframework.boot + spring-boot-testcontainers + test + + + + org.testcontainers + junit-jupiter + test + + + + org.testcontainers + cassandra + + + com.datastax.cassandra + cassandra-driver-core + + + + + + org.springframework.data.examples + spring-data-cassandra-example-utils + ${project.version} + test + + + + + diff --git a/cassandra/vector-search/src/main/java/example/springdata/vector/Comment.java b/cassandra/vector-search/src/main/java/example/springdata/vector/Comment.java new file mode 100644 index 000000000..b1b590a23 --- /dev/null +++ b/cassandra/vector-search/src/main/java/example/springdata/vector/Comment.java @@ -0,0 +1,73 @@ +/* + * Copyright 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 + * + * http://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 example.springdata.vector; + +import java.util.UUID; + +import org.springframework.data.annotation.Id; +import org.springframework.data.cassandra.core.mapping.SaiIndexed; +import org.springframework.data.cassandra.core.mapping.Table; +import org.springframework.data.cassandra.core.mapping.VectorType; +import org.springframework.data.domain.Vector; + +/** + * Sample entity containing a {@link Vector vector} {@link #embedding}. + */ +@Table +public class Comment { + + private @Id String id; + + private String country; + private String description; + + @SaiIndexed + @VectorType(dimensions = 5) private Vector embedding; + + public Comment() {} + + public Comment(String country, String description, Vector embedding) { + this.id = UUID.randomUUID().toString(); + this.country = country; + this.description = description; + this.embedding = embedding; + } + + public static Comment of(Comment source) { + return new Comment(source.getCountry(), source.getDescription(), source.getEmbedding()); + } + + public String getId() { + return id; + } + + public String getCountry() { + return country; + } + + public String getDescription() { + return description; + } + + public Vector getEmbedding() { + return embedding; + } + + @Override + public String toString() { + return "%s (%s)".formatted(getDescription(), getCountry()); + } +} diff --git a/cassandra/vector-search/src/main/java/example/springdata/vector/CommentRepository.java b/cassandra/vector-search/src/main/java/example/springdata/vector/CommentRepository.java new file mode 100644 index 000000000..35d982c18 --- /dev/null +++ b/cassandra/vector-search/src/main/java/example/springdata/vector/CommentRepository.java @@ -0,0 +1,32 @@ +/* + * Copyright 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 + * + * http://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 example.springdata.vector; + +import org.springframework.data.cassandra.repository.Query; +import org.springframework.data.domain.Limit; +import org.springframework.data.domain.Score; +import org.springframework.data.domain.ScoringFunction; +import org.springframework.data.domain.SearchResults; +import org.springframework.data.domain.Vector; +import org.springframework.data.repository.CrudRepository; + +public interface CommentRepository extends CrudRepository { + + SearchResults searchTop10ByEmbeddingNear(Vector embedding, ScoringFunction function); + + @Query("SELECT id, description, country, similarity_cosine(embedding,:embedding) AS score FROM comment ORDER BY embedding ANN OF :embedding LIMIT :limit") + SearchResults searchAnnotated(Vector embedding, Score distance, Limit limit); +} diff --git a/cassandra/vector-search/src/main/java/example/springdata/vector/VectorApp.java b/cassandra/vector-search/src/main/java/example/springdata/vector/VectorApp.java new file mode 100644 index 000000000..0c8bdb68e --- /dev/null +++ b/cassandra/vector-search/src/main/java/example/springdata/vector/VectorApp.java @@ -0,0 +1,52 @@ +/* + * Copyright 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 + * + * http://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 example.springdata.vector; + +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.data.cassandra.core.CassandraTemplate; +import org.springframework.data.domain.Vector; +import org.springframework.stereotype.Component; + +@SpringBootApplication +public class VectorApp { + + public static void main(String[] args) { + SpringApplication.run(VectorApp.class, args); + } + + @Component + static class DbInitializer implements CommandLineRunner { + + private final CassandraTemplate template; + + DbInitializer(CassandraTemplate template) { + this.template = template; + } + + @Override + public void run(String... args) { + + template.truncate(Comment.class); + + template.insert(new Comment("de", "comment 'one'", Vector.of(0.1001f, 0.22345f, 0.33456f, 0.44567f, 0.55678f))); + template.insert(new Comment("de", "comment 'two'", Vector.of(0.2001f, 0.32345f, 0.43456f, 0.54567f, 0.65678f))); + template.insert(new Comment("en", "comment 'three'", Vector.of(0.9001f, 0.82345f, 0.73456f, 0.64567f, 0.55678f))); + template.insert(new Comment("de", "comment 'four'", Vector.of(0.9001f, 0.92345f, 0.93456f, 0.94567f, 0.95678f))); + } + } +} diff --git a/cassandra/vector-search/src/main/resources/application.properties b/cassandra/vector-search/src/main/resources/application.properties new file mode 100644 index 000000000..767a36507 --- /dev/null +++ b/cassandra/vector-search/src/main/resources/application.properties @@ -0,0 +1,5 @@ +logging.level.org=WARN +logging.level.com.datastax=WARN + +spring.cassandra.schema-action=recreate +spring.cassandra.keyspace-name=vector_search_keyspace diff --git a/cassandra/vector-search/src/test/java/example/springdata/vector/CassandraDBConfiguration.java b/cassandra/vector-search/src/test/java/example/springdata/vector/CassandraDBConfiguration.java new file mode 100644 index 000000000..022a66e3b --- /dev/null +++ b/cassandra/vector-search/src/test/java/example/springdata/vector/CassandraDBConfiguration.java @@ -0,0 +1,58 @@ +/* + * Copyright 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 + * + * http://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 example.springdata.vector; + +import java.net.InetSocketAddress; + +import org.springframework.boot.autoconfigure.cassandra.CassandraConnectionDetails; +import org.springframework.boot.autoconfigure.cassandra.CassandraProperties; +import org.springframework.boot.autoconfigure.cassandra.CqlSessionBuilderCustomizer; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import org.testcontainers.cassandra.CassandraContainer; +import org.testcontainers.utility.DockerImageName; + +import com.datastax.oss.driver.api.core.CqlSession; + +@Configuration +public class CassandraDBConfiguration { + + @Bean + @ServiceConnection + CassandraContainer pgVectorContainer() { + return new CassandraContainer(DockerImageName.parse("cassandra:5")).withReuse(true); + } + + @Bean + CqlSessionBuilderCustomizer sessionBuilderCustomizer(CassandraConnectionDetails connectionDetails, + CassandraProperties properties) { + + return sessionBuilder -> { + + CqlSession session = CqlSession.builder() + .addContactPoints(connectionDetails.getContactPoints().stream() + .map(it -> new InetSocketAddress(it.host(), it.port())).toList()) + .withLocalDatacenter(connectionDetails.getLocalDatacenter()).build(); + + session.execute("CREATE KEYSPACE IF NOT EXISTS " + properties.getKeyspaceName() + " WITH replication = \n" + + "{'class':'SimpleStrategy','replication_factor':'1'};"); + session.close(); + }; + } + +} diff --git a/cassandra/vector-search/src/test/java/example/springdata/vector/CassandraVectorSearchTest.java b/cassandra/vector-search/src/test/java/example/springdata/vector/CassandraVectorSearchTest.java new file mode 100644 index 000000000..4690d67f8 --- /dev/null +++ b/cassandra/vector-search/src/test/java/example/springdata/vector/CassandraVectorSearchTest.java @@ -0,0 +1,65 @@ +/* + * Copyright 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 + * + * http://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 example.springdata.vector; + +import static org.springframework.data.domain.ScoringFunction.*; + +import example.springdata.cassandra.util.CassandraKeyspace; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.domain.Limit; +import org.springframework.data.domain.Score; +import org.springframework.data.domain.ScoringFunction; +import org.springframework.data.domain.SearchResult; +import org.springframework.data.domain.Vector; + +@CassandraKeyspace +@SpringBootTest +class CassandraVectorSearchTest { + + @Autowired CommentRepository repository; + + @BeforeEach + void beforeAll() throws InterruptedException { + Thread.sleep(5000); // a little time to think + } + + @Test + void vectorSearchUsingQueryMethod() { + + Vector vector = Vector.of(0.2001f, 0.32345f, 0.43456f, 0.54567f, 0.65678f); + + repository.searchTop10ByEmbeddingNear(vector, ScoringFunction.cosine()) + .forEach(CassandraVectorSearchTest::printResult); + } + + @Test + void vectorSearchUsingRawAtQuery() { + + Vector vector = Vector.of(0.2001f, 0.32345f, 0.43456f, 0.54567f, 0.65678f); + + repository.searchAnnotated(vector, Score.of(0.5, cosine()), Limit.of(10)) + .forEach(CassandraVectorSearchTest::printResult); + } + + private static void printResult(SearchResult result) { + System.out.printf("score: %s - %s\n", result.getScore(), result.getContent()); + } +} diff --git a/jpa/aot-optimization/pom.xml b/jpa/aot-optimization/pom.xml index 782e3f114..e28bed53a 100644 --- a/jpa/aot-optimization/pom.xml +++ b/jpa/aot-optimization/pom.xml @@ -14,7 +14,6 @@ Spring Data JPA - AOT Optimization Example - UTF-8 7.1.0.Final 2025.1.0-M6 7.0.0-M9 diff --git a/jpa/pom.xml b/jpa/pom.xml index 9b94d5869..956979935 100644 --- a/jpa/pom.xml +++ b/jpa/pom.xml @@ -30,6 +30,7 @@ vavr multitenant graalvm-native + vector-search diff --git a/jpa/vector-search/README.md b/jpa/vector-search/README.md new file mode 100644 index 000000000..6522d2da1 --- /dev/null +++ b/jpa/vector-search/README.md @@ -0,0 +1,36 @@ +# Spring Data JPA - Vector Search Example + +This project contains [Vector Search](https://docs.spring.io/spring-data/jpa/reference/4.0/repositories/vector-search.html) with Spring Data JPA and the `hibernate-vector` module. + +## Vector Support + +The Spring Data `Vector` type can be used in repository query methods. +Domain type properties of managed domain types are required to use a numeric array representation for embeddings. + +```java + +@Entity +@Table(name = "jpa_comment") +public class Comment { + + @Id + @GeneratedValue private Long id; + + private String country; + private String description; + + @JdbcTypeCode(SqlTypes.VECTOR) + @Array(length = 5) + private float[] embedding; + + // ... +} + + +public interface CommentRepository extends Repository { + + SearchResults searchTop10ByCountryAndEmbeddingNear(String country, Vector vector, Score distance); +} +``` + +This example contains a test class to illustrate vector search with a Repository in `JpaVectorSearchTest`. diff --git a/jpa/vector-search/pom.xml b/jpa/vector-search/pom.xml new file mode 100644 index 000000000..1ab07bcdb --- /dev/null +++ b/jpa/vector-search/pom.xml @@ -0,0 +1,59 @@ + + + 4.0.0 + + org.springframework.data.examples + spring-data-jpa-examples + 2.0.0.BUILD-SNAPSHOT + + + org.example + spring-data-jpa-vector-search + Spring Data JPA - Vector Search + + + 7.1.0.Final + 2025.1.0-M6 + 7.0.0-M9 + + + + + jakarta.persistence + jakarta.persistence-api + 3.2.0 + + + + org.hibernate.orm + hibernate-vector + ${hibernate.version} + + + + org.postgresql + postgresql + + + + org.springframework.boot + spring-boot-testcontainers + test + + + + org.testcontainers + junit-jupiter + test + + + + org.testcontainers + postgresql + + + + + diff --git a/jpa/vector-search/src/main/java/example/springdata/vector/Comment.java b/jpa/vector-search/src/main/java/example/springdata/vector/Comment.java new file mode 100644 index 000000000..49cf781ed --- /dev/null +++ b/jpa/vector-search/src/main/java/example/springdata/vector/Comment.java @@ -0,0 +1,75 @@ +/* + * Copyright 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 + * + * http://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 example.springdata.vector; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +import org.hibernate.annotations.Array; +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.type.SqlTypes; + +/** + * Sample entity containing a {@link SqlTypes#VECTOR vector} {@link #embedding}. + */ +@Entity +@Table(name = "jpa_comment") +public class Comment { + + @Id + @GeneratedValue private Long id; + + private String country; + private String description; + + @JdbcTypeCode(SqlTypes.VECTOR) + @Array(length = 5) private float[] embedding; + + public Comment() {} + + public Comment(String country, String description, float[] embedding) { + this.country = country; + this.description = description; + this.embedding = embedding; + } + + public static Comment of(Comment source) { + return new Comment(source.getCountry(), source.getDescription(), source.getEmbedding()); + } + + public long getId() { + return id; + } + + public String getCountry() { + return country; + } + + public String getDescription() { + return description; + } + + public float[] getEmbedding() { + return embedding; + } + + @Override + public String toString() { + return "%s (%s)".formatted(getDescription(), getCountry()); + } +} diff --git a/jpa/vector-search/src/main/java/example/springdata/vector/CommentRepository.java b/jpa/vector-search/src/main/java/example/springdata/vector/CommentRepository.java new file mode 100644 index 000000000..01f3eff35 --- /dev/null +++ b/jpa/vector-search/src/main/java/example/springdata/vector/CommentRepository.java @@ -0,0 +1,34 @@ +/* + * Copyright 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 + * + * http://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 example.springdata.vector; + +import org.springframework.data.domain.Score; +import org.springframework.data.domain.SearchResults; +import org.springframework.data.domain.Vector; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.CrudRepository; + +public interface CommentRepository extends CrudRepository { + + SearchResults searchTop10ByCountryAndEmbeddingNear(String country, Vector vector, Score distance); + + @Query(""" + SELECT c, cosine_distance(c.embedding, :embedding) as distance FROM Comment c + WHERE c.country = :country + AND cosine_distance(c.embedding, :embedding) <= :distance + ORDER BY distance asc""") + SearchResults searchAnnotated(String country, Vector embedding, Score distance); +} diff --git a/jpa/vector-search/src/main/java/example/springdata/vector/VectorApp.java b/jpa/vector-search/src/main/java/example/springdata/vector/VectorApp.java new file mode 100644 index 000000000..ae26d2656 --- /dev/null +++ b/jpa/vector-search/src/main/java/example/springdata/vector/VectorApp.java @@ -0,0 +1,54 @@ +/* + * Copyright 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 + * + * http://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 example.springdata.vector; + +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.stereotype.Component; + +@SpringBootApplication +public class VectorApp { + + public static void main(String[] args) { + SpringApplication.run(VectorApp.class, args); + } + + @Component + static class DbInitializer implements CommandLineRunner { + + private final CommentRepository repository; + + DbInitializer(CommentRepository repository) { + this.repository = repository; + } + + @Override + public void run(String... args) { + + repository.deleteAll(); + + repository + .save(new Comment("de", "comment 'one'", new float[] { 0.1001f, 0.22345f, 0.33456f, 0.44567f, 0.55678f })); + repository + .save(new Comment("de", "comment 'two'", new float[] { 0.2001f, 0.32345f, 0.43456f, 0.54567f, 0.65678f })); + repository + .save(new Comment("en", "comment 'three'", new float[] { 0.9001f, 0.82345f, 0.73456f, 0.64567f, 0.55678f })); + repository + .save(new Comment("de", "comment 'four'", new float[] { 0.9001f, 0.92345f, 0.93456f, 0.94567f, 0.95678f })); + } + } +} diff --git a/jpa/vector-search/src/main/resources/application.properties b/jpa/vector-search/src/main/resources/application.properties new file mode 100644 index 000000000..405d5dc2f --- /dev/null +++ b/jpa/vector-search/src/main/resources/application.properties @@ -0,0 +1,4 @@ +spring.sql.init.schema-locations=pgvector.sql +spring.sql.init.mode=always + +logging.level.org=WARN diff --git a/jpa/vector-search/src/main/resources/pgvector.sql b/jpa/vector-search/src/main/resources/pgvector.sql new file mode 100644 index 000000000..a0afa4ce4 --- /dev/null +++ b/jpa/vector-search/src/main/resources/pgvector.sql @@ -0,0 +1,11 @@ +CREATE EXTENSION IF NOT EXISTS vector; + +DROP TABLE IF EXISTS jpa_comment; + +DROP SEQUENCE IF EXISTS jpa_comment_seq; + +CREATE TABLE IF NOT EXISTS jpa_comment (id bigserial PRIMARY KEY, country varchar(10), description varchar(20), embedding vector(5)); + +CREATE SEQUENCE jpa_comment_seq INCREMENT 50; + +CREATE INDEX ON jpa_comment USING hnsw (embedding vector_l2_ops); diff --git a/jpa/vector-search/src/test/java/example/springdata/vector/JpaVectorSearchTest.java b/jpa/vector-search/src/test/java/example/springdata/vector/JpaVectorSearchTest.java new file mode 100644 index 000000000..5b8c55830 --- /dev/null +++ b/jpa/vector-search/src/test/java/example/springdata/vector/JpaVectorSearchTest.java @@ -0,0 +1,53 @@ +/* + * Copyright 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 + * + * http://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 example.springdata.vector; + +import static org.springframework.data.domain.ScoringFunction.*; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.domain.Score; +import org.springframework.data.domain.SearchResult; +import org.springframework.data.domain.Vector; + +@SpringBootTest +class JpaVectorSearchTest { + + @Autowired CommentRepository repository; + + @Test + void vectorSearchUsingQueryMethod() { + + Vector vector = Vector.of(0.2001f, 0.32345f, 0.43456f, 0.54567f, 0.65678f); + + repository.searchTop10ByCountryAndEmbeddingNear("de", vector, Score.of(0.5, cosine())) + .forEach(JpaVectorSearchTest::printResult); + } + + @Test + void vectorSearchUsingRawAtQuery() { + + Vector vector = Vector.of(0.2001f, 0.32345f, 0.43456f, 0.54567f, 0.65678f); + + repository.searchAnnotated("de", vector, Score.of(0.5, cosine())).forEach(JpaVectorSearchTest::printResult); + } + + private static void printResult(SearchResult result) { + System.out.printf("score: %s - %s\n", result.getScore(), result.getContent()); + } +} diff --git a/jpa/vector-search/src/test/java/example/springdata/vector/PGVectorConfiguration.java b/jpa/vector-search/src/test/java/example/springdata/vector/PGVectorConfiguration.java new file mode 100644 index 000000000..d5bd764e7 --- /dev/null +++ b/jpa/vector-search/src/test/java/example/springdata/vector/PGVectorConfiguration.java @@ -0,0 +1,35 @@ +/* + * Copyright 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 + * + * http://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 example.springdata.vector; + +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.testcontainers.containers.PostgreSQLContainer; +import org.testcontainers.utility.DockerImageName; + +/** + * Configuration to use PGvector with Testcontainers. + */ +@Configuration +class PGVectorConfiguration { + + @Bean + @ServiceConnection + PostgreSQLContainer pgVectorContainer() { + return new PostgreSQLContainer<>(DockerImageName.parse("pgvector/pgvector:pg17")).withReuse(true); + } +} diff --git a/mongodb/pom.xml b/mongodb/pom.xml index 04d2c282c..e733a04ba 100644 --- a/mongodb/pom.xml +++ b/mongodb/pom.xml @@ -37,6 +37,7 @@ linking util fragment-spi + vector-search diff --git a/mongodb/util/pom.xml b/mongodb/util/pom.xml index 90da78ade..f8e7d0926 100644 --- a/mongodb/util/pom.xml +++ b/mongodb/util/pom.xml @@ -29,6 +29,10 @@ mongodb ${testcontainers.version} + + org.springframework.boot + spring-boot-testcontainers + diff --git a/mongodb/util/src/main/java/example/springdata/mongodb/util/AtlasContainer.java b/mongodb/util/src/main/java/example/springdata/mongodb/util/AtlasContainer.java index f74158fc7..ba287498b 100644 --- a/mongodb/util/src/main/java/example/springdata/mongodb/util/AtlasContainer.java +++ b/mongodb/util/src/main/java/example/springdata/mongodb/util/AtlasContainer.java @@ -1,11 +1,11 @@ /* - * Copyright 2024 the original author or authors. + * Copyright 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 + * http://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, @@ -26,34 +26,35 @@ */ public class AtlasContainer extends GenericContainer { - private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("mongodb/mongodb-atlas-local"); - private static final String DEFAULT_TAG = "latest"; - private static final String MONGODB_DATABASE_NAME_DEFAULT = "test"; - - public AtlasContainer() { - this(DEFAULT_IMAGE_NAME.withTag(DEFAULT_TAG)); - } - - public AtlasContainer(String dockerImageName) { - this(DockerImageName.parse(dockerImageName)); - } - - public AtlasContainer(DockerImageName dockerImageName) { - super(dockerImageName); - dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); - setExposedPorts(List.of(27017)); - } - - public String getConnectionString() { - return getConnectionString(MONGODB_DATABASE_NAME_DEFAULT); - } - - /** - * Gets a connection string url. - * - * @return a connection url pointing to a mongodb instance - */ - public String getConnectionString(String database) { - return String.format("mongodb://%s:%d/%s?directConnection=true", getHost(), getMappedPort(27017), StringUtils.hasText(database) ? database : MONGODB_DATABASE_NAME_DEFAULT); - } + private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("mongodb/mongodb-atlas-local"); + private static final String DEFAULT_TAG = "latest"; + private static final String MONGODB_DATABASE_NAME_DEFAULT = "test"; + + public AtlasContainer() { + this(DEFAULT_IMAGE_NAME.withTag(DEFAULT_TAG)); + } + + public AtlasContainer(String dockerImageName) { + this(DockerImageName.parse(dockerImageName)); + } + + public AtlasContainer(DockerImageName dockerImageName) { + super(dockerImageName); + dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); + setExposedPorts(List.of(27017)); + } + + public String getConnectionString() { + return getConnectionString(MONGODB_DATABASE_NAME_DEFAULT); + } + + /** + * Gets a connection string url. + * + * @return a connection url pointing to a mongodb instance + */ + public String getConnectionString(String database) { + return String.format("mongodb://%s:%d/%s?directConnection=true", getHost(), getMappedPort(27017), + StringUtils.hasText(database) ? database : MONGODB_DATABASE_NAME_DEFAULT); + } } diff --git a/mongodb/util/src/main/java/example/springdata/mongodb/util/AtlasContainerConnectionDetailsFactory.java b/mongodb/util/src/main/java/example/springdata/mongodb/util/AtlasContainerConnectionDetailsFactory.java new file mode 100644 index 000000000..2cc6a60a4 --- /dev/null +++ b/mongodb/util/src/main/java/example/springdata/mongodb/util/AtlasContainerConnectionDetailsFactory.java @@ -0,0 +1,51 @@ +/* + * Copyright 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 + * + * http://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 example.springdata.mongodb.util; + +import org.springframework.boot.autoconfigure.mongo.MongoConnectionDetails; +import org.springframework.boot.ssl.SslBundle; +import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory; +import org.springframework.boot.testcontainers.service.connection.ContainerConnectionSource; + +import com.mongodb.ConnectionString; + +public class AtlasContainerConnectionDetailsFactory + extends ContainerConnectionDetailsFactory { + + AtlasContainerConnectionDetailsFactory() { + super(ANY_CONNECTION_NAME, new String[] { "com.mongodb.ConnectionString" }); + } + + protected MongoConnectionDetails getContainerConnectionDetails(ContainerConnectionSource source) { + return new MongoContainerConnectionDetails(source); + } + + private static final class MongoContainerConnectionDetails extends + ContainerConnectionDetailsFactory.ContainerConnectionDetails implements MongoConnectionDetails { + + private MongoContainerConnectionDetails(ContainerConnectionSource source) { + super(source); + } + + public ConnectionString getConnectionString() { + return new ConnectionString(this.getContainer().getConnectionString()); + } + + public SslBundle getSslBundle() { + return super.getSslBundle(); + } + } +} diff --git a/mongodb/util/src/main/java/example/springdata/mongodb/util/MongoContainers.java b/mongodb/util/src/main/java/example/springdata/mongodb/util/MongoContainers.java index 4019c3e0e..2baeeaa74 100644 --- a/mongodb/util/src/main/java/example/springdata/mongodb/util/MongoContainers.java +++ b/mongodb/util/src/main/java/example/springdata/mongodb/util/MongoContainers.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 the original author or authors. + * Copyright 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. @@ -26,18 +26,18 @@ */ public class MongoContainers { - private static final String IMAGE_NAME = "mongo:8.0"; - private static final String IMAGE_NAME_PROPERTY = "mongo.default.image.name"; + private static final String IMAGE_NAME = "mongo:8.0"; + private static final String IMAGE_NAME_PROPERTY = "mongo.default.image.name"; - private static final String ATLAS_IMAGE_NAME = "mongodb/mongodb-atlas-local:latest"; - private static final String ATLAS_IMAGE_NAME_PROPERTY = "mongo.atlas.image.name"; + private static final String ATLAS_IMAGE_NAME = "mongodb/mongodb-atlas-local:latest"; + private static final String ATLAS_IMAGE_NAME_PROPERTY = "mongo.atlas.image.name"; - public static MongoDBContainer getDefaultContainer() { - return new MongoDBContainer(DockerImageName.parse(System.getProperty(IMAGE_NAME_PROPERTY, IMAGE_NAME))) - .withReuse(true); - } + public static MongoDBContainer getDefaultContainer() { + return new MongoDBContainer(DockerImageName.parse(System.getProperty(IMAGE_NAME_PROPERTY, IMAGE_NAME))) + .withReuse(true); + } - public static AtlasContainer getAtlasContainer() { - return new AtlasContainer(System.getProperty(ATLAS_IMAGE_NAME_PROPERTY, ATLAS_IMAGE_NAME)).withReuse(true); - } + public static AtlasContainer getAtlasContainer() { + return new AtlasContainer(System.getProperty(ATLAS_IMAGE_NAME_PROPERTY, ATLAS_IMAGE_NAME)).withReuse(true); + } } diff --git a/mongodb/util/src/main/resources/META-INF/spring.factories b/mongodb/util/src/main/resources/META-INF/spring.factories new file mode 100644 index 000000000..8c6952234 --- /dev/null +++ b/mongodb/util/src/main/resources/META-INF/spring.factories @@ -0,0 +1,3 @@ +# Connection Details Factories +org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactory=\ +example.springdata.mongodb.util.AtlasContainerConnectionDetailsFactory diff --git a/mongodb/vector-search/README.md b/mongodb/vector-search/README.md new file mode 100644 index 000000000..cabe97014 --- /dev/null +++ b/mongodb/vector-search/README.md @@ -0,0 +1,36 @@ +# Spring Data MongoDB - Vector Search Example + +This project +contains [Vector Search](https://docs.spring.io/spring-data/mongodb/reference/5.0/mongodb/repositories/vector-search.html) +with Spring Data MongoDB. + +## Vector Support + +The Spring Data `Vector` type can be used in repository query methods. +Domain type properties of managed domain types are required to use a numeric array representation for embeddings. + +```java + +@Document +public class Comment { + + @Id + private ObjectId id; + + private String country; + private String description; + + private Vector embedding; + + // ... +} + + +public interface CommentRepository extends Repository { + + @VectorSearch(indexName = "cosine-index", searchType = VectorSearchOperation.SearchType.ANN) + SearchResults searchTop10ByCountryAndEmbeddingNear(String country, Vector vector, Score distance); +} +``` + +This example contains a test class to illustrate vector search with a Repository in `MongoDBVectorSearchTest`. diff --git a/mongodb/vector-search/pom.xml b/mongodb/vector-search/pom.xml new file mode 100644 index 000000000..40ed109ca --- /dev/null +++ b/mongodb/vector-search/pom.xml @@ -0,0 +1,42 @@ + + + 4.0.0 + + org.springframework.data.examples + spring-data-mongodb-examples + 2.0.0.BUILD-SNAPSHOT + + + org.example + spring-data-mongodb-vector-search + Spring Data MongoDB - Vector Search + + + 2025.1.0-M6 + 7.0.0-M9 + + + + + + org.springframework.boot + spring-boot-testcontainers + test + + + + org.testcontainers + mongodb + + + + org.springframework.data.examples + spring-data-mongodb-example-utils + test + + + + + diff --git a/mongodb/vector-search/src/main/java/example/springdata/vector/Comment.java b/mongodb/vector-search/src/main/java/example/springdata/vector/Comment.java new file mode 100644 index 000000000..6050e26ab --- /dev/null +++ b/mongodb/vector-search/src/main/java/example/springdata/vector/Comment.java @@ -0,0 +1,67 @@ +/* + * Copyright 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 + * + * http://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 example.springdata.vector; + +import org.bson.types.ObjectId; +import org.springframework.data.domain.Vector; +import org.springframework.data.mongodb.core.mapping.Document; + +/** + * Sample entity containing a {@link Vector vector} {@link #embedding}. + */ +@Document +public class Comment { + + private ObjectId id; + + private String country; + private String description; + + private Vector embedding; + + public Comment() {} + + public Comment(String country, String description, Vector embedding) { + this.country = country; + this.description = description; + this.embedding = embedding; + } + + public static Comment of(Comment source) { + return new Comment(source.getCountry(), source.getDescription(), source.getEmbedding()); + } + + public ObjectId getId() { + return id; + } + + public String getCountry() { + return country; + } + + public String getDescription() { + return description; + } + + public Vector getEmbedding() { + return embedding; + } + + @Override + public String toString() { + return "%s (%s)".formatted(getDescription(), getCountry()); + } +} diff --git a/mongodb/vector-search/src/main/java/example/springdata/vector/CommentRepository.java b/mongodb/vector-search/src/main/java/example/springdata/vector/CommentRepository.java new file mode 100644 index 000000000..3130d5f36 --- /dev/null +++ b/mongodb/vector-search/src/main/java/example/springdata/vector/CommentRepository.java @@ -0,0 +1,34 @@ +/* + * Copyright 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 + * + * http://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 example.springdata.vector; + +import org.springframework.data.domain.Limit; +import org.springframework.data.domain.Score; +import org.springframework.data.domain.SearchResults; +import org.springframework.data.domain.Vector; +import org.springframework.data.mongodb.core.aggregation.VectorSearchOperation; +import org.springframework.data.mongodb.repository.VectorSearch; +import org.springframework.data.repository.CrudRepository; + +public interface CommentRepository extends CrudRepository { + + @VectorSearch(indexName = "cosine-index", searchType = VectorSearchOperation.SearchType.ANN) + SearchResults searchTop10ByCountryAndEmbeddingNear(String country, Vector vector, Score distance); + + @VectorSearch(indexName = "cosine-index", filter = "{country: ?0}", numCandidates = "#{#limit.max*10}", + searchType = VectorSearchOperation.SearchType.ANN) + SearchResults searchAnnotated(String country, Vector vector, Score distance, Limit limit); +} diff --git a/mongodb/vector-search/src/main/java/example/springdata/vector/VectorApp.java b/mongodb/vector-search/src/main/java/example/springdata/vector/VectorApp.java new file mode 100644 index 000000000..61f847148 --- /dev/null +++ b/mongodb/vector-search/src/main/java/example/springdata/vector/VectorApp.java @@ -0,0 +1,51 @@ +/* + * Copyright 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 + * + * http://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 example.springdata.vector; + +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.data.domain.Vector; +import org.springframework.stereotype.Component; + +@SpringBootApplication +public class VectorApp { + + public static void main(String[] args) { + SpringApplication.run(VectorApp.class, args); + } + + @Component + static class DbInitializer implements CommandLineRunner { + + private final CommentRepository repository; + + DbInitializer(CommentRepository repository) { + this.repository = repository; + } + + @Override + public void run(String... args) { + + repository.deleteAll(); + + repository.save(new Comment("de", "comment 'one'", Vector.of(0.1001f, 0.22345f, 0.33456f, 0.44567f, 0.55678f))); + repository.save(new Comment("de", "comment 'two'", Vector.of(0.2001f, 0.32345f, 0.43456f, 0.54567f, 0.65678f))); + repository.save(new Comment("en", "comment 'three'", Vector.of(0.9001f, 0.82345f, 0.73456f, 0.64567f, 0.55678f))); + repository.save(new Comment("de", "comment 'four'", Vector.of(0.9001f, 0.92345f, 0.93456f, 0.94567f, 0.95678f))); + } + } +} diff --git a/mongodb/vector-search/src/main/resources/application.properties b/mongodb/vector-search/src/main/resources/application.properties new file mode 100644 index 000000000..d0f1c59b3 --- /dev/null +++ b/mongodb/vector-search/src/main/resources/application.properties @@ -0,0 +1 @@ +logging.level.org=WARN diff --git a/mongodb/vector-search/src/test/java/example/springdata/vector/MongoDBConfiguration.java b/mongodb/vector-search/src/test/java/example/springdata/vector/MongoDBConfiguration.java new file mode 100644 index 000000000..89350f872 --- /dev/null +++ b/mongodb/vector-search/src/test/java/example/springdata/vector/MongoDBConfiguration.java @@ -0,0 +1,33 @@ +/* + * Copyright 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 + * + * http://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 example.springdata.vector; + +import example.springdata.mongodb.util.AtlasContainer; +import example.springdata.mongodb.util.MongoContainers; + +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class MongoDBConfiguration { + + @Bean + @ServiceConnection(name = "mongo") + AtlasContainer atlasContainer() { + return MongoContainers.getAtlasContainer(); + } +} diff --git a/mongodb/vector-search/src/test/java/example/springdata/vector/MongoDBVectorSearchTest.java b/mongodb/vector-search/src/test/java/example/springdata/vector/MongoDBVectorSearchTest.java new file mode 100644 index 000000000..aa85261f2 --- /dev/null +++ b/mongodb/vector-search/src/test/java/example/springdata/vector/MongoDBVectorSearchTest.java @@ -0,0 +1,61 @@ +/* + * Copyright 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 + * + * http://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 example.springdata.vector; + +import static org.springframework.data.domain.ScoringFunction.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.domain.Limit; +import org.springframework.data.domain.Score; +import org.springframework.data.domain.SearchResult; +import org.springframework.data.domain.Vector; + +@SpringBootTest +class MongoDBVectorSearchTest { + + @Autowired CommentRepository repository; + + @BeforeEach + void beforeAll() throws InterruptedException { + Thread.sleep(5000); // a little time to think + } + + @Test + void vectorSearchUsingQueryMethod() { + + Vector vector = Vector.of(0.2001f, 0.32345f, 0.43456f, 0.54567f, 0.65678f); + + repository.searchTop10ByCountryAndEmbeddingNear("de", vector, Score.of(0.5, cosine())) + .forEach(MongoDBVectorSearchTest::printResult); + } + + @Test + void vectorSearchUsingRawAtQuery() { + + Vector vector = Vector.of(0.2001f, 0.32345f, 0.43456f, 0.54567f, 0.65678f); + + repository.searchAnnotated("de", vector, Score.of(0.5, cosine()), Limit.of(10)) + .forEach(MongoDBVectorSearchTest::printResult); + } + + private static void printResult(SearchResult result) { + System.out.printf("score: %s - %s\n", result.getScore(), result.getContent()); + } +}