Retrieval-Augmented Generation (RAG) grounds AI responses in your actual data — customer support knowledge bases, product documentation, internal wikis — rather than hallucinated training data. The core component is a vector database that stores semantic embeddings and retrieves the most relevant documents for each query. Running pgvector or Qdrant on a Hong Kong VPS keeps your proprietary knowledge base on your infrastructure, makes the vector search endpoint accessible to mainland Chinese applications via CN2 GIA, and eliminates per-query API costs from managed vector database services.
How RAG Works
<code">
[Document ingestion]
Your documents → Embedding model → Vectors → Vector DB
[Query time]
User question → Embedding → Vector similarity search
↓
Top-K relevant document chunks retrieved
↓
LLM prompt: "Answer using these documents: [chunks]"
↓
Grounded, accurate response
Option A: pgvector (PostgreSQL Extension)
pgvector adds vector similarity search to your existing PostgreSQL installation — no additional database required, queries join vector search with relational data.
Install pgvector
<code">apt install -y postgresql-16-pgvector
# Enable extension in your database
sudo -u postgres psql -d myapp << 'SQL'
CREATE EXTENSION IF NOT EXISTS vector;
-- Create a table with vector column (1536 dimensions for OpenAI text-embedding-3-small)
-- Use 768 for nomic-embed-text (Ollama), 384 for all-MiniLM
CREATE TABLE documents (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
content TEXT NOT NULL,
metadata JSONB,
embedding vector(768),
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Create IVFFlat index for approximate nearest-neighbour search
-- lists = sqrt(number of rows) is a good starting point
CREATE INDEX idx_documents_embedding
ON documents
USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);
SQLIngest Documents (Python)
<code"># pip install psycopg2-binary openai pgvector --break-system-packages
import psycopg2
import json
from openai import OpenAI
# Or use Ollama for free, local embeddings:
# client = OpenAI(base_url="http://localhost:11434/v1", api_key="ollama")
# model = "nomic-embed-text"
client = OpenAI(api_key="YOUR_KEY")
conn = psycopg2.connect("postgresql://myapp_user:pass@localhost/myapp")
cur = conn.cursor()
def ingest_document(text: str, metadata: dict = {}):
# Generate embedding
response = client.embeddings.create(
model="text-embedding-3-small",
input=text
)
embedding = response.data[0].embedding
# Store in PostgreSQL
cur.execute(
"INSERT INTO documents (content, metadata, embedding) VALUES (%s, %s, %s)",
(text, json.dumps(metadata), embedding)
)
conn.commit()
# Ingest your knowledge base
documents = [
("Our Hong Kong VPS plans start at 1 GB RAM with CN2 GIA routing.", {"source": "pricing", "category": "vps"}),
("CN2 GIA provides premium routing to mainland China at 30-50ms from Hong Kong.", {"source": "technical", "category": "network"}),
("To set up WireGuard VPN, install wireguard package and generate keypairs.", {"source": "docs", "category": "tutorial"}),
]
for content, metadata in documents:
# Chunk long documents (max ~500 tokens per chunk for best retrieval)
ingest_document(content, metadata)
print(f"Ingested {len(documents)} documents")Query (Similarity Search)
<code"># Retrieve top-5 most relevant documents for a query
def retrieve_context(query: str, top_k: int = 5) -> list[dict]:
# Embed the query
q_embedding = client.embeddings.create(
model="text-embedding-3-small",
input=query
).data[0].embedding
# Vector similarity search (cosine distance)
cur.execute("""
SELECT content, metadata,
1 - (embedding <=> %s::vector) AS similarity
FROM documents
WHERE 1 - (embedding <=> %s::vector) > 0.7 -- Minimum similarity threshold
ORDER BY embedding <=> %s::vector
LIMIT %s
""", (q_embedding, q_embedding, q_embedding, top_k))
results = []
for content, metadata, similarity in cur.fetchall():
results.append({
"content": content,
"metadata": metadata,
"similarity": float(similarity)
})
return results
# Use in RAG pipeline
def rag_answer(question: str) -> str:
# 1. Retrieve relevant documents
context_docs = retrieve_context(question)
context = "\n\n".join([doc["content"] for doc in context_docs])
# 2. Generate answer grounded in retrieved context
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": f"""Answer the user's question using ONLY the following context.
If the answer is not in the context, say "I don't have information about that."
Context:
{context}"""},
{"role": "user", "content": question}
]
)
return response.choices[0].message.content
# Test
print(rag_answer("What latency can I expect from Hong Kong to China?"))Option B: Qdrant (Dedicated Vector Database)
Qdrant is a purpose-built vector database with advanced filtering, payload storage, and a REST/gRPC API — better for large-scale RAG systems than pgvector.
<code">docker run -d \ --name qdrant \ --restart unless-stopped \ -p 127.0.0.1:6333:6333 \ -v /opt/qdrant/storage:/qdrant/storage \ qdrant/qdrant:latest # Verify curl http://localhost:6333/healthz
Create Collection and Ingest
<code"># pip install qdrant-client --break-system-packages
from qdrant_client import QdrantClient
from qdrant_client.models import Distance, VectorParams, PointStruct
import uuid
qdrant = QdrantClient(host="localhost", port=6333)
# Create collection
qdrant.create_collection(
collection_name="knowledge_base",
vectors_config=VectorParams(size=768, distance=Distance.COSINE),
)
# Ingest with rich metadata (Qdrant payload)
def ingest_to_qdrant(text: str, metadata: dict):
embedding = get_embedding(text) # Your embedding function
qdrant.upsert(
collection_name="knowledge_base",
points=[PointStruct(
id=str(uuid.uuid4()),
vector=embedding,
payload={"content": text, **metadata}
)]
)
# Search with filters
results = qdrant.search(
collection_name="knowledge_base",
query_vector=get_embedding("CN2 GIA network routing"),
query_filter={
"must": [{"key": "category", "match": {"value": "network"}}]
},
limit=5,
with_payload=True
)Build a RAG API Endpoint
<code"># FastAPI RAG endpoint — serves your internal knowledge base
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Question(BaseModel):
question: str
top_k: int = 5
@app.post("/api/ask")
async def ask(body: Question):
# 1. Retrieve relevant context
context_docs = retrieve_context(body.question, body.top_k)
if not context_docs:
return {"answer": "No relevant information found.", "sources": []}
# 2. Generate grounded answer
answer = rag_answer(body.question, context_docs)
return {
"answer": answer,
"sources": [
{"content": doc["content"][:200] + "...", "similarity": doc["similarity"]}
for doc in context_docs
]
}Document Chunking Strategy
<code"># Good chunking is critical for RAG quality
def chunk_document(text: str, chunk_size: int = 500, overlap: int = 50) -> list[str]:
"""Split document into overlapping chunks for better retrieval."""
words = text.split()
chunks = []
for i in range(0, len(words), chunk_size - overlap):
chunk = ' '.join(words[i:i + chunk_size])
if chunk:
chunks.append(chunk)
return chunks
# Ingest a large document
with open("product_manual.txt") as f:
full_text = f.read()
chunks = chunk_document(full_text)
for i, chunk in enumerate(chunks):
ingest_document(chunk, {
"source": "product_manual.txt",
"chunk_index": i,
"total_chunks": len(chunks)
})
print(f"Ingested {len(chunks)} chunks from product manual")Cost Comparison: Self-Hosted vs Managed Vector DB
| Solution | 10M vectors | 1M queries/month | China accessible |
|---|---|---|---|
| Pinecone (managed) | ~$70/month | ~$100/month | No (US only) |
| Weaviate Cloud | ~$50/month | ~$80/month | Variable |
| pgvector on HK VPS | Disk space only | $0 (CPU cost) | Yes (CN2 GIA) |
| Qdrant on HK VPS | Disk space only | $0 (CPU cost) | Yes (CN2 GIA) |
Conclusion
pgvector on your existing Hong Kong VPS PostgreSQL instance is the lowest-friction starting point for RAG — no new infrastructure, queries join vector search with relational data, and backups are part of your existing PostgreSQL backup routine. Qdrant adds advanced features (filtering, quantisation, distributed mode) as your vector dataset grows. Both approaches keep your proprietary knowledge base on your infrastructure, accessible to mainland Chinese applications via CN2 GIA at 30–50ms — a meaningful advantage over managed vector database services hosted in the US or EU.
Build your AI stack: Browse Server.HK Hong Kong VPS plans — pair a 8 GB VPS with pgvector and Ollama for a fully self-hosted RAG pipeline with no per-query costs.