Concepts
Matching and dials
Campaigns, the match matrix of segments by campaign batches, cells, match tables, provisional matching, and the reach dial (α_c, τ_c) read at serving time.
| # | Guarantee |
|---|---|
| G1 | Scores are computed at read time under the reader's pinned epoch; stored data never goes stale when an epoch changes |
| G2 | For every campaign whose drift check holds, every live article with s ≥ τ_c is returned, for any τ_c ≥ τ_min(c) |
| G3 | Changing α_c or τ_c above τ_min(c) rewrites nothing |
| G4 | Takedowns and deletes take effect on MATCH.ARTICLE immediately |
| G5 | Every computation is idempotent; a crash means recomputing identical bytes |
| G6 | A new article version is matched within 30 s of its write (target) |
A campaign is a protobuf document in the KV plane. Where it comes from does not matter: every source is a definition kind behind one interface, and every kind compiles to the same compiled campaign.
| Kind | What it is | On main |
|---|---|---|
keywords |
terms with weights, required groups, excluded terms, phrases, optional proximity | yes; proximity is refused |
query |
a JSON DSL, Lucene, or CQP query, in the campaign-safe subset | the query_string dialect only |
text |
ad-creative or landing-page text | no (YS9) |
seeds |
example articles | no (YS9) |
vectors |
caller-supplied vectors with their embedding fingerprint | yes |
matcher |
a registered matcher WASM app (extensions) |
no (YS17) |
A campaign also carries a filter (language, section, publication window), optional floor overrides, and the namespace of articles it targets.
Campaigns are frozen into immutable, content-addressed campaign batches.
What the YS3 prototype compiles, and what it refuses:
query_stringfollows Lucene's classic rules: the default operator is OR,+requires a clause, and-excludes it. A bare term or(a OR b)is scored,+"a b"is a required phrase, andprefix*is a scored prefix. AND, NOT, nesting, boosts, fuzzy, ranges, other wildcards, regexes, and optional or excluded phrases are refused withUNSUPPORTED_QUERY_FEATURE.- Phrases are predicates, checked exactly (slop 0). They are never scored.
- Proximity is refused with
PROXIMITY_UNSUPPORTED. Cells store integer features, and the proximity feature is fractional; it needs an integer definition first. - A term is one token. A term that analyses to more or fewer tokens is
refused (
TERM_ANALYSIS): use a phrase instead.
Compilation is synchronous: PutCampaign refuses a campaign it cannot
compile with FAILED_PRECONDITION and the typed code, never approximates it.
Every published segment must be joined with every active campaign. A new segment adds a row of the matrix; a new or changed campaign adds a column.
A cell is one segment × one campaign batch. Its key is a hash of the
segment, the batch, the field fingerprints, the matcher version, the vector
mode, and G_eval, the generation whose epoch it is evaluated against. A
recomputed cell therefore has the same key and the same bytes.
- Lexical cells percolate each campaign over the segment's forward index.
- Vector cells use an exact threshold scan over the vector lanes, pruned by cluster radii.
- A cell stores both channels' features for every pair above either floor.
Each namespace has a planner, which holds the namespace's planner lease. It creates cells in priority order: new segments against every campaign first, then new campaigns and column recomputes on the newest segments, then everything else. Matchers lease cells and report completions.
The table writer folds cells into two views of the same data:
- by-article: for every article, its matching campaigns and their features. This is the table the ad server's lookup reads.
- by-campaign: for every campaign, its matching articles, which the dial controller ranks.
An article's by-article row is also copied into the KV plane as a hot match
row m, so the ad server's lookup rarely touches S3.
The slow path takes minutes (A), so the partition's leading owner matches each
new article itself: it adds the article to its in-memory fresh tail,
extracts the same features a cell would, scores them under its serving epoch,
keeps the pairs above the campaigns' frozen floors, and writes a
provisional row n. When the table row for that article version arrives,
it supersedes the provisional one. A provisional row is served only when its
version equals the KV plane's current version of the article and no table row
for that version exists yet.
Each campaign has a dial (α_c, τ_c). The served score fuses the two
channels:
s = α_c · b̂ + (1 − α_c) · vb̂ is the lexical score normalised by its maximum, and v is the vector
channel, both in [0, 1]. There is no rank fusion: a rank depends on the
other articles, so it cannot be thresholded per article.
The dial controller turns a campaign's target into τ_c:
| Target | How τ_c is chosen |
On main |
|---|---|---|
threshold |
τ is the threshold |
yes |
count K |
the K-th largest s among the campaign's live rows (fewer than K rows give τ_min) |
yes |
impressions I over a window |
the largest τ whose articles' forecast impressions reach I |
no: needs the forecast, planned for YS4 and YS9 |
pacing |
a bounded controller on the measured delivery rate | no |
ctr_threshold |
a raw threshold from click calibration (needs the calibrator, YS12) | no |
Every computed τ_c is clamped to at least τ_min(c), the lowest cutoff at
which the stored set is exact. If a target needs less, the dial reports
BELOW_TAU_MIN. The specification has the planner then widen the campaign's
stored set with a new column. Nodes check the clamp themselves, and never
serve an inexact set silently.
A dial carries one of these statuses:
| Status | Meaning |
|---|---|
OK |
τ_c meets the target |
BELOW_TAU_MIN |
the target needs less than τ_min(c); τ_c is clamped to τ_min(c) |
FLOOR_PENDING |
a widened stored set is being computed |
DRIFT_RECOMPUTING |
the drift check failed; the column is being recomputed on a new baseline |
UNSUPPORTED_TARGET |
an impressions, pacing or ctr_threshold target; the last τ is kept, and a dial that never had one is not served |
How the YS3 prototype fills the gaps in the specification:
- Default dial. A campaign with no
SetDialhas the targetthreshold = 0andα = 1(0 without a lexical channel). It is therefore served atτ_min(c), with statusBELOW_TAU_MIN. - A dial the controller has not written yet makes the campaign
pending. It is never served at a guessed threshold. - Widening is manual. An operator lowers the campaign's floors; the next
freeze makes a new baseline, and the dial reports
FLOOR_PENDINGuntil it is complete. - A dial change writes only the dial.
SetDialwrites the target andα; the next round writes the controller's output. No cell, table, or object is rewritten (verified below).
MATCH.ARTICLE <article> returns the campaigns whose score, under the serving
epoch and the current dial, is at or above τ_c. A request can add
PLACEMENT <id> as calibration context, and CAMPAIGNS id… to restrict the
answer to the campaigns the ad server allows. The ad server, not ysearch,
decides placement eligibility. The complete lookup is in
architecture.
MATCH.PAIR explains one article × campaign pair: its features, its score
breakdown, and τ_min, or why it is absent. Its state is one of:
| State | Meaning |
|---|---|
served |
the pair is in the served set under the current dial |
below_tau |
the pair is stored, but its score is under τ_c |
not_stored |
below both floors, or a predicate dropped it |
pending |
no completed cell covers the article yet |
version_pending |
the stored row is not the article's current version |
deleted |
the article is deleted |
unknown_campaign |
no such campaign |
inactive |
the campaign is paused or archived |
ysearch.v1.Matching (api/ysearch/v1/matching.proto) is the YS3
matcher's client API. The full message list is in the
gRPC reference.
| RPC | What it does |
|---|---|
Article |
MATCH.ARTICLE, with its receipts: table generation, fencing epoch, pinned epoch and digest, pending and clamped campaigns |
Pair |
MATCH.PAIR |
CampaignMatches |
streams a campaign's served set under its dial, or every stored row |
PutCampaign, GetCampaign |
store and read a campaign as its JSON document; compilation runs synchronously |
SetCampaignStatus |
ACTIVE, PAUSED or ARCHIVED |
SetDial, GetDial |
set α and a threshold or count target; read the dial with its effective τ, τ_min and status |
Status |
coverage, the checked generation G_chk, the table generation, clamps |
ArticleMatches |
declared, answers UNIMPLEMENTED |
Refusals are typed. A request with unknown fields anywhere is
INVALID_ARGUMENT. Every matcher error carries its code as an ErrorInfo
reason in the domain ysearch.match. Match on the reason, never on the
message.
| Reason | gRPC code | When |
|---|---|---|
INVALID_ARGUMENT |
INVALID_ARGUMENT |
unknown fields, malformed request |
UNSUPPORTED_PARAMETER |
INVALID_ARGUMENT |
placement_id or site_id, until placement records exist (YS9) |
CAMPAIGNS_TOO_MANY |
INVALID_ARGUMENT |
more than 10⁴ eligible campaigns |
EXPLORE_DISABLED |
FAILED_PRECONDITION |
explore_request_id: exploration is off in v1 |
EPOCH_LAG |
UNAVAILABLE |
the node lags too far behind to pin a serving epoch |
PROXIMITY_UNSUPPORTED, UNSUPPORTED_QUERY_FEATURE, TERM_ANALYSIS, … |
FAILED_PRECONDITION |
a campaign that does not compile |
go run ./tools/matchcheck is the YS3 exit check. It publishes YS1 corpus
articles in catalog generations, runs the matcher, and compares every
article's MATCH.ARTICLE answer with a brute-force evaluation of every
pair, score bits included. Then it changes dials and counts what was
written.
The receipt bench/ys3/exit-100k-1k.json in the ysearch repository covers
10⁵ articles × 10³ campaigns (darwin/arm64, 24 CPUs, Go 1.27.0):
| Verified | Result |
|---|---|
| served sets equal brute force | 0 mismatches over 227.5·10⁶ pair evaluations, bit-exact scores |
| checked campaigns | 910; 54 refused with PROXIMITY_UNSUPPORTED, about 36 paused by the generator |
| a dial change writes only the dial | 218 target writes, 216 dial output writes, 0 other KV records, 0 objects, no table generation |
Not measured:
- The matcher's cost. The 233 s wall time covers the whole check on one host: segment builds, folds, cells, tables, and mostly the brute-force oracle. No per-stage cost is claimed from it.
- The KV plane: the check uses an in-memory sidecar fake and in-memory objects.
- The exact vector path at that scale. The YS1 corpus has no vectors, so it is checked only on a synthetic 3,000 × 200 subset.
The specification sizes the system for about 10⁶ new or updated articles per day and 10⁵ active campaigns, with 2·10⁵ stored matches per campaign on average. Every one of these is (A): assumed, not measured. YS4 runs the prototype on 3·10⁶ articles and 10⁴ campaigns and replaces them with receipts.