|
1 | | -# Jigen DB |
2 | | - |
3 | | -**Jigen DB** is a vector database written from scratch in C#. |
4 | | -It is a study/research project focused on vector search use cases and implementation trade-offs. |
5 | | - |
6 | | -The goal is to iteratively explore performance strategies, indexing approaches, and practical optimizations. |
| 1 | +<p align="center"> |
| 2 | + <picture> |
| 3 | + <source media="(prefers-color-scheme: dark)" srcset="assets/jigen-logo-full-dark.png"> |
| 4 | + <img src="assets/jigen-logo-full.png" alt="Jigen DB" width="320"/> |
| 5 | + </picture> |
| 6 | +</p> |
7 | 7 |
|
| 8 | +# Jigen DB |
8 | 9 |
|
9 | | -> Tech stack: **.NET (net8.0 / net10.0)**, **ASP.NET Core** (hosting), **gRPC**. |
10 | | -
|
11 | | ---- |
| 10 | +**Jigen DB** is a vector database written from scratch in C# for the .NET platform. |
12 | 11 |
|
13 | | -## Table of contents |
| 12 | +Use it **in-process** inside your application — like SQLite, but for vector search — or run it as a **standalone server** with gRPC and REST APIs, typed .NET client, and optional built-in text embedding generation powered by ONNX Runtime. |
14 | 13 |
|
15 | | -- [Jigen DB](#jigen-db) |
16 | | - - [Table of contents](#table-of-contents) |
17 | | - - [Prerequisites](#prerequisites) |
18 | | - - [High-level structure](#high-level-structure) |
19 | | - - [Running the gRPC server](#running-the-grpc-server) |
20 | | - - [Using the client](#using-the-client) |
21 | | - - [CORS / gRPC-Web](#cors--grpc-web) |
22 | | - - [Tests](#tests) |
23 | | - - [Troubleshooting](#troubleshooting) |
24 | | - - [HNSW index file (`*.hnsw.index`) is empty](#hnsw-index-file-hnswindex-is-empty) |
25 | | - - [HNSW returns fewer results than brute-force](#hnsw-returns-fewer-results-than-brute-force) |
26 | | - - [gRPC client cannot connect](#grpc-client-cannot-connect) |
27 | | - - [License](#license) |
| 14 | +> Tech stack: **.NET (net8.0 / net10.0)**, **ASP.NET Core**, **gRPC**, **ONNX Runtime**. License: **Apache-2.0**. |
28 | 15 |
|
29 | | ---- |
| 16 | +## Project Overview and Genesis |
30 | 17 |
|
31 | | -## Prerequisites |
| 18 | +This project was born as a research initiative to understand the inner workings of vector databases and to explore how performant a database written in pure C# can be. It also provided a great opportunity to dive deeper into specific C# and .NET runtime internals. |
32 | 19 |
|
33 | | -- A .NET SDK compatible with the project targets (net8.0 and/or net10.0) |
34 | | -- Read/write access to the local database folder used by the server |
35 | | -- Optional ONNX model files if you want to use server-side embedding generation |
| 20 | +**Architecture & Development** |
36 | 21 |
|
37 | | -Check your installation: |
| 22 | + - Core Storage & Persistence: Written entirely from scratch by human developer. |
| 23 | + - HNSW Indexing: Forked from a Microsoft library and heavily optimized/modified to support disk persistence. |
| 24 | + - Server Architecture: Built using a well-known CQRS pattern, written by human. |
| 25 | + - This software has been optimized using Fable. |
| 26 | + - AI-Generated UI: The frontend was written completely by AI. Since UI code isn't the focus of this project, a functional, AI-built interface was more than enough. |
| 27 | + - Testing & Debugging: Handled entirely by a human. |
| 28 | + - Messaging & IPC (Hikyaku): The communication layer uses Hikyaku, a custom fork of MediatR (v12) developed over several years. It introduces out-of-process capabilities via Kafka and RabbitMQ. (Note: The library was previously named Arbitrer, then Axonflow, and finally rebranded to Hikyaku to avoid naming conflicts on NuGet). |
38 | 29 |
|
39 | | -```bash |
40 | | -dotnet --info |
41 | | -``` |
42 | 30 |
|
43 | | -## High-level structure |
44 | 31 |
|
45 | | -- **Server**: [src/Server/Jigen](src/Server/Jigen) hosts the app and loads modules. |
46 | | -- **gRPC module**: [src/Server/Jigen.Grpc](src/Server/Jigen.Grpc) exposes `StoreCollectionService`. |
47 | | -- **Store engine**: [src/Jigen/Jigen.Store](src/Jigen/Jigen.Store) handles storage, indexing and search. |
48 | | -- **HNSW indexer**: [src/Jigen/Jigen.Indexer.HNSW](src/Jigen/Jigen.Indexer.HNSW). |
49 | | -- **Client library**: [src/Client/Jigen.Client](src/Client/Jigen.Client). |
50 | | -- **Tests**: [tests](tests) with integration/unit suites for store, client and HNSW. |
| 32 | +## Highlights |
51 | 33 |
|
| 34 | +- **In-process engine** (`Jigen.Store`): append-only memory-mapped storage, asynchronous ingestion, crash recovery, exact (brute-force) search out of the box. |
| 35 | +- **HNSW index** (`Jigen.Indexer.HNSW`): disk-backed approximate nearest neighbor graph with concurrent inserts, deletes, optional SQ8 quantization and exact reranking. |
| 36 | +- **Server**: multi-database host with gRPC (port 3223) and REST (port 13223) APIs, per-collection search with content filters, periodic durability checkpoints. |
| 37 | +- **Embeddings**: server-side text embedding generation (ONNX), in-process or scaled out to dedicated worker containers over RabbitMQ; CPU by default, GPU execution providers available. |
| 38 | +- **Typed .NET client** (`Jigen.Client`): dictionary-like collections, LINQ predicates translated to server-side filters. |
52 | 39 |
|
53 | | -## Running the gRPC server |
| 40 | +## Installation |
54 | 41 |
|
55 | | -Run the host project: |
| 42 | +**NuGet** (in-process): |
56 | 43 |
|
57 | 44 | ```bash |
58 | | -dotnet run --project src/Server/Jigen/Jigen.csproj |
| 45 | +dotnet add package Jigen.Store # embedded engine (net10.0) |
| 46 | +dotnet add package Jigen.Indexer.HNSW # ANN index for the engine |
59 | 47 | ``` |
60 | 48 |
|
61 | | -Default endpoints are configured in `src/Server/Jigen/Program.cs`: |
| 49 | +**NuGet** (client): |
62 | 50 |
|
63 | | -- `http://localhost:13223` (`Http1AndHttp2`) |
64 | | -- `http://localhost:3223` (`Http2`, gRPC) |
| 51 | +```bash |
| 52 | +dotnet add package Jigen.Client # client for the server (net8.0+) |
| 53 | +``` |
65 | 54 |
|
66 | | -The gRPC service is mapped by the module in [src/Server/Jigen.Grpc/Module.cs](src/Server/Jigen.Grpc/Module.cs). |
| 55 | +**Docker** (server): |
67 | 56 |
|
| 57 | +```bash |
| 58 | +# all-in-one: database + embedding generation in a single container |
| 59 | +docker run -d -p 3223:3223 -p 13223:13223 \ |
| 60 | + -v jigen-data:/data/jigendb -v ./models:/data/onnx \ |
| 61 | + ppossanzini/jigendb-all-in-one |
| 62 | +``` |
68 | 63 |
|
69 | | -## Using the client |
| 64 | +Three images are published: `ppossanzini/jigendb` (server only), `ppossanzini/jigendb-all-in-one` (server + embeddings) and `ppossanzini/jigen-embeddings` (embedding worker). See [Docker deployment](docs/server/docker.md). |
70 | 65 |
|
71 | | -The client package is in [src/Client/Jigen.Client](src/Client/Jigen.Client). |
| 66 | +## Quick taste |
72 | 67 |
|
73 | | -Minimal usage pattern: |
| 68 | +In-process: |
74 | 69 |
|
75 | 70 | ```csharp |
76 | | -using Jigen.Client; |
77 | | -using Jigen.Client.BaseTypes; |
| 71 | +using Jigen; |
| 72 | +using Jigen.DataStructures; |
| 73 | +using Jigen.Extensions; |
| 74 | +using Jigen.Indexer; |
78 | 75 |
|
79 | | -var ctx = new Context(new ConnectionOptions |
| 76 | +using var store = new Store(new StoreOptions |
80 | 77 | { |
81 | | - HostName = "localhost", |
82 | | - Port = 3223, |
83 | | - TLS = false, |
84 | | - DatabaseName = "Test" |
| 78 | + DataBasePath = "/data/jigendb", |
| 79 | + DataBaseName = "demo", |
| 80 | + Indexer = new SmallWorldIndexer(new SmallWorldOptions( |
| 81 | + m: 16, efConstruction: 200, efSearch: 64, storagePath: "/data/jigendb/hnsw")) |
85 | 82 | }); |
86 | 83 |
|
87 | | -var collection = new VectorCollection<MyDocument>(ctx); |
88 | | - |
89 | | -collection.Add(1, new VectorEntry<MyDocument> |
| 84 | +await store.AppendContent(new VectorEntry |
90 | 85 | { |
91 | | - Key = 1, |
92 | | - Content = new MyDocument { Id = Guid.NewGuid(), Text = "hello" }, |
93 | | - Embedding = Array.Empty<float>() |
| 86 | + Id = Guid.NewGuid().ToByteArray(), |
| 87 | + CollectionName = "articles", |
| 88 | + Content = MessagePackDocumentSerializer.Instance.Serialize("hello vectors"), |
| 89 | + Embedding = embedding // float[] from your embedding model |
94 | 90 | }); |
95 | | -``` |
96 | 91 |
|
97 | | -For a concrete wrapper pattern, see [tests/JigenClientTest/Model/DB.cs](tests/JigenClientTest/Model/DB.cs) and [tests/JigenClientTest/UnitTest1.cs](tests/JigenClientTest/UnitTest1.cs). |
| 92 | +var results = store.Search("articles", queryEmbedding, top: 10); |
| 93 | +await store.SaveChangesAsync(); |
| 94 | +``` |
98 | 95 |
|
| 96 | +Client against a server: |
99 | 97 |
|
100 | | -## CORS / gRPC-Web |
| 98 | +```csharp |
| 99 | +using Jigen.Client; |
101 | 100 |
|
102 | | -- General server CORS is configured in [src/Server/Jigen/Program.cs](src/Server/Jigen/Program.cs). |
103 | | -- gRPC-specific CORS policy is configured in [src/Server/Jigen.Grpc/Module.cs](src/Server/Jigen.Grpc/Module.cs). |
| 101 | +var ctx = new Context(new ConnectionOptions { HostName = "localhost", Port = 3223, DatabaseName = "demo" }); |
| 102 | +var articles = new VectorCollection<Article>(ctx); |
104 | 103 |
|
105 | | -Current status: |
| 104 | +articles.Add(Guid.NewGuid(), new Article { Title = "..." }, sentence: "text embedded by the server"); |
| 105 | +var hits = articles.Search("query text", x => x.Category == "news", top: 5); |
| 106 | +``` |
106 | 107 |
|
107 | | -- Native gRPC is enabled (`MapGrpcService<Server>()`). |
108 | | -- gRPC-Web mapping is currently commented out in the module: |
109 | | - - `.EnableGrpcWeb()` |
110 | | - - `.RequireCors(JigenGrpcCorsDefaultPolicy)` |
| 108 | +## Documentation |
111 | 109 |
|
112 | | -If you need browser gRPC-Web clients, enable those lines and verify CORS policy for your frontend origin. |
| 110 | +Full documentation lives in [`docs/`](docs/index.md) (also buildable with MkDocs / Read the Docs). |
113 | 111 |
|
| 112 | +| Section | Contents | |
| 113 | +|---|---| |
| 114 | +| [In-process engine](docs/in-process/overview.md) | [Getting started](docs/in-process/getting-started.md) · [Store options](docs/in-process/store-options.md) · [Collections](docs/in-process/collections.md) | |
| 115 | +| [Indexes](docs/indexes/hnsw.md) | [Brute force](docs/indexes/brute-force.md) · [HNSW](docs/indexes/hnsw.md) | |
| 116 | +| [Embeddings](docs/embeddings/overview.md) | [Configuration](docs/embeddings/configuration.md) · [Execution providers (CPU/GPU)](docs/embeddings/execution-providers.md) | |
| 117 | +| [Server](docs/server/overview.md) | [Configuration](docs/server/configuration.md) · [Docker](docs/server/docker.md) · [REST API](docs/server/rest-api.md) · [gRPC API](docs/server/grpc-api.md) | |
| 118 | +| [Client](docs/client/getting-started.md) | [Usage](docs/client/usage.md) | |
| 119 | +| [Benchmarks & hardware](docs/benchmarks.md) | Current numbers, supported and upcoming CPU/GPU technologies | |
114 | 120 |
|
115 | | -## Tests |
| 121 | +The server also ships a web administration UI (Jigen Insight) served on port 13223; it is not covered by this documentation yet. |
116 | 122 |
|
117 | | -Run all tests: |
| 123 | +## Building from source |
118 | 124 |
|
119 | 125 | ```bash |
120 | | -dotnet test Jigen.sln |
| 126 | +dotnet build Jigen.sln -m:1 # -m:1 required (StaticWebAssets breaks parallel builds) |
| 127 | +dotnet run --project src/Server/Jigen/Jigen.csproj |
121 | 128 | ``` |
122 | 129 |
|
123 | | -Run focused suites: |
| 130 | +Tests: |
124 | 131 |
|
125 | 132 | ```bash |
126 | | -dotnet test tests/HnswTest/HnswTest.csproj |
127 | 133 | dotnet test tests/JigenStoreTests/JigenStoreTests.csproj |
128 | | -dotnet test tests/JigenClientTest/JigenClientTest.csproj |
| 134 | +dotnet test tests/HnswTest/HnswTest.csproj |
129 | 135 | dotnet test tests/PrimitiveTests/PrimitiveTests.csproj |
| 136 | +# tests/JigenClientTest requires a running server on localhost:3223 |
130 | 137 | ``` |
131 | 138 |
|
132 | | - |
133 | | -## Troubleshooting |
134 | | - |
135 | | -### HNSW index file (`*.hnsw.index`) is empty |
136 | | - |
137 | | -If the `*.hnsw.index` file appears empty and HNSW returns no results after restart, ensure your app calls: |
138 | | - |
139 | | -1. `SaveChangesAsync()` |
140 | | -2. `Close()` |
141 | | - |
142 | | -Recent fixes added an explicit indexer flush during store close, so `StoredList` header/index are persisted before process exit. |
143 | | - |
144 | | -### HNSW returns fewer results than brute-force |
145 | | - |
146 | | -This is expected in principle because HNSW is ANN (approximate nearest neighbors), but large gaps usually indicate: |
147 | | - |
148 | | -- too-low `EfSearch` |
149 | | -- insufficient `EfConstruction` |
150 | | -- graph quality issues (construction/traversal bugs) |
151 | | - |
152 | | -Tune `EfSearch` and `EfConstruction` in `SmallWorldOptions` first. |
153 | | - |
154 | | -### gRPC client cannot connect |
155 | | - |
156 | | -Check: |
157 | | - |
158 | | -1. server is running |
159 | | -2. client uses `http://localhost:5001` for HTTP/2 gRPC |
160 | | -3. firewall/port blocks are not present |
161 | | - |
162 | | - |
163 | 139 | ## License |
164 | 140 |
|
165 | | -See [LICENSE.txt](LICENSE.txt). |
166 | | - |
| 141 | +Apache-2.0 — see [LICENSE.txt](LICENSE.txt). |
0 commit comments