RAG Pipeline#
Blinko's Retrieval-Augmented Generation (RAG) pipeline lets the AI assistant answer questions grounded in the user's own notes and attachments. When a chat query arrives with withRAG: true (the default), the pipeline retrieves semantically relevant notes, injects their content into the model's system prompt, and streams the response back to the client.
Key entry points:
AiService.completions()β the main chat handler; callsqueryVectorto build context before constructing the agent prompt.AiModelFactory.queryVector()β the retrieval core; embeds the query, searches the vector store, and returns matching notes.AiService.embeddingUpsert()β indexes note content into the vector store.AiService.embeddingInsertAttachments()β indexes attachment content into the vector store.
Vector Store#
Embeddings are stored in a local LibSQLVector database (@mastra/libsql) persisted at file:.blinko/vector/embeddings.db . The store is lazy-initialized on first access in AiUtilities.VectorStore(), which also creates the blinko index if it does not exist.
The index is always named blinko and uses cosine similarity. Dimensions are determined dynamically from the configured embedding model :
| Model family | Dimensions |
|---|---|
| text-embedding-3-small | 1536 |
| text-embedding-3-large | 3072 |
| BGE-M3, Voyage, BGE-large | 1024 |
| BGE / BERT / BCE-base | 768 |
| all-MiniLM | 384 |
| Custom | configurable |
For non-standard models, users must set the dimension explicitly in AI Settings β Embed Settings β Advanced Settings .
Indexing: Notes and Attachments#
Note content#
embeddingUpsert() handles note indexing:
- Tag exclusion: If
excludeEmbeddingTagIdis configured, notes containing that tag are skipped . - Chunking: Content is chunked via
MDocument.fromMarkdown(content).chunk()(from@mastra/rag) . - Embedding: Each chunk is embedded with
embedMany(), and the timestamp (createTime,updatedAt) is appended to the chunk text before embedding . - Upsert: Vectors are stored in the
blinkoindex with metadata:{ text, id, noteId, createTime, updatedAt }. - Status flag: On success,
note.metadata.isIndexedis set totrue.
On updates, the old vectors are deleted first via queryAndDeleteVectorById(), which runs a raw SQL DELETE against the LibSQLVector Turso client .
Attachment content#
embeddingInsertAttachments() handles file attachments:
- Images: Described via
AiModelFactory.describeImage(), which sends the image to a vision-capable LLM and returns a textual description for embedding. - PDFs: Loaded with
PDFLoader(LangChain community). - DOCX / DOC: Loaded with
DocxLoader. - TXT: Loaded with
TextLoader. - CSV: Loaded with
CSVLoader. - Other formats: Fall back to
UnstructuredLoader.
The extracted text is chunked with MDocument.fromText(content).chunk(), embedded, and upserted with metadata { text, id, noteId, isAttachment: true, updatedAt } . A successful run sets note.metadata.isAttachmentsIndexed = true .
Retrieval and Context Construction#
queryVector(query, accountId) is the retrieval function:
- Embed the query: Calls
embed({ value: query, model: Embeddings }). - ANN search: Queries the
blinkoindex withtopK(default:embeddingTopKfrom global config, fallback 3) . - Score filter: Drops results below
embeddingScore(default:0.4) . - Account isolation: Fetches full note records from Postgres via Prisma, filtering by
accountIdand the IDs returned by the vector store β ensuring cross-user data leakage is impossible at the DB layer . - Returns:
{ notes, aiContext }βnotesis the full note array with tags, attachments, references, and comment counts;aiContextis their content joined by newlines .
In completions(), the RAG context is assembled into a single system prompt string :
<prior system messages>
Current user name: <name>
<custom systemPrompt>
This is the note content <note bodies> <aiContext>
All prior system messages are merged into one to avoid provider-specific "multiple system messages" errors (e.g., Qwen/DashScope) .
Rebuilding the Embedding Index#
RebuildEmbeddingJob (at server/jobs/rebuildEmbeddingJob.ts) handles full and incremental index rebuilds:
- Full rebuild: Deletes and recreates the
blinkoindex, then re-embeds all non-recycled notes and their non-image attachments in batches of 5 . - Incremental rebuild: Resumes from saved progress, skipping already-processed note IDs .
- Retry logic: Each note and attachment is retried up to 3 times on failure .
- Progress tracking: Progress is persisted to a cache table (key:
"rebuild-embedding-progress") and updated after every note, enabling resume across restarts . - Cancellation: A
forceStopFlagis checked at the batch and note level to allow graceful shutdown .
Trigger via AiService.rebuildEmbeddingIndex() , which delegates to RebuildEmbeddingJob.ForceRebuild(force).
Note on images during rebuild: Image attachments are explicitly skipped (recorded as
skip) during the rebuild job; they are only embedded on initial attachment insertion viadescribeImage().