Building RAG with Elasticsearch as a Vector Store

elasticsearchRagvector-searchlangchainollama
Building RAG with Elasticsearch as a Vector Store

Retrieval-Augmented Generation (RAG) has become the industry-standard pattern for building AI applications that reason over private data, internal documentation, logs, and enterprise knowledge bases.

Many RAG architectures introduce a dedicated vector database alongside a traditional database. While that approach works well, it also increases operational complexity.

An alternative is to use Elasticsearch as both the primary document store and the vector store.

In this guide, we'll build a production-oriented RAG architecture using:

  • Elasticsearch for document and vector storage
  • LangChain for orchestration
  • Ollama for local embedding generation

The result is a unified architecture that supports semantic search, metadata filtering, and enterprise-scale retrieval.


Why Use Elasticsearch for RAG?

A common RAG architecture looks like this:

Application
      │
      ├── PostgreSQL (metadata)
      │
      └── Vector Database
          (embeddings)

While functional, this introduces:

  • Additional infrastructure
  • Synchronization challenges
  • Multiple query paths
  • Increased operational overhead

Elasticsearch allows both structured documents and vector embeddings to live together.

Application
      │
      ▼
 Elasticsearch
 ├── Documents
 ├── Metadata
 └── Vectors

This unified model offers several advantages.


Unified Storage

Store:

  • Raw text
  • Metadata
  • Vector embeddings

inside a single system.

No duplication or synchronization between databases is required.


Advanced Filtering

Elasticsearch excels at structured querying.

You can combine:

  • Semantic search
  • Keyword search
  • User permissions
  • Categories
  • Timestamps

within the same query.


Scalability

Elasticsearch is designed for:

  • Horizontal scaling
  • High availability
  • Distributed indexing
  • Enterprise workloads

This makes it a strong fit for production RAG deployments.


Hybrid Search

One of Elasticsearch's strongest features is the ability to combine:

  • Traditional keyword search (BM25)
  • Dense vector similarity search

This often produces better retrieval quality than vector search alone.


High-Level Architecture

The workflow follows a straightforward path from raw documents to retrieved context.

Data Source
     │
     ▼
Embedding Model
 (Ollama)
     │
     ▼
Elasticsearch
(Vector Store)
     │
     ▼
Retriever
     │
     ▼
Relevant Documents
     │
     ▼
LLM

Each layer has a distinct responsibility.


Data Source

Documents can originate from:

  • JSON files
  • PDFs
  • Databases
  • Internal APIs
  • Knowledge bases

For this example, we'll use structured JSON data.


Embedding Layer

The embedding model converts text into dense vector representations.

These vectors capture semantic meaning and allow similarity comparisons.


Elasticsearch Vector Store

Elasticsearch stores:

  • Text content
  • Metadata
  • Vector embeddings

and performs similarity searches across indexed vectors.


Query Layer

When a user submits a query:

  1. The query is embedded.
  2. Elasticsearch performs vector similarity search.
  3. Optional metadata filters are applied.
  4. Relevant documents are returned.

Technical Stack

The implementation uses:

  • Python
  • LangChain
  • Elasticsearch
  • Ollama

The embedding model runs locally, avoiding external API costs.


Configuring Ollama Embeddings

We'll use the qwen3-embedding:4b model.

from langchain_community.embeddings import OllamaEmbeddings

embeddings = OllamaEmbeddings(
    base_url="http://localhost:11434",
    model="qwen3-embedding:4b"
)

Benefits of local embeddings include:

  • No API fees
  • Improved privacy
  • Reduced latency
  • Full control over infrastructure

Designing the RAG Class

A clean implementation encapsulates storage and retrieval logic inside a dedicated class.

from typing import List
from langchain_elasticsearch import ElasticsearchStore

class TermmtrixRag:

    def __init__(self):
        self.vector_store = self.load_es_store()
        self.docs: List = []
        self.doc_ids: List = []

    def load_es_store(self):
        return ElasticsearchStore(
            index_name="termtrix",
            embedding=embeddings,
            es_url="http://localhost:9200/",
            es_password="your_password",
            es_user="elastic"
        )

This class becomes the central interface for ingestion and retrieval.

Document Ingestion and Embedding

The next step is indexing documents.

Each document is converted into a LangChain Document object.

This preserves metadata alongside the text.

from langchain_core.documents import Document

Example Ingestion Logic

def ingest_data(self, data_list):

    for item in data_list:

        self.docs.append(
            Document(
                page_content=item["text"],
                metadata=item["metadata"]
            )
        )

        self.doc_ids.append(item["id"])

    self.vector_store.add_documents(
        documents=self.docs,
        ids=self.doc_ids
    )

