Blog
Artificial Intelligence2024-11-1812 min

RAG in production: what we learned building Tuki with pgvector and Claude

Implementing a RAG system in production has complexities no tutorial mentions. Here we document the real decisions we made building Tuki.

The problem with RAG tutorials

When we started building Tuki, resources about RAG were abundant but shallow. They all showed the same flow: load a PDF, split it into chunks, generate embeddings, store them in a vector store, and do cosine similarity search when a question comes in.

What no tutorial showed was what happens when that system hits production with a real tenant like AAETAV, with official documentation, regulations, course lists, and member questions arriving in real time.

Why we chose pgvector over specialized solutions

The first decision was the vector database. The most visible options were Pinecone, Weaviate and Qdrant. However, for a multi-tenant system where we already had PostgreSQL running on Coolify, adding another managed service made no operational sense.

pgvector is a PostgreSQL extension that adds native vector support. The decision meant zero additional infrastructure (same Postgres, same Prisma connection, same backup), ACID transactions on embeddings, and hybrid queries: filter by tenant_id and do vector search in the same SQL query.

Voyage AI vs OpenAI Embeddings

OpenAI text-embedding-3-small was cheap with acceptable results in English, but for technical Spanish the results were noticeably worse. Voyage AI voyage-large-2-instruct changed everything. Semantic search precision in Spanish improved considerably. Questions like 'what are the requirements for the GCI exam?' found the correct regulation paragraphs even without using those exact words.

Chunking: the detail nobody documents well

We ended up implementing chunks by logical section (titles and subtitles as separators) with a 100-token overlap between consecutive chunks. The overlap is key: it prevents a question that falls exactly on the boundary between two chunks from finding no answer.

The Claude prompt that worked

What worked was a prompt that explicitly defines the source hierarchy: retrieved documents first, general knowledge only for context completion, and never invent domain-specific information.

The most important instruction: if the information is not in the provided documents, explicitly say it is not available and suggest contacting the organization directly.

Multi-tenancy: the isolation detail

We implemented a tenant_id field in the embeddings table and all vector search queries include a tenant filter. We added an automated test that verifies searches from one tenant never return results from another.