Vectorization Functions
Operaide provides built-in vectorization functions for building RAG (Retrieval-Augmented Generation) applications and semantic search. The @operaide/vector package handles the complete lifecycle from document ingestion to vector-based retrieval.
Overview
Vectorization converts documents into numerical vector embeddings that capture semantic meaning. You can then search these embeddings to find content relevant to a user's query. Operaide structures this into two pipelines:
- Embedding Pipeline — ingests files, extracts text, chunks it, generates embeddings, and stores everything in a vector database
- Retrieval Pipeline — takes a search query, converts it to a vector, finds similar chunks, and formats the results
Both pipelines follow a strategy pattern: each stage has a default implementation you can override with your own logic.
Vector Database
Before using either pipeline, you need a vector database. aktorVektorDatabase() creates and initializes a LibSQL database with four tables:
| Table | Purpose |
|---|---|
files | Raw file metadata and optional binary storage |
documents | Extracted markdown content with metadata |
chunks | Text segments with token counts |
vectors | Embedding vectors (F32_BLOB, 1536 dimensions) |
import { aktorVektorDatabase } from '@operaide/vector';
const db = aktorVektorDatabase();
The database ID is configurable via aktorSetting in the Reaktor's settings panel.
For datasets exceeding ~100k vectors, you can enable a DiskANN index for faster approximate nearest-neighbor search. Use aktorInitVectorDB({ client, useIndex: true }) instead of the convenience wrapper. Below 100k vectors, brute-force search is fast enough (~50–150ms).
Embedding Pipeline
aktorVektorEmbeddingPipeline processes a file through seven stages:
- File Storage — stores the raw file with deduplication (content hash)
- Document Extraction — converts the file to markdown (default: Azure Document Intelligence)
- Text Cleaning — normalizes whitespace and formatting
- Chunking — splits text into token-aware segments (default: markdown-header-based)
- Metadata Enrichment — attaches contextual metadata to each chunk
- Embedding Generation — creates vector embeddings using an AI model
- Post-Processing — final transformation and result summary
Parameters
import { aktorVektorEmbeddingPipeline } from '@operaide/vector';
aktorVektorEmbeddingPipeline({
file: Aktor<File>, // The file to process
client: Aktor<Client>, // LibSQL database client
// Optional strategy overrides
documentLoaderStrategy?: AktorDocumentLoaderStrategy,
textCleanerStrategy?: AktorTextCleanerStrategy,
chunkingStrategy?: AktorChunkingStrategy,
metadataStrategy?: AktorMetadataEnricherStrategy,
embeddingStrategy?: AktorEmbeddingStrategy,
postProcessingStrategy?: AktorPostProcessorStrategy,
// Optional model override
embeddingModel?: Aktor<AIProviderModelProps>,
});
Single-File Embedding
import { createAktorComposition, registerReaktorDefinition } from '@operaide/aktor';
import { aktorVektorDatabase, aktorVektorEmbeddingPipeline } from '@operaide/vector';
import { z } from 'zod';
import { aktorBase64ToFile } from '../aktors/embedding.aktor';
const aktorVectorEmbedding = createAktorComposition('aktorVectorEmbedding', ({ base64File }) => {
const file = aktorBase64ToFile({ base64Data: base64File });
const db = aktorVektorDatabase();
return aktorVektorEmbeddingPipeline({
file,
client: db,
});
});
registerReaktorDefinition({
reaktorDefinitionId: 'vector-embedding',
label: 'Vector Embedding',
description: 'Process a file and store its vector embeddings',
aktor: aktorVectorEmbedding,
inputSchema: z.object({
base64File: z.string()
.describe('[file-upload] Base64 encoded file (data URL format)')
.refine((val) => val.startsWith('data:') && val.includes(';base64,'), {
message: 'Must be a data URL with base64 encoding',
}),
}),
outputSchema: z.object({
documentsProcessed: z.number(),
chunksCreated: z.number(),
vectorsStored: z.number(),
errors: z.array(z.string()),
processingTime: z.number(),
}),
});
Multi-File Embedding
For processing multiple files, use aktorForEachStream and aktorStreamProcessor to iterate over an array:
import {
createAktorComposition,
registerReaktorDefinition,
aktorForEachStream,
aktorStreamProcessor,
aktorVar,
} from '@operaide/aktor';
import { aktorVektorDatabase, aktorVektorEmbeddingPipeline } from '@operaide/vector';
import { z } from 'zod';
import { aktorBase64ToFile } from '../aktors/embedding.aktor';
const aktorVectorEmbeddingMulti = createAktorComposition('aktorVectorEmbeddingMulti', ({ base64Files }) => {
const db = aktorVektorDatabase();
const fileStream = aktorForEachStream(base64Files);
const currentBase64 = aktorVar<string>('');
const currentFile = aktorBase64ToFile({ base64Data: currentBase64 });
const pipeline = aktorVektorEmbeddingPipeline({
file: currentFile,
client: db,
});
return aktorStreamProcessor({
stream: fileStream,
currentItem: currentBase64,
processor: pipeline,
});
});
registerReaktorDefinition({
reaktorDefinitionId: 'vector-embedding-multi',
label: 'Vector Embedding (Multi-File)',
description: 'Process multiple files for vector embedding',
aktor: aktorVectorEmbeddingMulti,
inputSchema: z.object({
base64Files: z.array(
z.string().describe('Base64 encoded file')
).describe('[file-upload] Array of base64 encoded files'),
}),
outputSchema: z.array(
z.object({
documentsProcessed: z.number(),
chunksCreated: z.number(),
vectorsStored: z.number(),
errors: z.array(z.string()),
processingTime: z.number(),
})
),
});
Retrieval Pipeline
aktorVektorRetrievalPipeline searches the vector database in six stages:
- Query Preprocessing — normalizes the search query
- Query Embedding — converts the query to a vector
- Vector Search — finds similar chunks by cosine distance
- Metadata Filtering — optionally filters results by metadata
- Reranking — re-scores results for relevance
- Post-Processing — formats results with context (uses LLM)
Parameters
import { aktorVektorRetrievalPipeline } from '@operaide/vector';
aktorVektorRetrievalPipeline({
client: Aktor<Client>, // LibSQL database client
query: Aktor<string>, // Search query
metadataQuery?: Aktor<string | undefined>, // Optional metadata filter
limit?: Aktor<number | undefined>, // Max results (default: 5)
// Optional strategy overrides
queryPreprocessingStrategy?: AktorQueryPreprocessingStrategy,
queryEmbeddingStrategy?: AktorQueryEmbeddingStrategy,
vectorSearchStrategy?: AktorVectorSearchStrategy,
metadataFilterStrategy?: AktorMetadataFilterStrategy,
rerankingStrategy?: AktorRerankingStrategy,
postProcessingStrategy?: AktorRetrievalPostProcessingStrategy,
});
Standalone Retrieval
import { createAktorComposition, registerReaktorDefinition } from '@operaide/aktor';
import { aktorVektorDatabase, aktorVektorRetrievalPipeline } from '@operaide/vector';
import { z } from 'zod';
const aktorVectorRetrieval = createAktorComposition('aktorVectorRetrieval', ({ query, metadataQuery, limit }) => {
const db = aktorVektorDatabase();
return aktorVektorRetrievalPipeline({
client: db,
query,
...(metadataQuery && { metadataQuery }),
...(limit && { limit }),
});
});
registerReaktorDefinition({
reaktorDefinitionId: 'vector-retrieval',
label: 'Vector Retrieval',
description: 'Search documents using semantic similarity',
aktor: aktorVectorRetrieval,
inputSchema: z.object({
query: z.string().describe('The search query'),
metadataQuery: z.string().optional().describe('Optional metadata filter'),
limit: z.number().optional().describe('Max results (default: 5)'),
}),
outputSchema: z.any(),
});
RAG Chat Pattern
A common pattern is giving an LLM agent access to the retrieval pipeline as a tool. This lets the model decide when to search the knowledge base during a conversation:
import { aktorConst, aktorSetting, createAktorComposition } from '@operaide/aktor';
import { z } from 'zod';
import { aktorVektorDatabase, aktorVektorRetrievalPipeline } from '@operaide/vector';
import {
aktorAICall,
aktorAISettingProviderModel,
aktorPatchMessages,
aktorToolSet,
aktorToTool,
registerChatReaktorDefinition,
} from '@operaide/ai';
import type { LlmOptions } from '@operaide/ai';
const aktorDocumentChat = createAktorComposition('aktorDocumentChat', ({ messages }) => {
const db = aktorVektorDatabase();
// Expose retrieval as a tool the LLM can call
const vectorSearchTool = aktorToTool({
aktor: (toolParams: { client: any; query: any }) =>
aktorVektorRetrievalPipeline({
client: toolParams.client,
query: toolParams.query,
}),
description: 'Search the knowledge database using semantic similarity.',
parameters: z.object({
query: z.string().describe('Search query for semantic vector search.'),
}),
dependencies: { client: db },
});
const toolBox = aktorToolSet({ vectorSearchTool });
const prompt = aktorSetting(
z.string().describe('[textarea] System Prompt'),
'You are a helpful assistant with access to a knowledge database. ' +
'Use vectorSearchTool to find relevant information.',
'System Message'
);
return aktorAICall({
messages: aktorPatchMessages({
messages,
system: prompt,
}),
providerModel: aktorAISettingProviderModel(),
tools: toolBox,
llmOptions: aktorConst<LlmOptions>({ max_steps: 5 }),
});
});
registerChatReaktorDefinition({
reaktorDefinitionId: 'document-chat',
label: 'Document Chat',
description: 'Chat with your documents using semantic search',
aktor: aktorDocumentChat,
});
Standalone Embeddings
If you only need to generate a vector from a string — without the full pipeline — use aktorAIEmbedding from @operaide/ai:
import { aktorSetting, createAktorComposition, registerReaktorDefinition } from '@operaide/aktor';
import { aktorAIEmbedding } from '@operaide/ai';
import { z } from 'zod';
const aktorGenerateEmbedding = createAktorComposition('aktorGenerateEmbedding', ({ embedValue }) => {
const model = aktorSetting(
z.object({ provider: z.string(), model: z.string() }),
{ provider: 'openai', model: 'text-embedding-3-small' },
'EmbeddingProviderModel'
);
return aktorAIEmbedding({
providerModel: model,
value: embedValue,
});
});
registerReaktorDefinition({
reaktorDefinitionId: 'vector-embedding',
label: 'Generate Embedding Vector',
description: 'Create a vector from an input string',
aktor: aktorGenerateEmbedding,
inputSchema: z.object({
embedValue: z.string().openapi({ example: 'The quick brown fox' }),
}),
outputSchema: z.object({
value: z.string(),
embedding: z.number().array(),
usage: z.object({ tokens: z.number().optional() }),
}),
});
Customizing Strategies
Both pipelines accept optional strategy overrides. Each strategy is an Aktor function you can replace with your own implementation.
Embedding Strategies
| Strategy | Default | Purpose |
|---|---|---|
documentLoaderStrategy | Azure Document Intelligence | Converts files to markdown |
textCleanerStrategy | Whitespace normalization | Cleans extracted text |
chunkingStrategy | Markdown-aware chunking | Splits text into segments |
metadataStrategy | Pass-through enrichment | Adds metadata to chunks |
embeddingStrategy | Platform embedding model | Generates vector embeddings |
postProcessingStrategy | Result summary | Final transformation |
Retrieval Strategies
| Strategy | Default | Purpose |
|---|---|---|
queryPreprocessingStrategy | Pass-through | Normalizes the search query |
queryEmbeddingStrategy | Platform embedding model | Converts query to vector |
vectorSearchStrategy | Cosine similarity search | Finds similar vectors in DB |
metadataFilterStrategy | Metadata-based filtering | Filters results by metadata |
rerankingStrategy | Pass-through | Re-scores result relevance |
postProcessingStrategy | LLM-formatted output | Formats results with context |
All strategies are exported from @operaide/vector with both plain function and Aktor variants (e.g., markdownChunkingStrategy and aktorMarkdownChunkingStrategy).
Default Chunking Behavior
The default chunking strategy (aktorMarkdownChunkingStrategy) is optimized for embedding quality:
| Parameter | Value | Purpose |
|---|---|---|
maxTokens | 800 | Optimal chunk size for embedding models |
minTokens | 100 | Avoids tiny, low-information chunks |
overlapTokens | 100 | Preserves context between consecutive chunks |
The chunker uses a multi-level splitting hierarchy:
- Markdown headers — splits at H1/H2 boundaries, preserving document structure
- Paragraphs — recursively splits oversized header sections
- Sentences — fallback for very large paragraphs
Token counting uses gpt-tokenizer for accuracy. Overlap is added by appending trailing sentences from the previous chunk to the beginning of the next.
Best Practices
-
Use
aktorVektorDatabase()to let the platform manage database initialization and configuration through settings. -
Prefer the pipeline functions (
aktorVektorEmbeddingPipeline,aktorVektorRetrievalPipeline) over assembling individual stages — they handle storage, error propagation, and stage ordering for you. -
Use
aktorAISettingEmbeddingModel()for consistent embedding model configuration across your application. This reads from the platform's default embedding provider settings. -
Consider the DiskANN index when your dataset grows beyond ~100k vectors. Below that threshold, brute-force cosine search is performant enough.
-
Use the RAG chat pattern (retrieval as a tool) when you want the LLM to decide when to search, rather than always searching on every message.
Summary
Vectorization in Operaide provides a complete pipeline for document ingestion and semantic retrieval:
- Use
aktorVektorEmbeddingPipelineto ingest documents into vector storage - Use
aktorVektorRetrievalPipelineto search by semantic similarity - Use
aktorAIEmbeddingfor standalone vector generation - Combine retrieval with
aktorToToolandaktorAICallfor RAG chat applications - Customize any pipeline stage by providing your own strategy functions