Recommendation Systems

Intermediate4 min

Nearest neighbours of an item are a recommender you already have. It measures whether two things are about the same subject, which is not the same as predicting what a person will engage with.

#use-cases

Turning search into a recommendation

If you already embed your catalogue for search, you have most of a recommender.

Performing similarity search finds the nearest neighbours of a query. Pass an item instead of a query and the neighbours are the items most like it. That is "more like this", and it costs one lookup.

// Someone viewed item X. Recommend its nearest neighbours.
const { vector } = await store.get(itemId)
const similar = await index.query({ vector, topK: 11 })
const recommendations = similar.filter((r) => r.id !== itemId).slice(0, 10)

Anomaly detection reads the same distance as strangeness. This reads it as affinity. One mechanism, three uses, which is worth checking before you add infrastructure for the third.

What the distance measures

Here is the gap that determines whether this approach works for you.

Embedding similarity measures whether two items are about the same thing. A recommender is usually expected to predict what a person will engage with.

Those overlap enough to be useful and they are not the same claim. Nothing about any person enters the calculation. No purchase history, no click, no rating. The vectors were computed from item text alone, so the system can tell you that two camping stoves are similar and can never learn that people who buy this stove tend to buy a particular book.

That is the trade. Collaborative approaches learn from behaviour and need behaviour to learn from. Content similarity needs none, which means it works on day one and for items nobody has touched, and it will never discover a relationship that is not visible in the text.

Knowing which of those you need is the decision. If your value is "customers also bought", this is the wrong tool. If it is "more like this one", it is close to the right one.

Representing a person rather than an item

To recommend for someone rather than beside an item, you need a vector for them.

Average their recent items. One vector, one query, cheap. It works when someone's interests are coherent and fails when they are not, for the same reason a centroid fails in anomaly detection: the average of camping gear, baby clothes and jazz records is a point near none of them, and the recommendations will be near nothing in particular.

Query from each recent item and merge. Several lookups, then combine and de-duplicate. It preserves distinct interests, costs more per request, and needs a rule for how to interleave results from different interests.

Use the most recent item only. Cheapest, and strongly session-focused. Right when intent is immediate and wrong when someone is browsing across categories.

The second is usually the honest choice for a real person, because real people have several interests at once and the averaging approach quietly assumes they do not.

Three failures that follow from the mechanism

None of these are surprises once you see what the distance measures.

Near-duplicates rank highest. The nearest neighbour of a product is often the same product in another size, or the same article syndicated elsewhere. A recommendation list that is ten variants of the item someone just viewed is technically correct and useless. Any usable version needs a de-duplication or diversity step, and that step is not optional.

Long text ranks higher, if your vectors are not normalised. Whether a model returns unit-length vectors decides which similarity function is appropriate, and with unnormalised vectors Euclidean distance partly measures magnitude, which often tracks description length. The symptom is that items with long descriptions dominate every list. The fix is cosine similarity or normalising first, and it is a one-line fix that is hard to find afterwards.

A new user has nothing. No items, no average, no query. Content similarity does solve the mirror problem, since a brand new item has a position the moment it is embedded, which is where it beats a behaviour-based system. For a new person you need a fallback that is not this system at all: popular items, editor picks, or an explicit question.

When to stop and reach for something else

This approach is doing its job when the request is about similarity: related articles, more products like this one, other documents on the same topic.

Stop when the requirement starts including the word "because". Because other customers did. Because it sold well last quarter. Because this person historically prefers the cheaper option. None of those are in the text, so none of them are in the vectors, and no amount of tuning the similarity function will find them.

At that point you need interaction data and a system built to use it, which is a different project with a different shape. Reaching it is a good outcome: it means the cheap version worked well enough to be worth replacing.

Further reading

Knowledge check

Question 1 of 4

Your product wants "customers also bought". You have embeddings of every item description. What should you conclude?

Sign in to save your progress and pick up where you left off.