Building a Local RAG System with Ollama and LangChain4j
Introduction
In the rapidly evolving landscape of AI, Retrieval-Augmented Generation (RAG) has emerged as a powerful technique to enhance the capabilities of Large Language Models (LLMs) by grounding them with external knowledge. While cloud-based solutions are popular, there’s a growing need for local, privacy-preserving, and cost-effective alternatives. Enter Ollama and LangChain4j.
Ollama simplifies running LLMs locally, and LangChain4j brings the power of LangChain to the JVM. In this guide, we’ll build a fully local RAG system using these two tools. You’ll learn how to ingest documents, generate embeddings, store them in a vector store, and query the system with a natural language interface—all without any cloud dependencies.
By the end, you’ll have a solid understanding of the components involved and a working Java application you can extend for your own use cases.
Prerequisites
Before diving in, ensure you have the following installed:
- Java 17+ (we’ll use Java 21 features for demonstration)
- Maven (or Gradle, but we’ll use Maven)
- Ollama – Download and install for your OS
Once Ollama is installed, pull the models we’ll need:
1 | ollama pull llama3.2 |
llama3.2 is a powerful chat model, and nomic-embed-text is a high-quality embedding model. Both run locally.
Understanding RAG
RAG combines a retrieval system with a generative model. The process works in two phases:
- Indexing: Documents are split into chunks, each chunk is converted into a vector embedding, and the embeddings are stored in a vector database.
- Querying: A user query is embedded, similar vectors are retrieved, and the retrieved context is fed to the LLM along with the query to generate an answer.
This approach ensures the model has access to relevant, up-to-date information without retraining.
Setting Up the Project
Create a new Maven project and add the LangChain4j dependencies. We’ll use the langchain4j and langchain4j-ollama modules, along with a vector store. For simplicity, we’ll use the in-memory EmbeddingStore from LangChain4j, but you can easily swap it for a persistent store like Pinecone or Weaviate.
pom.xml
1 | <dependencies> |
Now, let’s write the core code.
Building the RAG Pipeline
We’ll create a class LocalRagSystem that handles document ingestion and querying.
Step 1: Initialize Models and Embedding Store
First, we need to set up the chat model and embedding model using Ollama’s API.
1 | import dev.langchain4j.model.ollama.OllamaChatModel; |
Step 2: Ingest Documents
We’ll load a text document from the filesystem, split it into chunks, embed each chunk, and store it.
1 | import dev.langchain4j.data.document.splitter.DocumentSplitters; |
Step 3: Create Conversational Retrieval Chain
Now we’ll build a chain that retrieves relevant segments and feeds them to the chat model.
1 | import dev.langchain4j.chain.ConversationalRetrievalChain; |
Full Example Code
Combine everything into a single class. Here’s the complete LocalRagSystem.java:
1 | import dev.langchain4j.chain.ConversationalRetrievalChain; |
Testing the System
Create a sample text file at data/sample.txt with some content, e.g., about the history of Java. Then run the application. You should see output like:
1 | Ingested 4 segments. |
The system retrieves relevant chunks and generates a coherent answer. To test further, ask follow-up questions.
How It Works Under the Hood
Let’s break down what happens when you ask a question:
- Query Embedding: The query is converted into a vector using
nomic-embed-text. - Similarity Search: The vector is compared to all stored segment embeddings using cosine similarity. The top
maxResultssegments are returned. - Prompt Construction: The retrieved segments are inserted into a prompt template along with the original question. LangChain4j’s
ConversationalRetrievalChainhandles this automatically. - Generation: The chat model (
llama3.2) processes the prompt and generates an answer.
This process ensures the answer is grounded in the provided documents.
Customizing the Pipeline
Changing the Chunk Size
The DocumentSplitters.recursive(500, 50) splits documents into chunks of 500 characters with a 50-character overlap. You can tune these based on your data. Smaller chunks are more precise but may lose context; larger chunks retain context but may be less relevant.
Using a Persistent Vector Store
For production, you’ll want a persistent store. LangChain4j supports many, including Pinecone, Weaviate, and Chroma. Here’s an example with Chroma (requires running Chroma separately):
1 | import dev.langchain4j.store.embedding.chroma.ChromaEmbeddingStore; |
Adding Memory
The ConversationalRetrievalChain automatically maintains chat memory. You can control it by passing a ChatMemory object.
Performance Considerations
Running models locally is resource-intensive. Here are some tips:
- Use a smaller chat model like
llama3.2:1bfor faster responses if accuracy is less critical. - Quantize models – Ollama supports quantizations like
q4_K_Mto reduce memory usage. - Batch embedding – When ingesting many documents, process them in batches to avoid memory spikes.
Troubleshooting Common Issues
- Ollama not reachable: Ensure Ollama is running (
ollama serve). - Model not found: Pull the model first (
ollama pull llama3.2). - Out of memory: Reduce chunk size or use a smaller model.
Conclusion
In this post, we’ve built a fully local RAG system using Ollama and LangChain4j. We covered the essential components: document ingestion, embedding, retrieval, and generation. The code is simple yet extensible, allowing you to integrate various vector stores and models.
Local RAG systems offer significant advantages in terms of privacy, cost, and control. With the tools we’ve used, you can build powerful AI applications entirely on your own hardware.
Key Takeaways
- RAG enhances LLMs by retrieving relevant context from documents, improving accuracy and relevance.
- Ollama provides an easy way to run LLMs and embedding models locally.
- LangChain4j offers a Java-friendly API to build RAG pipelines with minimal boilerplate.
- In-memory stores are great for prototyping; switch to persistent stores for production.
- Tuning chunk size and retrieval parameters can significantly impact answer quality.
- Local RAG ensures data privacy and reduces dependency on cloud services.
Now, go ahead and build your own local RAG system. Happy coding!