# Relevance

> [!NOTE] **Not enabled yet.** AI enrichment ships when a production model is connected; every number on this page comes from the pre-release test provider and will change. It is switched on per account, so nothing here describes real-world judgment quality yet. Where it is not enabled, a request that asks for it with the default `strict=true` is refused with `503 AI_UNAVAILABLE` at no charge; with `strict=false` you get the rows back unjudged. Enrichment is also switched on per platform: only the endpoints in the [table on the Labels page](https://onekey.dev/docs/labels.md#which-endpoints-accept-it) accept these parameters.

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](https://onekey.dev/docs/labels.md), 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](https://onekey.dev/docs/search-multi.md) 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.

```bash
curl "https://api.onekey.dev/v1/hackernews/search?q=rust+compiler&relevance=filter&threshold=0.5" \
  -H "x-api-key: YOUR_API_KEY"
```

```python
import requests

res = requests.get(
    "https://api.onekey.dev/v1/hackernews/search",
    params={"q": "rust compiler", "relevance": "filter", "threshold": "0.5"},
    headers={"x-api-key": "YOUR_API_KEY"},
)
body = res.json()

if body["success"]:
    print(len(body["data"]), "kept,", body["dropped"], "dropped")
    print(body["relevance"]["status"], body["relevance"]["labelled"], "of", body["relevance"]["rows"], "judged")
else:
    print(body["error"]["type"], body["error"]["message"])
```

```javascript
const params = new URLSearchParams({ q: "rust compiler", relevance: "filter", threshold: "0.5" });
const res = await fetch(`https://api.onekey.dev/v1/hackernews/search?${params}`, {
  headers: { "x-api-key": "YOUR_API_KEY" },
});
const body = await res.json();

if (body.success) console.log(body.data.length, "kept,", body.dropped, "dropped");
else console.error(body.error.type, body.error.message);
```

| Parameter | Values | What it does |
| --- | --- | --- |
| `relevance` | `score` or `filter` | `score` adds `computed.relevance.on_topic` to each row and drops nothing. `filter` also drops rows below `threshold`. |
| `threshold` | 0 to 1, default `0.5` | The cut-off for `filter`. Only valid together with `relevance`; otherwise it is a free `422 INVALID_PARAMS`. |
| `strict` | `true` (default) or `false` | As on [Labels](https://onekey.dev/docs/labels.md#when-nothing-could-be-judged). |

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`](https://onekey.dev/docs/search-multi.md):

- [`bluesky.search`](https://onekey.dev/docs/api/bluesky/search.md)
- [`devto.search`](https://onekey.dev/docs/api/devto/search.md)
- [`gitlab.search`](https://onekey.dev/docs/api/gitlab/search.md)
- [`hackernews.search`](https://onekey.dev/docs/api/hackernews/search.md)
- [`lemmy.search_posts`](https://onekey.dev/docs/api/lemmy/search_posts.md)
- [`reddit.search_posts`](https://onekey.dev/docs/api/reddit/search_posts.md)
- [`stackexchange.search`](https://onekey.dev/docs/api/stackexchange/search.md)

The full opt-in table, with every other endpoint that accepts labels and themes, is on [Labels](https://onekey.dev/docs/labels.md#which-endpoints-accept-it). `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](https://onekey.dev/docs/labels.md#the-response) (`rows`, `labelled`, `cached`, `skipped`, `unjudged`, `status`, `reason`, `methodology_version`) plus:

| Field | Meaning |
| --- | --- |
| `query` | The text the rows were judged against. |
| `threshold` | The cut-off used. |
| `dropped` | How many rows `filter` removed. |
| `dropped_ids` | Their 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](https://onekey.dev/docs/labels.md#which-endpoints-accept-it) is what is read, not the page behind a link.
- **`methodology_version`** names the method, as on [Labels](https://onekey.dev/docs/labels.md#how-to-read-it-honestly). Judgments from the pre-release test provider are a keyword heuristic, not model quality.

## Multi-platform search

On [`/v1/search/multi`](https://onekey.dev/docs/search-multi.md), `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](https://onekey.dev/docs/ai-pricing.md).

## Errors it can return

| Status | Type | When |
| --- | --- | --- |
| 422 | [`INVALID_PARAMS`](https://onekey.dev/docs/errors.md#invalid-params) | `relevance` 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. |
| 429 | [`AI_BUDGET_EXCEEDED`](https://onekey.dev/docs/errors.md#ai-budget-exceeded) | Nothing could be judged because today's allowance is used up and `strict=true`. |
| 503 | [`AI_UNAVAILABLE`](https://onekey.dev/docs/errors.md#ai-unavailable) | Nothing could be judged because enrichment is off or unavailable and `strict=true`. |
| 402 | [`INSUFFICIENT_CREDITS`](https://onekey.dev/docs/errors.md#insufficient-credits), [`KEY_BUDGET_EXCEEDED`](https://onekey.dev/docs/errors.md#key-budget-exceeded) | The reservation for the judgments cannot be covered. |

See [Errors](https://onekey.dev/docs/errors.md) for the full list.
