How OmniRoute's Persistent Memory System Combines FTS5 and Qdrant for Semantic Retrieval
OmniRoute stores every conversational memory in SQLite with FTS5 for keyword search and Qdrant for semantic retrieval, automatically falling back through a three-tier hierarchy when vector services are unavailable.
The OmniRoute persistent memory system implements a hybrid architecture that balances local persistence with high-performance vector search. According to the diegosouzapw/OmniRoute source code, all memory data lives in SQLite while supporting both full-text and semantic retrieval strategies through a pluggable backend system.
SQLite Foundation with FTS5 Indexing
All memory records reside in the memories table, which stores raw text, metadata, TTL values, and unique keys. For fast keyword retrieval, OmniRoute creates an FTS5 virtual table named memory_fts that indexes the content and key columns.
The schema is established in migration src/lib/db/migrations/022_add_memory_fts5.sql, which creates both the base table and the virtual FTS5 table. A subsequent migration in src/lib/db/migrations/023_fix_memory_fts_uuid.sql stabilizes the FTS5 table to use integer rowids for consistent indexing.
The src/lib/db/memoryVec.ts module provides CRUD helpers for interacting with the memories table and the related memory_vec_meta table, ensuring all vector metadata stays synchronized with the persistent SQLite store.
Three-Tier Vector Store Architecture
The retrieval logic in src/lib/memory/retrieval.ts implements a tiered backend selection strategy based on the settings.vectorStore configuration:
- Qdrant – External Qdrant cluster accessed via HTTP for high-performance semantic search
- sqlite-vec – In-process SQLite extension for approximate nearest neighbor (ANN) search without external dependencies
- FTS5 – Pure keyword search against the
memory_ftsvirtual table as final fallback
When settings.vectorStore equals "qdrant", the system attempts semantic search first. If Qdrant returns no results or fails, the pipeline automatically degrades to sqlite-vec, then finally to FTS5 keyword matching.
Semantic and Hybrid Retrieval Flow
For semantic retrieval, the retrieveMemories function in src/lib/memory/retrieval.ts executes the following resolution path:
import { retrieveMemories } from "@/lib/memory/retrieval";
const apiKeyId = "user-api-key-123";
const result = await retrieveMemories(apiKeyId, {
query: "how to reset my password",
retrievalStrategy: "semantic",
maxTokens: 2000,
});
console.log(result.map(m => ({
id: m.id,
tier: (m as any).tier, // "qdrant" | "vector" | "fts5"
})));
If Qdrant is enabled, the function calls searchSemanticMemory from src/lib/memory/qdrant.ts, then hydrates results from SQLite using fetchMemoriesByIds. The returned objects include a tier property indicating which backend served the query.
For hybrid retrieval, when using the sqlite-vec backend, OmniRoute performs reciprocal rank fusion (RRF) to combine vector similarity scores with FTS5 keyword ranking. The retrievePreview utility allows dry-run testing of this logic:
import { retrievePreview } from "@/lib/memory/retrieval";
const preview = await retrievePreview(null, "project roadmap", {
strategy: "hybrid",
maxTokens: 3000,
limit: 10,
});
console.log(preview.resolution); // shows which backend (qdrant / sqlite-vec) was used
Qdrant Configuration and Operations
The src/lib/memory/qdrant.ts module handles all external vector store operations. It constructs QdrantConfig from environment variables or UI settings, manages collection creation, and handles semantic search with optional quantization modes (none, int8, or binary).
When upserting memories, the upsertSemanticMemoryPoint function synchronizes data between SQLite and the Qdrant collection:
import { upsertSemanticMemoryPoint } from "@/lib/memory/qdrant";
await upsertSemanticMemoryPoint({
id: "mem-001",
apiKeyId: "user-api-key-123",
sessionId: "sess-456",
key: "reset-password",
content: "To reset your password, click the 'Forgot password' link …",
metadata: {},
createdAt: new Date().toISOString(),
expiresAt: null,
});
Engine Observability and Status
The system exposes current backend health through the /api/settings/memory/engine-status endpoint, defined in src/shared/schemas/memory.ts. This reports whether FTS5, the embedding source, vector store, Qdrant health, and optional rerank service are active.
An optional rerank service running at http://127.0.0.1:20128/v1/rerank can reorder results after primary retrieval for improved relevance.
Summary
- SQLite as source of truth: All memories persist in the
memoriestable with FTS5 indexing viamemory_fts - Tiered fallback: Retrieval prefers Qdrant, falls back to sqlite-vec, then degrades to FTS5 keyword search
- Hybrid scoring: Reciprocal rank fusion combines vector and keyword results when using sqlite-vec
- Configurable backends: Select between external Qdrant, local sqlite-vec, or pure FTS5 via
settings.vectorStore - Full observability: Engine status endpoint exposes backend health and active retrieval tiers
Frequently Asked Questions
What is the fallback order when retrieving memories in OmniRoute?
OmniRoute attempts retrieval in a strict hierarchy: first Qdrant if configured, then sqlite-vec for local vector search, and finally FTS5 keyword search against the memory_fts virtual table. This ensures queries always return results even when external vector services are unavailable.
How does OmniRoute handle full-text search without a vector store?
When vector stores are disabled or unavailable, the system queries the memory_fts FTS5 virtual table directly using the buildFtsRows helper. This provides fast exact-match and keyword ranking against the indexed content and key columns without requiring embeddings.
Can OmniRoute use both Qdrant and sqlite-vec simultaneously?
The architecture selects a single vector store backend per query based on settings.vectorStore, but the tiered fallback means sqlite-vec acts as a secondary option when Qdrant fails. However, hybrid retrieval with reciprocal rank fusion specifically requires the sqlite-vec backend to combine vector and FTS5 scores locally.
Where is the memory schema defined in the codebase?
The database schema is defined in src/lib/db/migrations/022_add_memory_fts5.sql for the initial tables and src/lib/db/migrations/023_fix_memory_fts_uuid.sql for rowid stabilization. Runtime access patterns are implemented in src/lib/db/memoryVec.ts with TypeScript interfaces declared in src/shared/schemas/memory.ts.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →