Skip to main content
Version: 3.1

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:

TablePurpose
filesRaw file metadata and optional binary storage
documentsExtracted markdown content with metadata
chunksText segments with token counts
vectorsEmbedding 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.

tip

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:

  1. File Storage — stores the raw file with deduplication (content hash)
  2. Document Extraction — converts the file to markdown (default: Azure Document Intelligence)
  3. Text Cleaning — normalizes whitespace and formatting
  4. Chunking — splits text into token-aware segments (default: markdown-header-based)
  5. Metadata Enrichment — attaches contextual metadata to each chunk
  6. Embedding Generation — creates vector embeddings using an AI model
  7. 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:

  1. Query Preprocessing — normalizes the search query
  2. Query Embedding — converts the query to a vector
  3. Vector Search — finds similar chunks by cosine distance
  4. Metadata Filtering — optionally filters results by metadata
  5. Reranking — re-scores results for relevance
  6. 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

StrategyDefaultPurpose
documentLoaderStrategyAzure Document IntelligenceConverts files to markdown
textCleanerStrategyWhitespace normalizationCleans extracted text
chunkingStrategyMarkdown-aware chunkingSplits text into segments
metadataStrategyPass-through enrichmentAdds metadata to chunks
embeddingStrategyPlatform embedding modelGenerates vector embeddings
postProcessingStrategyResult summaryFinal transformation

Retrieval Strategies

StrategyDefaultPurpose
queryPreprocessingStrategyPass-throughNormalizes the search query
queryEmbeddingStrategyPlatform embedding modelConverts query to vector
vectorSearchStrategyCosine similarity searchFinds similar vectors in DB
metadataFilterStrategyMetadata-based filteringFilters results by metadata
rerankingStrategyPass-throughRe-scores result relevance
postProcessingStrategyLLM-formatted outputFormats 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:

ParameterValuePurpose
maxTokens800Optimal chunk size for embedding models
minTokens100Avoids tiny, low-information chunks
overlapTokens100Preserves context between consecutive chunks

The chunker uses a multi-level splitting hierarchy:

  1. Markdown headers — splits at H1/H2 boundaries, preserving document structure
  2. Paragraphs — recursively splits oversized header sections
  3. 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

  1. Use aktorVektorDatabase() to let the platform manage database initialization and configuration through settings.

  2. Prefer the pipeline functions (aktorVektorEmbeddingPipeline, aktorVektorRetrievalPipeline) over assembling individual stages — they handle storage, error propagation, and stage ordering for you.

  3. Use aktorAISettingEmbeddingModel() for consistent embedding model configuration across your application. This reads from the platform's default embedding provider settings.

  4. Consider the DiskANN index when your dataset grows beyond ~100k vectors. Below that threshold, brute-force cosine search is performant enough.

  5. 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 aktorVektorEmbeddingPipeline to ingest documents into vector storage
  • Use aktorVektorRetrievalPipeline to search by semantic similarity
  • Use aktorAIEmbedding for standalone vector generation
  • Combine retrieval with aktorToTool and aktorAICall for RAG chat applications
  • Customize any pipeline stage by providing your own strategy functions