04 · Features and representations

Embeddings and similarity search

9 min read

An embedding is a list of numbers, usually a few hundred of them, that stands for one item, user or query. Two things with similar meaning get lists that are close together. It lets a system answer ‘find me things like this’ with arithmetic instead of rules. The design decisions it changes are how the lists are learned, how closeness is measured, how the search over millions of lists is kept fast, and how the lists are kept compatible when the model that produces them changes.

An everyday example

A well-run clothes shop arranges its floor so that similar things sit near each other. Plain white shirts are next to striped shirts, which are next to light jumpers, and a customer who holds up a shirt and asks for ‘something like this’ is walked two metres along the rail. The person who decided where everything goes had to settle what ‘like this’ means, by colour, by occasion or by price, and every later recommendation depends on that choice.

An embedding is a position on that floor, except the floor has a few hundred dimensions instead of two and the positions are learned from data rather than decided by a manager. The model that assigns positions is the encoder, the floor is the embedding space, and ‘walk two metres along the rail’ is a nearest neighbour search, meaning the lookup of the items whose positions are closest to the one in hand.

Size is where the shop floor stops helping, because a shop floor has two dimensions and a few thousand items, so you can see the neighbours at a glance. A catalogue has 10 million items with 128 numbers each, and checking the distance to every one of them for every query is the cost this topic has to design around. The running example is visual search in a shopping app, where a customer photographs a jacket and wants to buy one like it.

Decide what similar should mean

The encoder is the model that turns an image, a product or a user into its list of numbers, and what it was trained on decides what ‘close’ means. In visual search, finding the exact same jacket from a different photo and finding a jacket in the same style are different tasks, and an encoder trained for one will disappoint on the other. The training data is pairs, two photos of the same jacket for the first task or two jackets a stylist calls similar for the second, together with unrelated jackets that the encoder is taught to push apart.

A two-tower model is the common shape for matching a query to items. One encoder handles the query, which for visual search is the customer’s photo, and a second encoder handles the catalogue items, and training pushes the two lists together for matching pairs and apart for the unrelated ones. The split matters for serving because the item side does not depend on the request, so all 10 million item lists can be computed overnight and stored, and at request time only the photo is encoded. Ten million lists of 128 numbers at four bytes each is about 5.1 gigabytes, which fits in the memory of one serving machine.

Retrieval with precomputed item vectorsPhoto, Customer’s picture. Query encoder, Make the list of numbers. Vector index, Compare with stored items. Candidates, Check stock, then rerank. Only the first two boxes run when the customer taps.Retrieval with precomputed item vectorsPhotoCustomer’s pictureQuery encoderMake the list ofnumbersVector indexCompare with storeditemsCandidatesCheck stock, thenrerank
Only the first two boxes run when the customer taps.On a narrow screen, scroll the figure sideways to see every step.

Compare exact and approximate search

A similarity function is the rule for turning two lists into one closeness number. The three usual ones are called dot product, cosine similarity and Euclidean distance, and each is a different recipe for that number. They do not rank the same neighbours, because the length of a list affects two of them and not the third. If the encoder was trained with lists scaled to length one, then the dot product and the Euclidean distance agree, and that scaling has to happen at search time too. An index that measures distance differently from the training rule returns neighbours the encoder never meant, like a shop assistant measuring the rail in a different unit from the person who laid it out.

Exact search compares the query with every stored list, and with 10 million items of 128 numbers that is about 1.3 billion multiplications per query, which is a few hundred milliseconds on one processor and too slow at checkout. Approximate nearest neighbour search, usually shortened to ANN, organises the lists into regions and only searches the few regions nearest the query, and it finishes in a few milliseconds at the price of sometimes missing a true neighbour. The quality of the shortcut is measured as ANN recall, the share of the true top ten that the approximate search also returned, and 95 percent is a common target.

Filters remove results after the search has done its work. If the search returns the 100 closest jackets and 95 of them are out of stock in the customer’s size, the customer sees five results, however good the search was. The remedies are to filter inside the index so it skips ineligible items as it searches, to retrieve more candidates than you need and filter afterwards, or to keep a separate index per large category. Each needs testing on the rare sizes and the newest items, where the filter removes the most.

From ten million items to a result list10 million items, Lists of numbers stored overnight. 100 nearest, Approximate search in milliseconds. 5 in stock, After the size filter. The narrowing from 100 to 5 is invisible to the search metric.From ten million items to a result list10 million itemsLists of numbers stored overnight100 nearestApproximate search in milliseconds5 in stockAfter the size filter
The narrowing from 100 to 5 is invisible to the search metric.On a narrow screen, scroll the figure sideways to see every step.

