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

Click feedback is turned on per organization while it is in early access. Until it is on for yours, search responses carry no queryId and the endpoints below answer 404 click_feedback_not_enabled.

How it works

  1. Every search response carries a queryId, such as q_7d2k9x1m4q8z0a3b. It names that search.
  2. When someone clicks a result, send a search_result_click event with the queryId and the result's document id. Send a conversion when they buy or sign up.
  3. 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

POST/v1/relevance/events

Records 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.

Auth admin or search keyReturns 202
Request body
{
  "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
    }
  ]
}
Response
{
  "accepted": 2,
  "rejected": []
}
ParameterTypeDefaultDescription
typestringrequiredsearch_result_click (or click) or conversion
indexstringrequiredThe index the result came from
docIdstring | numberrequiredThe result's document id (its primary key)
queryIdstringnoneThe queryId of the search that showed the result. Without it the event counts in the totals but never changes ranking
positionnumbernone1-based position in the whole result list. When the search showed the result, Headband uses the position it showed it at
sessionIdstringnoneYour visitor's session id, up to 128 characters. Strongly recommended: a search-key event without one counts half
valuenumbernoneNon-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.

React
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

PATCH/v1/indexes/{uid}/click-feedback

Turns 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.

Auth admin key onlyReturns 200
Request body
{
  "enabled": true,
  "strength": 0.5
}
Response
{
  "index": "products",
  "engine": "typesense",
  "enabled": true,
  "strength": 0.5,
  "window": 50,
  "support": { "ranking": true, "note": "..." },
  "engines": { "typesense": { "enabled": true, "strength": 0.5, "updatedAt": "..." } }
}
ParameterTypeDefaultDescription
enabledbooleanfalseUse click feedback in ranking
strengthnumber0.50 to 1. At strength S a result can move up at most S × 50 places, and gets there only with clicks from many visitors
enginestringthe index's enginemeilisearch, 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, page or hitsPerPage paging, and a page that needs more than 250 results. The X-Headband-Click-Feedback response header says what happened: applied, off, empty_query, sorted, paged, deep_page, too_many or invalid.
  • /v1/search and /v1/multi-search use 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.

EngineRanking with clicksNotes
MeilisearchYesRe-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.
TypesenseYesRe-ranks the top 50 results of each query. Nothing changes in the collection schema, so no reindex is needed.
ElasticsearchYesRe-ranks the top 50 results of each query. Your mappings and queries are unchanged.
VisorYesRe-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

DataHoldsKept
Search recordsThe query after redaction, the ids of the page shown, a keyed hash of the query2 days
EventsType, document id, query id, position, value. The session id and the network address only as keyed hashes; never the address itself, never query text2 days
Searches per query and dayCounts and the redacted query90 days
Click scoresPer query and document: the decayed score and click and conversion countsUntil 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.