Kosha docs
Documentation menu

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.

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.

EndpointDoes
GET /v1/healthzLiveness probe. Returns {"status":"ok"}.
POST /v1/namespaces/{ns}/documentsBulk-index documents into the write buffer.
POST /v1/namespaces/{ns}/flushForce the buffer into a segment, making it queryable.
POST /v1/namespaces/{ns}/searchEverything on the read path — text, vectors, filters, aggregations.
POST /v1/namespaces/{ns}/deleteDelete by filter, via tombstones.
POST /v1/namespaces/{ns}/existsCheck which of a list of document ids are present.
GET /v1/namespaces/{ns}/statsDocument count, segment count, manifest version.
GET /v1/statsThe same, across every namespace on the node.

Authentication is a header, either form:

headers
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.