Building RAG Search Service Design with Spring AI
The system architecture uses a layered, modular design with these core components:
Vector Store: Qdrant serves as the vector database, persisting semantic vector representations of documents for similarity-based retrieval. Spring AI provides an abstract VectorStore interface that encapsulates database operations. By integrating Spring AI's Qdrant module, we can use Qdrant as the VectorStore implementation and execute efficient similarity queries within the application.
Full-text Search Engine: Elasticsearch stores document text or keyword indexes to support boolean and full-text keyword retrieval. Elasticsearch's inverted index enables effective matching of keywords in user queries.
Hybrid Retriever: The retrieval layer component that combines keyword search and vector semantic search results. During queries, we execute keyword matching against Elasticsearch and vector similarity search against Qdrant simultaneously, obtaining document lists from both sources and merging the results. Hybrid retrieval leverages both keyword precision matching and semantic similarity, improving recall and accuracy. Spring AI provides the DocumentRetriever interface for abstract document retrieval and the VectorStoreDocumentRetriever implementation. Our approach implements a custom HybridRetriever that internally orchestrates both Elasticsearch and Qdrant retrievals, then uses a document merger to combine results (via simple deduplication or score-based fusion). For example, Spring AI's ConcatenationDocumentJoiner can merge result sets from multiple sources into a single document list.
RAG Service Layer: Encapsulates the core "retrieve-then-generate" workflow in a service (typically called RagService). It receives user queries, calls the Hybrid Retriever to fetch relevant documents, submits retrieved documents as context to the LLM, and returns the generated answer. The RAG service interacts with the underlying LLM via Spring AI's ChatClient. ChatClient abstracts the common interface for interacting with AI models, supporting both synchronous and streaming prompt submission and response retrieval. Spring AI's modular design lets us easily swap the underlying LLM or vector database without major business code changes.
LLM Inference Layer: The large language model generates answers based on user queries and retrieved document context. Through Spring AI's ChatModel and ChatClient abstractions, we can seamlessly switch between different models and vendors. For example, we can configure ChatClient to call OpenAI's GPT when using OpenAI, or leverage Ollama for local LLM inference. ChatClient abstracts away implementation differences and provides a unified API for sending prompts and receiving model responses.
Embedding Model: Converts text into vector representations. The system supports two approaches:
- Local HuggingFace Model: Through Spring AI's Transformers ONNX support, we load locally pre-trained Transformer models (such as all-MiniLM-L6-v2) to generate text embeddings. This ensures data stays local, offers fast response times, and requires no external API calls.
- OpenAI Embedding API: Uses OpenAI's embedding endpoint (e.g., text-embedding-ada-002) to obtain vector representations, requiring network calls and API credentials. Spring AI provides the ready-to-use OpenAiEmbeddingModel implementation; we only need to supply the OpenAI API key.
These components integrate through Spring Boot configuration and organization, ultimately providing a RESTful API service that includes document ingestion and question-answering query endpoints. The diagram below illustrates how system components interact:
(Architecture overview: Users submit requests through the Controller. RAG Service calls Hybrid Retriever, which queries both Elasticsearch and Qdrant separately to fetch documents. The embedding model generates vectors for Qdrant storage and retrieval. ChatClient wraps LLM model invocation (local or cloud). Retrieved relevant documents serve as prompt context, which the LLM uses to generate an answer returned to the user.)
Core Module Design
Vector Storage and Hybrid Retrieval Module
Qdrant Vector Store Configuration: The system uses Qdrant to persist document vectors and metadata. By adding the spring-ai-qdrant-store dependency, Spring AI automatically configures Qdrant-related beans. We need to provide the Qdrant client and VectorStore bean, for example:
@Bean
public QdrantClient qdrantClient() {
// 构建连接到 Qdrant 的 gRPC 客户端
QdrantGrpcClient.Builder grpcClientBuilder =
QdrantGrpcClient.newBuilder("qdrant-host", 6334, false);
grpcClientBuilder.withApiKey("<QDRANT_API_KEY>");
return new QdrantClient(grpcClientBuilder.build());
}
@Bean
public VectorStore vectorStore(QdrantClient client, EmbeddingModel embeddingModel) {
return QdrantVectorStore.builder(client, embeddingModel)
.collectionName("documents") // 指定集合名称
.initializeSchema(true) // 若未预建集合则自动创建
.build();
}
This configuration creates a Qdrant implementation of the VectorStore interface for storing vectors and performing similarity searches. The injected EmbeddingModel is used to generate vector representations when storing documents. Note: you can create collections in Qdrant beforehand, specifying vector dimensions and similarity metrics, or let the program auto-create them via initializeSchema(true) (by default using Cosine distance, with dimensions depending on the embedding model). Ensure the Qdrant instance is running and accessible, for example by launching a Qdrant container via Docker.
Elasticsearch Full-text Search Configuration: Elasticsearch stores text indexes for documents, enabling keyword-based retrieval. Integration can use Spring Data Elasticsearch or the REST API. Configuration requires the Elasticsearch address (e.g., localhost:9200), index name (e.g., documents), and index mapping (containing fields like document ID and content). For simplicity, leverage Spring Data Elasticsearch by defining a document entity and corresponding Repository. For example, define DocumentEntity(id, content, metadata) and establish full-text indexes.
Document Index Structure: To facilitate merging hybrid retrieval results, we assign each document segment a unique ID and share that ID across Qdrant and Elasticsearch as the primary key. When documents enter the system, they typically split into multiple chunks—each chunk is a text segment suited to retrieval and context length. We index each segment as an independent Document. During retrieval, whether we find a segment via vector or keyword search, we can identify its content by ID. Store document content in the Elasticsearch index, while Qdrant primarily stores vectors and essential metadata (such as document ID and source) as payload.
Hybrid Retriever Implementation: Create a hybrid retriever, for example HybridRetriever implements DocumentRetriever. Its retrieve(Query query) method performs these steps:
Elasticsearch keyword query: Construct a match or bool query to search the content field for the query text, returning the top K results. Implement this using Elasticsearch's REST client or Repository methods, yielding a set of Document segments (including ID and content).
Qdrant similarity query: Call the configured vectorStore.similaritySearch() method to perform vector search on the user query, retrieving the top K most similar document segments. Spring AI provides SearchRequest to specify similarity thresholds and topK parameters. For example:
List<Document> vectorDocs = vectorStore.similaritySearch(
SearchRequest.builder()
.query(userQuery)
.topK(5)
.build()
);
This returns the 5 document segments most similar to the query.
Merge results: Combine keyword and vector results. Use document IDs to eliminate duplicates and avoid repeating the same segment. A simple strategy is direct concatenation; for fine-grained fusion, rescore or reweight results based on Elasticsearch relevance scores and vector similarity scores. Since scores from different retrievals are not directly comparable, a practical engineering approach is to retain the top K results from each, letting the upstream LLM judge relevance among candidate segments. Here we use a simple deduplication merge and limit total segments (e.g., no more than 6–10) to control prompt length.
Output: Return the merged Document list. Each Document contains content text and optional metadata (e.g., source identifiers for citation).
Through HybridRetriever, we combine semantic and exact matching to maximize finding relevant content. For instance, when user queries employ different phrasing than document terminology, vector retrieval finds semantically similar content while keyword retrieval ensures exact matching terms are not missed.
LLM Inference and Switching Module
ChatClient and ChatModel Abstraction: Spring AI provides the ChatModel interface representing concrete large language models (such as OpenAI ChatGPT, Anthropic Claude, or local Llama), and ChatClient encapsulates a unified client for interacting with these models. This service injects a ChatClient instance to invoke the LLM for answer generation. Thanks to ChatClient's abstract design, we can flexibly swap the underlying ChatModel without changing business code. For example, we could switch from OpenAI's GPT-4 to a local Llama2 model through configuration changes.
1. OpenAI Model Integration: When using OpenAI's cloud service, add the spring-ai-openai dependency and provide OpenAI's API key and chosen model name in application configuration. For example, in application.yml:
spring:
ai:
openai:
api-key: YOUR_OPENAI_API_KEY
chat:
model: gpt-4
This enables Spring AI's automatic configuration for OpenAI Chat API. The injected ChatClient will default to the configured OpenAI model. Through ChatClient's fluent API, we can conveniently send conversation messages and receive replies. For example:
ChatResponse response = chatClient.prompt()
.user("请解释RAG的作用")
.call()
.chatResponse();
String answer = response.content();
This call sends the user message to the configured OpenAI model and retrieves an answer.
2. Local Ollama Model Integration: When adopting local LLM models, install and run the Ollama service. Ollama manages local model inference and interacts with the application via REST endpoints. After adding the spring-ai-ollama dependency, Spring AI provides OllamaChatModel and OllamaApi to connect to a local Ollama instance. Configuration-wise, specify the Ollama service address and model in application.yml, for example:
spring:
ai:
ollama:
base-url: http://localhost:11434
default-model: llama2
Assuming you've downloaded a model named "llama2" via "ollama pull", Ollama will provide inference using that model. When the application starts, it automatically creates an API client connecting to Ollama (OllamaApi) and the corresponding ChatModel implementation. ChatClient can then use the local model for conversation.
When using Ollama, the call pattern resembles OpenAI, except the local service produces the result. For example:
ChatResponse response = chatClient.prompt()
.system("你是资深Java助理") // 可以设定初始系统提示
.user("给出RAG搜索服务的优势")
.call()
.chatResponse();
ChatClient sends the request to the local Ollama and retrieves the model's reply.
3. Inference Mode Switching: To toggle between local and cloud models, use Spring Profiles or configuration switches. For instance, define an llm.mode configuration property (values: local or openai). Through conditional configuration, inject the Ollama ChatClient bean in local mode and the OpenAI ChatClient bean in openai mode. Spring AI's support for different ChatModels is out-of-the-box; developers need only change configuration to seamlessly switch LLM providers, greatly reducing the code changes required for switching.
Text Embedding Generation Module
1. Local HuggingFace Embeddings: To ensure data security and reduce external dependencies, our approach supports local embedding models. Using Spring AI's Transformer ONNX support, we can directly load HuggingFace embedding models (converted to ONNX format) in Java and generate vectors. For example, choose the sentence-transformers/all-MiniLM-L6-v2 model, which outputs 768-dimensional sentence vectors. First, prepare the ONNX model file and tokenizer file (Spring AI can automatically download and cache these). Then configure the EmbeddingModel bean:
@Bean
public EmbeddingModel embeddingModel() {
TransformersEmbeddingModel model = new TransformersEmbeddingModel();
// 可选:指定模型和分词器资源路径或URL(否则使用默认的all-MiniLM-L6-v2)
model.setModelResource("classpath:/onnx/all-MiniLM-L6-v2/model.onnx");
model.setTokenizerResource("classpath:/onnx/all-MiniLM-L6-v2/tokenizer.json");
return model;
}
This TransformersEmbeddingModel loads the local ONNX model. When embed() is called, it outputs corresponding vector representations for input text lists. As a Spring bean, Spring AI automatically calls its afterPropertiesSet() for initialization. On first use, model files may be loaded or downloaded and cached from the specified location. Subsequently, call embeddingModel.embed(List
If using Ollama as the local engine, an alternative is leveraging Ollama's Embedding API. Spring AI provides OllamaEmbeddingModel wrapping Ollama's vector generation interface. Pull the corresponding embedding model (Ollama supports directly pulling HuggingFace embedding models, e.g., ollama pull hf.co/intfloat/e5-small-v2), then:
@Bean
public EmbeddingModel embeddingModel(OllamaApi ollamaApi) {
// 假设使用一个名为'e5-small-v2'的embedding模型
var options = OllamaOptions.builder().model("e5-small-v2").build();
return new OllamaEmbeddingModel(ollamaApi, options);
}
With this approach, all text vectors are generated by the local Ollama service, equivalent to directly using the HuggingFace model.
2. OpenAI Embedding API: To quickly obtain embeddings using OpenAI's pre-trained models, use OpenAI's embedding endpoint (e.g., the text-embedding-ada-002 model). Spring AI's OpenAiEmbeddingModel wraps the invocation logic; provide the API key and optionally the model name:
@Bean
public EmbeddingModel embeddingModel() {
return new OpenAiEmbeddingModel(new OpenAiApi(System.getenv("OPENAI_API_KEY")));
}
By default, this uses the Ada model to generate 1536-dimensional embedding vectors. This approach suits leveraging OpenAI's powerful vector quality but requires considering network latency and API costs.
3. Embedding Mode Switching: Similar to LLM selection, switch embedding model sources via configuration. For instance, set embedding.mode to local or openai. When set to local, enable TransformersEmbeddingModel or OllamaEmbeddingModel; when openai, use OpenAiEmbeddingModel. Provide a default implementation during development and allow modification via configuration files. For example, default to local models, but if the user provides an OpenAI API key, automatically switch to OpenAI embedding service.
Regardless of implementation, our VectorStore binds an EmbeddingModel during initialization, transforming text during vector database storage and retrieval. For instance, the configured embeddingModel injects into QdrantVectorStore; when vectorStore.add(documents) is called, each Document's text content is converted to vectors by EmbeddingModel and stored in Qdrant.
Data Flow Explanation
Document Indexing Process (ETL Offline Ingestion)
Document Acquisition and Preprocessing: Through an administration interface or batch processing task, obtain raw documents for inclusion in the knowledge base. Documents can be plain text, PDF, Markdown, etc. Use Spring AI's document reader to convert documents into text content, then chunk the text by paragraphs or fixed length to generate a segment list. Encapsulate each segment as a Spring AI Document object containing content text and metadata (such as document ID, title, source).
Embedding Vector Generation: For each document segment, call EmbeddingModel to generate its semantic vector representation. For example, use a local model to convert each segment into a 768-dimensional vector. Vectors typically reside in the Document object's embedding field or are computed during vector database insertion. This illustrates the conversion from document to vector: first partition documents into small chunks, then map each chunk to a high-dimensional vector representation using the embedding model, capturing semantic information.
Vector Storage Ingestion: Call vectorStore.add(documents) to batch-insert document segments into the Qdrant vector database. Spring AI's VectorStore interface performs insertion against the underlying database and automatically handles vector data and metadata storage. For Qdrant, each Document's embedding vector and supplementary payload are stored in the designated collection. If the collection doesn't exist and initializeSchema=true, the VectorStore implementation automatically creates the collection (with appropriate dimensions and index parameters). After successful insertion, the document's semantic representation is persisted in Qdrant for similarity retrieval.
Full-text Index Ingestion: Index document segments into Elasticsearch. For each Document, extract necessary fields (id, content, metadata) and invoke Elasticsearch's indexing API. If using Spring Data Elasticsearch, call Repository's save() method to batch-save segment entities. If using Elasticsearch's REST API directly, construct bulk indexing requests. Each segment becomes a document record in Elasticsearch, with the content field processed for full-text indexing. Consider applying Elasticsearch's built-in Chinese tokenization (if content is in Chinese) to improve retrieval effectiveness.
Indexing Complete: After the above process, knowledge base documents are effectively stored in a hybrid form: Qdrant holds each segment's vector plus metadata, Elasticsearch holds the segment's searchable text. Subsequent queries can leverage both storage capabilities.
Note: The document ingestion process can run as offline batch processing or via an API for dynamic document uploads. This design can implement a /documents/load endpoint (or use the /load concept from Stackademic articles) accepting file upload requests. The service receives files and executes the above steps to complete indexing, returning status or results to the client. For brevity, endpoint details are not elaborated here.
Online Retrieval and QA Flow
User Query Input: Users submit a query request to the RAG service via frontend or API (e.g., HTTP GET/POST /ask?question=...). The query is a natural language question, such as "How does this system support local models?". The Controller layer receives the request, wraps the query string as a Query object, and passes it to the service layer.
Hybrid Retrieval: The RAG service calls HybridRetriever to retrieve relevant information from the knowledge base:
- Elasticsearch keyword search: Query Elasticsearch using match or multi_match against the content field to search for keywords in the user's question. If matches exist, return the top N highest-relevance segments, e.g., N=5.
- Qdrant vector search: Encode the user question string to a vector via EmbeddingModel, call vectorStore.similaritySearch() to obtain the top M most similar segments, e.g., M=5. Optionally set a similarity threshold to ignore results below it.
- Result fusion: Merge the segment sets from both steps. Suppose we obtain up to 10 relevant segments. We can simply sort these segments by importance (e.g., placing Elasticsearch results first if they contain keywords) or pass them unsorted to the LLM. In this design, we pass segment text directly as context, so order has minimal impact, but placing more relevant segments first can improve answer quality.
Prompt Construction: Assemble the user question and retrieved document segments into prompt content for the LLM. Typically use a predefined prompt template, for example:
请根据以下文档内容回答问题。如果无法从中找到答案,请回复“未找到相关信息”。
文档内容:
{{{context}}}
问题: {{{question}}}
答案:
The {{{context}}} placeholder is filled by concatenating retrieved document segment texts; {{{question}}} is the original user question. The resulting complete prompt provides reference information for the question. Spring AI's PromptTemplate can assist this step, or manually concatenate strings. This shows using PromptTemplate to insert retrieved content during query and render the final prompt before sending to the model.
Spring AI also supports using QuestionAnswerAdvisor directly to simplify the process. If using ChatClient.prompt().advisors(new QuestionAnswerAdvisor(vectorStore)), it automatically queries the vector store before calling the model and attaches results. Since we've implemented a custom HybridRetriever (including Elasticsearch), we construct the prompt ourselves to incorporate both retrieval results.
- LLM Answer Generation: Call the LLM model to generate an answer. Execute via the previously configured ChatClient:
String promptText = promptTemplate.render(Map.of("context", combinedDocsText, "question", userQuestion));
ChatResponse response = chatClient.prompt(promptText).call().chatResponse();
String answer = response.content();
ChatClient sends our complete prompt to the underlying ChatModel (OpenAI or Ollama). The model, receiving the query with reference content, formulates an answer based on the provided context, reducing hallucination and error probability. The generated answer returns via ChatResponse; extract its content string as the final answer.
Result Return: Wrap the LLM's answer in a response and return it to the caller via Controller. You can return only the answer text or include metadata such as a list of source documents cited. This design focuses on core functionality and assumes only answer text is returned. On the frontend interface, users see the answer generated based on their question.
Conversation Memory (Optional): For multi-turn conversation support, combine Spring AI's ChatMemory functionality, providing previous questions and answers as conversation history to ChatClient, enabling context recall. However, basic RAG QA scenarios can omit conversation history, treating each query independently.
The entire query flow, from the user's perspective, is simply providing a question and receiving an answer. Behind the scenes, dual-channel retrieval (keyword and semantic) occurs, then the large model synthesizes the documents to formulate a result, realizing "knowledge-base-grounded QA". This RAG technique effectively mitigates LLM context length limits, knowledge cutoff, and hallucination issues.
Key Components and Interface Definitions
Configuration and Component Summary
Main Dependencies: The project includes these critical dependencies:
- Spring Boot 3.x (core framework)
- Spring AI core (spring-ai-bom BOM and required starters)
- Spring AI OpenAI Starter (if using OpenAI API)
- Spring AI Ollama Starter (if using local Ollama)
- Spring AI Qdrant Vector Store Starter (spring-ai-qdrant-store-spring-boot-starter)
- Spring AI ONNX Embedding (if using local Transformer models; optional spring-ai-embeddings-transformer-onnx)
- Spring Data Elasticsearch or Elasticsearch Java client
- Qdrant Java SDK or Spring AI's QdrantClient support
- Optionally: Spring AI document readers for PDF/text parsing if needed
Application Configuration: Manage the above mode-switching parameters via application.yml, for example:
app:
llm: mode: local # 本地(local)或openai
embedding: mode: local # 本地(local)或openai
spring:
ai:
# OpenAI 配置(仅当使用openai模式)
openai:
api-key: xxxx
chat.model: gpt-4
# Ollama 配置(仅当使用local模式)
ollama:
base-url: http://ollama:11434 # 假设Docker容器内服务名
default-model: llama2-7b
# Qdrant 向量库配置
vectorstore:
qdrant:
collection-name: documents
initialize-schema: true
This example shows custom app configuration segments controlling modes and Spring AI's built-in configuration properties. Through profiles or conditional injection, select different ChatModel configurations based on app.llm.mode.
Key Beans: A summary of important Spring beans involved in this design and their configuration methods:
- EmbeddingModel Bean: As described above, can be OpenAiEmbeddingModel or TransformersEmbeddingModel, responsible for text embedding.
- QdrantClient Bean: Encapsulates the connection to the Qdrant service using gRPC or REST client.
- VectorStore Bean: For example, QdrantVectorStore, requiring injection of QdrantClient and EmbeddingModel during construction.
- ChatClient Bean: Typically generated via Spring AI's auto-configured ChatClient.Builder. You can inject ChatClient directly or its Builder. When using multiple ChatModels, you can also inject multiple named ChatClients, for example:
@Bean
@ConditionalOnProperty(name="app.llm.mode", havingValue="openai")
ChatClient chatClientOpenAI(OpenAiChatModel model) {
return ChatClient.builder(model).build();
}
@Bean
@ConditionalOnProperty(name="app.llm.mode", havingValue="local")
ChatClient chatClientOllama(OllamaChatModel model) {
return ChatClient.builder(model).build();
}
The pseudo-code above illustrates selecting different ChatModels based on configuration to create ChatClient. In practice, using Spring Boot Starter and configuration files, most cases simply @Autowired ChatClient (the underlying configuration has already selected the model).
- HybridRetriever Bean: If implementing a HybridRetriever class, declare it as a bean, @Autowired the necessary Elasticsearch client and VectorStore internally, for executing retrieval logic.
- RagService Bean: Encapsulates the RAG workflow service class. It will @Autowired HybridRetriever and ChatClient to implement the answerQuestion(String question) method.
RagService Interface Definition
RagService can define the following interface for Controller invocation:
public interface RagService {
/** 根据用户问题返回答案 */
String getAnswer(String question);
}
The implementation class RagServiceImpl completes the work following the online retrieval QA flow steps:
@Service
public class RagServiceImpl implements RagService {
@Autowired private HybridRetriever retriever;
@Autowired private ChatClient chatClient;
@Value("${app.maxDocs:6}") private int maxDocs;
@Override
public String getAnswer(String question) {
// 1. 调用混合检索获取相关文档片段列表
List<Document> docs = retriever.retrieve(new Query(question));
if (docs.isEmpty()) {
return "抱歉,未能找到相关信息。";
}
// 截取最多maxDocs篇,以防过长
List<Document> topDocs = docs.size() > maxDocs ? docs.subList(0, maxDocs) : docs;
// 2. 构造提示上下文文本
StringBuilder context = new StringBuilder();
for (Document doc : topDocs) {
context.append(doc.getContent()).append("\n");
}
String prompt = String.format("基于以下内容回答问题:\n%s\n问题:%s\n回答:", context, question);
// 3. 调用LLM生成回答
ChatResponse response = chatClient.prompt(prompt).call().chatResponse();
return response.content();
}
}
In the above code:
- retriever.retrieve returns a Document list, each Document containing content text.
- Segment content is concatenated into context (production applications should add delimiters and source attribution).
- ChatClient.prompt() sends the assembled prompt to receive a response. If no relevant segments exist, return a response indicating information cannot be found.
Through this implementation, RagService provides a high-level interface externally, concealing internal complexity, and Controller need only pass the question string to obtain an answer.
Example Controller
Finally, provide an example controller demonstrating interface definition and invocation flow:
@RestController
@RequestMapping("/api")
public class QaController {
@Autowired
private RagService ragService;
// 提问接口
@GetMapping("/ask")
public ResponseEntity<String> askQuestion(@RequestParam("q") String question) {
String answer = ragService.getAnswer(question);
return ResponseEntity.ok(answer);
}
// 文档上传接口(可选,实现文档入库)
@PostMapping(value="/documents", consumes=MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<String> uploadDocument(@RequestPart("file") MultipartFile file) {
// 调用文档处理服务将文件内容存入ES和Qdrant
// DocumentLoader.load(file);
return ResponseEntity.ok("文档已上传并索引完成");
}
}
- The /api/ask endpoint accepts query parameter q as the user question, calls RagService to fetch an answer, and returns it directly. GET is used for simplicity; POST could also submit complex queries.
- The /api/documents endpoint example shows how to receive a file and invoke the document loading process to index content into the system. Document processing can follow the document indexing flow described earlier.
Through the above Controller, frontend or users interact with the RAG service via HTTP requests, enabling dynamic QA.
Docker Compose Environment Deployment
For convenience in development and deployment, a Docker Compose configuration simultaneously launches required external services (Qdrant, Elasticsearch, Ollama) and the Spring Boot application. The project's docker-compose.yml example is as follows:
version: '3.9'
services:
elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:8.9.0
container_name: elasticsearch
environment:
- discovery.type=single-node
- xpack.security.enabled=false # 禁用安全认证
- ES_JAVA_OPTS=-Xms1g -Xmx1g # 内存配置,可根据需要调整
ports:
- "9200:9200"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9200"]
interval: 30s
retries: 3
qdrant:
image: qdrant/qdrant:v1.3.5
container_name: qdrant
ports:
- "6333:6333" # HTTP API
- "6334:6334" # gRPC API
volumes:
- qdrant_data:/qdrant/storage
ollama:
image: ollama/ollama:0.1.9
container_name: ollama
ports:
- "11434:11434" # Ollama REST API 默认端口
volumes:
- ollama_data:/root/.ollama
rag-service:
build: .
container_name: rag-service
environment:
SPRING_PROFILES_ACTIVE: "local" # 激活本地LLM配置。如切换OpenAI则设为cloud等。
OPENAI_API_KEY: "${OPENAI_API_KEY:-}" # 可选,提供OpenAI密钥
QDRANT_GRPC_HOST: "qdrant" # 若应用读取环境配置连接Qdrant
QDRANT_GRPC_PORT: 6334
depends_on:
- elasticsearch
- qdrant
- ollama
ports:
- "8080:8080"
Explanation:
- Elasticsearch: Uses the official ES 8.9 image, configured as single-node mode with security disabled for simplified development testing. Port 9200 maps for REST access.
- Qdrant: Uses the official Qdrant image. Port 6333 is exposed for HTTP API (if needed) and port 6334 for gRPC clients. Volume qdrant_data persists vector data.
- Ollama: Uses the official Ollama Docker image. Port 11434 maps for communication with the Spring application (Ollama provides REST API). Volume ollama_data saves downloaded models, avoiding needing to re-pull models after container restart. Before startup, run commands like docker exec -it ollama ollama pull llama2 to download required models, or trigger model download via Ollama API during application initialization.
- RAG Service: Assuming the Spring Boot application is packaged as an image (Compose builds via the build directive). Configure the application profile and required parameters via environment variables: for example, activating the local profile indicates using local Ollama; for OpenAI, provide OPENAI_API_KEY in the corresponding profile. depends_on ensures other services start first. Port 8080 exposes HTTP endpoints externally.
Network Configuration: Compose places these services in the same network by default; container names serve as hostnames. Therefore, when the application connects to Qdrant and Elasticsearch, it can use qdrant:6334 and elasticsearch:9200 as addresses. Spring AI's Docker Compose integration module can even automatically discover services and configure connections based on container names—for instance, containers with names containing "ollama/ollama" are recognized as Ollama services.
Startup: Run docker-compose up -d to start all containers in the background. After Elasticsearch and Qdrant complete startup (verify via health check logs or API), the Spring Boot application automatically connects to them. At this point:
- Call POST /api/documents to upload documents (or pre-index documents through other means);
- Then call GET /api/ask?q=your%20question to obtain answers.
Through Docker Compose, deploy the entire RAG system's dependencies with one command, convenient for testing locally or on servers.
Summary
This design document details a comprehensive RAG search service solution built on the Java Spring ecosystem. Through Spring AI's abstractions and integration capabilities, we've realized a modular Retriever-VectorStore-RAG Service architecture supporting hybrid retrieval, flexible LLM and embedding switching, and containerized deployment. Core technology selections—Spring Boot, Spring AI, Elasticsearch, Qdrant, and Ollama—are currently popular and well-supported components. The document provides comprehensive architecture and data flow analysis, delivers clear configuration and code examples, and ensures implementation details are reproducible. Developers can construct a verifiable RAG QA system following this guide, balancing answer accuracy, deployment flexibility, and data privacy, providing robust support for enterprise knowledge base QA, intelligent search, and similar application scenarios.