Treat the encoder and index as a versioned pair

A new version of the encoder rearranges the floor. It may keep the same 128 numbers per item while putting shirts where jumpers used to be. A query encoded with the new encoder and searched against an index built by the old one then returns neighbours that are nonsense, with no error anywhere. The encoder and the index it built are one versioned pair, and a deployment replaces both at once or runs the new pair alongside the old one until it is checked.

The index also has to cope with change between rebuilds. New items arrive hourly, items sell out, and some are withdrawn for legal reasons that take effect immediately. The design therefore needs an incremental path that adds and removes a few items without a full rebuild, plus an eligibility check at serving time for anything that must not appear regardless of what the index says. With the floor laid out and searchable, the next topic is what to do for the customer or item that has no history to be placed by, which is the cold start problem.

Switching encoders safelyNew encoder, The layout has changed. Rebuild the index, Same version pair. Old index queried, Neighbours are nonsense. The lower branch gives no error message, only bad results.Switching encoders safelyNew encoderThe layout has changedRebuild the indexSame version pairOld index queriedNeighbours arenonsense
The lower branch gives no error message, only bad results.On a narrow screen, scroll the figure sideways to see every step.

Where people get confused

A high ANN recall gets read as proof that results are good. ANN recall says the approximate search found the same neighbours as the exact search in the same space, and if the encoder learned the wrong idea of similar, both searches agree on the wrong neighbours.

The similarity function also gets mixed up between training and serving. An encoder trained with cosine similarity and an index using the raw dot product will rank long lists above close ones, and the symptom is a handful of items that appear as neighbours to everything.

Updating the encoder like any other model is the confusion that costs a full rebuild. Every other model can be swapped behind the same inputs, while an encoder changes the meaning of the stored data it produced, so the stored data has to be regenerated with it.

In the interview

In any search or recommendation design, the modelling block spends three to five minutes on embeddings and retrieval. Introduce them with the two-tower split and the reason for it, for example ‘I would encode items overnight into an index and encode the query at request time. Then I would search with an approximate nearest neighbour index that trades a little recall for a lookup in a few milliseconds’. The follow-ups are almost always about filters, the retrain, or what similar means, and each has a section above.

Interview questions

ANN recall is 98 percent, but users say results are irrelevant. What is missing from your evaluation?

ANN recall only tells me that the approximate search is finding almost the same neighbours as an exact search would in the same embedding space. It says nothing about whether that space puts things users consider similar near each other, so a 98 percent score can sit on top of an encoder that learned the wrong idea of similar.

I would add an evaluation of the results themselves, either judged by people or measured by what users click and buy, broken down by query type. I would also check the filters and index freshness, since a stale or over-filtered index also looks irrelevant. If the space is wrong, I would retrain the encoder on pairs that reflect what similar means to users, because no amount of ANN tuning repairs a bad space.

How would you serve ‘find similar jackets’ over a catalogue of 10 million items within 50 milliseconds?

I would use a two-tower setup, encoding all 10 million items overnight into an index of 128-number lists, about 5 gigabytes, which fits in memory on one serving machine, and encoding only the customer’s photo at request time. Exact search over 10 million items would take a few hundred milliseconds, so I would use an approximate nearest neighbour index that searches only the nearest regions and returns the top few hundred in a few milliseconds.

The trade is a little recall, say 95 percent of the true top ten, for a lookup that fits the budget. I would retrieve more candidates than I need, around 500, because the stock and size filter will remove most of them, and then rerank the survivors with a richer model in the remaining time.

You retrain the image encoder with better data. What has to change in production?

The index has to change, because the new encoder places items differently and the old index was built by the old one. A query encoded with the new encoder against the old index returns nonsense neighbours with no error, so I would rebuild the full item index with the new encoder before switching anything.

I would deploy the encoder and its index as one versioned pair, run the new pair alongside the old one on a share of traffic to compare results, and keep the old pair ready to switch back to. The cost is a full re-encode of 10 million items, which is a batch job of hours, and that cost is why encoder changes are planned rather than swapped in without warning.

What to practice next

Design these systems on the canvas and get your design graded.

These problems have you measure closeness between two lists of numbers, find the nearest neighbours by brute force, and write the loss that pulls matching pairs together and pushes others apart, which are the three pieces an embedding search is built from.