1. Overview
With traditional databases, we typically rely on exact keyword or basic pattern matching to implement our search functionality. While sufficient for simple applications, this approach fails to understand the meaning and context behind natural language queries fully.
Vector stores address this limitation by storing data as numeric vectors that capture their meaning. Similar words are clustered together, which allows for similarity search, where the database returns relevant results even if they don’t contain the exact keywords used in the query.
Oracle Database 23ai integrates this vector store capability into its existing ecosystem, allowing us to build AI applications without needing a separate vector store. Using the same database, we can create solutions that use both traditional structured data management and vector similarity search.
In this tutorial, we’ll explore integrating the Oracle vector database with Spring AI. We’ll implement native similarity search to find semantically related content. Then, we’ll build upon this capability to implement a Retrieval-Augmented Generation (RAG) chatbot.
2. Setting up the Project
Before we dive into the implementation, we’ll need to include the necessary dependencies and configure our application correctly.
2.1. Dependencies
Let’s start by adding the necessary dependencies to our project’s pom.xml file:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-vector-store-oracle</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-advisors-vector-store</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
<version>1.0.0</version>
</dependency>
The Oracle vector store starter dependency enables us to establish a connection with the Oracle vector database and interact with it. Additionally, we import the vector store advisors dependency for our RAG implementation.
Finally, we import Spring AI’s OpenAI starter dependency, which we’ll use to interact with the chat completion and embedding models.
Given that we’re using multiple Spring AI starters in our project, let’s also include the Spring AI Bill of Materials (BOM) in our pom.xml:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>1.0.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
With this addition, we can now remove the version tag from our starter dependencies. The BOM eliminates the risk of version conflicts and ensures the Spring AI dependencies are compatible with each other.
2.2. Configuring AI Models and Vector Store Properties
To convert our text data into vectors that the Oracle vector database can store and search, we’ll need an embedding model. Additionally, for our RAG chatbot, we’ll also need a chat completion model.
For our demonstration, we’ll use the models provided by OpenAI. Let’s configure the OpenAI API key and the models in our application.yaml file:
spring:
ai:
openai:
api-key: ${OPENAI_API_KEY}
embedding:
options:
model: text-embedding-3-large
chat:
options:
model: gpt-4o
We use the ${} property placeholder to load the value of our API key from an environment variable.
Additionally, we specify text-embedding-3-large and gpt-4o as our embedding and chat completion models, respectively. On configuring these properties, Spring AI automatically creates a bean of type ChatModel, which we’ll use later in the tutorial.
Alternatively, we can use different models, as the specific AI model or provider is irrelevant for this demonstration.
Next, to store and search data in our vector database, we must first initialize its schema:
spring:
ai:
vectorstore:
oracle:
initialize-schema: true
Here, we set the spring.ai.vectorstore.oracle.initialize-schema to true.
This instructs Spring AI to create the necessary default vector store schema automatically on application startup, which is convenient for local development and testing. However, for production applications, we should define the schema manually using a database migration tool, like Flyway.
3. Populating Oracle Vector Database
With our configurations in place, let’s set up a workflow to populate our Oracle vector database with some sample data during application startup.
3.1. Fetching Quote Records From an External API
For our demonstration, we’ll use the Breaking Bad Quotes API to fetch quotes.
Let’s create a QuoteFetcher utility class for this:
class QuoteFetcher {
private static final String BASE_URL = "https://api.breakingbadquotes.xyz/v1/quotes/";
private static final int DEFAULT_COUNT = 150;
static List<Quote> fetch() {
return fetch(DEFAULT_COUNT);
}
static List<Quote> fetch(int count) {
return RestClient
.create()
.get()
.uri(URI.create(BASE_URL + count))
.retrieve()
.body(new ParameterizedTypeReference<>() {});
}
}
record Quote(String quote, String author) {
}
Using RestClient, we invoke the external API with the default count of 150 and use ParameterizedTypeReference to deserialize the API response to a list of Quote records.
3.2. Storing Documents in Vector Database
Now, to populate our Oracle vector database with quotes during application startup, we’ll create a VectorStoreInitializer class that implements the ApplicationRunner interface:
@Component
class VectorStoreInitializer implements ApplicationRunner {
private final VectorStore vectorStore;
// standard constructor
@Override
public void run(ApplicationArguments args) {
List<Document> documents = QuoteFetcher
.fetch()
.stream()
.map(quote -> {
Map<String, Object> metadata = Map.of("author", quote.author());
return new Document(quote.quote(), metadata);
})
.toList();
vectorStore.add(documents);
}
}
In our VectorStoreInitializer class, we autowire an instance of VectorStore, which Spring AI automatically creates for us.
Inside the run() method, we use our QuoteFetcher utility class to retrieve a list of Quote records. Then, we map each quote into a Document and configure the author field as metadata.
Finally, we store all the documents in our database. When we invoke the add() method, Spring AI automatically converts our plaintext content into a vector representation before storing it in the database.
4. Setting up Local Test Environment With Testcontainers
To facilitate local development and testing, we’ll use Testcontainers to set up the Oracle vector database, the prerequisite for which is an active Docker instance.
First, let’s add the necessary test dependencies to our pom.xml:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-spring-boot-testcontainers</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>oracle-free</artifactId>
<scope>test</scope>
</dependency>
We import the Spring AI Testcontainers dependency for Spring Boot and the Oracle Database module of Testcontainers.
These dependencies provide the necessary classes to spin up an ephemeral Docker instance for the Oracle vector database.
Next, let’s create a @TestConfiguration class to define our Testcontainers bean:
@TestConfiguration(proxyBeanMethods = false)
class TestcontainersConfiguration {
@Bean
@ServiceConnection
OracleContainer oracleContainer() {
return new OracleContainer("gvenzl/oracle-free:23-slim");
}
}
We specify the latest stable slim version of the Oracle database image when creating the OracleContainer bean.
Additionally, we annotate our bean method with @ServiceConnection. This dynamically registers all the datasource properties required to set up a connection with the Docker container.
Now, we can use this configuration in our integration tests by annotating our test classes with the @Import(TestcontainersConfiguration.class) annotation.
Now that we’ve set up our local testing environment and populated our Oracle vector database with Breaking Bad quotes, let’s explore how to perform similarity searches.
5.1. Basic Similarity Search
Let’s start by performing basic similarity search operations to find quotes matching various Breaking Bad themes:
private static final int MAX_RESULTS = 5;
@Autowired
private VectorStore vectorStore;
@ParameterizedTest
@ValueSource(strings = { "Sarcasm", "Regret", "Violence and Threats", "Greed, Power, and Money" })
void whenSearchingBreakingBadTheme_thenRelevantQuotesReturned(String theme) {
SearchRequest searchRequest = SearchRequest
.builder()
.query(theme)
.topK(MAX_RESULTS)
.build();
List<Document> documents = vectorStore.similaritySearch(searchRequest);
assertThat(documents)
.hasSizeGreaterThan(0)
.hasSizeLessThanOrEqualTo(MAX_RESULTS)
.allSatisfy(document -> {
assertThat(document.getText())
.isNotBlank();
assertThat(String.valueOf(document.getMetadata().get("author")))
.isNotBlank();
});
}
Here, we pass the main themes from the Breaking Bad series to our test method using @ValueSource. Then, we create a SearchRequest object with the theme as the query. Additionally, we limit results to the top five most similar quotes by passing MAX_RESULTS to the topK() method.
Next, we call the similaritySearch() method of our vectorStore bean, with our searchRequest. Similar to the add() method of the VectorStore, Spring AI converts our query to its vector representation before querying the database.
The returned documents will contain quotes that are semantically related to the given theme, even if they don’t include the exact keyword.
In addition to performing basic similarity searches, the Oracle vector database also supports filtering search results based on the saved metadata. This is useful when we need to narrow down our search and perform semantic searches within a subset of data.
Let’s again search for quotes related to a given theme, but filter them by a specific author:
@ParameterizedTest
@CsvSource({
"Walter White, Pride",
"Walter White, Control",
"Jesse Pinkman, Abuse and foul language",
"Mike Ehrmantraut, Wisdom",
"Saul Goodman, Law"
})
void whenSearchingCharacterTheme_thenRelevantQuotesReturned(String author, String theme) {
SearchRequest searchRequest = SearchRequest
.builder()
.query(theme)
.topK(MAX_RESULTS)
.filterExpression(String.format("author == '%s'", author))
.build();
List<Document> documents = vectorStore.similaritySearch(searchRequest);
assertThat(documents)
.hasSizeGreaterThan(0)
.hasSizeLessThanOrEqualTo(MAX_RESULTS)
.allSatisfy(document -> {
assertThat(document.getText())
.isNotBlank();
assertThat(String.valueOf(document.getMetadata().get("author")))
.contains(author);
});
}
Here, we use the @CsvSource annotation to find quotes using various character-theme combinations.
We build our SearchRequest as before, but this time, we use the filterExpression() method to restrict results to quotes from a specific author.
6. Building a RAG Chatbot
While native similarity search is powerful on its own, we can build upon this capability to create an intelligent, context-aware RAG chatbot.
6.1. Defining a Prompt Template
To better guide the LLM’s behaviour, we’ll define a custom prompt template. Let’s create a new prompt-template.st file in the src/main/resources directory:
You are a chatbot built for analyzing quotes from the 'Breaking Bad' television series.
Given the quotes in the CONTEXT section, answer the query in the USER_QUESTION section.
The response should follow the guidelines listed in the GUIDELINES section.
CONTEXT:
<question_answer_context>
USER_QUESTION:
<query>
GUIDELINES:
- Base your answer solely on the information found in the provided quotes.
- Provide concise, direct answers without mentioning "based on the context" or similar phrases.
- When referencing specific quotes, mention the character who said them.
- If the question cannot be answered using the context, respond with "The provided quotes do not contain information to answer this question."
- If the question is unrelated to the Breaking Bad show or the quotes provided, respond with "This question is outside the scope of the available Breaking Bad quotes."
Here, we clearly define the chatbot’s persona and provide it with a set of guidelines to follow.
In our template, we use two placeholders enclosed in angle brackets. Spring AI will automatically replace the question_answer_context and the query placeholders with the retrieved context from the vector database and the user’s question, respectively.
6.2. Configuring a ChatClient Bean
Next, let’s define a bean of type ChatClient, which acts as the main entry point for interacting with the configured chat completion model:
private static final int MAX_RESULTS = 10;
@Bean
PromptTemplate promptTemplate(
@Value("classpath:system-prompt.st") Resource promptTemplate) {
String template = promptTemplate.getContentAsString(StandardCharsets.UTF_8);
return PromptTemplate
.builder()
.renderer(StTemplateRenderer
.builder()
.startDelimiterToken('<')
.endDelimiterToken('>')
.build())
.template(template)
.build();
}
@Bean
ChatClient chatClient(
ChatModel chatModel,
VectorStore vectorStore,
PromptTemplate promptTemplate) {
return ChatClient
.builder(chatModel)
.defaultAdvisors(
QuestionAnswerAdvisor
.builder(vectorStore)
.promptTemplate(promptTemplate)
.searchRequest(SearchRequest
.builder()
.topK(MAX_RESULTS)
.build())
.build()
)
.build();
}
Here, we first retrieve the contents of our prompt template using the @Value annotation and use it to define a PromptTemplate bean. We also configure it to use the angle brackets as delimiters.
Next, we use the PromptTemplate bean, along with the ChatModel and VectorStore beans, to define our ChatClient bean. We use the defaultAdvisors() method to register a QuestionAnswerAdvisor, which is the component that implements the RAG pattern.
Additionally, within the advisor, we configure a SearchRequest to retrieve the top 10 most relevant quotes. Spring AI will inject them into the prompt template before making the call to the LLM.
Now, with the ChatClient bean configured, let’s see how we can interact with it to ask natural language questions:
@Autowired
private ChatClient chatClient;
@ParameterizedTest
@ValueSource(strings = {
"How does the show portray the mentor-student dynamic?",
"Which characters in the show portray insecurity through their quotes?",
"Does the show contain quotes with mature themes inappropriate for young viewers?"
})
void whenQuestionsRelatedToBreakingBadAsked_thenRelevantAnswerReturned(String userQuery) {
String response = chatClient
.prompt(userQuery)
.call()
.content();
assertThat(response)
.isNotBlank();
.doesNotContain(OUT_OF_SCOPE_MESSAGE, NO_INFORMATION_MESSAGE);
}
Here, when we pass the userQuery to the prompt() method, our configured QuestionAnswerAdvisor performs the RAG workflow behind the scenes. The advisor queries the Oracle vector database for quotes related to the user’s question, injects them into the prompt template, and sends the combined prompt to the configured LLM for a response.
We verify that the response is not blank and doesn’t contain the fallback messages we defined in our template.
7. Conclusion
In this article, we explored how to integrate the Oracle vector database with Spring AI.
We walked through the necessary configurations and implemented two key vector store capabilities: similarity search and RAG. Using Testcontainers, we set up the Oracle vector database, creating a local test environment.
First, we fetched quotes from the Breaking Bad quotes API to populate our vector store during application startup. Then, we implemented a similarity search on the stored data to fetch quotes matching common themes from the series.
Finally, we implemented a RAG chatbot that uses the retrieved quotes from similarity search as context to answer user queries.
The code backing this article is available on GitHub. Once you're
logged in as a Baeldung Pro Member, start learning and coding on the project.