A Retrieval-Augmented Generation (RAG) backend system with multi-layer caching and database support.
This project uses the following stack:
- MongoDB β Main database
- Mongo Express β Web UI for MongoDB
- Redis β Fast caching layer (exact + normalized)
- Qdrant β Vector database for semantic caching
- Docker Compose β Local development environment
This project uses Mongo Express, a web-based GUI for MongoDB running locally.
It allows you to:
- Inspect collections
- View documents
- Edit database records
- Debug stored data easily
cp .env.local.example .env.localdocker compose up --build --watchdocker stop rag-project-api-1npm run devcp .env.docker.example .env.dockerdocker compose up --build --watch- Application server
- MongoDB
- Mongo Express
- Redis
- Qdrant
- Ollama
The project uses a 3-layer caching system to optimize performance and reduce LLM calls:
- Hash-based caching (Redis)
- Normalized question caching (Redis)
- Semantic caching (Qdrant)
A SHA-256 hash of the raw question is used as the cache key.
- Exact match caching
- Fastest lookup
const hashedq = hashQuestion(question);
const hkey = `rag:${botId}:qhash:${hashedq}`;
await SetCache(hkey, answer);function hashQuestion(q: string) {
return crypto
.createHash("sha256")
.update(q)
.digest("hex");
}rag:bot123:qhash:ab3429f9c83a...
Improves cache hits by normalizing the question before hashing.
- Lowercase conversion
- Remove punctuation
- Collapse spaces
- Trim whitespace
- Hash normalized text
const normq = normlizedQuestion(question);
const normlzqKey = `rag:${botId}:qnormlz:${normq}`;
await SetCache(normlzqKey, answer);function normlizedQuestion(q: string) {
const normlzq = q
.toLowerCase()
.replace(/[^\w\s]/g, "")
.replace(/\s+/g, " ")
.trim();
return crypto
.createHash("sha256")
.update(normlzq)
.digest("hex");
}rag:bot123:qnormlz:98a2bca1a42...
This layer enables meaning-based caching using vector embeddings.
Instead of matching exact text, it matches semantic similarity.
const embeddedQuestion = await OllamaQuestionToEmbedded(question);const res = await qdrant.search("qa_cache", {
vector: embedding,
limit: 1,
filter: {
must: [
{
key: "botId",
match: { value: botId }
}
]
}
});if (res.length && res[0].score > 0.92) {
return res[0]?.payload?.answer;
}await qdrant.upsert("qa_cache", {
points: [
{
id: crypto.randomUUID(),
vector: embeddedQuestion,
payload: {
botId,
question,
answer
}
}
]
});Handles different phrasing with same meaning.
"What is RAG?"
"Explain retrieval augmented generation"
"Tell me about RAG architecture"
When a user asks a question:
1. Check Hash Cache (Redis)
2. Check Normalized Cache (Redis)
3. Generate Embedding
4. Check Semantic Cache (Qdrant)
5. Call LLM (if all miss)
6. Store result in:
- Hash Cache
- Normalized Cache
- Semantic Cache
rag:{botId}:qhash:{hash}
rag:{botId}:qnormlz:{hash}
Collection: qa_cache
{
"botId": "bot123",
"question": "What is RAG?",
"answer": "..."
}| Cache Layer | Speed β‘ | Accuracy π― | Use Case |
|---|---|---|---|
| Hash Cache | Very High | Exact only | Same question |
| Normalized Cache | High | Medium | Slight variation |
| Semantic Cache | Medium | High | Meaning-based |
- Redis β ultra-fast lookup caching
- Qdrant β semantic similarity search
- MongoDB β persistent data storage
- Similarity threshold (
0.92) can be tuned
- Add TTL to Redis keys
- Use top-k semantic matches instead of 1
- Cache embeddings to avoid recomputation
- Add metrics/logging for cache hit rates