Vellar SDK
ExplorerGitHubOpen Vellar Wallet

Architecture

Search and Retrieval

How the Bazaar catalog ranks search results, what the current evaluation shows, and what is honestly not yet measured.

By the end of this page you will understand the two-stage ranking pipeline behind /discovery/search, what the measured quality numbers actually say and where they stop being informative, and what the pre-mainnet plan is for extending evaluation beyond a 19-entry corpus.

The ranking pipeline

/discovery/search runs two independent rankers over the catalog and fuses their outputs with Reciprocal Rank Fusion. Neither stage replaces the other: each is good at a class of query the other handles poorly, and RRF combines them by rank position rather than by score, so the two stages do not have to share a scale.

Stage 1: lexical ranking

The lexical stage scores token overlap between the query and the catalog entry. Its correctness depends less on any single clever step than on both sides of the comparison being processed identically.

Tokenization. The query and the catalog fields are split on the same path, and both sides normalise identically. A tokenizer applied to one side only would silently drop matches that a reader would expect to work.

Synonym expansion. 8 bidirectional groups, expanded before stemming. The order matters: stemming first would transform whole words that the synonym map is keyed on, and the lookups would then miss.

Stemming. 6 suffix rules (tion/sion, ing, ly, ed, er, and plural s with an ss exemption), applied to both sides. Stemming one side against an unstemmed other breaks matches, which is the same symmetry requirement as tokenization.

Field weighting. Not all catalog fields carry equal signal about what a resource is:

FieldWeight
serviceName4x
tags3x
description2x
resource URL1x

Trust ranking. Relevance ties are broken by observed usage: settlements x2 plus uniquePayers. An empty query ranks by trust rather than recency, so a new listing with no settlements ranks below established ones. That is a deliberate choice: recency ordering rewards whoever listed last, trust ordering rewards whoever has actually been paid.

Note: This stage is token-scored relevance, not semantic matching, in the sense the /discovery/search endpoint description uses. Stage 2 is where meaning enters.

Stage 2: semantic ranking

The semantic stage embeds catalog entries with Voyage AI voyage-code-3 at 1024 dimensions and stores one embedding per catalog entry. At query time it computes cosine similarity over an in-memory cache and fuses the resulting ranking with the lexical one via RRF.

Fusion: why rank position rather than score

The two stages produce numbers with no common unit. A lexical score is unbounded and grows with match length, while a cosine similarity is bounded in [-1, 1] and, on this model, clusters tightly around 0.4 to 0.7 even for unrelated pairs. Adding or averaging them compares quantities that do not share a scale, and whichever has the larger spread silently becomes the only one that matters. Ranks are unitless, so neither side can dominate by accident and no per-corpus normalisation has to be re-tuned as the catalog grows.

The fusion is:

rrfScore(lexicalRank, vectorRank, k = 60)
  = 1 / (k + lexicalRank) + 1 / (k + vectorRank)

Ranks are 1-based. k = 60 is the value from the original Cormack et al. paper and the de-facto default across search stacks. Its effect is to flatten the top of each list: at k = 60 the gap between rank 1 and rank 2 is small (1/61 against 1/62), so a resource must rank well in both lists to beat one that is second in both. That is the behaviour wanted here, because the lexical list is the one already known to be good, and a confident-but-wrong vector hit should not displace it on the strength of a single first place.

This is the arm that answers a query sharing no vocabulary with any listing. The lexical stage cannot: with zero token overlap there is nothing to score.

⚠️ A Voyage outage degrades the ranking, it does not break the endpoint. With the semantic arm unavailable, /discovery/search returns lexical results with no semantic component. It does not hang. A query that depends on the semantic arm will rank worse or return nothing, and the response gives no indication that half the pipeline was missing.

Measured quality

Two query sets of 10 each were run against a catalog of 19 entries. Semantic queries share no vocabulary with any listing; keyword queries have direct token overlap.

Semantic queries:

RankerMRRNDCG@3
Lexical only0.2640.263
Hybrid0.7170.789

Keyword queries:

RankerMRRNDCG@3
Lexical only0.9500.963
Hybrid0.9500.963

The shape of these two tables is the entire argument for the architecture. Hybrid improves semantic queries substantially and leaves keyword queries unchanged. That is why hybrid was chosen over replacing the lexical ranker with the semantic one: the lexical stage is already close to ceiling on the queries it is good at, and swapping it out would have risked that for a gain that fusion delivers anyway.

⚠️ These numbers describe a 19-entry catalog, not the ranking in general. At n=19 standard IR metrics cannot distinguish a good ranker from a lucky one. A ranker that had memorised 19 entries would score identically. Read them as evidence that the semantic arm changed something real on this corpus, not as a quality claim about search at scale.

MRR 0.717 on semantic queries means the relevant result appears in the top 1 to 2 positions on average across the 10 test queries. It does not mean the first result is always correct.

What is not yet measured

Stated plainly, so that nobody has to infer it from the absence of a number:

  • No precision@1 or recall@k on a diverse corpus.
  • No human relevance judgment set beyond the current query set.
  • No dev/locked hash split preventing overfitting.
  • No CI regression floor, so a ranking change does not currently fail the build.
  • No ablation across corpus sizes.
  • No measurement of zero-result rate against real user queries.

The last two matter most for anyone assessing whether these results carry to a larger catalog. Without an ablation across corpus sizes there is no evidence about how the fusion behaves as n grows, and without a zero-result measurement against real queries there is no evidence about the queries users actually type.

The honest current state

Before the semantic arm was added, a query sharing no token with any listing returned nothing at all: an empty result set rather than a weak ranking. That was the failure worth fixing, and it is fixed. All 10 semantic test queries now return a relevant result.

What remains is where those results rank, not whether they are retrieved. 5 of the 10 semantic queries reach the top 3 but not first place. The problem has moved from retrieval to ordering, which is a different and narrower problem.

The pre-mainnet plan

Four commitments, scoped as pre-mainnet engineering work rather than a post-launch nice-to-have:

  1. Improve top-1 accuracy on semantic queries. This is a reranking problem rather than a retrieval one, since the relevant results are already being returned.
  2. Extend the eval harness beyond one seller's 19-entry catalog.
  3. Add documented metrics with a published floor that gates the build.
  4. Document the quality-tracking process so ranking regressions are caught before deployment.

See Search evaluation for the fuller account, including the query sets and the reasoning behind each metric.

When it fails

SymptomCauseFix
A query returns nothingNo lexical token overlap and the semantic arm is unavailable, so neither stage can score the corpusRetry once Voyage is reachable, or rephrase using vocabulary that appears in listings (serviceName, tags, description)
A relevant result ranks below a less relevant oneTrust ranking (settlements x2 plus uniquePayers) breaks ties toward established listings, so a new entry with no settlements ranks lowExpected behaviour, not a bug. Sellers accumulate rank by taking settled payments; see Get discovered
Semantic-style queries suddenly rank much worseVoyage is unavailable and the pipeline degraded to lexical ranking only, with no signal in the responseCompare against a known keyword query, which is unaffected, then retry the semantic query later

Next steps