立即开始使用 ClickHouse 云,并获得 300 美元的信用额度。要了解有关我们基于容量的折扣的更多信息,请联系我们或访问我们的定价页面。
目录
简介
这篇博文继续我们关于向量搜索的系列文章,在前一篇博文文章的基础上,我们概述了向量搜索是什么,它与传统的基于倒排索引的方法的关系,目前它能带来价值的潜在用例,以及一些高级的实现方法。在本博文中,我们将通过实际示例详细探讨 ClickHouse 中的向量搜索,并回答“我什么时候应该使用 ClickHouse 进行向量搜索?”
在我们的示例中,我们使用了一个包含 60 个内核和每个节点 240GB 内存的 ClickHouse 云集群。但是,这些示例应该可以在等效大小的自管理集群中复制。或者,立即开始使用您的 ClickHouse 云 集群,并获得 300 美元的信用额度。让我们来处理基础设施,您只需要查询!
我什么时候应该使用 ClickHouse 进行向量搜索?
ClickHouse 是一个实时 OLAP 数据库,它支持完整的 SQL 并具有广泛的功能来帮助用户编写分析查询。其中一些功能和数据结构执行向量之间的距离操作,使 ClickHouse 可以用作向量数据库。
由于完全并行的查询管道,ClickHouse 可以非常快速地处理向量搜索操作,尤其是在通过对所有行进行线性扫描执行精确匹配时,提供的处理速度可与专用向量数据库相媲美。
通过自定义压缩编解码器进行调整的高压缩率,可以存储和查询非常大的数据集。ClickHouse 不受内存限制,允许查询包含嵌入的多 TB 数据集。
计算两个向量之间距离的功能只是另一个 SQL 函数,可以有效地与更传统的 SQL 过滤和聚合功能相结合。这允许将向量与元数据甚至富文本一起存储和查询,从而实现广泛的用例和应用程序。
最后,ClickHouse 的实验性功能,如近似最近邻 (ANN) 索引,支持更快地近似匹配向量,并提供了一个有前景的开发方向,旨在进一步增强 ClickHouse 的向量匹配功能。
总之,如果满足以下任何条件,ClickHouse 就是一个有效的向量搜索平台
- 您希望将向量匹配与元数据过滤和/或聚合或联接功能相结合
- 您需要对非常大的向量数据集执行线性距离匹配,并希望在没有额外工作或配置的情况下,在多个 CPU 内核上并行化和分布此工作
- 您需要匹配向量数据集的大小,在这种情况下,由于成本或硬件可用性,依靠内存索引是不可行的
- 您将从查询向量时获得完整的 SQL 支持中受益
- 您有一个现有的嵌入生成管道来生成向量,并且不需要此功能成为您的存储引擎的原生功能
- 您已经在 ClickHouse 中拥有相关数据,并且不希望为数百万向量学习另一种工具而产生额外开销和成本
- 您主要需要快速并行化向量精确匹配,并且不需要生产环境中的 ANN 实现(至少现在不需要!)
- 您是经验丰富或好奇的 ClickHouse 用户,并且相信我们能够改进我们的向量匹配功能,并希望参与这一旅程
虽然这涵盖了广泛的用例,但也有一些情况下 ClickHouse 可能不太适合作为向量存储引擎,您可能需要考虑其他替代方案,例如Faiss,或专用的向量数据库。如果满足以下条件,ClickHouse 目前可能无法作为向量搜索引擎提供很多优势
- 您的向量数据集很小,可以轻松放入内存。虽然 ClickHouse 可以轻松完成对小数据集的向量搜索,但在这种情况下,它可能过于强大。
- 您没有与向量相关的任何其他元数据,并且只需要距离匹配和排序。如果将向量搜索结果与其他元数据联接没有用,并且您的数据集很小,那么正如上面所述,ClickHouse 可能过于强大。
- 您需要一个每秒查询量 (QPS) 很高,超过每秒数千次的解决方案。通常,对于这些用例,数据集将适合内存,并且需要几毫秒的匹配时间。虽然 ClickHouse 可以处理这些用例,但简单的内存索引可能就足够了。
- 您需要一个包含开箱即用嵌入生成功能的解决方案,其中模型在插入和查询时集成在一起。向量数据库,如 Weaviate,专为这种用例而设计,如果需要这些功能,它们可能更合适。
考虑到这一点,让我们探索 ClickHouse 的向量功能。
设置示例
LAION 数据集
正如我们在上一篇文章中所讨论的,向量搜索对嵌入进行操作 - 代表上下文含义的向量。嵌入是通过将原始内容(如图像或文本)传递给预先训练的机器学习模型而生成的。
对于这篇文章,我们使用了一组准备好的嵌入,这些嵌入是公开提供的,可以公开下载,被称为 LAION 50 亿测试集。我们选择这个数据集是因为我们认为,在撰写本文时,它是可用于测试的最大可用预先计算的嵌入数据集。它包含对互联网上数十亿公共图像及其标题的 768 维嵌入,这些嵌入是通过对互联网进行公开爬取而生成的。该数据集专门用于测试大规模向量搜索,它还包含元数据,这些元数据反过来对于说明如何在 ClickHouse 中将通用分析功能与向量搜索相结合非常有用。
在 LAION 数据集中,每个图像及其关联的标题都生成了嵌入 - 为每个对象提供了两个嵌入。在这篇文章中,我们只关注英语子集,它包含 22 亿个对象。虽然这些对象中的每一个都有两个嵌入,分别对应图像和标题,但我们将每对存储在 ClickHouse 中的单行中,总共产生近 22 亿行和 44 亿个向量。对于每一行,我们都将元数据作为列包含在内,这些元数据包含图像尺寸、图像相似度和标题嵌入等信息。这种相似性(余弦距离)使我们能够识别标题和图像在概念上不一致的对象,从而有可能在查询中将其过滤掉。
我们要感谢原始作者为整理此数据集并生成供公众使用的嵌入所付出的努力。我们建议您阅读完整的生成此数据集的过程,该过程克服了许多具有挑战性的数据工程挑战,例如以合理的时间和可接受的成本高效地下载和调整数十亿图像的大小。
使用 CLIP 模型生成嵌入
这些 LAION 嵌入是使用 ViT-L/14 模型生成的,该模型由 LAION 使用openCLIP(OpenAI 开发的 CLIP 模型的开源实现)训练的。这不是一个便宜的过程!对于 4 亿张图像,这大约需要 30 天,并且需要 592 个 V100 GPU(在 AWS 按需实例上约为 100 万美元)。
CLIP(对比语言-图像预训练)是一种多模态模型,这意味着它被设计为训练多种相关类型的数据,例如图像和关联文本。CLIP 已被证明能够有效地学习文本的视觉表示,并在 OCR、地理定位和动作识别方面取得了可喜的成果。为了对图像进行编码,CLIP 的作者使用了 Resnet50 和视觉转换器 (ViT),而对文本的编码则使用了类似于 GPT-2 的转换器。生成的嵌入表示为两组独立的向量。
训练过程的关键结果是,两种数据类型的嵌入是可比较的 - 如果图像和标题的向量接近,则它们可以被认为在概念上相似。像 CLIP 这样好的模型会导致嵌入在距离方面接近,或者对于图像向量及其关联的标题向量,余弦相似度会得到接近 1 的高值。这在下图中有所体现,其中 T1 是第一个图像标题的嵌入表示,而 I1 是图像本身的编码。这意味着我们希望在训练过程中最大化该矩阵的对角线,其中图像和文本重合。
作为后处理步骤,作者丢弃了与文本标题的余弦相似度小于 0.28 的图像,从而过滤掉标题和图像不一致的潜在低质量结果。通过图像大小、标题长度、潜在非法性和删除重复项进行进一步过滤,将总数据集从 50 亿个减少到 22 亿个。
来源:https://openai.com/research/clip
准备数据以进行加载
LAION 数据集可以从多个来源下载。选择英语子集,我们使用了Hugging Face 托管的版本。此服务依赖于 Git 大型文件存储 (LFS),需要安装客户端才能下载文件。安装完成后,下载数据只需要一条命令。为此,请确保您至少有 20TB 的磁盘空间可用。
git lfs install
git clone https://huggingface.co/datasets/laion/laion2b-en-vit-l-14-embeddings
下载包括三个文件夹;其中两个包含以 npy
格式(实际上是一种多维数组格式)表示图像和标题的嵌入,第三个目录包含 Parquet 文件,其中包含每个图像和标题对的元数据。
ubuntu@ip-172-31-2-70:/data$ ls -l ./laion2b-en-vit-l-14-embeddings
total 456
drwxrwxr-x 2 ubuntu ubuntu 77824 May 16 12:28 img_emb
drwxrwxr-x 2 ubuntu ubuntu 110592 May 16 12:27 metadata
drwxrwxr-x 2 ubuntu ubuntu 270336 May 16 12:28 text_emb
为了将这些数据加载到 ClickHouse 中,我们希望为每个嵌入对生成一行,并包含元数据以进行丰富。这将需要一个将每个对象的相应嵌入和元数据合并在一起的过程。考虑到 ClickHouse 中的向量可以表示为浮点数数组,由此过程生成的 JSON 行可能如下所示
{
"key": "196060024",
"url": "https://cdn.shopify.com/s/files/1/1194/1070/products/[email protected]?v=1477414012",
"caption": "MERCEDES BENZ G65 RIDE-ON TOY CAR WITH PARENTAL REMOTE | CHERRY",
"similarity": 0.33110910654067993,
"width": "220",
"height": "147",
"original_width": "220",
"original_height": "147",
"status": "success",
"NSFW": "UNLIKELY",
"exif": {
"Image Orientation": "Horizontal (normal)",
"Image XResolution": "72",
"Image YResolution": "72",
"Image ResolutionUnit": "Pixels/Inch",
"Image YCbCrPositioning": "Centered",
"Image ExifOffset": "102",
"EXIF ExifVersion": "0210",
"EXIF ComponentsConfiguration": "YCbCr",
"EXIF FlashPixVersion": "0100",
"EXIF ColorSpace": "Uncalibrated",
"EXIF ExifImageWidth": "220",
"EXIF ExifImageLength": "147"
},
"text_embedding": [
0.025299072265625,
...
-0.031829833984375
],
"image_embedding": [
0.0302276611328125,
...
-0.00667572021484375
]
}
数据集预处理的完整代码可以在这里找到。由此过程生成的最终 2313 个 Parquet 文件大约占用 5.9TB 的磁盘空间。我们已经将这些文件合并成一个 6TB 的 Parquet 数据集,我们的用户可以s3://datasets-documentation/laion/,并用于复制示例。
将向量存储在 ClickHouse 中
将生成的 Parquet 文件加载到 ClickHouse 中需要几个简单的步骤。
模式和加载过程
以下是我们的表模式,嵌入存储为Array(Float32)
列。
CREATE TABLE laion
(
`_file` LowCardinality(String),
`key` String,
`url` String,
`caption` String,
`similarity` Float64,
`width` Int64,
`height` Int64,
`original_width` Int64,
`original_height` Int64,
`status` LowCardinality(String),
`NSFW` LowCardinality(String),
`exif` Map(String, String),
`text_embedding` Array(Float32),
`image_embedding` Array(Float32),
`orientation` String DEFAULT exif['Image Orientation'],
`software` String DEFAULT exif['Image Software'],
`copyright` String DEFAULT exif['Image Copyright'],
`image_make` String DEFAULT exif['Image Make'],
`image_model` String DEFAULT exif['Image Model']
)
ENGINE = MergeTree
ORDER BY (height, width, similarity)
exif
列包含我们稍后可以用于过滤和聚合的元数据。我们将其映射为Map(String,String)
,以实现灵活性和模式简洁性。该列包含超过 100,000 个唯一的元标签。访问子键需要从列中加载所有键,这可能会减慢某些查询的速度,因此我们使用DEFAULT
语法将五个感兴趣的属性提取到根节点中,以便稍后进行分析。对于有兴趣了解可用元属性完整列表的用户,可以使用以下查询来识别可用的 Map 键及其频率
SELECT
arrayJoin(mapKeys(exif)) AS keys,
count() AS c
FROM laion
GROUP BY keys
ORDER BY c DESC
LIMIT 10
我们的模式还包括一个 _file
列,表示生成这些数据的原始 Parquet 文件。这使我们能够在插入 ClickHouse 过程中失败时重新启动特定文件的加载。
为了便于将来使用,我们将这些数据加载到公共 S3 存储桶中。要将这些数据插入 ClickHouse,用户可以执行以下查询
INSERT INTO laion SELECT * FROM s3('https://datasets-documentation.s3.eu-west-3.amazonaws.com/laion/*.parquet')
这是一个相当大的数据量,未优化的加载需要几个小时。我们建议用户对加载过程进行批处理,以避免网络连接问题等中断。用户可以使用通配符模式定位特定子集,例如,s3(https://datasets-documentation.s3.eu-west-3.amazonaws.com/laion/00*.parquet)
。_file
列可用于通过确认 ClickHouse 中的计数与原始 Parquet 文件中的计数来协调任何加载问题。
对于下面的示例,我们创建了不同大小的表格,后缀表示行数;例如,laion_100m 包含 1 亿行。这些表格使用适当的通配符模式创建。
INSERT INTO laion_sample (_file, key, url, caption, similarity, width, height, original_width, original_height, status, NSFW, exif, text_embedding, image_embedding) SELECT
_file,
key,
url,
caption,
similarity,
width,
height,
original_width,
original_height,
status,
NSFW,
exif,
text_embedding,
image_embedding
FROM s3('https://datasets-documentation.s3.eu-west-3.amazonaws.com/laion/*.parquet')
压缩后的存储性能
ClickHouse 的列式结构意味着某一列的值是排序的并按顺序写入。磁盘上相同和相似值的聚类通常会导致高压缩率。ClickHouse 甚至提供了几个模式和编解码器,允许用户根据数据的属性调整其配置。对于浮点数数组,很难实现高压缩率,因为嵌入的值没有可利用的域无关属性。利用了完整的 32 位范围,对于大多数编解码器来说,嵌入中相邻值之间的关系是随机的。出于这个原因,我们建议使用 ZSTD 编解码器来压缩嵌入。下面我们展示了四个大小不断增加的表格(1m、10m、100m 和 20 亿行)中向量列的压缩率。
SELECT
table,
name,
formatReadableSize(sum(data_compressed_bytes)) AS compressed_size,
formatReadableSize(sum(data_uncompressed_bytes)) AS uncompressed_size,
round(sum(data_uncompressed_bytes) / sum(data_compressed_bytes), 2) AS ratio
FROM system.columns
WHERE (table IN ('laion_100m', 'laion_1m', 'laion_10m', 'laion_2b')) AND (name IN ('text_embedding', 'image_embedding'))
GROUP BY
table,
name
ORDER BY table DESC
┌─table──────┬─name────────────┬─compressed_size─┬─uncompressed_size─┬─ratio─┐
│ laion_1m │ text_embedding │ 1.60 GiB │ 2.50 GiB │ 1.56 │
│ laion_1m │ image_embedding │ 1.61 GiB │ 2.50 GiB │ 1.55 │
│ laion_10m │ text_embedding │ 18.36 GiB │ 28.59 GiB │ 1.56 │
│ laion_10m │ image_embedding │ 18.36 GiB │ 28.59 GiB │ 1.56 │
│ laion_100m │ text_embedding │ 181.64 GiB │ 286.43 GiB │ 1.58 │
│ laion_100m │ image_embedding │ 182.29 GiB │ 286.43 GiB │ 1.57 │
│ laion_1b │ image_embedding │ 1.81 TiB │ 2.81 TiB │ 1.55 │
│ laion_1b │ text_embedding │ 1.81 TiB │ 2.81 TiB │ 1.55 │
└────────────┴─────────────────┴─────────────────┴───────────────────┴───────┘
6 rows in set. Elapsed: 0.006 sec.
虽然压缩率可以通过选择主鍵通常可以影响,但这种 1.56 的恒定压缩率不太可能受到数据排序方式的影响。ZSTD 编解码器的压缩级别可以在 ClickHouse Cloud 中从其默认值 1 增加。这会带来大约 10% 的改进,将 1000 万行样本中的数据压缩到 1.71。
SELECT
table,
name,
formatReadableSize(sum(data_compressed_bytes)) AS compressed_size,
formatReadableSize(sum(data_uncompressed_bytes)) AS uncompressed_size,
round(sum(data_uncompressed_bytes) / sum(data_compressed_bytes), 2) AS ratio
FROM system.columns
WHERE (table IN ('laion_10m_zstd_3')) AND (name IN ('text_embedding', 'image_embedding'))
GROUP BY
table,
name
ORDER BY table DESC
┌─table────────────┬─name────────────┬─compressed_size─┬─uncompressed_size─┬─ratio─┐
│ laion_10m_zstd_3 │ text_embedding │ 16.68 GiB │ 28.56 GiB │ 1.71 │
│ laion_10m_zstd_3 │ image_embedding │ 16.72 GiB │ 28.56 GiB │ 1.71 │
└──────────────────┴─────────────────┴─────────────────┴───────────────────┴───────┘
2 rows in set. Elapsed: 0.026 sec.
请注意,ZSTD 的更高值会减慢压缩和数据插入速度,但解压缩速度应该保持合理稳定(大约20% 的差异)。
浮点数的压缩是一个研究领域,有几个基于量化的有损候选,例如 SZ 算法是 ClickHouse 的可能补充。其他选项包括将浮点数的精度降低到 16 位。我们在下面的“提高压缩率”部分讨论了这一点。
在 ClickHouse 中搜索向量
正如我们在本系列的第 1 部分中所述,执行向量搜索意味着将输入向量与向量库进行比较,以找到最接近的匹配项。
输入向量表示感兴趣的概念。在本例中,它要么是编码的图像,要么是标题。向量库表示我们希望与其进行比较的其他图像及其标题。
执行搜索时,向量会比较其接近度或距离。距离接近的两个向量表示相似的概念。最接近的两个向量是集合中最相似的。
选择距离函数
考虑到向量的维数很高,比较距离的方法有很多。这些不同的机制被称为距离函数。
ClickHouse 支持各种距离函数 - 您可以根据您的用例选择最适合您的函数。在这篇文章中,我们重点介绍了两个在向量搜索中非常常用的函数
- 余弦距离 -
cosineDistance(vector1, vector2)
- 这使我们能够获得 2 个向量之间的余弦距离(1 - 余弦相似度)。更具体地说,这测量了两个向量之间角度的余弦,即点积除以长度。这会产生一个介于 -1 和 1 之间的数值,其中 1 表示两个嵌入是成比例的,因此在概念上是相同的。可以解析列名和输入嵌入以进行向量搜索。如果向量尚未规范化,则此函数特别有用,并且提供了有用的有界范围,可用于过滤。 - L2 距离-
L2Distance(vector1, vector2)
- 这测量了 2 个点之间的 L2 距离。实际上,这是两个输入向量之间的欧几里得距离,即向量所表示的点之间的直线的长度。距离越低,源对象的相似度越高。
这两个函数都计算一个分数,该分数用于比较向量嵌入。对于我们的预训练 CLIP 模型,L2 距离代表最合适的距离函数,因为它基于用于官方示例的内部评分。
有关可用距离函数和向量规范化函数的完整列表,请参见此处。我们很乐意听取您如何利用这些函数搜索您的嵌入!
生成输入向量
既然我们已经确定了要使用的距离函数,那么我们需要将输入(我们想要搜索的图像或标题)转换为向量嵌入。
这要求我们调用 CLIP 模型。这可以通过一个简单的 Python 脚本轻松实现。此脚本的依赖项安装说明可以在这里找到。我们在下面展示了这个脚本
#!/usr/bin/python3
import argparse
from PIL import Image
import clip
import torch
if __name__ == '__main__':
parser = argparse.ArgumentParser(
prog='generate',
description='Generate CLIP embeddings for images or text')
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument('--text', required=False)
group.add_argument('--image', required=False)
parser.add_argument('--limit', default=1)
parser.add_argument('--table', default='laion_1m')
args = parser.parse_args()
device = "cuda" if torch.cuda.is_available() else "cpu"
print(f"using {device}")
device = torch.device(device)
model, preprocess = clip.load("ViT-L/14")
model.to(device)
images = []
if args.text:
inputs = clip.tokenize(args.text)
with torch.no_grad():
print(model.encode_text(inputs)[0].tolist())
elif args.image:
image = preprocess(Image.open(args.image)).unsqueeze(0).to(device)
with torch.no_grad():
print(model.encode_image(image)[0].tolist())
此版本的脚本接受文本或图像路径作为输入,并将嵌入输出到命令行。请注意,如果存在,这将利用支持 CUDA 的 GPU。这会对生成时间产生巨大影响 - 在 Mac M1 2021 上测试时,100 个标题的生成时间大约为 6 秒,而在具有 1 个 GPU 内核的p3.2xlarge 上为 1 秒。
例如,让我们将文本“一只昏昏欲睡的罗得西亚脊背犬”转换为嵌入。为了简洁起见,我们已经裁剪了完整的嵌入结果,您可以在这里找到。
python generate.py --text "a sleepy ridgeback dog"
[0.5736801028251648, 0.2516217529773712, ..., -0.6825592517852783]
现在我们有了表示文本“一只昏昏欲睡的罗得西亚脊背犬”的向量嵌入。这是我们的搜索输入向量。现在我们可以将此输入向量与我们的向量嵌入库进行比较,以找到表示概念上相似事物的图像及其标题。
将所有内容整合在一起
以下查询搜索概念上相似的嵌入,并按距离排序。嵌入存储在image_embedding
列中。距离存储为similarity
。我们过滤掉所有大于 0.2 的距离以减少噪声。
SELECT
url,
caption,
L2Distance(image_embedding, [0.5736801028251648, 0.2516217529773712, ..., -0.6825592517852783]) AS score
FROM laion_10m WHERE similarity >= 0.2
ORDER BY score ASC
LIMIT 2
FORMAT Vertical
Row 1:
──────
url: https://thumb9.shutterstock.com/image-photo/stock-photo-front-view-of-a-cute-little-young-thoroughbred-african-rhodesian-ridgeback-hound-dog-puppy-lying-in-450w-62136922.jpg
caption: Front view of a cute little young thoroughbred African Rhodesian Ridgeback hound dog puppy lying in the woods outdoors and staring.
score: 12.262665434714496
Row 2:
──────
url: https://m.psecn.photoshelter.com/img-get2/I0000_1Vigovbi4o/fit=180x180/fill=/g=G0000x325fvoXUls/I0000_1Vigovbi4o.jpg
caption: SHOT 1/1/08 3:15:27 PM - Images of Tanner a three year-old male Vizsla sleeping in the sun on the couch in his home in Denver, Co. The Hungarian Vizsla, is a dog breed originating in Hungary. Vizslas are known as excellent hunting dogs, and also have a level personality making them suited for families. The Vizsla is a medium-sized hunting dog of distinguished appearance and bearing. Robust but rather lightly built, they are lean dogs, have defined muscles, and are similar to a Weimaraner but smaller in size. The breed standard calls for the tail to be docked to two-thirds of its original length in smooth Vizslas and to three-fourths in Wirehaired Vizslas..(Photo by Marc Piscotty/ (c) 2007)
score: 12.265194306913513
2 rows in set. Elapsed: 1.595 sec. Processed 9.92 million rows, 32.52 GB (6.22 million rows/s., 20.38 GB/s.)
结果显示,我们的输入向量“一只困倦的罗得西亚脊背犬”在概念上最类似于数据集中非洲罗得西亚脊背猎犬的照片,并且在概念上也与一只正在睡觉的猎犬的图像非常相似。
我的狗 Kibo
为了进一步展示这些模型的实用性,作为用文本搜索的替代方案,我们可以从一张正在睡觉的狗的照片开始,并以此方式搜索类似的图像。我们生成一个代表这张照片的输入向量,并搜索概念上相似的结果。
为此,我们使用text_embedding
列重复上面的查询。完整的嵌入可以在这里找到。
python generate.py --image images/ridgeback.jpg
[0.17179889976978302, 0.6171532273292542, ..., -0.21313616633415222]
SELECT
url,
caption,
L2Distance(text_embedding, [0.17179889976978302, ..., -0.21313616633415222]
) AS score
FROM laion_10m WHERE similarity >= 0.2
ORDER BY score ASC
LIMIT 2
FORMAT Vertical
Row 1:
──────
url: https://i.pinimg.com/236x/ab/85/4c/ab854cca81a3e19ae231c63f57ed6cfe--submissive--year-olds.jpg
caption: Lenny is a 2 to 3 year old male hound cross, about 25 pounds and much too thin. He has either been neglected or on his own for a while. He is very friendly if a little submissive, he ducked his head and tucked his tail a couple of times when I...
score: 17.903361349936052
Row 2:
──────
url: https://d1n3ar4lqtlydb.cloudfront.net/c/a/4/2246967.jpg
caption: American Pit Bull Terrier/Rhodesian Ridgeback Mix Dog for adoption in San Clemente, California - MARCUS = Quite A Friendly Guy!
score: 17.90681696075351
2 rows in set. Elapsed: 1.516 sec. Processed 9.92 million rows, 32.52 GB (6.54 million rows/s., 21.45 GB/s.)
为了方便起见,我们提供了一个简单的结果生成器search.py,它对传递的图像或文本进行编码并执行查询,将查询结果呈现为本地 html 文件。然后,此文件将自动在本地浏览器中打开。上面查询的结果文件如下所示
python search.py search --image images/ridgeback.jpg --table laion_10m
在这两个例子中,我们都匹配了不同模态的嵌入,即图像输入的嵌入与text_embedding
列匹配,反之亦然。这与之前描述的原始模型训练一致,也是预期的应用。虽然已经探索过将输入嵌入与同一类型匹配,但之前的尝试导致了混合的结果。
SQL 的优势
在实践中,使用向量搜索时,我们通常不仅仅是在嵌入中进行搜索。通常,在搜索中结合过滤或聚合元数据会更有效。
使用元数据进行过滤
例如,假设我们希望对非版权图像执行向量搜索。这种查询将结合向量搜索和基于版权元数据的过滤。
再举一个例子,假设我们希望将搜索限制为仅包含大图像 - 至少 300px*500px,并且标题相似度满足 0.3 的较高余弦相似度分数。对于此示例,让我们从搜索“伟大的动物迁徙”开始。幸运的是,将此作为 SQL 查询进行表述很简单。下面,我们对 1 亿张图像执行此查询。
SELECT
url,
caption,
L2Distance(image_embedding, [<embedding>]) AS score
FROM laion_100m
WHERE (width >= 300) AND (height >= 500) AND (copyright = '') AND similarity > 0.3
ORDER BY score ASC
LIMIT 10
FORMAT Vertical
Row 1:
──────
url: https://aentcdn.azureedge.net/graphics/items/sdimages/a/500/3/6/5/4/1744563.jpg
caption: Great Migrations
width: 366
height: 500
score: 16.242750635008512
Row 2:
──────
url: https://naturefamiliesdotorg.files.wordpress.com/2017/01/on-the-move.jpg?w=418&h=557
caption: on-the-move
width: 384
height: 512
score: 16.26983713529263
10 rows in set. Elapsed: 2.010 sec. Processed 6.82 million rows, 22.52 GB (3.39 million rows/s., 11.20 GB/s.)
这说明了使用 SQL 和元数据将向量比较限制在子集中的好处。在本例中,我们查询了 1 亿个向量,但由于元数据,实际的距离匹配减少到不到 700 万个。
为了方便起见,我们还在search.py中添加了传递额外过滤器的功能,使我们能够验证上述匹配的质量。
python search.py search --filter "(width >= 300) AND (height >= 500) AND (copyright = '') AND simularity > 0.3" --text "great animal migrations"
使用元数据进行聚合
除了过滤之外,我们还可以对元数据执行聚合。作为列式数据库,ClickHouse非常适合此任务。
例如,假设我们想确定用于“野生动物园照片”的主要相机型号。我们在这里执行该搜索。
WITH results AS
(
SELECT
image_make,
image_model,
L2Distance(image_embedding, [<embedding>]) AS score
FROM laion_100m
WHERE (image_make != '') AND (image_model != '')
ORDER BY score ASC
LIMIT 1000
)
SELECT
image_make,
image_model,
count() AS c
FROM results
GROUP BY
image_make,
image_model
ORDER BY c DESC
LIMIT 10
┌─image_make────────┬─image_model───────────┬──c─┐
│ Canon │ Canon EOS 7D │ 64 │
│ Canon │ Canon EOS-1D X │ 51 │
│ Canon │ Canon EOS 5D Mark III │ 49 │
│ NIKON CORPORATION │ NIKON D700 │ 26 │
│ NIKON CORPORATION │ NIKON D800 │ 24 │
│ Canon │ Canon EOS 5D Mark II │ 23 │
│ NIKON CORPORATION │ NIKON D810 │ 23 │
│ NIKON CORPORATION │ NIKON D7000 │ 21 │
│ Canon │ Canon EOS 40D │ 18 │
│ Canon │ Canon EOS 60D │ 17 │
└───────────────────┴───────────────────────┴────┘
10 rows in set. Elapsed: 23.897 sec. Processed 100.00 million rows, 286.70 GB (4.18 million rows/s., 12.00 GB/s.)
显然,佳能应该是您下次去野生动物园旅行时选择的相机。请注意,这里我们只使用了前 1000 个结果。与无界的余弦距离不同,欧几里得距离没有上限,这使得施加阈值变得具有挑战性。
使用倒排索引
注意:倒排索引是 ClickHouse 中的实验性功能。
ClickHouse 的实验性二级索引功能对于处理向量也很有用。
例如,我们可能希望强制执行一个过滤器,将我们的野生动物园照片限制为包含狮子的照片。为此,我们可以施加令牌限制 - 要求caption
列包含字符串lions
。
如果没有倒排索引,我们的搜索可能看起来像下面这样。在这里,我们利用了嵌入,用于以下图片,并针对 1 亿个向量进行搜索。
SELECT url, caption, L2Distance(text_embedding, [<embedding>]) AS score FROM laion_10m WHERE SELECT
url,
caption,
L2Distance(text_embedding, [-0.17659325897693634, …, 0.05511629953980446]) AS score
FROM laion_100m
WHERE hasToken(lower(caption), 'lions')
ORDER BY score ASC
LIMIT 10
FORMAT Vertical
Row 1:
──────
url: https://static.wixstatic.com/media/c571fa_25ec3694e6e04a39a395d07d63ae58fc~mv2.jpg/v1/fill/w_420,h_280,al_c,q_80,usm_0.66_1.00_0.01/Mont%20Blanc.jpg
caption: Travel on a safari to Tanzania, to the rolling plains of the Serengeti, the wildlife-filled caldera of the Ngorongoro Crater and the lions and baobabs of Tarangire; Tanzania will impress you like few other countries will. This tailor-made luxury safari will take you to three very different parks in northern Tanzania, each with their own scenery and resident wildlife. As with all our private tours, this sample itinerary can be completely tailored to create the perfect journey of discovery for you.
score: 18.960329963316692
Row 2:
──────
url: https://thumbs.dreamstime.com/t/jeepsafari-ngorongoro-tourists-photographers-watching-wild-lions-who-walk-jeeps-79635001.jpg
caption: Jeep safari in Ngorongoro3. Tourists and photographers are watching wild lions, who walk between the jeeps Stock Image
score: 18.988379350742093
hasToken(lower(caption), 'lions') ORDER BY score ASC LIMIT 10 FORMAT Vertical
10 rows in set. Elapsed: 6.194 sec. Processed 93.82 million rows, 79.00 GB (15.15 million rows/s., 12.75 GB/s.)
为了加速这种元数据查询,我们可以利用倒排索引,并为caption
列添加一个倒排索引。
SET allow_experimental_inverted_index=1
ALTER TABLE laion_100m ADD INDEX caption_idx(lower(caption)) TYPE inverted;
ALTER TABLE laion_100m MATERIALIZE INDEX caption_idx;
重复我们之前的查询,我们可以看到这在查询时间方面带来了显著的改进。倒排索引可用于将距离比较的行数限制为 3000 万,将时间从 6 秒缩短到 3 秒。
SELECT url, caption, L2Distance(text_embedding, [<embedding>]) AS score FROM laion_10m WHERE SELECT
url,
caption,
L2Distance(text_embedding, [-0.17659325897693634, ..., 0.05511629953980446]) AS score
FROM laion_100m
WHERE hasToken(lower(caption), 'lions')
ORDER BY score ASC
LIMIT 10
FORMAT Vertical
Row 1:
──────
url: https://static.wixstatic.com/media/c571fa_25ec3694e6e04a39a395d07d63ae58fc~mv2.jpg/v1/fill/w_420,h_280,al_c,q_80,usm_0.66_1.00_0.01/Mont%20Blanc.jpg
caption: Travel on a safari to Tanzania, to the rolling plains of the Serengeti, the wildlife-filled caldera of the Ngorongoro Crater and the lions and baobabs of Tarangire; Tanzania will impress you like few other countries will. This tailor-made luxury safari will take you to three very different parks in northern Tanzania, each with their own scenery and resident wildlife. As with all our private tours, this sample itinerary can be completely tailored to create the perfect journey of discovery for you.
score: 18.960329963316692
Row 2:
──────
url: https://thumbs.dreamstime.com/t/jeepsafari-ngorongoro-tourists-photographers-watching-wild-lions-who-walk-jeeps-79635001.jpg
caption: Jeep safari in Ngorongoro3. Tourists and photographers are watching wild lions, who walk between the jeeps Stock Image
score: 18.988379350742093
10 rows in set. Elapsed: 3.554 sec. Processed 32.96 million rows, 74.11 GB (9.27 million rows/s., 20.85 GB/s.)
此查询的结果如下
python search.py search --image ./images/safari.jpg --table laion_100m --filter "hasToken(lower(caption), 'lions')"
高级功能
近似最近邻 (Annoy)
注意:Annoy 索引在 ClickHouse 中处于高度实验阶段。
Annoy 索引旨在提高大规模最近邻向量搜索的效率。它在准确性和计算效率之间进行了权衡。
具体来说,Annoy 索引是一种数据结构,用于在高维空间中查找近似最近邻。Annoy 通过将向量组织到树结构中来工作。它使用随机超平面(二维空间中的线,三维空间中的平面等)将高维空间划分为分区。这些超平面将空间划分为更小的区域,每个区域只包含数据点的一个子集。这些分区反过来用于构建树结构(通常是二叉树),其中每个节点代表一个超平面,子节点代表分割平面的区域。树的叶节点包含实际数据点。平衡和优化技术,例如随机化插入和使用启发式方法来确定最佳超平面进行分区,确保树是高效且平衡的。
构建 Annoy 索引后,可用于搜索。在提供一个向量时,可以通过将每个向量与每个内部节点的超平面进行比较来遍历树。在树的每一级,Annoy 估计查询向量与子节点所代表的区域之间的距离。距离度量确定要进一步探索哪个子节点。到达根节点或指定节点后,它所遇到的节点集合将被返回。结果是一个近似的结果集,其搜索时间可能比线性扫描快得多。
Annoy 分割的超平面图像
在为 ClickHouse 创建 Annoy 索引时,我们可以指定 NumTree 和 DistanceName。后者表示使用的距离函数,默认为L2Distance
,适用于我们的 LAION 数据集。前者表示算法将创建的树的数量。树越大,它运行速度越慢(在 CREATE 和 SELECT 请求中都是如此),但准确性越高(针对随机性进行调整)。默认情况下,NumTree 设置为 100。
下面,我们展示了 LAION 数据集的模式,其中每个嵌入字段都有一个 Annoy 索引。我们使用索引的默认值,并用 1 亿行填充表。
SET allow_experimental_annoy_index = 1
CREATE TABLE default.laion_100m_annoy
(
`_file` LowCardinality(String),
`key` String,
`url` String,
`caption` String,
`similarity` Float64,
`width` Int64,
`height` Int64,
`original_width` Int64,
`original_height` Int64,
`status` LowCardinality(String),
`NSFW` LowCardinality(String),
`exif` Map(String, String),
`text_embedding` Array(Float32),
`image_embedding` Array(Float32),
`orientation` String DEFAULT exif['Image Orientation'],
`software` String DEFAULT exif['Image Software'],
`copyright` String DEFAULT exif['Image Copyright'],
`image_make` String DEFAULT exif['Image Make'],
`image_model` String DEFAULT exif['Image Model'],
INDEX annoy_image image_embedding TYPE annoy(1000) GRANULARITY 1000,
INDEX annoy_text text_embedding TYPE annoy(1000) GRANULARITY 1000
)
ENGINE = MergeTree
ORDER BY (height, width, similarity)
INSERT INTO laion_100m_annoy SELECT * FROM laion_100m
0 rows in set. Elapsed: 1596.941 sec. Processed 100.00 million rows, 663.68 GB (62.62 thousand rows/s., 415.59 MB/s.)
如所示,Annoy 索引在插入时的开销很大,上述插入对于 1 亿行大约需要 27 分钟。相比之下,没有这些索引的表需要 10 分钟。下面,我们重复了之前的查询,该查询大约需要 24 秒(热)。
SELECT
url,
caption,
L2Distance(image_embedding, [embedding]) AS score
FROM laion_100m_annoy
ORDER BY score ASC
LIMIT 10 FORMAT Vertical
Row 1:
──────
url: https://i.dailymail.co.uk/i/pix/2012/04/26/article-2135380-12C5ADBC000005DC-90_634x213.jpg
caption: Pampered pets: This hammock-style dog bed offers equal levels of pet comfort
score: 12.313203570174357
Row 2:
──────
url: https://i.pinimg.com/originals/15/c2/11/15c2118a862fcd0c4f9f6c960d2638a0.jpg
caption: rhodesian ridgeback lab mix puppy
score: 12.333195649580162
10 rows in set. Elapsed: 1.456 sec. Processed 115.88 thousand rows, 379.06 MB (79.56 thousand rows/s., 260.27 MB/s.)
Annoy 索引在查询性能方面带来了显著的改进,该查询需要 1 到 2 秒,但牺牲了一些搜索质量。
这里的测试嵌入代表我们的“一只困倦的罗得西亚脊背犬”文本。我们可以在下面看到图像结果。
python search.py search --text "a sleepy ridgeback dog" --table laion_100m_annoy
在 ClickHouse 中,重要的是要注意 Annoy 索引可用于加速利用ORDER BY DistanceFunction(Column, vector)
或WHERE DistanceFunction(Column, Point) < MaxDistance
但并非两者兼而有之的查询。必须在查询上施加 LIMIT 以返回前 N 个匹配项。为了返回前 N 个匹配项,将使用基于优先级队列的缓冲区来收集匹配向量。一旦填满,收集将停止,缓冲区将被排序。此缓冲区的大小受设置max_limit_for_ann_queries
(默认值为 1000000)限制。
用户自定义函数 (UDF)
ClickHouse 的用户定义函数或UDF允许用户通过创建可以利用 SQL 结构和函数的 lambda 表达式来扩展 ClickHouse 的行为。然后,这些函数可以在查询中像任何内置函数一样使用。
到目前为止,我们一直依赖于在 ClickHouse 之外执行我们的向量生成,并在查询时从我们的search.py
脚本传递生成的嵌入。虽然这已经足够了,但如果我们能够直接在 SQL 查询中传递文本或图像路径(甚至 URL!)会更好。
我们可以使用 UDF 来完成此任务。下面定义的 UDF 分别称为embedText
和embedImage
。
SELECT
url,
caption,
L2Distance(image_embedding, embedText('a sleepy ridgeback dog')) AS score
FROM laion_10m
ORDER BY score ASC
LIMIT 10
SELECT
url,
caption,
L2Distance(text_embedding, embedImage("https://dogpictures.com/ridgeback.jpg")) as score
FROM laion_100m
ORDER BY score ASC
LIMIT 10
为了定义embedText
UDF,我们首先调整我们之前用于生成嵌入的generate.py
,将其改为下面的embed_text.py。
注意:这应该保存在 ClickHouse 的user_scripts
文件夹中。
#!/usr/bin/python3
import clip
import torch
import sys
device = "cuda" if torch.cuda.is_available() else "cpu"
model, preprocess = clip.load("ViT-L/14", device=device)
if __name__ == '__main__':
for text in sys.stdin:
inputs = clip.tokenize(text)
with torch.no_grad():
text_features = []
text_features = model.encode_text(inputs)[0].tolist()
print(text_features)
sys.stdout.flush()
然后,此embed_text.py脚本可以通过自定义函数embedText
公开。以下配置可以放置在 ClickHouse 配置目录(默认情况下为/etc/clickhouse-server/
)下,并命名为embed_text__function.xml
。
注意:用户应确保已为clickhouse
用户安装了此脚本的依赖项 - 有关步骤,请参见此处。
<functions>
<function>
<type>executable</type>
<name>embedText</name>
<return_type>Array(Float32)</return_type>
<argument>
<type>String</type>
<name>text</name>
</argument>
<format>TabSeparated</format>
<command>embed_text.py</command>
<command_read_timeout>1000000</command_read_timeout>
</function>
</functions>
注册函数后,我们现在可以像之前示例中所示那样使用它。
SELECT
url,
caption,
L2Distance(image_embedding, embedText('a sleepy ridgeback dog')) AS score
FROM laion_10m
ORDER BY score ASC
LIMIT 10
对于我们类似的embedImage
函数,我们根据以下 python 脚本embed_image.py添加另一个 UDF。
#!/usr/bin/python3
from io import BytesIO
from PIL import Image
import requests
import clip
import torch
import sys
device = "cuda" if torch.cuda.is_available() else "cpu"
model, preprocess = clip.load("ViT-L/14", device=device)
if __name__ == '__main__':
for url in sys.stdin:
response = requests.get(url.strip())
response.raise_for_status()
image = preprocess(Image.open(BytesIO(response.content))).unsqueeze(0).to(device)
with torch.no_grad():
print(model.encode_image(image)[0].tolist())
sys.stdout.flush()
<functions>
<function>
<type>executable_pool</type>
<name>embedImage</name>
<return_type>Array(Float32)</return_type>
<argument>
<type>String</type>
</argument>
<format>TabSeparated</format>
<command>embed_image.py</command>
<command_read_timeout>1000000</command_read_timeout>
</function>
</functions>
当 UDF 设置为类型executable_pool
时,ClickHouse 会维护一个预加载的 Python 实例池,准备接收输入。对于我们的函数,这是有益的,因为它可以减少第一次执行后的模型加载时间。这使得随后的调用速度快得多。有关如何控制池大小和其他配置参数的更多详细信息,请参见此处。
现在,这两个 UDF 都已配置,我们可以按如下方式进行查询。
SELECT embedImage('https://cdn.britannica.com/12/236912-050-B39F82AF/Rhodesian-Ridgeback-dog.jpg')
...
1 row in set. Elapsed: 13.421 sec.
SELECT embedImage('https://cdn.britannica.com/12/236912-050-B39F82AF/Rhodesian-Ridgeback-dog.jpg')
...
1 row in set. Elapsed: 0.317 sec.
SELECT
url,
caption,
L2Distance(image_embedding, embedImage('https://cdn.britannica.com/12/236912-050-B39F82AF/Rhodesian-Ridgeback-dog.jpg')) AS score
FROM laion_10m
ORDER BY score ASC
LIMIT 10
完成此操作后,我们可以使用embed_concept.py脚本和函数embedConcept
公开我们之前的内容数学功能。
select embedConcept('(berlin - germany) + (uk + bridge)')
SELECT
url,
caption,
L2Distance(image_embedding, embedConcept('(berlin - germany) + (uk + bridge)')) AS score
FROM laion_10m
ORDER BY score ASC
LIMIT 10
请注意,上面的示例不包括错误处理和输入验证。我们将此作为练习留给读者。希望这些示例能为结合用户定义函数、嵌入模型和向量搜索提供一些启发!
改进压缩
增强的压缩技术可以帮助减少总体数据大小和存储需求。例如,我们之前的模式和生成的压缩统计信息基于将我们的向量存储为类型Array(Float32)
。但是,对于某些模型,32 位浮点精度不是必需的,并且可以通过将此精度降低到 16 位来实现类似的匹配质量。
虽然 ClickHouse 没有原生 16 位浮点类型,但我们仍然可以将精度降低到 16 位,并重复使用Float32
类型,每个值只需用零填充即可。这些零将使用 ZSTD 编码器(ClickHouse Cloud 中的标准)高效地压缩,从而减少压缩后的存储需求。
为了实现这一点,我们需要确保 16 位浮点数的编码正确。幸运的是,Google 的 bloat16 类型 在机器学习用例中表现良好,只需截断 32 位浮点数的最后 16 位,假设后者使用 IEE-754 编码。
图片来源:https://cloud.google.com/tpu/docs/bfloat16
虽然 bfloat16 目前不是 ClickHouse 的原生类型,但它可以轻松地用其他函数复制。我们将在下面针对 image_embedding
和 text_embedding
列进行说明。
为此,我们将从 laion_100m
表(包含 1 亿行)中选择所有行,并使用 INSERT INTO SELECT
语句插入到 laion_100m_bfloat16
表中。在 SELECT
期间,我们将嵌入中的值转换为 BFloat16 表示。
这种 bfloat16 转换是使用 arrayMap
函数实现的,即 arrayMap(x -> reinterpretAsFloat32(bitAnd(reinterpretAsUInt32(x), 4294901760)), image_embedding)
。
这将迭代向量嵌入中的每个值 x
,执行转换 reinterpretAsFloat32(bitAnd(reinterpretAsUInt32(x), 4294901760))
- 这使用函数 reinterpretAsUInt32
将二进制序列解释为 Int32,并使用值 4294901760
执行 bitAnd
。后者的二进制序列为 000000000000000001111111111111111
。因此,此操作将尾随的 16 位清零,执行有效的截断。然后,将得到的二进制值重新解释为 float32。
我们将在下面说明这个过程。
INSERT INTO default.laion_1m_bfloat16 SELECT
_file,
key,
url,
caption,
similarity,
width,
height,
original_width,
original_height,
status,
NSFW,
exif,
arrayMap(x -> reinterpretAsFloat32(bitAnd(reinterpretAsUInt32(x), 4294901760)), text_embedding) AS text_embedding,
arrayMap(x -> reinterpretAsFloat32(bitAnd(reinterpretAsUInt32(x), 4294901760)), image_embedding) AS image_embedding,
orientation,
software,
copyright,
image_make,
image_model
FROM laion_1m
如下所示,这将使我们的压缩数据减少了 35% 以上 - 0 的压缩效果非常好。
SELECT
table,
name,
formatReadableSize(sum(data_compressed_bytes)) AS compressed_size,
formatReadableSize(sum(data_uncompressed_bytes)) AS uncompressed_size,
round(sum(data_uncompressed_bytes) / sum(data_compressed_bytes), 2) AS ratio
FROM system.columns
WHERE (table IN ('laion_100m', 'laion_100m_bfloat16', 'laion_10m', 'laion_10m_bfloat16')) AND (name IN ('text_embedding', 'image_embedding'))
GROUP BY
table,
name
ORDER BY table DESC
┌─table───────────────┬─name────────────┬─compressed_size─┬─uncompressed_size─┬─ratio─┐
│ laion_10m_bfloat16 │ text_embedding │ 13.51 GiB │ 28.46 GiB │ 2.11 │
│ laion_10m_bfloat16 │ image_embedding │ 13.47 GiB │ 28.46 GiB │ 2.11 │
│ laion_10m │ text_embedding │ 18.36 GiB │ 28.59 GiB │ 1.56 │
│ laion_10m │ image_embedding │ 18.36 GiB │ 28.59 GiB │ 1.56 │
│ laion_100m_bfloat16 │ image_embedding │ 134.02 GiB │ 286.75 GiB │ 2.14 │
│ laion_100m_bfloat16 │ text_embedding │ 134.82 GiB │ 286.75 GiB │ 2.13 │
│ laion_100m │ text_embedding │ 181.64 GiB │ 286.43 GiB │ 1.58 │
│ laion_100m │ image_embedding │ 182.29 GiB │ 286.43 GiB │ 1.57 │
└─────────────────────┴─────────────────┴─────────────────┴───────────────────┴───────┘
8 rows in set. Elapsed: 0.009 sec.
由于我们的精度降低到 16 位,因此在 ZSTD 压缩级别上的进一步提高将比 32 位表示的影响更小。如下所示,ZSTD(3) 对我们的压缩 bfloat16 几乎没有影响。
SELECT
table,
name,
formatReadableSize(sum(data_compressed_bytes)) AS compressed_size,
formatReadableSize(sum(data_uncompressed_bytes)) AS uncompressed_size,
round(sum(data_uncompressed_bytes) / sum(data_compressed_bytes), 2) AS ratio
FROM system.columns
WHERE (table IN ('laion_100m_bfloat16', 'laion_100m_bfloat16_zstd_3')) AND (name IN ('text_embedding', 'image_embedding'))
GROUP BY
table,
name
ORDER BY table DESC
┌─table──────────────────────┬─name────────────┬─compressed_size─┬─uncompressed_size─┬─ratio─┐
│ laion_100m_bfloat16_zstd_3 │ text_embedding │ 128.12 GiB │ 286.85 GiB │ 2.24 │
│ laion_100m_bfloat16_zstd_3 │ image_embedding │ 127.28 GiB │ 286.85 GiB │ 2.25 │
│ laion_100m_bfloat16 │ image_embedding │ 133.80 GiB │ 286.75 GiB │ 2.14 │
│ laion_100m_bfloat16 │ text_embedding │ 134.59 GiB │ 286.75 GiB │ 2.13 │
└────────────────────────────┴─────────────────┴─────────────────┴───────────────────┴───────┘
除了减少磁盘空间外,压缩率的提高还有其他潜在的好处。我们将通过包含 1000 万行和 1 亿行的表的查询性能来展示这些好处,分别使用编码为 float32 和 bfloat16 的嵌入。这些结果基于 之前使用的同一个查询。
表格 | 编码 | 冷启动(秒) | 热启动(秒) |
---|---|---|---|
laion_10m | Float32 | 12.851 秒 | 2.406 秒 |
laion_10m | bloat16 | 7.285 秒 | 1.554 秒 |
laion_100m | Float32 | 111.857 秒 | 24.444 秒 |
laion_100m | bloat16 | 71.362 秒 | 16.271 秒 |
我们在此线性扫描速度方面的提升是显而易见的,bfloat16 变体将 1 亿行数据集的冷查询性能从 111 秒提高到 71 秒。
一个显而易见的问题可能是,这种精度降低如何影响我们用向量表示概念的能力,以及是否会导致搜索质量下降。毕竟,我们已经减少了我们多维空间中编码的信息,有效地将我们的向量“压缩”到更近的位置。下面我们展示了之前“一只困倦的脊背犬”查询的结果,使用我们新的 laion_100m_v2
表和 search.py
脚本。
python search.py search --text "a sleepy ridgeback dog" --table laion_100m_bfloat16
虽然此搜索没有明显的搜索质量下降,但这可能需要在更广泛的查询样本中进行相关性测试。用户需要在他们的特定模型和数据集上测试这种精度降低技术,结果可能因情况而异。
向量乐趣加成
在阅读了一篇关于如何使用向量数学在高维空间中移动的有趣的博文之后,我们认为看看相同概念是否可以应用于我们生成的 CLIP 嵌入可能很有趣。
例如,假设我们有单词 Berlin
、Germany
、United Kingdom
和 Bridge
的嵌入。可以对它们各自的向量执行以下数学运算。
(berlin - germany) + ('united kingdom' + bridge)
如果我们逻辑上减去和添加上述概念,我们可以假设结果将代表伦敦的一座桥。
为了测试这个想法,我们增强了简单的 search.py
脚本,使其支持一个基本解析器,该解析器可以接受类似于上述内容的输入。此解析器支持运算符 +
、-
、*
和 /
,以及 '
来表示多项输入,并且通过 concept_math
命令公开。
得益于优秀的 pyparsing
库,为这种语法构建解析器非常简单。总之,上述短语将被解析为以下语法树。
反过来,我们可以递归地计算上述树中文本项(叶子)的向量。然后,可以使用 ClickHouse 中的等效向量函数,根据指定的数学运算符来组合分支。此过程以深度优先的方式执行,将整个树解析为单个查询(它应该代表等效概念)。
最后,此函数使用与标准搜索相同的方式与 image_embedding
列匹配。因此,上述内容将解析为以下查询。
SELECT url, caption,
L2Distance(image_embedding,
arrayMap((x,y) -> x+y,
arrayMap((x,y) -> x-y, [berlin embedding], [germany embedding]),
arrayMap((x,y) -> x+y, ['united kingdom' embedding], [bridge embedding])
)
) AS score FROM laion_10m ORDER BY score ASC LIMIT 10
请注意,我们使用 arrayMap 函数来向下推我们的逐点加法和减法(对 +
和 -
运算符作为逐点运算的支持 正在考虑中)。
我们在下面展示了此结果,在 1000 万行样本上进行匹配。
python search.py concept_math —-text "(berlin - germany) + ('united kingdom' + bridge)"
太棒了!它起作用了!请注意,文本中没有提到伦敦桥 - 第一张图片是克劳德·莫奈的滑铁卢桥绘画系列的一部分。
最后,我们认为增强语法解析器以支持整数常数可能很有用。具体来说,我们想知道两个对比概念的中点是否会产生一些有趣的东西。例如,cubism
和 surrealism
概念之间的艺术可能代表什么?这可以用数学方式表示为 (cubism+surrealism)/2
。执行此搜索实际上产生了一些有趣的东西。
我们将留给读者中的艺术家来评论这里的相关性和准确性。
这展示了另一种将向量组合在一起的有趣可能性。毫无疑问,这种基本向量数学在其他情况下可能有用。我们很乐意听到任何例子!
结论
在这篇博文中,我们展示了如何将包含 20 亿行的向量数据集转换为 Parquet 格式并加载到 ClickHouse 中。我们已经证明,这种方法压缩效果很好,线性搜索可以使用 CPU 进行扩展,并使用元数据补充完整的基于 SQL 的分析。最后,我们展示了 ClickHouse 的一些较新的 ANN 功能,并探讨了如何使用 UDF 来提供生成嵌入的优雅函数。
立即开始使用 ClickHouse Cloud 并获得 300 美元的信用额度。在 30 天试用期结束时,您可以继续使用按使用付费计划,或者 联系我们 了解有关我们基于数量的折扣的更多信息。请访问我们的 价格页面 了解详情。