AI & LLM Integration

Building a RAG System on Domain India VPS: Vector Databases, Embeddings, and Retrieval

By Domain India Team · DomainIndia EngineeringPublished 10 min read
Knowledge base article
Contents (14 sections)

A chatbot that answers from your own documents is one of the most useful AI features a business can add. The technique behind it is called retrieval-augmented generation, and you can build it with PostgreSQL and a few hundred lines of code.

Key takeaways

Retrieval-Augmented Generation (RAG) combines an LLM with your own data — product docs, knowledge base, legal contracts — so the AI answers from your content instead of hallucinating. This guide walks through the full RAG stack: chunking, embeddings, vector databases (pgvector, Qdrant, Weaviate), and retrieval, running on your own VPS.

What RAG solves

A raw LLM only knows what it learned at training. It doesn't know:

  • Your product manual written last month
  • Your company's internal policies
  • Your customer's support history
  • Today's pricing

RAG fixes this in three steps:

  1. Index — split your docs into chunks, compute embeddings, store in a vector DB
  2. Retrieve — on user question, find the most relevant chunks by vector similarity
  3. Generate — send question + retrieved chunks to the LLM, which answers grounded in your data

End result: AI that speaks in your voice, from your source of truth.

Where each part runs on Domain India

ComponentShared hostingApp PlatformVPS
LLM API calls (OpenAI, Anthropic)Yes on cPanel; test cURL on DirectAdminYesYes
Embedding API callsSame as aboveYesYes
pgvectorNo (you can't install database extensions)PostgreSQL included; pgvector not confirmed, ask supportYes, you install it
Qdrant, WeaviateNoNo (they are separate services)Yes
Bulk indexing jobsNo (long-running processes are stopped)Ask supportYes

For anything beyond a prototype, use a VPS: you need a database with vector support and somewhere to run indexing jobs. A Domain India VPS is self-managed, so you install and maintain the software yourself.

Step 1 — Choose a vector database

Vector DBSetupBest forMemory
pgvectorPostgreSQL extensionYou already use PostgreSQLGrows with vector count (see FAQ)
QdrantSingle binary or containerMedium to large projects, filteringGrows with vector count; on-disk mode available
WeaviateContainerLarger scale, built-in hybrid searchGrows with vector count
ChromaPython library, embeddedPrototypes and small collectionsGrows with vector count

For most small projects, pgvector is the winner: if you already run PostgreSQL, it is one extension and no new service to operate.

Step 2 — Install pgvector on your VPS

The easiest route on an AlmaLinux or Rocky Linux 9 VPS is the official PostgreSQL (PGDG) repository, which packages pgvector. Replace 17 with the PostgreSQL major version you want, and pick the repository file for your OS version from postgresql.org:

  1. Log in to the VPS over SSH as root, or as a user with sudo
  2. Add the PGDG repository, then install PostgreSQL and pgvector:
    bash
    sudo dnf install -y https://download.postgresql.org/pub/repos/yum/reporpms/EL-9-x86_64/pgdg-redhat-repo-latest.noarch.rpm
    sudo dnf -qy module disable postgresql
    sudo dnf install -y postgresql17-server pgvector_17
    sudo /usr/pgsql-17/bin/postgresql-17-setup initdb
    sudo systemctl enable --now postgresql-17
  3. Enable the extension in your database:
    bash
    sudo -u postgres psql -d your_db -c "CREATE EXTENSION IF NOT EXISTS vector;"
  4. Verify: SELECT '[1,2,3]'::vector; should return [1,2,3].

On Debian or Ubuntu, the PostgreSQL APT repository has an equivalent postgresql-17-pgvector package. You can also build pgvector from source (see its GitHub README).

Step 3 — Design your schema

sql
CREATE TABLE documents (
    id          bigserial PRIMARY KEY,
    source      text NOT NULL,         -- "docs/setup.md" or URL
    chunk_index int NOT NULL,          -- 0, 1, 2 ...
    content     text NOT NULL,         -- the chunk itself
    embedding   vector(1536),          -- must match your embedding model's dimensions
    metadata    jsonb,                 -- {category, author, date, ...}
    created_at  timestamptz DEFAULT now()
);

CREATE INDEX ON documents USING hnsw (embedding vector_cosine_ops);
CREATE INDEX ON documents USING gin (metadata);

HNSW is pgvector's approximate nearest-neighbour index: fast at scale. Plain vector columns can be indexed up to 2,000 dimensions; for larger embeddings, store them as halfvec or ask the model for fewer dimensions.

Step 4 — Chunk your documents

Naive approach: split by character count. Better: split by headings/paragraphs with overlap.

Python example using LangChain's text splitter (pip install langchain-text-splitters):

python
from langchain_text_splitters import RecursiveCharacterTextSplitter

splitter = RecursiveCharacterTextSplitter(
    chunk_size=1000,
    chunk_overlap=200,
    separators=["\n## ", "\n### ", "\n\n", "\n", ". ", " "],
)

with open("docs/setup.md") as f:
    chunks = splitter.split_text(f.read())

print(f"Got {len(chunks)} chunks")

Rule of thumb: chunk size 500–1500 characters, overlap 10–20%. Smaller = more precise retrieval but more chunks; bigger = more context per chunk but less focused.

Step 5 — Compute embeddings

Embedding APIs are cheap compared with chat models, but check the provider's current price. This example uses OpenAI's text-embedding-3-small (1,536 dimensions) with psycopg 3 and the pgvector adapter (pip install openai "psycopg[binary]" pgvector numpy):

python
import numpy as np
import psycopg
from openai import OpenAI
from pgvector.psycopg import register_vector
from psycopg.types.json import Jsonb

client = OpenAI()

def embed(text):
    return np.array(client.embeddings.create(
        model="text-embedding-3-small",
        input=text,
    ).data[0].embedding)

conn = psycopg.connect("dbname=your_db")
register_vector(conn)
cur = conn.cursor()

for i, chunk in enumerate(chunks):
    cur.execute(
        "INSERT INTO documents (source, chunk_index, content, embedding, metadata) "
        "VALUES (%s, %s, %s, %s, %s)",
        ("docs/setup.md", i, chunk, embed(chunk), Jsonb({"category": "setup"})),
    )
conn.commit()

Batch embeddings. Send many chunks per call (OpenAI accepts a list of inputs, within per-request limits). You pay the same per token, but it is far faster and uses far fewer requests against your rate limit:

python
response = client.embeddings.create(
    model="text-embedding-3-small",
    input=chunks_batch  # list of strings
)
vecs = [np.array(item.embedding) for item in response.data]
Don't re-embed what hasn't changed

Re-embedding everything on every update wastes money. Store a hash of each chunk's text next to its embedding, and only re-embed chunks whose hash has changed. If you switch embedding models, re-embed everything: vectors from different models can't be compared.

Step 6 — Retrieve

On user question, embed the question and find similar chunks:

python
def retrieve(question, k=5):
    q_vec = embed(question)
    cur.execute(
        """
        SELECT content, metadata,
               1 - (embedding <=> %(q)s) AS similarity
        FROM documents
        ORDER BY embedding <=> %(q)s
        LIMIT %(k)s
        """,
        {"q": q_vec, "k": k},
    )
    return cur.fetchall()

question = "How do I reset my password?"
results = retrieve(question)
for chunk, meta, sim in results:
    print(f"[{sim:.3f}] {chunk[:100]}...")

<=> is pgvector's cosine distance operator: lower means more similar.

Step 7 — Generate the answer

Send question + retrieved chunks to the LLM:

python
import os

context = "\n---\n".join(chunk for chunk, _, _ in results)

completion = client.chat.completions.create(
    model=os.environ["OPENAI_MODEL"],   # a current model ID from the provider's models page
    messages=[
        {"role": "system", "content": (
            "You are a helpful assistant answering questions from the provided context. "
            "If the answer isn't in the context, say 'I don't know'. Don't make things up."
        )},
        {"role": "user", "content": f"Context:\n{context}\n\nQuestion: {question}"},
    ],
)

print(completion.choices[0].message.content)

Hybrid search — better than pure vector

Pure vector retrieval misses exact matches (product codes, SKUs, error numbers). Combine it with PostgreSQL full-text search:

sql
SELECT content, metadata
FROM documents
WHERE (
    embedding <=> %(q)s < 0.3                                        -- semantic match
    OR to_tsvector('english', content) @@ websearch_to_tsquery('english', %(text)s)  -- keyword match
)
ORDER BY (
    0.7 * (1 - (embedding <=> %(q)s))
    + 0.3 * ts_rank(to_tsvector('english', content), websearch_to_tsquery('english', %(text)s))
) DESC
LIMIT 5;

websearch_to_tsquery accepts raw user text safely, where to_tsquery throws errors on stray symbols. For larger tables, add a generated tsvector column with a GIN index, or run the vector and keyword searches separately and merge them with reciprocal rank fusion, so both indexes are used.

Evaluating retrieval quality

Build a small eval set — 20–50 question-answer pairs from your actual docs. Measure:

  • Recall@5 — is the right chunk in the top 5? Target >80%.
  • Precision@5 — of the top 5, how many are relevant?
  • Answer accuracy — does the LLM give correct answers?

Tools: RAGAS (Python lib) measures all of these automatically.

Common pitfalls

Chunks too small
You retrieve fragments without enough context. Increase chunk size.
Chunks too big
Retrieval returns huge blocks and the LLM loses focus. Decrease chunk size.
Embedding cost spirals
Re-indexing the whole corpus every night. Hash chunks and re-embed only what changed.
Stale data
The vector table doesn't sync itself with your source. Run a scheduled refresh or update on every content change.
Prompt injection through retrieved content
Someone plants "Ignore all instructions..." in a document you index. Treat retrieved text as data, wrap it in delimiters, and never let answers trigger actions unchecked.
Too many retrieved chunks
The context fills up and answers get worse. Five to ten good chunks is usually enough.

FAQ

pgvector or Qdrant?

pgvector if you already use PostgreSQL and your collection is in the millions of chunks or fewer: one database, transactions, and joins with your other data. Qdrant or another dedicated vector database if you need very high query rates, advanced filtering or multi-tenancy at scale. For most small projects, pgvector is enough.

How much RAM does a vector database need?

Do the arithmetic. A 1,536-dimension vector stored as 32-bit floats is about 6 KB, so one million vectors is roughly 6 GB before the HNSW index, which adds more. For good speed the index should fit in RAM. To shrink it, use halfvec (half the size), an embedding model with fewer dimensions, or a vector database with on-disk or quantised storage.

Can I use open-source embeddings instead of OpenAI?

Yes. The sentence-transformers library runs embedding models locally on CPU with no API cost. Small models such as all-MiniLM-L6-v2 need little RAM but give lower quality than larger or newer models; test several on your own questions and check the model's licence.

How do I stop the LLM from hallucinating even with RAG?

Use a strict system prompt ("If the answer is not in the context, say you don't know"), retrieve fewer but better chunks, and return citations (the source of each chunk) so users can check. Measure answer accuracy on an evaluation set rather than trusting spot checks.

What about Claude instead of OpenAI?

It works the same way. Anthropic doesn't offer its own embedding model, so pair Claude for generation with any embedding provider or a local embedding model. The embeddings and the chat model don't need to come from the same company.

Running this on Domain India

  • VPS: self-managed with full root access, so you can install PostgreSQL with pgvector, Qdrant or Weaviate, and run indexing jobs with cron or systemd timers. Size RAM to your vector count (see the FAQ).
  • Backups: VPS plans include no backups or snapshots. Dump the database regularly (pg_dump) and copy it off the server.
  • Shared hosting: you can host the chat front end and call LLM APIs (PHP cURL works on cPanel; test it on DirectAdmin), but you can't install pgvector, and long-running indexing jobs are stopped.
  • App Platform: includes PostgreSQL on every plan. Whether pgvector is enabled there is not confirmed, so ask support before you design around it.

Ready to build? Read the OpenAI and Claude API integration guide for production patterns, see fine-tuning with LoRA for when RAG isn't enough, or choose a Domain India VPS. For hosting questions, open a ticket.

Host your RAG stack on a VPS

A self-managed VPS with full root access for PostgreSQL and pgvector, your indexing jobs and your chat backend.

View VPS plans

Was this article helpful?

Your answer helps us decide what to improve next.

Still need help? Open a support ticket and our team will reply.

Prefer an app? Add this site to your home screen.Get the app
Build a RAG System on a VPS: Vector DB and Embeddings