Documentation
Kosha
Kosha is a storage-disaggregated search engine written in Rust. S3 holds the only copy that matters, local NVMe is a transparent read-through cache, and every compute node is disposable. It does BM25 lexical search and HNSW vector search over the same documents, through one API.
Alpha
Phase 1 is complete: indexing, BM25 scoring, filtering, aggregations, wildcard and phrase queries, highlighting, and approximate nearest-neighbour search all work today. Reciprocal Rank Fusion and the rerank hook are Phase 2, so lexical and vector retrieval are still requested separately rather than fused server-side.
What it does
Every capability below has its own page. They compose: a single search request can carry a query string, a filter, a sort, a highlight config, and a set of aggregations, and get one response back.
k1/b.
Vector search
HNSW approximate nearest neighbours, with a filter applied to the candidate set.
Filtering
term, terms, range, and bool clauses that also prune whole segments.
Aggregations
Terms buckets, cardinality estimates, and composite paging over the matched set.
Phrase search
Position-aware match_phrase with configurable slop.
Wildcard queries
* and ? patterns against one field, case-insensitive by default.
Highlighting
Matched fragments returned per hit, with your own pre/post tags.
Sorting & pagination
Field sorts, from offsets, and deep search_after cursors.
Schema & field types
Seven field types, and what each one is indexed, stored, and filterable for.
Deletes & updates
Tombstoned deletes, delete-by-query, id upserts, and existence checks.
The shape of the system
Three things are worth internalising before reading any feature page, because every feature behaves the way it does because of them.
S3 is the source of truth
Documents are buffered on write, then flushed into an immutable segment — a self-contained directory of objects in S3 holding the inverted index, the document store, the columnar filter fields, the vector index, and a footer with offsets and statistics. Segments are never mutated, only superseded. Losing a node loses cache warmth, not data.
Local NVMe is a cache, never authoritative
A query node reads segment files through a read-through NVMe cache. A miss costs an S3 GET and then populates the cache; a hit is a local disk read. This is what keeps a disaggregated design at latencies competitive with a cluster that pins every shard to a local disk forever. It is also why a namespace has a cold and a warm latency profile rather than one number.
A manifest gives you a point-in-time view
Each namespace has a versioned manifest: an ordered list of the live segments plus tombstones for deleted documents, published with compare-and-swap semantics. A query reads one manifest version and executes entirely against it, so results are always internally consistent — the same trick Iceberg and Delta Lake use for tables, applied to a search index.
The API in one screen
HTTP/JSON is the primary transport. Every path is versioned under
/v1/, and every request carries an API key that resolves to a
tenant, which is prefixed onto the namespace before anything touches storage.
| Endpoint | Does |
|---|---|
GET /v1/healthz | Liveness probe. Returns {"status":"ok"}. |
POST /v1/namespaces/{ns}/documents | Bulk-index documents into the write buffer. |
POST /v1/namespaces/{ns}/flush | Force the buffer into a segment, making it queryable. |
POST /v1/namespaces/{ns}/search | Everything on the read path — text, vectors, filters, aggregations. |
POST /v1/namespaces/{ns}/delete | Delete by filter, via tombstones. |
POST /v1/namespaces/{ns}/exists | Check which of a list of document ids are present. |
GET /v1/namespaces/{ns}/stats | Document count, segment count, manifest version. |
GET /v1/stats | The same, across every namespace on the node. |
Authentication is a header, either form:
Authorization: Bearer sk-kosha-dev
X-Api-Key: sk-kosha-dev # alternative, for edge/serverless callers
The key maps to a tenant id that is prepended to every namespace argument, so
POST /v1/namespaces/my-index/search from tenant
acme-corp actually reads acme-corp/my-index. Tenants
cannot reach each other's data by guessing a namespace name.
Where to go next
Quickstart gets a server running and a scored result back in under five minutes. After that, the feature pages are written to be read in any order — each one is self-contained and shows the exact request and response JSON.
The design document is the place for the reasoning behind the write path, the compaction policy, and the caching strategy. It is long, opinionated, and argues with itself in places.