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.
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
- Sentence Transformers, Pretrained models: which models return unit-length vectors, and which similarity function each needs.
- Performing similarity search: the lookup this is built on.
- Anomaly detection: the same distance read as strangeness, including the centroid failure.
- OpenAI embeddings API: producing the vectors.
Knowledge check
Question 1 of 4
Sign in to save your progress and pick up where you left off.