返回实用示例

食品发现演示

Kacper Łukawski

·

2023年9月05日

Food Discovery Demo

并不是每一次搜索旅程都始于一个特定的目的地。有时候,你只是想探索一下,看看外面有什么以及你可能喜欢什么。 尤其在食物方面,这一点尤其真实。你可能渴望一些甜食,但你不知道是什么。你也可能在寻找一道新的菜肴来尝试, 你只是想看看有哪些可选的选项。在这些情况下,无法用文本查询来表达你的需求,因为你正在寻找的东西尚未定义。 Qdrant的图像语义搜索在你很难用语言表达你的口味时非常有用。

总体架构

我们很高兴地宣布我们的食品发现演示的更新版本。这次作为一个开源项目可用,因此您可以轻松地在自己的设备上部署并进行试验。如果您更喜欢直接查看源代码,请随时查看GitHub 存储库。否则,请继续阅读以了解更多关于演示及其工作原理的信息!

通常,我们的应用程序由三部分组成:一个FastAPI后端,一个React前端,以及一个Qdrant实例。下面的架构图展示了这些组件之间的相互作用:

Archtecture diagram

我们为什么使用CLIP模型?

CLIP是一个神经网络,可以将图像和文本编码为向量。更重要的是,图像和文本都被向量化到同一个潜在空间,因此我们可以直接比较它们。这让您可以使用文本查询对图像执行语义搜索,反之亦然。例如,如果您搜索“带配料的平面面包”,您将获得比萨饼的图像。或者如果您搜索“比萨饼”,您将获得一些带配料的平面面包的图像,即使它们没有标记为“比萨饼”。这是因为CLIP嵌入捕捉了图像和文本的语义,并且可以找到它们之间的相似性,无论措辞如何。

CLIP model

CLIP 有多种不同的使用方式。我们使用了在 句子变换器 库中可用的预训练 clip-ViT-B-32 模型,因为这是开始的最简单方法。

数据集

该演示基于Wolt数据集。它包含来自不同餐厅的超过2M道菜肴的图像,以及一些附加的元数据。 这是一道单独菜肴的有效负载的样子:

{
    "cafe": {
        "address": "VGX7+6R2 Vecchia Napoli, Valletta",
        "categories": ["italian", "pasta", "pizza", "burgers", "mediterranean"],
        "location": {"lat": 35.8980154, "lon": 14.5145106},
        "menu_id": "610936a4ee8ea7a56f4a372a",
        "name": "Vecchia Napoli Is-Suq Tal-Belt",
        "rating": 9,
        "slug": "vecchia-napoli-skyparks-suq-tal-belt"
    },
    "description": "Tomato sauce, mozzarella fior di latte, crispy guanciale, Pecorino Romano cheese and a hint of chilli",
    "image": "https://wolt-menu-images-cdn.wolt.com/menu-images/610936a4ee8ea7a56f4a372a/005dfeb2-e734-11ec-b667-ced7a78a5abd_l_amatriciana_pizza_joel_gueller1.jpeg",
    "name": "L'Amatriciana"
}

处理这些记录需要一些时间,所以我们预先计算了CLIP嵌入,将它们存储在Qdrant集合中,并将集合导出为快照。您可以在这里下载

不同的搜索模式

FastAPI后端 仅暴露一个端点,但是它处理多个场景。让我们逐一深入了解它们以及它们为何必要。

冷启动

推荐系统在冷启动问题上苦苦挣扎。当一个新用户加入系统时,关于他们的偏好没有任何数据,因此很难推荐任何东西。我们的演示也适用同样的情况。当你打开它时,你将看到一组选随机菜肴,并且每次刷新页面时都会更改。内部来说,演示 选择一些随机点 在向量空间中。

Random points selection

该过程应导致返回多样化的结果,因此我们有更高的机会向用户展示一些有趣的内容。

由于该演示存在冷启动问题,我们实现了一种文本搜索模式,便于开始探索数据。您可以通过点击右上角的搜索图标输入任何文本查询。该演示将使用CLIP模型将查询编码为向量,然后在向量空间中搜索最近邻。

Random points selection

这实现为一个对Qdrant的组搜索查询。我们没有使用简单搜索,而是按餐厅进行了分组,以获取更丰富的结果。搜索组是一种类似于GROUP BY子句的机制,在你想要获得每个组的特定数量的结果时非常有用(在我们的例子中仅为一个)。

import settings

# Encode query into a vector, model is an instance of
# sentence_transformers.SentenceTransformer that loaded CLIP model
query_vector = model.encode(query).tolist()

# Search for nearest neighbors, client is an instance of 
# qdrant_client.QdrantClient that has to be initialized before
response = client.search_groups(
    settings.QDRANT_COLLECTION,
    query_vector=query_vector,
    group_by=settings.GROUP_BY_FIELD,
    limit=search_query.limit,
)

