Spring AI with Oracle Vector DB
This guide walks you through building a Retrieval-Augmented Generation (RAG) chatbot using Spring Boot, Spring AI, and Oracle Database 23ai. You will see how to ingest documents, store embeddings in Oracle’s native VECTOR
datatype, perform semantic search, and generate grounded answers using a real LLM. Instead of dummy clients, this version demonstrates actual embedding creation and LLM interaction using Ollama in Docker with Spring AI.
1. Introduction
Vector search (semantic search) stores document embeddings and retrieves nearest neighbors by vector distance rather than keywords. Modern AI apps like chatbots, document search, and recommendation engines rely on this.
Oracle Database 23ai introduces the native VECTOR
datatype and vector search operators. Combined with Spring AI, developers can use Oracle as a first-class vector database in Spring Boot applications.
1.1 Use Cases
- RAG chatbots grounded on enterprise/private data
- Semantic search across documents, FAQs, or knowledge bases
- Recommendation engines using vector similarity
- Enterprise apps combining transactional data with embeddings
2. Setting up the Project
Ensure you have a running Oracle 23ai instance with vector support. Run locally via Docker:
docker run -d --name oracle23ai -p 1521:1521 -e ORACLE_PWD=demo gvenzl/oracle-xe:23.3.0
Create a schema and table for vector storage:
CREATE USER demo IDENTIFIED BY demo; GRANT CONNECT, RESOURCE TO demo; CREATE TABLE docs ( id NUMBER GENERATED ALWAYS AS IDENTITY PRIMARY KEY, title VARCHAR2(400), content CLOB, embedding VECTOR(1536) );
2.1 Maven Dependencies
Include Spring Boot JDBC, Spring AI, and Oracle JDBC:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-core</artifactId> <version>latest__jar__version__from__maven</version> </dependency> <dependency> <groupId>com.oracle.database.jdbc</groupId> <artifactId>ojdbc11</artifactId> <version>latest__jar__version__from__maven</version> </dependency>
2.2 Main Application
This class bootstraps the Spring Boot application. It contains the main
method that launches the RAG bot using Spring’s auto-configuration and component scanning.
package com.example.ragbot; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class RagBotApplication { public static void main(String[] args) { SpringApplication.run(RagBotApplication.class, args); } }
The @SpringBootApplication
annotation enables auto-configuration and scanning of beans across the project. Running this class starts the embedded Spring Boot application.
2.3 Configure Embeddings and LLM (Using Ollama + Spring AI)
In this step, we configure Spring Boot to connect to a locally running Ollama instance, specifying both the vector embedding model and the chat LLM model. This ensures that all embedding generation and chat completions use the correct models directly from Spring-managed beans.
docker run -d --name ollama -p 11434:11434 ollama/ollama:latest
Once the container is running, you also need to pull the specific models you plan to use. For this guide, we’ll use nomic-embed-text
for embeddings and llama2
for chat:
# Enter the running container docker exec -it ollama bash # Pull the embedding model ollama pull nomic-embed-text # Pull the chat model ollama pull llama2
Now Ollama has both required models locally and can serve requests from Spring AI. Then define Spring beans for real embedding generation and LLM interaction:
package com.example.ragbot.config; import org.springframework.ai.client.OllamaAiClient; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class AIConfig { @Bean public OllamaAiClient springAiClient() { return OllamaAiClient.builder() .baseUrl("https://localhost:11434") .embeddingModel("nomic-embed-text") // embedding model .chatModel("llama2") // LLM for chat .build(); } @Bean public EmbeddingClient embeddingClient(OllamaAiClient ai) { return new EmbeddingClient(ai); } @Bean public LlmClient llmClient(OllamaAiClient ai) { return new LlmClient(ai); } public static class EmbeddingClient { private final OllamaAiClient ai; public EmbeddingClient(OllamaAiClient ai) { this.ai = ai; } public float[] embedText(String text) { return ai.embed(text, 1536); } } public static class LlmClient { private final OllamaAiClient ai; public LlmClient(OllamaAiClient ai) { this.ai = ai; } public String complete(String prompt) { return ai.complete(prompt); } } }
This Spring configuration class defines how the application connects to a locally running Ollama instance for both embeddings and language model completions; the springAiClient
bean creates an OllamaAiClient
pointing to https://localhost:11434
with nomic-embed-text
set as the embedding model and llama2
as the chat model, while two helper beans, EmbeddingClient
and LlmClient
, wrap this client to expose simpler methods: embedText(String text)
generates a vector representation of text using the embedding model, and complete(String prompt)
sends a prompt to the LLM for natural language completion, making the AI functionality modular, reusable, and easily injectable throughout the Spring Boot application.
2.4 Data Ingestion
This service is responsible for converting raw text into embeddings and inserting them into the Oracle Vector Database table docs
. It uses Spring’s JdbcTemplate
for persistence and the EmbeddingClient
for generating vector representations.
package com.example.ragbot.service; import com.example.ragbot.config.AppConfig.EmbeddingClient; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; @Service public class DataIngestionService { private final JdbcTemplate jdbc; private final EmbeddingClient embeddingClient; public DataIngestionService(JdbcTemplate jdbc, EmbeddingClient embeddingClient) { this.jdbc = jdbc; this.embeddingClient = embeddingClient; } public void ingest(String title, String content) { float[] emb = embeddingClient.embedText(content); String sql = "INSERT INTO docs (title, content, embedding) VALUES (?, ?, ?)"; jdbc.update(sql, ps - > { ps.setString(1, title); ps.setString(2, content); ps.setObject(3, emb); }); } }
In this class, ingest()
first generates an embedding vector from the given content and then executes an INSERT
into the Oracle docs
table. The key part is persisting the VECTOR
type. With Oracle’s JDBC driver and 23ai, the embedding can be stored directly in the vector column.
2.5 Semantic Search
This service performs a similarity search by embedding the user’s query and retrieving the top-k closest documents from the Oracle Vector Database. It leverages Oracle’s VECTOR_DISTANCE
function to order results by similarity.
package com.example.ragbot.service; import com.example.ragbot.config.AppConfig.EmbeddingClient; import com.example.ragbot.model.DocResult; import java.util.List; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; @Service public class SemanticSearchService { private final JdbcTemplate jdbc; private final EmbeddingClient embeddingClient; public SemanticSearchService(JdbcTemplate jdbc, EmbeddingClient embeddingClient) { this.jdbc = jdbc; this.embeddingClient = embeddingClient; } public List<DocResult> search(String query, int k) { float[] emb = embeddingClient.embedText(query); String sql = "SELECT id, title, content, VECTOR_DISTANCE(embedding, ?) AS score " + "FROM docs ORDER BY score ASC FETCH FIRST ? ROWS ONLY"; return jdbc.query( sql, ps -> { ps.setObject(1, emb); ps.setInt(2, k); }, (rs, i) -> new DocResult(rs.getLong("id"), rs.getString("title"), rs.getString("content"), rs.getDouble("score"))); } }
Here, the search()
method converts the query text into an embedding vector, then uses Oracle’s VECTOR_DISTANCE
function to calculate similarity between the stored embeddings and the query. Results are ordered by ascending distance (i.e., most similar first) and mapped into DocResult
objects. The parameter k
controls how many documents to return.
2.5.1 DocResult Class
The DocResult
class defines a simple immutable model for storing document search results.
package com.example.ragbot.model; public class DocResult { private final long id; private final String title; private final String content; private final double score; public DocResult(long id, String title, String content, double score) { this.id = id; this.title = title; this.content = content; this.score = score; } public long getId() { return id; } public String getTitle() { return title; } public String getContent() { return content; } public double getScore() { return score; } }
The DocResult
class is a simple immutable data model that represents the result of a document search, containing four fields: a unique identifier (id
), the document’s title (title
), the document’s text content (content
), and a similarity score (score
) that indicates how relevant the document is to a query. These values are initialized through the constructor and exposed only via getter methods, without setters, which makes the class immutable, thread-safe, and well-suited for use in semantic search or retrieval-augmented generation workflows where consistent, lightweight result objects are required.
2.6 Chat Service
This service brings everything together. It takes a user’s question, retrieves the most relevant documents from Oracle Vector Database using semantic search, builds a context, and then asks the LLM to generate a grounded answer.
package com.example.ragbot.service; import com.example.ragbot.config.AppConfig.LlmClient; import com.example.ragbot.model.DocResult; import java.util.List; import org.springframework.stereotype.Service; @Service public class RagChatService { private final SemanticSearchService searchService; private final LlmClient llmClient; public RagChatService( SemanticSearchService searchService, LlmClient llmClient) { this.searchService = searchService; this.llmClient = llmClient; } public String answer(String question) { List<DocResult> docs = searchService.search(question, 3); String context = docs.stream() .map(d -> "--- " + d.getTitle() + " ---\n" + d.getContent()) .collect(Collectors.joining("\n")); return llmClient.complete( "Use the following docs:\n" + context + "\nQ: " + question + "\nA:"); } }
In this class, the answer()
method retrieves the top three relevant documents from the semantic search service, constructs a context string from them, and feeds both the context and the original user question into the LlmClient
. The result is a synthesized answer grounded in the retrieved documents.
2.7 Create an Ingestion Controller Class
This controller exposes a REST endpoint for document ingestion. By sending a JSON payload with a title and content, you can insert new documents into Oracle Vector Database through the DataIngestionService
.
package com.example.ragbot.controller; import com.example.ragbot.service.DataIngestionService; import org.springframework.web.bind.annotation.*; import java.util.Map; @RestController @RequestMapping("/api/ingest") public class IngestController { private final DataIngestionService ingestionService; public IngestController(DataIngestionService ingestionService) { this.ingestionService = ingestionService; } @PostMapping public String ingest(@RequestBody Map<String,String> doc) { ingestionService.ingest(doc.get("title"), doc.get("content")); return "Document ingested: " + doc.get("title"); } }
The /api/ingest
endpoint accepts a POST request with a JSON body such as {"title": "Doc1", "content": "Some text"}
. It passes this data to the ingestion service, which creates an embedding and stores the document in Oracle. The response confirms successful ingestion.
2.8 Create a Rag Controller Class
This controller provides the REST endpoint to interact with the chatbot. It accepts a user’s question in the request body, forwards it to the RagChatService
, and returns the generated answer as JSON.
package com.example.ragbot.controller; import com.example.ragbot.service.RagChatService; import org.springframework.web.bind.annotation.*; import java.util.Collections; import java.util.Map; @RestController @RequestMapping("/api/rag") public class RagController { private final RagChatService ragChatService; public RagController(RagChatService ragChatService) { this.ragChatService = ragChatService; } @PostMapping("/ask") public Map<String,String> ask(@RequestBody Map<String,String> payload) { String answer = ragChatService.answer(payload.get("question")); return Collections.singletonMap("answer", answer); } }
When you send a POST
request to /api/rag/ask
with a JSON body like {"question": "What is Oracle Vector Database?"}
, the controller delegates to the chat service, which runs the full RAG pipeline and returns a JSON response containing the answer.
2.9 Add Application Properties
These properties configure the Spring Boot application, including the Oracle database connection, JPA settings, and logging levels. Update the connection details to match your environment.
spring.application.name=rag-bot spring.datasource.url=jdbc:oracle:thin:@//localhost:1521/XEPDB1 spring.datasource.username=demo spring.datasource.password=demo spring.datasource.driver-class-name=oracle.jdbc.OracleDriver spring.jpa.hibernate.ddl-auto=update spring.jpa.show-sql=true logging.level.root=INFO logging.level.org.springframework.jdbc.core.JdbcTemplate=DEBUG
The spring.datasource
properties define the JDBC connection to your Oracle 23ai instance. JPA is configured with ddl-auto=update
so schema changes are applied automatically. Logging levels are tuned to display executed SQL statements for debugging database interactions.
2.10 Code Run and Demo
Once all components are ready, you can run the RAG bot like any other Spring Boot application.
// Run the application from your IDE (IntelliJ/Eclipse/VS Code) // or by executing the Maven/Gradle command: // With Maven: mvn spring-boot:run
After the application starts, you can test the endpoints using curl
or Postman:
# 1. Ingest a document into Oracle Vector DB curl -X POST https://localhost:8080/api/ingest \ -H "Content-Type: application/json" \ -d '{"title":"Oracle RAG Intro","content":"Oracle 23ai provides vector database features for semantic search and retrieval augmented generation."}' # Response: # "Document ingested: Oracle RAG Intro" # 2. Ask a question to the RAG bot curl -X POST https://localhost:8080/api/rag/ask \ -H "Content-Type: application/json" \ -d '{"question":"What does Oracle 23ai provide for RAG?"}' # Response: # {"answer":"Oracle 23ai provides vector database features that enable semantic search and power RAG pipelines."}
At this point, you have a fully functional Spring Boot RAG bot running locally. The ingestion endpoint stores documents into Oracle Vector Database, and the /ask
endpoint uses semantic search + LLM completion to provide contextual answers.
3. Conclusion
Integrating Oracle Vector Database (23ai) with Spring AI provides a powerful way to combine enterprise data, vector search, and LLMs to build RAG-based apps without moving data out of your database. Oracle 23ai offers native VECTOR datatypes and vector search primitives, and when possible, you can use DBMS_VECTOR for in-database embedding generation. Spring AI exposes an Oracle VectorStore, allowing Spring applications to store and retrieve embeddings easily while focusing on orchestration instead of SQL details.