Data Schema
The vectorization system uses a simple 4-table hierarchy that separates concerns and provides flexibility for document processing and retrieval.
Schema Overview
UML Entity-Relationship Diagram
Database Tables
Files Table
Stores raw file metadata and source information.
CREATE TABLE IF NOT EXISTS files (
file_id TEXT PRIMARY KEY,
filename TEXT NOT NULL,
source_path TEXT,
url TEXT,
extension TEXT,
mime_type TEXT,
size_bytes INTEGER,
uploaded_at TEXT NOT NULL DEFAULT (datetime('now')),
file_hash TEXT NOT NULL,
binary_data BLOB
);
Purpose: Track original files with optional binary content storage
- Flexible Sources: Support both local files (
source_path) and remote files (url) - File Metadata: Extension, MIME type, and size for processing decisions
- Binary Storage: Optional
binary_datafield stores the actual file content - HTTP File Serving: Files with
binary_datacan be served directly via the built-in HTTP endpoint at/databases/:databaseId/files/:fileId. See File Serving for details. - Deduplication: Use
file_hashto prevent duplicate uploads - Audit Trail: Track when files were uploaded
Documents Table
Stores processed content extracted from files.
CREATE TABLE IF NOT EXISTS documents (
document_id TEXT PRIMARY KEY,
file_id TEXT NOT NULL,
title TEXT NOT NULL,
markdown TEXT NOT NULL,
metadata JSON DEFAULT '{}',
processed_at TEXT NOT NULL DEFAULT (datetime('now')),
content_hash TEXT NOT NULL,
FOREIGN KEY (file_id) REFERENCES files(file_id) ON DELETE CASCADE
);
Purpose: Store processed, searchable content
- Markdown Format: Standardized content format for all document types
- One-to-Many: One file can produce multiple documents (e.g., PDF chapters)
- Flexible Metadata: JSON field for document-level information
- Processing Tracking: Know when content was extracted
- Content Integrity: Hash for change detection
Chunks Table
Stores text segments optimized for embedding and retrieval.
CREATE TABLE IF NOT EXISTS chunks (
chunk_id TEXT PRIMARY KEY,
document_id TEXT NOT NULL,
chunk_index INTEGER NOT NULL,
text TEXT NOT NULL,
token_count INTEGER NOT NULL,
metadata JSON DEFAULT '{}',
FOREIGN KEY (document_id) REFERENCES documents(document_id) ON DELETE CASCADE
);
Purpose: Break documents into searchable segments
- Ordered Chunks:
chunk_indexmaintains document structure - Token Tracking: Monitor embedding costs and context limits
- Flexible Metadata: JSON field for strategy-specific data
- Configurable Size: Support different chunking strategies
Vectors Table
Stores embeddings for similarity search.
CREATE TABLE IF NOT EXISTS vectors (
chunk_id TEXT PRIMARY KEY,
vector F32_BLOB(1536) NOT NULL,
embedding_model TEXT NOT NULL,
FOREIGN KEY (chunk_id) REFERENCES chunks(chunk_id) ON DELETE CASCADE
);
-- LibSQL vector index for similarity search
CREATE INDEX IF NOT EXISTS vectors_idx ON vectors (libsql_vector_idx(vector));
Purpose: Enable fast similarity search
- LibSQL Vectors: Native F32_BLOB type for efficient storage
- Model Tracking: Know which embedding model was used
- Vector Index: Optimized similarity search with DiskANN
- Separate Storage: Allows re-embedding without re-chunking
Design Rationale
Why Separate Tables?
1. Processing Pipeline Flexibility
Each table represents a distinct processing stage:
File Upload → Content Extraction → Chunking → Embedding
You can:
- Upload files without immediate processing
- Extract content without chunking
- Chunk without embedding (useful for batch processing)
- Re-embed with new models without re-processing
2. Storage Optimization
- Files: Metadata only, no content duplication
- Documents: Text storage optimized for reading
- Chunks: Indexed for retrieval
- Vectors: Specialized storage with vector indexes
3. Multiple Embeddings
The separation allows:
- Testing different embedding models on same chunks
- A/B testing retrieval quality
- Gradual migration to new models
- Multi-modal embeddings (future)
4. Clear Data Ownership
- Files: Source tracking and audit
- Documents: Content ownership and licensing
- Chunks: Processing configuration
- Vectors: Model and cost tracking
Why This Structure?
Files → Documents Separation
- Multiple Formats: PDF, Word, HTML → unified Markdown
- Multiple Documents: One PDF → multiple chapters/sections
- Processing Status: Track failed extractions separately
- Source Preservation: Keep original metadata without storing files
Documents → Chunks Separation
- Flexible Chunking: Different strategies for different content
- Overlap Control: Chunks can share content at boundaries
- Size Optimization: Balance between context and efficiency
- Metadata Enrichment: Add chunk-specific information
Chunks → Vectors Separation
- Model Evolution: Upgrade embeddings without reprocessing
- Cost Management: Track embedding API usage
- Selective Embedding: Not all chunks need vectors
- Multiple Embeddings: Compare different models
TypeScript Types
Database Record Types (with Zod Validation)
import { z } from 'zod';
// File Record Schema
export const FileRecordSchema = z.object({
file_id: z.string(),
filename: z.string(),
source_path: z.string().optional(),
url: z.string().optional(),
extension: z.string().optional(),
mime_type: z.string().optional(),
size_bytes: z.number().optional(),
uploaded_at: z.string().optional(),
file_hash: z.string(),
binary_data: z.instanceof(Buffer).optional(),
});
export type FileRecord = z.infer<typeof FileRecordSchema>;
// Document Record Schema
export const DocumentRecordSchema = z.object({
document_id: z.string(),
file_id: z.string(),
title: z.string(),
markdown: z.string(),
metadata: z.string(), // JSON string
processed_at: z.string(),
content_hash: z.string(),
});
export type DocumentRecord = z.infer<typeof DocumentRecordSchema>;
// Chunk Record Schema
export const ChunkRecordSchema = z.object({
chunk_id: z.string(),
document_id: z.string(),
chunk_index: z.number(),
text: z.string(),
token_count: z.number(),
metadata: z.string(), // JSON string
});
export type ChunkRecord = z.infer<typeof ChunkRecordSchema>;
// Vector Record Schema
export const VectorRecordSchema = z.object({
chunk_id: z.string(),
vector: z.instanceof(Float32Array), // LibSQL F32_BLOB
embedding_model: z.string(),
});
export type VectorRecord = z.infer<typeof VectorRecordSchema>;
Pipeline Processing Types
// Document metadata interface
export interface DocumentMetadata {
type: string;
source: string;
createdAt?: string;
tags?: string[];
[key: string]: any;
}
// Base document chunk interface
export interface DocumentChunk {
id: string;
documentId: string;
content: string;
index: number;
metadata: DocumentMetadata;
}
// Enriched chunk with additional metadata
export interface EnrichedChunk extends DocumentChunk {
enrichedMetadata?: Record<string, any>;
}
// Embedded chunk with vector embedding
export interface EmbeddedChunk extends EnrichedChunk {
embedding: number[];
embeddingModel: string;
embeddedAt?: string;
}
// Stored chunk with database fields
export interface StoredChunk extends DocumentChunk {
chunk_id: string;
token_count: number;
stored_at: string;
}
// Stored enriched chunk
export interface StoredEnrichedChunk extends EnrichedChunk {
chunk_id: string;
token_count: number;
stored_at: string;
}
// Vector storage result
export interface StoredVector {
chunkId: string;
documentId: string;
stored: boolean;
error?: string;
}
// Vector with full data
export interface StoredVectorData extends StoredVector {
vector: Float32Array;
embedding_model: string;
stored_at: string;
}
// Processing pipeline result
export interface ProcessingResult {
documentsProcessed: number;
chunksCreated: number;
vectorsStored: number;
errors: string[];
processingTime: number;
}
Query Patterns
Search Query with Joins
SELECT
c.chunk_id,
c.text as content,
c.metadata,
d.title as document_title,
f.filename as source_file,
vector_distance_cos(v.vector, vector32(?)) as distance
FROM vector_top_k('vectors_idx', vector32(?), ?) AS top_k
JOIN vectors v ON v.chunk_id = top_k.id
JOIN chunks c ON c.chunk_id = v.chunk_id
JOIN documents d ON d.document_id = c.document_id
JOIN files f ON f.file_id = d.file_id
ORDER BY distance ASC;
Document Chunk Count
SELECT
d.document_id,
d.title,
COUNT(c.chunk_id) as chunk_count,
SUM(c.token_count) as total_tokens
FROM documents d
LEFT JOIN chunks c ON c.document_id = d.document_id
GROUP BY d.document_id;
Best Practices
ID Generation
- Use descriptive prefixes:
file_,doc_,chunk_ - Include timestamps for debugging
- Ensure uniqueness across distributed systems
Metadata Usage
Document metadata JSON:
- Language (detected or specified)
- Tags and categories
- Summary or abstract
- Author information
- Processing method used
- Document version
- Custom application data
Chunk metadata JSON:
- Section titles and hierarchy
- Page numbers (for PDFs)
- Language detection results
- Quality scores
- Semantic coherence scores
- Entity mentions
- Custom strategy data
Data Integrity
- Use transactions for multi-table operations
- Cascade deletes to maintain consistency
- Hash verification for content changes
- Regular cleanup of orphaned records
Performance
- Index foreign keys for join performance
- Batch inserts for large documents
- Use appropriate chunk sizes (512 tokens recommended)
- Monitor vector index performance
Available CRUD Operations
File Operations
aktorCreateFile- Create new file recordaktorGetFileById- Retrieve file by IDaktorListFiles- List files with paginationaktorUpdateFile- Update file metadataaktorDeleteFile- Delete file and cascade to related records
Document Operations
aktorCreateDocument- Create new document recordaktorGetDocumentById- Retrieve document by IDaktorGetDocumentsByFileId- Get all documents for a fileaktorListDocuments- List documents with paginationaktorUpdateDocument- Update document content or metadataaktorDeleteDocument- Delete document and cascade to chunks/vectors
Chunk Operations
aktorCreateChunk- Create new chunk recordaktorGetChunkById- Retrieve chunk by IDaktorGetChunksByDocumentId- Get all chunks for a documentaktorListChunks- List chunks with paginationaktorUpdateChunk- Update chunk content or metadataaktorDeleteChunk- Delete chunk and cascade to vectorsaktorCreateMultipleChunks- Batch create chunks
Vector Operations
aktorCreateVector- Create new vector record with upsert logicaktorGetVectorByChunkId- Retrieve vector by chunk IDaktorGetVectorsByDocumentId- Get all vectors for a documentaktorListVectors- List vectors with paginationaktorUpdateVector- Update vector embedding or modelaktorDeleteVector- Delete single vectoraktorDeleteVectorsByDocumentId- Delete all vectors for a documentaktorCreateMultipleVectors- Batch create vectorsaktorSearchSimilarVectors- Vector similarity search
Storage Operations
aktorStoreChunk- Store document chunk with metadataaktorStoreMultipleChunks- Batch store chunksaktorStoreEnrichedChunk- Store enriched chunk with metadataaktorStoreMultipleEnrichedChunks- Batch store enriched chunksaktorStoreEmbeddedChunkVector- Store embedded chunk as vectoraktorStoreMultipleEmbeddedChunkVectors- Batch store embedded chunks as vectorsaktorGetStoredChunk- Retrieve stored chunk by IDaktorGetStoredChunksByDocumentId- Get stored chunks for documentaktorGetStoredVectorData- Get vector with full metadataaktorSearchSimilarVectorsWithChunks- Search with chunk context
Future Considerations
The schema is designed to support:
- Multi-modal embeddings: Image/audio vectors in same structure
- Version control: Document revision tracking
- Access control: User/group permissions per document
- Hybrid search: Combined vector and full-text search
- Incremental updates: Chunk-level document updates