探索结果

演示的主要特点是能够探索菜品的空间。您可以点击其中任何一个以查看更多详细信息,但最重要的是您可以喜欢或不喜欢它,演示将相应地更新搜索结果。

Recommendation results

仅负反馈

Qdrant 推荐 API 至少需要一个正例才能工作。然而,在我们的演示中,我们希望能够仅提供负例。这是因为我们希望能够说“我不喜欢这道菜”,而不需要首先喜欢任何东西。为了实现这一点,我们使用了一个技巧。我们对不喜欢的菜肴的向量进行取反,并使用它们的均值作为查询。这样,不喜欢的菜肴将被推离搜索结果。 这是有效的,因为余弦距离是基于两个向量之间的角度,而一个向量与其取反之间的角度为180度。

CLIP model

食品发现演示 实现了这个技巧 通过调用 Qdrant 两次。最初,我们使用 滚动 API 来找到不喜欢的项目, 然后计算它们所有向量的否定均值。这允许使用 搜索组 API 找到否定均值向量的最近邻居。

import numpy as np

# Retrieve the disliked points based on their ids
disliked_points, _ = client.scroll(
    settings.QDRANT_COLLECTION,
    scroll_filter=models.Filter(
        must=[
            models.HasIdCondition(has_id=search_query.negative),
        ]
    ),
    with_vectors=True,
)

# Calculate a mean vector of disliked points
disliked_vectors = np.array([point.vector for point in disliked_points])
mean_vector = np.mean(disliked_vectors, axis=0)
negated_vector = -mean_vector

# Search for nearest neighbors of the negated mean vector
response = client.search_groups(
    settings.QDRANT_COLLECTION,
    query_vector=negated_vector.tolist(),
    group_by=settings.GROUP_BY_FIELD,
    limit=search_query.limit,
)

正面和负面反馈

因为推荐 API至少需要一个正例,所以我们只能在用户至少喜欢了一道菜时使用它。理论上,我们可以使用上面相同的技巧来否定不喜欢的菜肴,但这有点奇怪,因为 Qdrant 已经内置了这个功能,我们可以一次调用它来完成这个工作。总是更好在服务器端进行搜索。因此,在这种情况下我们只需调用 Qdrant 服务器,带上正例和负例的列表,这样它就可以找到一些接近正例而远离负例的点。

response = client.recommend_groups(
    settings.QDRANT_COLLECTION,
    positive=search_query.positive,
    negative=search_query.negative,
    group_by=settings.GROUP_BY_FIELD,
    limit=search_query.limit,
)

从用户的角度来看,与之前的情况相比没有变化。

最后但同样重要的是,位置在食品发现过程中发挥着重要作用。您肯定在寻找可以在附近找到的东西,而不是在地球的另一边。因此,您当前位置可以作为过滤条件进行切换。您可以通过点击右上角的“在我附近寻找”图标来启用它。这样,您可以在您所在的社区找到最好的比萨,而不是在整个世界中。Qdrant 地理半径过滤器 是一个完美的选择。它让您可以按距离给定点过滤结果。

from qdrant_client import models

# Create a geo radius filter
query_filter = models.Filter(
    must=[
        models.FieldCondition(
            key="cafe.location",
            geo_radius=models.GeoRadius(
                center=models.GeoPoint(
                    lon=location.longitude,
                    lat=location.latitude,
                ),
                radius=location.radius_km * 1000,
            ),
        )
    ]
)

这样的过滤器需要 一个有效载荷索引 才能有效工作,并且它是在我们用于创建快照的集合上创建的。当你将它导入到你的实例中时,索引将已经存在。

使用演示

食品发现演示 在线提供,但如果你更喜欢在本地运行,可以使用Docker。自述文件更详细地描述了所有步骤,但这里是快速开始:

git clone git@github.com:qdrant/demo-food-discovery.git
cd demo-food-discovery
# Create .env file based on .env.example
docker-compose up -d

演示将在 http://localhost:8001 上可用,但在您 将快照导入到您的 Qdrant 实例 之前,您无法搜索任何内容。如果您不想麻烦地托管一个本地的,您可以使用 Qdrant Cloud 集群。4 GB 内存足以加载所有 200 万条目。

分叉和重用

我们的演示完全是开源的。随意分叉它,使用自己的数据集进行更新或将应用程序调整为您的用例。无论您是想了解语义搜索的机制,还是希望有一个基础来构建更大的项目,这个演示都可以作为起点。请查看食品发现演示仓库以开始。如果您有任何问题,请随时通过Discord与我们联系。

这个页面有用吗?

感谢您的反馈!🙏

我们很遗憾听到这个消息。 😔 你可以 编辑 这个页面在 GitHub上,或者 create 一个 GitHub 问题。