Concepts
Architecture
The two storage planes, the six roles of the one ysearch binary, and how an article travels from a write to a matched, served answer.
ysearch has two storage planes that share a key space, a cluster, and one binary.
| KV plane | Segment plane | |
|---|---|---|
| Holds | documents, traffic, campaigns, dials, locators, hot and provisional match rows | immutable segments, catalog generations, match cells and tables, archived documents |
| Consistency | AP and convergent (CRDT), bounded staleness | immutable objects; one fenced writer per catalog |
| Durability | replicated three ways on PVCs | S3 |
| Storage engine | WavesDB, one database per node | WavesDB checkpoints, one per segment, opened remotely from S3 |
| Code | a Go port of marekvs's design | yolosearch's code, forked and extended |
| Client API | RESP and gRPC | gRPC |
Two rules connect them:
- Everything the segment plane derives is idempotent. A crash means recompute: a recomputed match cell has the same bytes and the same key.
- The KV plane never waits for the segment plane. Indexing, matching, and table building lag behind writes, and the lag is visible.
One binary runs any combination of roles (--roles kv,search). The
Kubernetes operator deploys each role as its own workload, and every pod joins
the cluster gossip.
| Role | Workload | What it does |
|---|---|---|
kv |
StatefulSet with PVCs | a KV plane node; RESP and gRPC; ingest pipelines; traffic aggregation; MATCH.ARTICLE. For partitions it leads: the indexer, the in-memory fresh tail, and provisional matching |
control |
StatefulSet, 3 replicas | a meta Raft group plus one Raft group per namespace, for leases and fencing. Per namespace it runs the publisher, planner, and dial controller |
search |
Deployment | search over published segments plus the fresh tail |
matcher |
Deployment, autoscaled | computes match cells leased from the planner |
compactor |
Deployment | compacts segments; the pod assigned as table writer builds match tables |
source |
Deployment | Kafka-protocol consumers for documents, traffic, campaigns, dials, and ad actions |
The roles that exist on main today are the inherited engine's. How they run
is in deployment.
The slow path, which makes everything durable on S3:
- A writer sends
DOC.PUTto anykvnode. The node runs the namespace's write pipeline, stamps a hybrid logical clock, commits, acknowledges, and replicates. Every node that applies the write appends a changelog entry in the same WavesDB transaction. - The partition's leading owner (H1) drains the changelog into a segment build, uploads it to S3, and submits it to the publisher.
- The publisher adds the segment to the next catalog generation and updates the locator records in the KV plane.
- Every scoring node folds the new segment's statistics into its local statistics database: the next epoch.
- The planner sees new cells, one per new segment and campaign batch. Matchers compute them and write match cells to S3.
- The table writer folds the cells into match tables and writes hot rows into the KV plane.
That takes minutes (A). The owner's target is under 30 seconds from write to matched, so a fast path runs beside it on the H1:
- Fresh tail. Within a second (A) of the changelog entry, the document is in the H1's in-memory index and searchable.
- Provisional matching. The H1 extracts the same statistics-free features a matcher would, scores them under its pinned serving epoch, keeps the pairs above each campaign's frozen floors, and writes a provisional match row.
- Supersession. When the table writer's row for that version arrives, it wins over the provisional one.
MATCH.ARTICLE <article> on any kv node:
- Return nothing if the article is deleted or taken down.
- Read the article's table row: the hot row in the KV plane, or the row in the S3 table. A row whose version differs from the locator's is always dropped.
- Serve the table row for the current version, or else a provisional row for
the current version, or else the older table row with
version_pending. - Score each (campaign, features) entry with the serving epoch and the
campaign's current dial
(α_c, τ_c), and return the campaigns at or aboveτ_c.
A request may name the ad server's placement. That is context for click calibration, not a filter: the ad server decides placement eligibility.
| State | Model |
|---|---|
| Document fields, traffic, campaigns, dials | AP, per-field or per-slot CRDT merges |
| Locators, hot match rows | AP storage with a single fenced writer |
| Namespace registry, enabled capabilities, cluster settings | Raft meta group, 2 of 3 replicas |
| A namespace's leases and fencing counters | that namespace's Raft group |
| Catalog generations | one publisher per namespace, fenced by its lease epoch |
| Segments, cells, tables | immutable; cells are content-addressed |
| Statistics epoch | nobody writes it: a deterministic fold of the catalog |
| Gossip | a hint channel, except for membership |
Membership comes from ysearch's own SWIM-style gossip, which also carries catalog and epoch heads, serving lag, load, and hot-key signals. Authoritative state stays in KV records, Raft logs, and S3.