Once inserted:

  • Embeddings are generated
  • Vectors are stored
  • Metadata remains attached
  • Documents become searchable

Why This Ingestion Pattern Works

There are several practical advantages.

One-Time Embedding Cost

Embeddings are generated once during indexing.

Retrieval becomes significantly cheaper because vectors already exist.

Index Once
      │
      ▼
Generate Embeddings
      │
      ▼
Store Vectors


Metadata Persistence

Metadata remains attached to every document.

Examples include:

{
  "source": "tech_knowledge_base",
  "author": "Engineering",
  "created_at": "2025-01-01"
}

This becomes extremely valuable during retrieval.

Semantic Search and Filtering

Once documents are indexed, users can search using natural language.

Instead of matching exact keywords:

"How do I reset my API key?"

the retriever finds semantically related content even if those exact words don't appear.

Filtered Similarity Search

In production systems, unrestricted vector search is rarely sufficient.

You often need constraints such as:

  • Department
  • Tenant
  • User permissions
  • Document type
  • Date ranges

Elasticsearch makes this straightforward.

def similarity_search(self, query):

    results = (
        self.vector_store.similarity_search_with_score(
            query=query,
            k=1,
            filter=[
                {
                    "term": {
                        "metadata.source":
                        "tech_knowledge_base"
                    }
                }
            ],
        )
    )

    for doc, score in results:
        print(
            f"* [Score={score:3f}] "
            f"{doc.page_content} "
            f"[{doc.metadata}]"
        )

This ensures retrieval remains both relevant and secure.


Why Metadata Filtering Matters

Imagine a multi-tenant SaaS platform.

Without filtering:

Tenant A
      │
      ▼
Could retrieve
Tenant B documents ❌

With metadata filters:

Tenant A
      │
      ▼
Only Tenant A data ✅

This is one of the most important requirements for production RAG systems.

Practical Use Cases

This Elasticsearch-based architecture works particularly well for enterprise workloads.

Log Analysis

Store logs with metadata such as:

  • Service name
  • Environment
  • Timestamp

Then perform semantic searches for:

Why are checkout requests failing?

instead of manually searching error codes.

Internal Knowledge Bases

Build assistants that retrieve information from:

  • Engineering documentation
  • HR policies
  • Legal guidelines
  • Customer support content

while respecting access controls.

Documentation Search

Retrieve the most relevant version of documentation based on:

  • Product version
  • Release date
  • Category

before generating responses.

Limitations and Considerations

While Elasticsearch is a strong choice, there are tradeoffs.

Memory Usage

Vector search can become memory-intensive.

Factors include:

  • Number of documents
  • Embedding dimensions
  • Index settings
  • Query volume

Plan cluster sizing accordingly.

Re-Indexing Requirements

Embeddings are model-dependent.

If you switch from:

qwen3-embedding:4b

to another model:

nomic-embed-text

all documents must be re-embedded and re-indexed.

The old vectors are no longer comparable to the new ones.

Retrieval Quality Depends on Chunking

Even the best vector database cannot compensate for poor chunking.

Pay attention to:

  • Chunk size
  • Overlap strategy
  • Metadata design

These choices directly impact retrieval quality.

Taking It Further: Adding an LLM

At this point, Elasticsearch acts as an intelligent retrieval layer.

The next step is connecting retrieval to a language model.

User Question
       │
       ▼
Retriever
       │
       ▼
Relevant Documents
       │
       ▼
LLM
       │
       ▼
Final Answer

This transforms semantic search into a fully conversational assistant.

Conclusion

Using Elasticsearch as both a document store and vector store simplifies RAG architectures while preserving enterprise-grade capabilities.

Instead of maintaining separate systems for:

  • Documents
  • Metadata
  • Embeddings

everything lives in a single platform.

Combined with:

  • LangChain for orchestration
  • Ollama for local embeddings
  • Elasticsearch for retrieval

you can build scalable, privacy-focused RAG applications that are ready for real-world production workloads.

Whether you're creating internal assistants, documentation search tools, or log-analysis systems, this architecture provides a powerful foundation.

Key Takeaways

✅ Elasticsearch can serve as both a document database and vector store.

✅ Metadata filtering is essential for production-grade RAG.

✅ Local embedding models through Ollama provide privacy and cost advantages.

✅ LangChain simplifies indexing and retrieval workflows.

✅ Hybrid search combines keyword and semantic retrieval for improved results.

✅ Re-indexing is required whenever embedding models change.

✅ A well-designed retrieval layer is the foundation of an effective AI assistant.