Click Feedback
Click feedback records which search results people click and buy, per query. Turn on "Use click feedback in ranking" for an index and the results people pick for a query move up for that query. It works the same way on Meilisearch, Typesense, Elasticsearch and Visor, and needs no experiment.
Early access
queryId and the endpoints below answer 404 click_feedback_not_enabled.How it works
- Every search response carries a
queryId, such asq_7d2k9x1m4q8z0a3b. It names that search. - When someone clicks a result, send a
search_result_clickevent with thequeryIdand the result's document id. Send aconversionwhen they buy or sign up. - Turn on "Use click feedback in ranking" in the dashboard (Indexes, the pointer button on the index) or with the API. Pick a strength. It is off by default.
Send events
/v1/relevance/eventsRecords up to 100 click and conversion events. Search keys can send events (they have analytics.write by default), so you can send them straight from the browser.
An event that cannot be used is listed in rejected with its place in the array and a reason (invalid_type, invalid_doc_id, invalid_query_id, index_not_found, bot and so on). The others are still accepted.
{
"events": [
{
"type": "search_result_click",
"index": "products",
"docId": "123",
"queryId": "q_7d2k9x1m4q8z0a3b",
"position": 3,
"sessionId": "s_8f2k1m"
},
{
"type": "conversion",
"index": "products",
"docId": "123",
"queryId": "q_7d2k9x1m4q8z0a3b",
"sessionId": "s_8f2k1m",
"value": 8900
}
]
}{
"accepted": 2,
"rejected": []
}| Parameter | Type | Default | Description |
|---|---|---|---|
type | string | required | search_result_click (or click) or conversion |
index | string | required | The index the result came from |
docId | string | number | required | The result's document id (its primary key) |
queryId | string | none | The queryId of the search that showed the result. Without it the event counts in the totals but never changes ranking |
position | number | none | 1-based position in the whole result list. When the search showed the result, Headband uses the position it showed it at |
sessionId | string | none | Your visitor's session id, up to 128 characters. Strongly recommended: a search-key event without one counts half |
value | number | none | Non-negative, for a conversion: an order value, for example |
With the React SDK, useClickTracking() fills in the index, the queryId, the position and a session id. It is in @headband/react 0.2.1, which is not on npm yet. Until then, post the events to /v1/relevance/events yourself.
import { useClickTracking, useHits } from "@headband/react";
function Results() {
const { hits } = useHits();
const { trackClick } = useClickTracking();
return hits.map((hit, i) => (
<a key={String(hit.id)} href={String(hit.url)} onClick={() => trackClick(hit, i)}>
{String(hit.title)}
</a>
));
}
// On the order page. It names the search the product was clicked from.
const { trackConversion } = useClickTracking();
trackConversion(productId, { value: 8900 });Without React: client.trackClick(...), client.trackConversion(...) and client.sendEvents([...]) on the headband() client. Events go out with keepalive, so a click that opens a new page still arrives.
Turn it on
/v1/indexes/{uid}/click-feedbackTurns ranking with click feedback on or off for the index, and sets its strength. The setting belongs to the engine the index runs on now: if the index later moves to another engine, ranking starts off there. Send engine to set another engine's value ahead of a move.
Searches pick up a change within 30 seconds. GET on the same path reads it. DELETE deletes every click, conversion and search record of the index and turns ranking off.
{
"enabled": true,
"strength": 0.5
}{
"index": "products",
"engine": "typesense",
"enabled": true,
"strength": 0.5,
"window": 50,
"support": { "ranking": true, "note": "..." },
"engines": { "typesense": { "enabled": true, "strength": 0.5, "updatedAt": "..." } }
}| Parameter | Type | Default | Description |
|---|---|---|---|
enabled | boolean | false | Use click feedback in ranking |
strength | number | 0.5 | 0 to 1. At strength S a result can move up at most S × 50 places, and gets there only with clicks from many visitors |
engine | string | the index's engine | meilisearch, typesense, elasticsearch or visor |
How ranking changes
- Clicks count per query. A click on a result for "trail shoes" lifts it for "trail shoes", not for every query. Case, width and extra spaces do not matter: "Trail Shoes" is the same query.
- Headband asks the engine for the top 50 results, keeps the engine's order as the base, and adds each result's click score for the query. A result outside the top 50 cannot be pulled in, and pages past result 50 are exactly the engine's.
- A conversion counts 5 clicks. A click lower down counts more than one at the top (a click at position 9 counts 3), because people click the first result most whatever it is. A click on a result the named search did not show counts a quarter.
- Scores halve every 14 days, so old clicks fade.
- Some searches are left as the engine ranks them: an empty query, an explicit
sort,pageorhitsPerPagepaging, and a page that needs more than 250 results. TheX-Headband-Click-Feedbackresponse header says what happened:applied,off,empty_query,sorted,paged,deep_page,too_manyorinvalid. /v1/searchand/v1/multi-searchuse it. Hybrid search, AI search and the engines' own APIs do not.
Engines
Every engine gets the same re-rank, so a strength means the same thing whichever engine an index runs on. Nothing is written into your documents and no setting or schema changes, so turning it on never reindexes or rebuilds anything.
| Engine | Ranking with clicks | Notes |
|---|---|---|
| Meilisearch | Yes | Re-ranks the top 50 results of each query. Meilisearch ranking rules sort in sequence and cannot blend a click score into text relevance, so Headband applies it after the engine answers. |
| Typesense | Yes | Re-ranks the top 50 results of each query. Nothing changes in the collection schema, so no reindex is needed. |
| Elasticsearch | Yes | Re-ranks the top 50 results of each query. Your mappings and queries are unchanged. |
| Visor | Yes | Re-ranks the top 50 results of each query. Visor settings are unchanged, so nothing is rebuilt. |
One visitor cannot game it
- The same visitor clicking the same result again within 30 minutes counts once.
- One session, and one network address, can add at most 5 to a result per day. At the default strength that moves it about two places at most.
- A session counts at most 50 events an hour.
- A search key sends at most 120 event requests a minute from one address. Crawlers and headless browsers are refused as
bot. - Admin keys are trusted, for servers that send events for many visitors: no session or address caps, but repeats still count once.
What is stored
| Data | Holds | Kept |
|---|---|---|
| Search records | The query after redaction, the ids of the page shown, a keyed hash of the query | 2 days |
| Events | Type, document id, query id, position, value. The session id and the network address only as keyed hashes; never the address itself, never query text | 2 days |
| Searches per query and day | Counts and the redacted query | 90 days |
| Click scores | Per query and document: the decayed score and click and conversion counts | Until the score fades to nothing, or you delete it |
Before a query is stored, emails, phone numbers, card numbers, US social security numbers, IP addresses, runs of 9 or more digits and Headband keys in it are replaced with tokens such as [email]. The query sent to the engine is not changed. DELETE /v1/indexes/{uid}/click-feedback (or "Delete click data" in the dashboard) deletes everything at once, and deleting an index deletes its click data with it.