Docs
View as Markdown

Relevance

Search results are often only loosely about your query. relevance=score adds an on_topic probability to every row, judged against the query you searched for. relevance=filter does that and also drops the rows that fall below a threshold, telling you exactly which ones it dropped.

Like labels, the score is a signal, not a verdict, and a row that could not be judged is never dropped.

When to use it#

  • A broad search returns noise and you would rather not pay for, store or read the off-topic rows.
  • You want to rank or cut on a number you control (threshold), not on a yes or no.
  • You run multi-platform search and want each platform's off-topic rows removed before the lists are fused.

The request#

Add relevance=filter to a search endpoint. The query it judges against is the one you searched for.

curl "https://api.onekey.dev/v1/hackernews/search?q=rust+compiler&relevance=filter&threshold=0.5" \
  -H "x-api-key: YOUR_API_KEY"
ParameterValuesWhat it does
relevancescore or filterscore adds computed.relevance.on_topic to each row and drops nothing. filter also drops rows below threshold.
threshold0 to 1, default 0.5The cut-off for filter. Only valid together with relevance; otherwise it is a free 422 INVALID_PARAMS.
stricttrue (default) or falseAs on Labels.

You can combine it with label= or include=labels and themes=true on the same call. Relevance runs first, so labels and themes are computed only over the rows that were kept, and you are not charged to label rows that were dropped.

Where it works#

Relevance needs a query to judge against, so it is accepted on the search endpoints that also accept enrichment (7 endpoints on 7 platforms, generated from the specs) and on /v1/search/multi:

The full opt-in table, with every other endpoint that accepts labels and themes, is on Labels. relevance on any other endpoint is a free 422 INVALID_PARAMS.

The response#

Each row gets computed.relevance. The relevance summary sits at the top level next to data, and dropped and dropped_ids are repeated beside it for convenience. Recorded on the pre-release test provider (rows shortened):

json
{
  "success": true,
  "data": [
    {
      "content": { "title": "Writing a Rust compiler in C", "…": "…" },
      "computed": { "relevance": { "on_topic": 1.0 } }
    }
  ],
  "relevance": {
    "query": "rust compiler",
    "threshold": 0.5,
    "status": "complete",
    "rows": 10,
    "labelled": 10,
    "dropped": 0,
    "dropped_ids": [],
    "extra_credits": 4.969,
    "methodology_version": "fake/1"
  },
  "dropped": 0,
  "dropped_ids": [],
  "metadata": { "credits_charged": 9.969, "extra_credits": 4.969, "…": "…" },
  "error": null
}

The summary has the same counters as the labels summary (rows, labelled, cached, skipped, unjudged, status, reason, methodology_version) plus:

FieldMeaning
queryThe text the rows were judged against.
thresholdThe cut-off used.
droppedHow many rows filter removed.
dropped_idsTheir ids, so you can tell exactly what is missing. For a search result the id is the record's own id; a row without one is row-<index>. Only present with filter.

computed.relevance is {"on_topic": p} with p from 0 to 1, or {"on_topic": null} for a row that was not judged.

How to read it honestly#

  • A row that was not judged is kept. on_topic: null is "unknown", and unknown rows are never dropped, so a provider failure part way cannot silently empty your results. Check unjudged and status.
  • dropped_ids is the audit trail. Removing rows is the only place enrichment changes what you get back, so it always lists them. Keep them if you need to show what was excluded.
  • A threshold is your decision. 0.5 is only a default. A higher threshold drops more, and a probability is not a guarantee that a kept row is on topic or that a dropped one was not.
  • Judged on the text in the row. The title or text field named in the opt-in table is what is read, not the page behind a link.
  • methodology_version names the method, as on Labels. Judgments from the pre-release test provider are a keyword heuristic, not model quality.

On /v1/search/multi, relevance is applied to each platform's rows separately, before any fuse=rrf step, so the fused list is built only from kept rows. Each platform's row carries its own relevance, dropped and dropped_ids (ids there are prefixed platform:), and data.relevance adds the legs up. A platform whose search endpoint does not accept enrichment is listed under unsupported, and its rows come back untouched and are never dropped.

What it costs#

A separate charge for each judged row, on top of the search's own price. Rows that were not judged, and texts already judged for the same query in the last 30 days, are free. See AI pricing.

Errors it can return#

StatusTypeWhen
422INVALID_PARAMSrelevance is not score or filter, threshold is outside 0 to 1 or given without relevance, or the endpoint is not a search that accepts enrichment. Free.
429AI_BUDGET_EXCEEDEDNothing could be judged because today's allowance is used up and strict=true.
503AI_UNAVAILABLENothing could be judged because enrichment is off or unavailable and strict=true.
402INSUFFICIENT_CREDITS, KEY_BUDGET_EXCEEDEDThe reservation for the judgments cannot be covered.

See Errors for the full list.