Semantic search with Spring AI | OpenAI
Demonstrating semantic search with Spring AI using pgVector
12th Jan 2025
Overview
In this tutorial, I'm going to show you how to use the power of Spring AI to perform semantic search using pgvector, an extension that allows Postgres to act as a vector database. So that we can store embeddings (this is key, so keep it in mind).
Things you will need:
- OpenAI key (make sure it has enough credits)
- Docker (because we will be using
ankane/pgvector
) - Java 17+
What is semantic search
Semantic search allows us to search content based on meaning rather than exact match. For instance "people running" and "people jogging" are very much alike. It has many applications, ranging from:
- Allowing news sites to show you related content as articles may convey similar information
- Allowing tech support applications to allow users to find articles that are similar to the issue they're posting about
Where might you have seen semantic search? If you've ever posted on StackOverflow you may have noticed that it shows you similar articles to yours before you post. Allowing you to make the decision if it's worth posting or rather seeking help from those posts that are already similar to yours.
How does Spring AI help us with this?
Spring AI makes it easy to work with AI models from OpenAI and many other vendors. In this article, our focus is on using Spring AI with OpenAI.
Spring AI helps us with semantic search by converting our documents to embeddings using OpenAI and then storing them in pgVector so we can later do similarity search on them. So in other words, it does the heavy lifting for us.
To further understand how this works under the hood here's Claude's take:
In a Spring AI application using pgvector, when you want to find documents related to your query, the first step is converting both your query and all stored documents into high-dimensional vectors using language models. These vectors capture the semantic meaning of the text, where similar concepts end up close to each other in the vector space. Cosine similarity is then used to measure how related these vectors are by calculating the angle between them - the smaller the angle, the more related the texts are. To make searching through these vectors efficient, pgvector uses HNSW (Hierarchical Navigable Small World) indexing. Instead of comparing your query vector to every single document vector, which would be slow, HNSW creates a layered graph structure where each vector is connected to its most similar neighbors. When searching, it starts at the top layer and quickly traverses this graph, using cosine similarity at each step to navigate toward the most promising matches, effectively finding the most relevant documents in logarithmic rather than linear time.
Demo with Spring AI
Visit the repo so you can download this to play along with it. Alright, enough talk. Here's the code:
@SpringBootApplication
public class PgvectorSemanticSearchApplication {
public static void main(String[] args) {
SpringApplication.run(PgvectorSemanticSearchApplication.class, args);
}
@Bean
public CommandLineRunner runner(VectorStore vectorStore) {
return args -> {
// Create some documents for PGVector
List<Document> documents = List.of(
new Document("Kids playing soccer with a white ball", Map.of("meta1", "meta1")),
new Document("A sport jersey"),
new Document("A kid that appears to be playing soccer"),
new Document("Chicken is a good food", Map.of("meta2", "meta2")));
// Add the documents to PGVector
vectorStore.add(documents);
// Retrieve documents similar to a query
List<Document> results = vectorStore.similaritySearch(SearchRequest.query("child playing soccer").withTopK(1));
results.stream().distinct().forEach(System.out::println);
};
}
}
So because I didn't want to build a full-fledged web application to demonstrate this, I built just a console application using
CommandLineRunner
. You can extend all this and incorporate it into an application that can benefit from semantic search.
For instance, there's a version of Document
above that allows you to pass along an id
, so maybe you can use that to associate them with some of your database entities.
public Document(String id, String content, Map<String, Object> metadata) {
this(id, content, List.of(), metadata);
To run this make sure docker is running as Spring Docker Compose support will automatically start up an instance of pgVector.
export OPENAI_API_KEY="yourKey"
./mvnw spring-boot:run
After running, you should see this in the output:
Document{id='9f7d9e63-93af-4d9c-8e4a-1775a88b80ad', metadata={distance=0.054281462}, content='A kid that appears to be playing soccer', media=[]}
So what happened here is that our vector store sees that the query "child playing soccer"
is very similar to the document we added:
new Document("A kid that appears to be playing soccer"),
That's it folks!
Summary
- Semantic search allows you to do searches that go beyond mere title matches and rather match based on meaning
- Spring AI via pgVector simplifies the process of creating embeddings and allowing us to search them
- To run this demo, you'll need docker running
- To make searching efficient, pgVector indexes using HNSW (Hierarchical Navigable Small World) and cosine similarity when performing searches