Document Type: Architecture Decision Record + Technical Specification
Author: SharpCoreDB Architecture Team
Status: Approved for Implementation
Date: 2026-02
Applies to: SharpCoreDB 1.2.0+
- Executive Summary
- Lessons Learned from Existing Solutions
- Architecture Options Analysis
- Chosen Architecture: Hybrid Native + DI Extension
- SIMD Strategy
- Memory Management & Scaling
- Storage Format
- Index Architecture (HNSW)
- SQL Integration Points
- Thread Safety & Concurrency
- NativeAOT & Trimming Compatibility
- Error Handling Strategy
SharpCoreDB Vector Search adds optional vector embedding storage and similarity search to the existing database engine. The design goal is zero impact on existing users while providing a path from embedded (10K vectors on mobile) to enterprise scale (10M vectors on server).
Key design constraint: SharpCoreDB wants to be both a file-based embedded database AND scale to a full-featured RDBMS. This means:
- Memory usage MUST be configurable and bounded
- Indexing MUST be optional (exact search for small datasets, HNSW for large)
- All features MUST work with both
StorageMode.DirectoryandStorageMode.SingleFile - Vector module MUST NOT introduce mandatory dependencies
What they did right:
- Virtual table approach keeps core SQLite untouched
- Function-based API (
vec_distance_cosine()) is SQL-standard friendly - Binary quantization support from day one
- Builds on SQLite's existing BLOB storage
Mistakes we learn from:
- ❌ MATCH operator syntax is non-standard and confusing (
WHERE embedding MATCH ?)
→ We use: StandardORDER BY vec_distance_cosine(col, @q) LIMIT k - ❌ Virtual table overhead — every row access goes through vtab interface, even for non-vector columns
→ We use: Native column type with BLOB storage, zero overhead for non-vector columns - ❌ No concurrent writes — SQLite's write lock means HNSW build blocks everything
→ We use: Read-write lock on HNSW (concurrent reads, exclusive write) - ❌ C-only implementation means platform-specific builds for each target
→ We use: Pure managed C# — single binary for all platforms
What they did right:
vector(N)column type is clean and intuitive- Supports both IVF and HNSW index types
- Integrated with PostgreSQL's query planner
Mistakes we learn from:
- ❌ Memory explosion with HNSW — no configurable memory limits, graph lives entirely in shared_buffers
→ We add:MaxMemoryMBhard limit, lazy loading, eviction under memory pressure - ❌ Custom operators (
<=>,<->,<#>) are PostgreSQL-specific and not portable
→ We use: Standard function syntaxvec_distance_cosine(a, b)— works in any SQL tool - ❌ No quantization in early versions — had to retrofit scalar/binary quantization
→ We design: Quantization support in the storage format from day one (reserved header bits) - ❌ Index build blocks writes — HNSW index creation on 1M rows blocks all inserts
→ We plan: Background index building with WAL integration (Phase 4)
What they did right:
- Simple API for non-SQL users
- Multiple distance metrics from day one
- Metadata filtering alongside vector search
Mistakes we learn from:
- ❌ Started HNSW-only — had to add flat search later when users complained about small-dataset overhead
→ We start: With flat (exact) search as default, HNSW as optional upgrade - ❌ No persistence story initially — in-memory only, lost data on restart
→ We have: Full persistence from day one (both Directory and SingleFile storage modes) - ❌ Python GIL kills concurrent performance
→ We use: C# withLockclass, no GIL, true parallel reads
What they did right:
- Excellent memory management with memory-mapped segments
- Configurable quantization (scalar, product, binary)
- Pre-filtering + post-filtering hybrid search
Mistakes we learn from:
- ❌ Server-only architecture — can't be embedded
→ We are: Embedded-first, server-optional - ❌ Complex configuration — 50+ config parameters overwhelm users
→ We provide: 3 presets (Embedded, Standard, Enterprise) + fine-grained override
What they did right (and we follow):
SqlVector<float>type with dimension metadataVectorDistance()function — clean, SQL-standard- Integration with existing query planner
Key insight: Microsoft chose function-based syntax over custom operators, validating our approach.
What they did right:
VectorDistance()in standard SQL queries- Support for IVF, HNSW, and DiskANN in one product
ORDER BY VectorDistance(...) LIMIT kpattern
Key insight: The ORDER BY function() LIMIT k pattern is becoming the industry standard for vector search in SQL databases.
Add DataType.Vector to the enum, modify parser, serializer, and function evaluator.
| Aspect | Assessment |
|---|---|
| Backwards compat | ✅ Enum is additive |
| Optionality | |
| Performance | ✅ Zero overhead (branch prediction on DataType switch) |
| Maintenance | ❌ Changes spread across 10+ files, hard to ship as separate NuGet |
| AOT/Trimming | ✅ No reflection |
| Scalability |
Verdict: Good performance but poor modularity. Cannot be a separate NuGet package.
Register vector handling via dependency injection. Core has interfaces, vector module implements them.
| Aspect | Assessment |
|---|---|
| Backwards compat | ✅ Nothing registered = exact same behavior |
| Optionality | ✅ Separate NuGet, opt-in via AddVectorSupport() |
| Performance | |
| Maintenance | ✅ Vector code fully isolated, core stays clean |
| AOT/Trimming | ✅ DI is AOT-safe, no reflection needed |
| Scalability | ✅ Module manages its own memory |
Verdict: Best modularity but slight function call overhead.
Load vector module dynamically at runtime.
| Aspect | Assessment |
|---|---|
| Backwards compat | ✅ DLL not present = no behavior change |
| Optionality | ✅ Physical file presence controls feature |
| Performance | ❌ Reflection-based dispatch, no inlining |
| Maintenance | ❌ Cross-assembly debugging, version compat issues |
| AOT/Trimming | ❌ BLOCKER — AssemblyLoadContext not available in NativeAOT |
| Scalability | N/A — eliminated |
Verdict: ELIMINATED — Incompatible with PublishTieredAot=true in SharpCoreDB.csproj.
VectorTable wraps a regular Table with sidecar HNSW index.
| Aspect | Assessment |
|---|---|
| Backwards compat | ✅ Separate table type |
| Optionality | ✅ Feature flag |
| Performance | |
| Maintenance | |
| AOT/Trimming | ✅ No reflection |
| Scalability |
Verdict: Over-engineered for the benefit. Hooks in Table create coupling.
| Sub-option | Assessment |
|---|---|
| 5A: FAISS via P/Invoke | ❌ No ARM64 builds, cross-platform nightmare, NativeAOT P/Invoke complexity |
| 5B: ML.NET | ❌ ~50MB dependency, massive overkill |
5C: Pure managed System.Numerics.Vector<float> |
✅ Zero dependencies, auto-SIMD, cross-platform |
Verdict: 5A and 5B eliminated. 5C is the SIMD strategy for any architecture choice.
Combine Option 2 (DI extension) + DataType.Vector in core (minimal) + Option 5C (pure SIMD)
The key insight is that we need a two-layer architecture:
-
Core layer (SharpCoreDB): Minimal extension points —
DataType.Vectorenum value,ICustomFunctionProviderinterface, and fallback inParseDataType()/EvaluateFunction(). This is ~50 LOC of changes. -
Extension layer (SharpCoreDB.VectorSearch): All vector logic — serialization, distance metrics, HNSW index, memory management, SQL function implementations. This is a separate NuGet package.
Adding Vector to the DataType enum (one line) gives us:
- Proper type metadata in table schema
- Correct serialization routing without interface dispatch on hot path
- Schema validation (reject non-vector data in VECTOR columns)
- Compatibility with existing tooling (Dapper, EF Core mappers)
The alternative (storing as BLOB with metadata) means every BLOB access must check "is this secretly a vector?" — adding overhead to ALL BLOB queries.
Vector functions (vec_distance_cosine, etc.) are called in query evaluation — a moderately hot path but not the innermost loop. Interface dispatch (~2ns) is negligible compared to the actual distance computation (~50μs for 1536-dim cosine).
┌─────────────────────────────────────────────────────────────┐
│ User Application │
├──────────────┬──────────────────────────────────────────────┤
│ SQL API │ services.AddSharpCoreDB() │
│ │ .AddVectorSupport() ← OPTIONAL │
├──────────────┴──────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────┐ │
│ │ SharpCoreDB (Core) │ │
│ │ │ │
│ │ DataType.Vector (enum) │ ← 1 line change │
│ │ ICustomFunctionProvider │ ← new interface │
│ │ ICustomTypeProvider │ ← new interface │
│ │ SqlFunctions fallback │ ← ~10 LOC change │
│ │ ParseDataType fallback │ ← ~5 LOC change │
│ │ ColumnDefinition.Dimensions │ ← 1 property │
│ │ │ │
│ │ Everything else: UNCHANGED │ │
│ └─────────────┬───────────────────┘ │
│ │ DI registration │
│ ┌─────────────▼───────────────────┐ │
│ │ SharpCoreDB.VectorSearch │ ← SEPARATE NuGet │
│ │ (optional, zero-dependency) │ │
│ │ │ │
│ │ VectorFunctionProvider │ cosine, L2, dot │
│ │ VectorTypeProvider │ serialize/deserialize │
│ │ DistanceMetrics (SIMD) │ System.Numerics.Vector │
│ │ HnswIndex │ pure managed │
│ │ VectorSearchOptions │ memory config │
│ │ VectorSerializer │ float[] ↔ byte[] │
│ └─────────────────────────────────┘ │
│ │
├──────────────────────────────────────────────────────────────┤
│ Storage Layer (unchanged) │
│ ┌──────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │Directory │ │ SingleFile │ │ Columnar │ │
│ │ Storage │ │ (.scdb) │ │ Storage │ │
│ └──────────┘ └──────────────┘ └──────────────┘ │
│ Vectors stored as typed BLOBs — no storage layer changes │
└──────────────────────────────────────────────────────────────┘
| File | Change | LOC |
|---|---|---|
DataTypes.cs |
Add Vector = 10 to enum |
3 |
Interfaces/ICustomFunctionProvider.cs |
New interface | 25 |
Interfaces/ICustomTypeProvider.cs |
New interface | 30 |
Services/SqlFunctions.cs |
Add provider fallback in EvaluateFunction() |
15 |
DataStructures/Table.cs |
Add Vector case to ParseDataType() |
3 |
Services/SqlParser.Helpers.cs |
Add Vector case to ParseValue() |
5 |
Services/SqlAst.DML.cs |
Add Dimensions property to ColumnDefinition |
5 |
DatabaseExtensions.cs |
Accept providers in AddSharpCoreDB() |
10 |
| Total core changes | ~96 LOC |
SharpCoreDB already detects SIMD capabilities:
Avx512F.IsSupported→ 512-bit (16 floats/cycle)Avx2.IsSupported→ 256-bit (8 floats/cycle)Sse2.IsSupported→ 128-bit (4 floats/cycle)AdvSimd.IsSupported→ ARM NEON 128-bit (4 floats/cycle)
Distance computation hot path:
1. Vector<float> loop ← PRIMARY (cross-platform, auto-width)
Uses System.Numerics.Vector<float> which auto-selects
AVX-512/AVX2/SSE2/NEON based on hardware.
Processes Vector<float>.Count floats per iteration.
2. Scalar tail loop ← FALLBACK (handles remaining elements)
Standard float arithmetic for elements that don't fill
a complete SIMD register.
No platform-specific intrinsics needed!
System.Numerics.Vector<T> handles everything.
- Auto-width:
Vector<float>.Countis 4 (SSE2), 8 (AVX2), or 16 (AVX-512) at runtime - Cross-platform: Same code path for x64 and ARM64
- No branching: No need for
if (Avx2.IsSupported)chains - JIT-friendly: RyuJIT aggressively optimizes these operations
- Matches existing pattern: SharpCoreDB already uses
SimdHelperwith similar approach
For 1536-dimension cosine distance (OpenAI embedding size):
| Platform | SIMD Width | Floats/Cycle | Time per Distance | Throughput |
|---|---|---|---|---|
| x64 AVX-512 | 512-bit | 16 | ~0.8 μs | 1.2M/s |
| x64 AVX2 | 256-bit | 8 | ~1.5 μs | 650K/s |
| x64 SSE2 | 128-bit | 4 | ~3 μs | 330K/s |
| ARM64 NEON | 128-bit | 4 | ~3.5 μs | 285K/s |
| Scalar | 32-bit | 1 | ~12 μs | 83K/s |
SharpCoreDB serves both embedded (Raspberry Pi, mobile) and server (64GB RAM) deployments. Vector search memory must be bounded and configurable.
public sealed class VectorSearchOptions
{
// === Memory Limits ===
/// <summary>
/// Hard memory limit for all vector indexes combined (MB).
/// 0 = unlimited (server mode). Default: 256 MB.
/// </summary>
public int MaxMemoryMB { get; set; } = 256;
/// <summary>
/// Load HNSW index only on first vector query (saves startup memory).
/// Default: true for embedded, false for server.
/// </summary>
public bool LazyIndexLoading { get; set; } = true;
/// <summary>
/// Release index memory when GC detects memory pressure.
/// Index will be rebuilt on next query (lazy loading must be enabled).
/// Default: true for embedded/mobile.
/// </summary>
public bool EvictIndexOnMemoryPressure { get; set; } = true;
// === Presets ===
public static VectorSearchOptions Embedded => new()
{
MaxMemoryMB = 50,
LazyIndexLoading = true,
EvictIndexOnMemoryPressure = true,
DefaultIndexType = VectorIndexType.Flat, // Exact search, low memory
MaxDimensions = 1536,
};
public static VectorSearchOptions Standard => new()
{
MaxMemoryMB = 512,
LazyIndexLoading = true,
EvictIndexOnMemoryPressure = false,
DefaultIndexType = VectorIndexType.Hnsw,
MaxDimensions = 4096,
};
public static VectorSearchOptions Enterprise => new()
{
MaxMemoryMB = 0, // Unlimited
LazyIndexLoading = false,
EvictIndexOnMemoryPressure = false,
DefaultIndexType = VectorIndexType.Hnsw,
DefaultQuantization = QuantizationType.Scalar8,
MaxDimensions = 4096,
};
}Per vector (1536-dim, float32):
Raw data: 1536 × 4 bytes = 6,144 bytes = 6 KB
HNSW node: ~256 bytes (M=16, avg 2 layers)
Total: ~6.4 KB per vector
For 100K vectors:
Raw: 600 MB
HNSW graph: 25 MB
Total: ~625 MB
With scalar quantization (int8):
Raw: 150 MB (4x reduction)
HNSW graph: 25 MB (unchanged)
Total: ~175 MB
With binary quantization (1-bit):
Raw: ~19 MB (32x reduction)
HNSW graph: 25 MB (unchanged)
Total: ~44 MB
Dataset Size Recommended Approach Memory
───────────── ──────────────────────────────── ──────────
< 1K Flat (exact), no index < 10 MB
1K - 10K Flat (exact), optional HNSW 10 - 100 MB
10K - 100K HNSW (M=16, ef=200) 100 MB - 1 GB
100K - 1M HNSW + scalar quantization 250 MB - 2.5 GB
1M - 10M HNSW + binary quantization 200 MB - 2 GB
> 10M DiskANN (future Phase 5) Disk-based
Vectors are stored as typed BLOBs in the existing storage layer. The DataType.Vector enum value tells the serializer to use the optimized vector binary format.
Vector Binary Format:
┌──────────────┬───────────┬──────────────────────────────┐
│ Header (8B) │ Dims (4B) │ Float32 Data (dims × 4B) │
├──────────────┼───────────┼──────────────────────────────┤
│ Magic: 0xVEC │ 1536 │ [f32][f32][f32]...[f32] │
│ Version: 1 │ │ │
│ Flags: 0x00 │ │ Total: 1536 × 4 = 6144 bytes│
│ Reserved │ │ │
└──────────────┴───────────┴──────────────────────────────┘
Header flags (future-proof):
Bit 0: Quantization type (00=none, 01=scalar8, 10=binary, 11=reserved)
Bit 2: Normalized flag (1 = pre-normalized, skip norm in cosine)
Bit 3-7: Reserved for future use
- Version header allows future format changes without migration
- Dimension count enables runtime validation (detect column dimension mismatch)
- Flags support quantization without changing the container format
- Magic bytes prevent accidental interpretation of regular BLOBs as vectors
- Overhead: 12 bytes per vector (< 0.2% for 1536-dim) — negligible
HNSW Index Storage Format (sidecar to table data):
┌────────────────────────────────────────────────────────┐
│ Header │
│ Magic: "HNSW" │
│ Version: 1 │
│ Dimensions: uint32 │
│ M: uint16, efConstruction: uint16 │
│ MaxLevel: uint8 │
│ EntryPointId: int64 │
│ NodeCount: uint32 │
├────────────────────────────────────────────────────────┤
│ Node Table (sorted by ID for binary search) │
│ [NodeId: int64][Level: uint8][NeighborCounts: uint8[]] │
│ [L0_Neighbors: int64[]][L1_Neighbors: int64[]]... │
├────────────────────────────────────────────────────────┤
│ Checksum: CRC32 │
└────────────────────────────────────────────────────────┘
Storage location:
Directory mode: {dbPath}/{tableName}_{columnName}.hnsw
SingleFile mode: Stored as a metadata block in .scdb file
HNSW (Hierarchical Navigable Small World) builds a multi-layer proximity graph:
Level 3: [A] ─────────────────── [D]
Level 2: [A] ──── [B] ───────── [D]
Level 1: [A] ── [B] ── [C] ── [D] ── [E]
Level 0: [A]-[B]-[C]-[D]-[E]-[F]-[G]-[H]-[I]-[J]
- Insert: Navigate from top layer to target layer, connect to M nearest neighbors at each layer
- Search: Navigate from top to bottom, expand candidates at level 0 with
ef_search - Complexity: O(log N) for search, O(N log N) for build
| Parameter | Default | Range | Effect |
|---|---|---|---|
M |
16 | 4-64 | Connections per node. Higher = better recall, more memory |
efConstruction |
200 | 50-2000 | Build-time search width. Higher = better index quality, slower build |
efSearch |
50 | 10-500 | Query-time search width. Higher = better recall, slower query |
maxLevel |
auto | - | ln(N) / ln(M) — calculated automatically |
HNSW Operations:
Search (read): Concurrent ← Lock-free, ConcurrentDictionary for nodes
Insert (write): Exclusive ← Lock class for graph modifications
Delete (write): Exclusive ← Tombstone + lazy cleanup
Pattern: ReadWriteLock equivalent via Lock + concurrent data structures
// Each HNSW node:
internal sealed class HnswNode
{
internal long Id; // 8 bytes
internal float[] Vector; // dims × 4 bytes (reference)
internal int Level; // 4 bytes
internal long[][] Neighbors; // ~M × levels × 8 bytes
// For M=16, avg 2 levels, 1536-dim:
// 8 + 4 + (16 × 2 × 8) = 268 bytes per node (excluding vector)
}Affected files in SharpCoreDB core:
SqlParser.DDL.cs:
ExecuteCreateTable() — parse VECTOR(N) type → DataType.Vector + dimensions metadata
NEW: ExecuteCreateVectorIndex() — CREATE VECTOR INDEX ... USING HNSW(...)
NEW: ExecuteDropVectorIndex() — DROP VECTOR INDEX ...
SqlParser.DML.cs:
ExecuteInternal() — add case ("CREATE", "VECTOR") for vector index DDL
ExecuteInsert() — no change needed (vectors pass through as BLOB)
ExecuteSelect() — no change needed (ORDER BY function() already works)
SqlFunctions.cs:
EvaluateFunction() — add provider fallback for vec_* functions
Table.cs:
ParseDataType() — add "VECTOR" case → DataType.Vector
User SQL:
SELECT id, title, vec_distance_cosine(embedding, @query) AS distance
FROM documents
ORDER BY distance
LIMIT 10
Execution flow:
1. SqlParser.ExecuteSelect() → parse columns, FROM, ORDER BY, LIMIT
2. For each column expression:
- "id", "title" → direct column access (unchanged)
- "vec_distance_cosine(embedding, @query)" → FunctionCallNode
3. FunctionCallNode → SqlFunctions.EvaluateFunction("VEC_DISTANCE_COSINE", args)
4. EvaluateFunction → core doesn't know this function → fallback to ICustomFunctionProvider
5. VectorFunctionProvider.Evaluate() → DistanceMetrics.CosineDistance(a, b)
6. Results sorted by distance, LIMIT 10 applied
7. Return top-10 results
Future optimization (Phase 4):
- Query planner detects "ORDER BY vec_distance + LIMIT" pattern
- Routes to HNSW index instead of full table scan
- Index returns pre-sorted candidates, skipping sort step
The codebase uses:
Lockclass (C# 14) for mutual exclusionConcurrentDictionaryfor concurrent accessChannel<T>for producer-consumer patterns- Read-only snapshots for consistent reads
// VectorFunctionProvider: Stateless → thread-safe by design
// VectorSerializer: Static methods, no state → thread-safe
// DistanceMetrics: Pure functions → thread-safe
// HnswIndex: Custom synchronization
public sealed class HnswIndex
{
private readonly Lock _buildLock = new(); // For insert/delete
private readonly ConcurrentDictionary<long, HnswNode> _nodes = new(); // For reads
// Search: Lock-free (reads ConcurrentDictionary + immutable neighbor arrays)
// Insert: Acquires _buildLock (exclusive write)
// Delete: Acquires _buildLock (marks tombstone)
}<TieredPGO>true</TieredPGO>
<TieredPGOOptimize>true</TieredPGOOptimize>
<PublishTieredAot>true</PublishTieredAot>| Requirement | Vector Module Approach |
|---|---|
No Assembly.Load() |
✅ DI registration at compile time |
No Type.GetType() by name |
✅ Generic DI only |
No Activator.CreateInstance() |
✅ new constructor calls only |
System.Text.Json source-gen |
✅ JsonSerializer.Deserialize<float[]>() is AOT-safe |
No Expression.Compile() |
✅ No expression trees |
System.Numerics.Vector<T> AOT |
✅ Fully supported in NativeAOT |
MemoryMarshal AOT |
✅ Intrinsic, fully supported |
// If SharpCoreDB.VectorSearch is not referenced:
// - ICustomFunctionProvider/ICustomTypeProvider remain in core (tiny)
// - All vector code is trimmed away (not referenced)
// - Zero bytes of vector code in final binary// Dimension mismatch
ArgumentException: "Vector has 768 dimensions, column 'embedding' requires 1536"
// Invalid vector data
ArgumentException: "Cannot parse vector: expected JSON float array, got 'hello world'"
// Dimension too large
ArgumentException: "Vector dimensions 8192 exceeds maximum 4096"
// Function argument error
ArgumentException: "vec_distance_cosine requires 2 vector arguments, got 1"// Memory limit exceeded
InvalidOperationException: "HNSW index for 'documents.embedding' would exceed MaxMemoryMB (256 MB).
Consider scalar quantization or increasing the limit."
// Index not found
InvalidOperationException: "No vector index on 'documents.embedding'. Using exact search (slow for large tables)."
// ↑ This is a WARNING, not an error. Falls back to exact search automatically.Scenario Behavior
──────────────────────────────── ──────────────────────────────────────
Vector module not installed vec_* functions → NotSupportedException
HNSW index not created Falls back to exact (flat) search + warning
Memory limit exceeded Reject new HNSW index, existing exact search works
Dimension mismatch on INSERT Reject the INSERT with clear error message
NaN/Inf in vector Reject with validation error
Vector module installed, no use Zero overhead (providers registered but never called)
Rejected because:
- Requires parser changes that affect all query parsing (performance risk)
- Not standard SQL — users can't use the same queries with other tools
- pgvector's biggest complaint is operator confusion
Rejected because:
- SharpCoreDB has no virtual table infrastructure
- Would require building an entire vtab layer just for vectors
- Adds overhead to all row accesses through vtab indirection
Rejected because:
- Breaks single-file deployment model (.scdb)
- Transaction coordination between two files is complex
- Users expect one database = one connection
When SharpCoreDB scales to RDBMS mode:
- HNSW graph sharding across nodes
- Distributed approximate search with result merging
- Replication-aware index invalidation
Microsoft.Extensions.AI.IEmbeddingGenerator<string, Embedding<float>>compatibilitySharpCoreDB.EntityFrameworkCore—HasVectorProperty()+VectorDistance()LINQ supportSharpCoreDB.Data.Provider— ADO.NETDbTypemapping for vectors
Document version: 1.0 | Last updated: 2026-02