MemPalace.NET
April 24, 2026 · View on GitHub
Overview
MemPalace.NET uses a pluggable backend architecture for storing embeddings and metadata. The default backend is SQLite-based, offering a zero-dependency local storage solution with brute-force vector similarity search.
Backend Interface
All backends must implement the IBackend and ICollection interfaces defined in MemPalace.Core.Backends.
IBackend
The backend is responsible for:
- Creating and managing palace databases
- Creating, listing, and deleting collections within palaces
- Enforcing embedder identity consistency
- Health checking
Key methods:
ValueTask<ICollection> GetCollectionAsync(
PalaceRef palace,
string collectionName,
bool create = false,
IEmbedder? embedder = null,
CancellationToken ct = default);
ValueTask<IReadOnlyList<string>> ListCollectionsAsync(
PalaceRef palace,
CancellationToken ct = default);
ValueTask DeleteCollectionAsync(
PalaceRef palace,
string name,
CancellationToken ct = default);
ValueTask<HealthStatus> HealthAsync(CancellationToken ct = default);
ICollection
The collection is responsible for:
- CRUD operations on embedded records
- Vector similarity search (cosine distance)
- Metadata filtering via
WhereClauseDSL - Dimension and embedder identity validation
Key methods:
ValueTask AddAsync(IReadOnlyList<EmbeddedRecord> records, CancellationToken ct = default);
ValueTask UpsertAsync(IReadOnlyList<EmbeddedRecord> records, CancellationToken ct = default);
ValueTask<GetResult> GetAsync(IReadOnlyList<string>? ids = null, WhereClause? where = null, ...);
ValueTask<QueryResult> QueryAsync(IReadOnlyList<ReadOnlyMemory<float>> queryEmbeddings, int nResults = 10, ...);
ValueTask<long> CountAsync(CancellationToken ct = default);
ValueTask DeleteAsync(IReadOnlyList<string>? ids = null, WhereClause? where = null, ...);
Error Handling
Backends must throw these specific exceptions:
PalaceNotFoundException— palace does not exist whencreate=falseEmbedderIdentityMismatchException— embedder identity doesn't match collectionDimensionMismatchException— embedding dimensions don't match collectionUnsupportedFilterException— backend cannot handle a specificWhereClausetypeBackendClosedException— backend has been disposed
Default: SQLite Backend
Package: MemPalace.Backends.Sqlite
Dependencies: Microsoft.Data.Sqlite (9.0.0)
Architecture
- One database per palace: Each
PalaceRefmaps to apalace.dbfile in{LocalPath}/palace.dbor{BaseDirectory}/{PalaceId}/palace.dbif noLocalPathis provided. - One table per collection: Collections are stored in tables named
collection_{name}. - Metadata table:
_metatable stores embedder identity and dimensionality per collection.
Schema
Metadata Table
CREATE TABLE _meta (
collection_name TEXT PRIMARY KEY,
embedder_identity TEXT NOT NULL,
dimensions INTEGER NOT NULL
)
Collection Table
CREATE TABLE [collection_{name}] (
id TEXT PRIMARY KEY,
document TEXT NOT NULL,
metadata TEXT NOT NULL, -- JSON
embedding BLOB NOT NULL, -- float32 array as bytes
dim INTEGER NOT NULL
)
Vector Storage
Current implementation: Embeddings are stored as BLOBs (byte arrays of float32 values) and searched using brute-force cosine similarity in C#.
Why brute-force?
- Simple, reliable, no external dependencies
- Sufficient for collections up to ~100K records on modern hardware
- Zero setup overhead
Future options:
- sqlite-vec extension: Native vector search with HNSW indexing. Currently not available as a stable NuGet package, but could be integrated when available.
- Microsoft.SemanticKernel.Connectors.Sqlite: Heavier dependency but Microsoft-stewarded. Considered overkill for MemPalace's needs.
Cosine Distance Computation
distance = 1 - (dot_product / (magnitude_a * magnitude_b))
Lower distances indicate higher similarity. Results are sorted ascending by distance.
Filter Translation
WhereClause objects are translated to SQL using SQLite's json_extract function:
Eq("tag", "test") → json_extract(metadata, '$.tag') = 'test'
Gt("count", 5) → json_extract(metadata, '$.count') > 5
In("status", [1, 2]) → json_extract(metadata, '$.status') IN (1, 2)
And([clause1, clause2]) → (clause1) AND (clause2)
Supported operators: Eq, NotEq, Gt, Gte, Lt, Lte, In, NotIn, And, Or.
Unsupported clauses throw UnsupportedFilterException.
Usage
using MemPalace.Backends.Sqlite;
var backend = new SqliteBackend("/path/to/palaces");
var palace = new PalaceRef("my-palace");
var embedder = ...; // your IEmbedder implementation
var collection = await backend.GetCollectionAsync(
palace,
"documents",
create: true,
embedder: embedder);
// Add records
var records = new[] {
new EmbeddedRecord("id1", "hello world", metadata, embedding1),
...
};
await collection.AddAsync(records);
// Query
var queryResults = await collection.QueryAsync(
new[] { queryEmbedding },
nResults: 10);
// Dispose
await backend.DisposeAsync();
Writing a Custom Backend
- Create a new project referencing
MemPalace.Core. - Implement
IBackendandICollection. - Handle all required exceptions (
PalaceNotFoundException, etc.). - Pass
BackendConformanceTests:public class MyBackendConformanceTests : BackendConformanceTests { protected override IBackend CreateBackend() => new MyBackend(); } - Document vector search strategy (exact, approximate, hybrid).
- Document filter support (which
WhereClausetypes are handled).
Example: Qdrant Backend Stub
public class QdrantBackend : IBackend
{
private readonly QdrantClient _client;
public QdrantBackend(string url)
{
_client = new QdrantClient(url);
}
public async ValueTask<ICollection> GetCollectionAsync(...)
{
// Map palace.Id + collectionName to Qdrant collection
var qdrantCollectionName = $"{palace.Id}_{collectionName}";
if (!await _client.CollectionExistsAsync(qdrantCollectionName))
{
if (!create) throw new PalaceNotFoundException(...);
await _client.CreateCollectionAsync(qdrantCollectionName, ...);
}
return new QdrantCollection(_client, qdrantCollectionName, embedder);
}
// ... implement other methods
}
Conformance Testing
All backend implementations should pass the BackendConformanceTests suite in MemPalace.Tests. This ensures:
- Correct CRUD behavior
- Upsert idempotence
- Query ordering (by distance, ascending)
- Filter operators (Eq, And, etc.)
- Dimension and embedder identity guards
- Collection lifecycle (list, delete)
- Backend state management (closed state handling)
Run tests:
dotnet test --filter "FullyQualifiedName~BackendConformanceTests"
``$
## \text{Performance} \text{Considerations}
**\text{SQLite} \text{Backend}:**
- **\text{Reads}:** \text{O}(\text{n}) \text{for} \text{queries} (\text{full} \text{scan} \text{with} \text{filtering})
- **\text{Writes}:** \text{O}(1) \text{per} \text{record} (\text{indexed} \text{by} \text{ID})
- **\text{Space}:** ~4 \times \text{dimensions} \times \text{record\_count} \text{bytes} \text{for} \text{embeddings}
\text{For} \text{large} \text{collections} (>100\text{K} \text{records}) \text{or} \text{latency}-\text{critical} \text{queries}, \text{consider}:
- **\text{Qdrant}** — \text{distributed}, \text{HNSW}, \text{GPU}-\text{accelerated}
- **\text{Chroma}** — \text{Python}-\text{first}, \text{good} \text{for} \text{hybrid} \text{search}
- **\text{Pinecone}** — \text{managed}, \text{serverless}, \text{high}-\text{scale}
\text{All} \text{of} \text{these} \text{can} \text{be} \text{integrated} \text{by} \text{implementing} \text{the} $IBackend` interface.
## Summary
- **Default:** SQLite with brute-force cosine similarity
- **Pluggable:** Implement `IBackend` + `ICollection` for custom backends
- **Tested:** All backends must pass `BackendConformanceTests`
- **Scalable:** Swap to Qdrant/Chroma/Pinecone as needed
For questions or contributions, see the main [README](../README.md).