Files
landing_page/qdrant-landing/content/articles/new-recommendation-api.md
T
Andrey Vasnetsov cae8456ce1 Fixing links again (#697)
* use relative links instead of absolute

* add trailing slashes to avoid 301 redirect

* add trailing slashes to avoid 301 redirect

* make link checker unhappy with local redirects

* test if ci fails (should fail)

* rollback: test if ci fails (should fail)
2024-03-07 20:31:05 +01:00

236 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: Deliver Better Recommendations with Qdrant’s new API
short_description: Qdrant 1.6 brings recommendations strategies and more flexibility to the Recommendation API.
description: Qdrant 1.6 brings recommendations strategies and more flexibility to the Recommendation API.
preview_dir: /articles_data/new-recommendation-api/preview
social_preview_image: /articles_data/new-recommendation-api/preview/social_preview.png
small_preview_image: /articles_data/new-recommendation-api/icon.svg
weight: -80
author: Kacper Łukawski
author_link: https://medium.com/@lukawskikacper
date: 2023-10-25T09:46:00.000Z
---
The most popular use case for vector search engines, such as Qdrant, is Semantic search with a single query vector. Given the
query, we can vectorize (embed) it and find the closest points in the index. But [Vector Similarity beyond Search](/articles/vector-similarity-beyond-search/)
does exist, and recommendation systems are a great example. Recommendations might be seen as a multi-aim search, where we want
to find items close to positive and far from negative examples. This use of vector databases has many applications, including
recommendation systems for e-commerce, content, or even dating apps.
Qdrant has provided the [Recommendation API](/documentation/concepts/search/#recommendation-api) for a while, and with the latest release, [Qdrant 1.6](https://github.com/qdrant/qdrant/releases/tag/v1.6.0),
we're glad to give you more flexibility and control over the Recommendation API.
Here, we'll discuss some internals and show how they may be used in practice.
### Recap of the old recommendations API
The previous [Recommendation API](/documentation/concepts/search/#recommendation-api) in Qdrant came with some limitations. First of all, it was required to pass vector IDs for
both positive and negative example points. If you wanted to use vector embeddings directly, you had to either create a new point
in a collection or mimic the behaviour of the Recommendation API by using the [Search API](/documentation/concepts/search/#search-api).
Moreover, in the previous releases of Qdrant, you were always asked to provide at least one positive example. This requirement
was based on the algorithm used to combine multiple samples into a single query vector. It was a simple, yet effective approach.
However, if the only information you had was that your user dislikes some items, you couldn't use it directly.
Qdrant 1.6 brings a more flexible API. You can now provide both IDs and vectors of positive and negative examples. You can even
combine them within a single request. That makes the new implementation backward compatible, so you can easily upgrade an existing
Qdrant instance without any changes in your code. And the default behaviour of the API is still the same as before. However, we
extended the API, so **you can now choose the strategy of how to find the recommended points**.
```http
POST /collections/{collection_name}/points/recommend
{
"positive": [100, 231],
"negative": [718, [0.2, 0.3, 0.4, 0.5]],
"filter": {
"must": [
{
"key": "city",
"match": {
"value": "London"
}
}
]
},
"strategy": "average_vector",
"limit": 3
}
```
There are two key changes in the request. First of all, we can adjust the strategy of search and set it to `average_vector` (the
default) or `best_score`. Moreover, we can pass both IDs (`718`) and embeddings (`[0.2, 0.3, 0.4, 0.5]`) as both positive and
negative examples.
## HNSW ANN example and strategy
Let’s start with an example to help you understand the [HNSW graph](/articles/filtrable-hnsw/). Assume you want
to travel to a small city on another continent:
1. You start from your hometown and take a bus to the local airport.
2. Then, take a flight to one of the closest hubs.
3. From there, you have to take another flight to a hub on your destination continent.
4. Hopefully, one last flight to your destination city.
5. You still have one more leg on local transport to get to your final address.
This journey is similar to the HNSW graph’s use in Qdrant's approximate nearest neighbours search.
![Transport network](/articles_data/new-recommendation-api/example-transport-network.png)
HNSW is a multilayer graph of vectors (embeddings), with connections based on vector proximity. The top layer has the least
points, and the distances between those points are the biggest. The deeper we go, the more points we have, and the distances
get closer. The graph is built in a way that the points are connected to their closest neighbours at every layer.
All the points from a particular layer are also in the layer below, so switching the search layer while staying in the same
location is possible. In the case of transport networks, the top layer would be the airline hubs, well-connected but with big
distances between the airports. Local airports, along with railways and buses, with higher density and smaller distances, make
up the middle layers. Lastly, our bottom layer consists of local means of transport, which is the densest and has the smallest
distances between the points.
You don’t have to check all the possible connections when you travel. You select an intercontinental flight, then a local one,
and finally a bus or a taxi. All the decisions are made based on the distance between the points.
The search process in HNSW is also based on similarly traversing the graph. Start from the entry point in the top layer, find
its closest point and then use that point as the entry point into the next densest layer. This process repeats until we reach
the bottom layer. Visited points and distances to the original query vector are kept in memory. If none of the neighbours of
the current point is better than the best match, we can stop the traversal, as this is a local minimum. We start at the biggest
scale, and then gradually zoom in.
In this oversimplified example, we assumed that the distance between the points is the only factor that matters. In reality, we
might want to consider other criteria, such as the ticket price, or avoid some specific locations due to certain restrictions.
That means, there are various strategies for choosing the best match, which is also true in the case of vector recommendations.
We can use different approaches to determine the path of traversing the HNSW graph by changing how we calculate the score of a
candidate point during traversal. The default behaviour is based on pure distance, but Qdrant 1.6 exposes two strategies for the
recommendation API.
### Average vector
The default strategy, called `average_vector` is the previous one, based on the average of positive and negative examples. It
simplifies the recommendations process and converts it into a single vector search. It supports both point IDs and vectors as
parameters. For example, you can get recommendations based on past interactions with existing points combined with query vector
embedding. Internally, that mechanism is based on the averages of positive and negative examples and was calculated with the
following formula:
$$
\text{average vector} = \text{avg}(\text{positive vectors}) + \left( \text{avg}(\text{positive vectors}) - \text{avg}(\text{negative vectors}) \right)
$$
The `average_vector` converts the problem of recommendations into a single vector search.
### The new hotness - Best score
The new strategy is called `best_score`. It does not rely on averages and is more flexible. It allows you to pass just negative
samples and uses a slightly more sophisticated algorithm under the hood.
The best score is chosen at every step of HNSW graph traversal. We separately calculate the distance between a traversed point
and every positive and negative example. In the case of the best score strategy, **there is no single query vector anymore, but a
bunch of positive and negative queries**. As a result, for each sample in the query, we have a set of distances, one for each
sample. In the next step, we simply take the best scores for positives and negatives, creating two separate values. Best scores
are just the closest distances of a query to positives and negatives. The idea is: **if a point is closer to any negative than to
any positive example, we do not want it**. We penalize being close to the negatives, so instead of using the similarity value
directly, we check if it’s closer to positives or negatives. The following formula is used to calculate the score of a traversed
potential point:
```rust
if best_positive_score > best_negative_score {
score = best_positive_score
} else {
score = -(best_negative_score * best_negative_score)
}
```
If the point is closer to the negatives, we penalize it by taking the negative squared value of the best negative score. For a
closer negative, the score of the candidate point will always be lower or equal to zero, making the chances of choosing that point
significantly lower. However, if the best negative score is higher than the best positive score, we still prefer those that are
further away from the negatives. That procedure effectively **pulls the traversal procedure away from the negative examples**.
If you want to know more about the internals of HNSW, you can check out the article about the
[Filtrable HNSW](/articles/filtrable-hnsw/) that covers the topic thoroughly.
## Food Discovery demo
Our [Food Discovery demo](/articles/food-discovery-demo/) is an application built on top of the new [Recommendation API](/documentation/concepts/search/#recommendation-api).
It allows you to find a meal based on liked and disliked photos. There are some updates, enabled by the new Qdrant release:
* **Ability to include multiple textual queries in the recommendation request.** Previously, we only allowed passing a single
query to solve the cold start problem. Right now, you can pass multiple queries and mix them with the liked/disliked photos.
This became possible because of the new flexibility in parameters. We can pass both point IDs and embedding vectors in the same
request, and user queries are obviously not a part of the collection.
* **Switch between the recommendation strategies.** You can now choose between the `average_vector` and the `best_score` scoring
algorithm.
### Differences between the strategies
The UI of the Food Discovery demo allows you to switch between the strategies. The `best_vector` is the default one, but with just
a single switch, you can see how the results differ when using the previous `average_vector` strategy.
If you select just a single positive example, both algorithms work identically.
##### One positive example
<video autoplay="true" loop="true" width="100%" controls><source src="/articles_data/new-recommendation-api/one-positive.mp4" type="video/mp4"></video>
The difference only becomes apparent when you start adding more examples, especially if you choose some negatives.
##### One positive and one negative example
<video autoplay="true" loop="true" width="100%" controls><source src="/articles_data/new-recommendation-api/one-positive-one-negative.mp4" type="video/mp4"></video>
The more likes and dislikes we add, the more diverse the results of the `best_score` strategy will be. In the old strategy, there
is just a single vector, so all the examples are similar to it. The new one takes into account all the examples separately, making
the variety richer.
##### Multiple positive and negative examples
<video autoplay="true" loop="true" width="100%" controls><source src="/articles_data/new-recommendation-api/multiple.mp4" type="video/mp4"></video>
Choosing the right strategy is dataset-dependent, and the embeddings play a significant role here. Thus, it’s always worth trying
both of them and comparing the results in a particular case.
#### Handling the negatives only
In the case of our Food Discovery demo, passing just the negative images can work as an outlier detection mechanism. While the dataset
was supposed to contain only food photos, this is not actually true. A simple way to find these outliers is to pass in food item photos
as negatives, leading to the results being the most "unlike" food images. In our case you will see pill bottles and books.
**The `average_vector` strategy still requires providing at least one positive example!** However, since cosine distance is set up
for the collection used in the demo, we faked it using [a trick described in the previous article](/articles/food-discovery-demo/#negative-feedback-only).
In a nutshell, if you only pass negative examples, their vectors will be averaged, and the negated resulting vector will be used as
a query to the search endpoint.
##### Negatives only
<video autoplay="true" loop="true" width="100%" controls><source src="/articles_data/new-recommendation-api/negatives-only.mp4" type="video/mp4"></video>
Still, both methods return different results, so they each have their place depending on the questions being asked and the datasets
being used.
#### Challenges with multimodality
Food Discovery uses the [CLIP embeddings model](https://huggingface.co/sentence-transformers/clip-ViT-B-32), which is multimodal,
allowing both images and texts encoded into the same vector space. Using this model allows for image queries, text queries, or both of
them combined. We utilized that mechanism in the updated demo, allowing you to pass the textual queries to filter the results further.
##### A single text query
<video autoplay="true" loop="true" width="100%" controls><source src="/articles_data/new-recommendation-api/text-query.mp4" type="video/mp4"></video>
Text queries might be mixed with the liked and disliked photos, so you can combine them in a single request. However, you might be
surprised by the results achieved with the new strategy, if you start adding the negative examples.
##### A single text query with negative example
<video autoplay="true" loop="true" width="100%" controls><source src="/articles_data/new-recommendation-api/text-query-with-negative.mp4" type="video/mp4"></video>
This is an issue related to the embeddings themselves. Our dataset contains a bunch of image embeddings that are pretty close to each
other. On the other hand, our text queries are quite far from most of the image embeddings, but relatively close to some of them, so the
text-to-image search seems to work well. When all query items come from the same domain, such as only text, everything works fine.
However, if we mix positive text and negative image embeddings, the results of the `best_score` are overwhelmed by the negative samples,
which are simply closer to the dataset embeddings. If you experience such a problem, the `average_vector` strategy might be a better
choice.
### Check out the demo
The [Food Discovery Demo](https://food-discovery.qdrant.tech/) is available online, so you can test and see the difference.
This is an open source project, so you can easily deploy it on your own. The source code is available in the [GitHub repository
](https://github.com/qdrant/demo-food-discovery/) and the [README](https://github.com/qdrant/demo-food-discovery/blob/main/README.md) describes the process of setting it up.
Since calculating the embeddings takes a while, we precomputed them and exported them as a [snapshot](https://storage.googleapis.com/common-datasets-snapshots/wolt-clip-ViT-B-32.snapshot),
which might be easily imported into any Qdrant instance. [Qdrant Cloud is the easiest way to start](https://cloud.qdrant.io/), though!