Skip to content

Commit a8f8407

Browse files
christophstroblmp911de
authored andcommitted
Add JPA vector search example.
Using Postgres PGvector. See #707
1 parent 9a59659 commit a8f8407

File tree

12 files changed

+365
-3
lines changed

12 files changed

+365
-3
lines changed

README.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ Contains also examples running on Virtual Threads.
4646
* `security` - Example of how to integrate Spring Data JPA Repositories with Spring Security.
4747
* `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.
4848
* `vavr` - Shows the support of https://www.vavr.io[Vavr] collection types as return types for query methods.
49+
* `vector-search` - Example how to do vector search with a Spring Data JPA repository and `hibernate-vector`.
4950

5051
== Spring Data LDAP
5152

jpa/pom.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@
3030
<module>security</module>
3131
<module>showcase</module>
3232
<module>vavr</module>
33+
<module>multitenant</module>
34+
<module>graalvm-native</module>
35+
<module>vector-search</module>
3336
</modules>
3437

3538
<dependencies>

jpa/vector-search/README.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Spring Data JPA - Vector Search Example
2+
3+
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.
4+
5+
## Vector Support
6+
7+
The Spring Data `Vector` type can be used in repository query methods.
8+
Domain type properties of managed domain types are required to use a numeric array representation for embeddings.
9+
10+
```java
11+
12+
@Entity
13+
@Table(name = "jpa_comment")
14+
public class Comment {
15+
16+
@Id
17+
@GeneratedValue private Long id;
18+
19+
private String country;
20+
private String description;
21+
22+
@JdbcTypeCode(SqlTypes.VECTOR)
23+
@Array(length = 5)
24+
private float[] embedding;
25+
26+
// ...
27+
}
28+
29+
30+
public interface CommentRepository extends Repository<Comment, String> {
31+
32+
SearchResults<Comment> searchTop10ByCountryAndEmbeddingNear(String country, Vector vector, Score distance);
33+
}
34+
```
35+
36+
This example contains a test class to illustrate vector search with a Repository
37+
in `VectorAppTest`.

jpa/vector-search/pom.xml

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
5+
<modelVersion>4.0.0</modelVersion>
6+
7+
<parent>
8+
<groupId>org.springframework.data.examples</groupId>
9+
<artifactId>spring-data-jpa-examples</artifactId>
10+
<version>4.0.0-SNAPSHOT</version>
11+
</parent>
12+
13+
<artifactId>spring-data-jpa-vector-search</artifactId>
14+
15+
<dependencies>
16+
<dependency>
17+
<groupId>org.hibernate.orm</groupId>
18+
<artifactId>hibernate-vector</artifactId>
19+
<version>${hibernate.version}</version>
20+
</dependency>
21+
22+
<dependency>
23+
<groupId>org.postgresql</groupId>
24+
<artifactId>postgresql</artifactId>
25+
</dependency>
26+
27+
<dependency>
28+
<groupId>org.springframework.boot</groupId>
29+
<artifactId>spring-boot-starter-test</artifactId>
30+
<scope>test</scope>
31+
</dependency>
32+
33+
<dependency>
34+
<groupId>org.springframework.boot</groupId>
35+
<artifactId>spring-boot-testcontainers</artifactId>
36+
<scope>test</scope>
37+
</dependency>
38+
39+
<dependency>
40+
<groupId>org.testcontainers</groupId>
41+
<artifactId>testcontainers-postgresql</artifactId>
42+
</dependency>
43+
</dependencies>
44+
45+
<build>
46+
<plugins>
47+
<plugin>
48+
<groupId>org.apache.maven.plugins</groupId>
49+
<artifactId>maven-compiler-plugin</artifactId>
50+
</plugin>
51+
<plugin>
52+
<groupId>org.springframework.boot</groupId>
53+
<artifactId>spring-boot-maven-plugin</artifactId>
54+
</plugin>
55+
</plugins>
56+
</build>
57+
58+
</project>
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/*
2+
* Copyright 2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package example.springdata.vector;
17+
18+
import jakarta.persistence.Entity;
19+
import jakarta.persistence.GeneratedValue;
20+
import jakarta.persistence.Id;
21+
import jakarta.persistence.Table;
22+
import org.hibernate.annotations.Array;
23+
import org.hibernate.annotations.JdbcTypeCode;
24+
import org.hibernate.type.SqlTypes;
25+
26+
/**
27+
* Sample entity containing a {@link SqlTypes#VECTOR vector} {@link #embedding}.
28+
*/
29+
@Entity
30+
@Table(name = "jpa_comment")
31+
public class Comment {
32+
33+
@Id
34+
@GeneratedValue private Long id;
35+
36+
private String country;
37+
private String description;
38+
39+
@JdbcTypeCode(SqlTypes.VECTOR)
40+
@Array(length = 5)
41+
private float[] embedding;
42+
43+
public Comment() {
44+
}
45+
46+
public Comment(String country, String description, float[] embedding) {
47+
this.country = country;
48+
this.description = description;
49+
this.embedding = embedding;
50+
}
51+
52+
public static Comment of(Comment source) {
53+
return new Comment(source.getCountry(), source.getDescription(), source.getEmbedding());
54+
}
55+
56+
public long getId() {
57+
return id;
58+
}
59+
60+
public String getCountry() {
61+
return country;
62+
}
63+
64+
public String getDescription() {
65+
return description;
66+
}
67+
68+
public float[] getEmbedding() {
69+
return embedding;
70+
}
71+
72+
@Override
73+
public String toString() {
74+
return "%s (%s)".formatted(getDescription(), getCountry());
75+
}
76+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright 2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package example.springdata.vector;
17+
18+
import org.springframework.data.domain.Score;
19+
import org.springframework.data.domain.SearchResults;
20+
import org.springframework.data.domain.Vector;
21+
import org.springframework.data.jpa.repository.Query;
22+
import org.springframework.data.repository.CrudRepository;
23+
24+
public interface CommentRepository extends CrudRepository<Comment, String> {
25+
26+
SearchResults<Comment> searchTop10ByCountryAndEmbeddingNear(String country, Vector vector, Score distance);
27+
28+
@Query("""
29+
SELECT c, cosine_distance(c.embedding, :embedding) as distance FROM Comment c
30+
WHERE c.country = :country
31+
AND cosine_distance(c.embedding, :embedding) <= :distance
32+
ORDER BY distance asc""")
33+
SearchResults<Comment> searchAnnotated(String country, Vector embedding, Score distance);
34+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* Copyright 2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package example.springdata.vector;
17+
18+
import org.springframework.boot.CommandLineRunner;
19+
import org.springframework.boot.SpringApplication;
20+
import org.springframework.boot.autoconfigure.SpringBootApplication;
21+
import org.springframework.stereotype.Component;
22+
23+
@SpringBootApplication
24+
public class VectorApp {
25+
26+
public static void main(String[] args) {
27+
SpringApplication.run(VectorApp.class, args);
28+
}
29+
30+
@Component
31+
static class DbInitializer implements CommandLineRunner {
32+
33+
private final CommentRepository repository;
34+
35+
DbInitializer(CommentRepository repository) {
36+
this.repository = repository;
37+
}
38+
39+
@Override
40+
public void run(String... args) {
41+
42+
repository.deleteAll();
43+
44+
repository.save(new Comment("de", "comment 'one'", new float[]{0.1001f, 0.22345f, 0.33456f, 0.44567f, 0.55678f}));
45+
repository.save(new Comment("de", "comment 'two'", new float[]{0.2001f, 0.32345f, 0.43456f, 0.54567f, 0.65678f}));
46+
repository.save(new Comment("en", "comment 'three'", new float[]{0.9001f, 0.82345f, 0.73456f, 0.64567f, 0.55678f}));
47+
repository.save(new Comment("de", "comment 'four'", new float[]{0.9001f, 0.92345f, 0.93456f, 0.94567f, 0.95678f}));
48+
}
49+
}
50+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
spring.sql.init.schema-locations=pgvector.sql
2+
spring.sql.init.mode=always
3+
4+
logging.level.org=WARN
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
CREATE EXTENSION IF NOT EXISTS vector;
2+
3+
DROP TABLE IF EXISTS jpa_comment;
4+
5+
DROP SEQUENCE IF EXISTS jpa_comment_seq;
6+
7+
CREATE TABLE IF NOT EXISTS jpa_comment (id bigserial PRIMARY KEY, country varchar(10), description varchar(20), embedding vector(5));
8+
9+
CREATE SEQUENCE jpa_comment_seq INCREMENT 50;
10+
11+
CREATE INDEX ON jpa_comment USING hnsw (embedding vector_l2_ops);
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Copyright 2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package example.springdata.vector;
17+
18+
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
19+
import org.springframework.context.annotation.Bean;
20+
import org.springframework.context.annotation.Configuration;
21+
import org.testcontainers.containers.PostgreSQLContainer;
22+
import org.testcontainers.utility.DockerImageName;
23+
24+
/**
25+
* Configuration to use PGvector with Testcontainers.
26+
*/
27+
@Configuration
28+
class PGVectorConfiguration {
29+
30+
@Bean
31+
@ServiceConnection
32+
PostgreSQLContainer<?> pgVectorContainer() {
33+
return new PostgreSQLContainer<>(DockerImageName.parse("pgvector/pgvector:pg17")).withReuse(true);
34+
}
35+
}

0 commit comments

Comments
 (0)