前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >向量搜索与ClickHouse-Part II

向量搜索与ClickHouse-Part II

作者头像
用户3578099
发布2023-09-01 13:53:36
7260
发布2023-09-01 13:53:36
举报
文章被收录于专栏:AI科技时讯

这篇博文延续了我们关于向量搜索的系列文章,建立在前一篇文章的基础上,我们概述了向量搜索是什么,它与历史上基于倒排索引的方法的关系,它目前提供价值的可能用例,以及一些高级实现方法。在这篇文章中,我们通过实际示例详细探讨了向量搜索与ClickHouse的关系,并回答了“我什么时候应该使用ClickHouse进行向量搜索?”

对于我们的示例,我们使用了一个ClickHouse云集群,每个节点总共有60个内核和240GB的RAM。然而,这些示例应该可以在同等大小的自管理集群上重现。或者,今天启动您的ClickHouse云集群,并获得300美元的信用额度。让我们担心基础设施,并开始查询!

ClickHouse是一个实时OLAP数据库,具有完整的SQL支持和广泛的功能来帮助用户编写分析查询,其中一些功能和数据结构执行向量之间的距离操作,使ClickHouse可以用作向量数据库。

由于完全并行化的查询管道,ClickHouse可以非常快速地处理向量搜索操作,特别是在通过对所有行的线性扫描执行精确匹配时,提供与专用向量数据库相当的处理速度。

通过自定义压缩编解码器可调的高压缩级别允许存储和查询非常大的数据集。ClickHouse不受内存限制,允许查询包含嵌入的多TB数据集。

计算两个向量之间距离的功能只是另一个SQL功能,可以有效地与更传统的SQL过滤和聚合功能相结合。这允许向量与元数据甚至富文本一起存储和查询,从而实现广泛的用例和应用程序。

最后,像近似最近邻(ANN)指数这样的实验ClickHouse功能支持更快的向量近似匹配,并提供了一个有希望的发展,旨在进一步增强ClickHouse的向量匹配能力。

总之,ClickHouse是一个有效的矢量搜索平台,当以下任何一个是正确的:

  • 您希望将向量匹配与元数据过滤和/或聚合或连接功能相结合
  • 您需要在非常大的矢量数据集上执行线性距离匹配,并希望在许多CPU内核上并行化和分配这项工作,而无需额外的工作或配置
  • 您需要在由于成本或硬件可用性而依赖仅内存索引不可行的大小的向量数据集上进行匹配
  • 在查询向量时,您将受益于全面的SQL支持
  • 您有一个现有的嵌入生成管道来生成您的向量,并且不需要此功能是存储引擎的原生功能
  • 您已经有相关的数据在ClickHouse不希望招致的开销和成本学习另一个工具为几百万向量
  • 您主要需要向量的快速并行化精确匹配,并且不需要ANN的生产实现(还!)
  • 您是一位经验丰富或好奇的ClickHouse用户,相信我们会提高我们的矢量匹配能力,并希望成为此次旅程的一部分

虽然这涵盖了广泛的用例,但在某些情况下,ClickHouse可能不太适合作为矢量存储引擎,您可能希望考虑替代方案,例如Faiss或专用矢量数据库。今天ClickHouse作为矢量搜索引擎可能提供的好处较少,如果:

  • 你的向量数据集很小,很容易放入内存。虽然ClickHouse可以很容易地完成小数据集的向量搜索,但它可能比这种情况下需要的更强大。
  • 向量中没有额外的元数据,只需要距离匹配和排序。如果将向量搜索结果与其他元数据连接没有用,并且您的数据集很小,那么如上所述,ClickHouse可能比您真正需要的更强大。
  • 您的QPS非常高,每秒大于几千。通常,对于这些用例,数据集将适合内存,并且需要几毫秒的匹配时间。虽然ClickHouse可以服务于这些用例,但简单的内存索引可能就足够了。
  • 您需要一个包含开箱即用的嵌入生成功能的解决方案,其中在插入和查询时集成模型。向量数据库,例如Weaviate,是专门为这个用例设计的,考虑到这些需求可能更合适。

考虑到这一点,让我们探索ClickHouse的矢量功能。

正如我们上一篇文章所讨论的,向量搜索对嵌入进行操作——表示上下文含义的向量。嵌入是通过预先训练的机器学习模型传递原始内容(如图像或文本)来生成的。

在这篇文章中,我们使用了一组准备好的嵌入,可公开下载,称为LAION50亿测试集。我们选择这个数据集是因为我们在编写本文时认为这是可用于测试的最大的可用预计算嵌入数据集。它由嵌入组成,维度为768,用于互联网上数十亿张公共图像及其标题,通过互联网的公共抓取生成。创建它的明确目的是大规模测试矢量搜索,它还包括元数据,这些元数据反过来有助于说明如何将ClickHouse的通用分析功能与矢量搜索相结合。

在LAION数据集中,已经为每个图像及其相关的标题生成了嵌入——为每个对象提供了两个嵌入。对于这篇文章,我们只关注英语子集,它由一个简化的22亿对象组成。尽管这些对象中的每一个都有两个嵌入,一个分别用于图像和一个标题,但我们将每对嵌入存储为ClickHouse中的一行,总共为我们提供了几乎22亿行和44亿向量。对于每一行,我们将元数据作为列包含在内,它捕获诸如图像维度、图像相似性和标题嵌入等信息。这种相似性,余弦距离,允许我们识别标题和图像在概念上不对齐的对象,可能会在查询中过滤掉这些对象。

我们要感谢原始作者整理此数据集并生成供公众使用的嵌入所需的努力。我们建议阅读此数据集的生成过程,它克服了许多具有挑战性的数据工程挑战,例如在合理的时间内以可接受的成本有效地下载和调整数十亿张图像的大小。

这些LAION嵌入是用ViT-L/14模型生成的,LAION使用openCLIP进行训练,openCLIP是OpenAI开发的CLIP模型的开源实现。这不是一个便宜的过程!在4亿图像上,这需要大约30天,需要592个V100图形处理器(AWS按需实例大约100万美元)。

CLIP(对比语言-图像预训练)是一种多模态模型,这意味着它被设计用于训练多种相关类型的数据,例如图像和相关文本。CLIP已被证明在学习文本的视觉表示方面是有效的,在OCR、地理定位和动作识别方面取得了有希望的结果。对于图像的编码,CLIP的作者使用了Resnet50和Vision Transformer(ViT),对于文本的编码使用了类似于GPT-2的转换器。结果嵌入被表示为两组独立的向量。

训练过程的关键结果是两种数据类型的嵌入是可比较的——如果图像和标题的向量接近,那么它们可以被认为在概念上是相似的。像CLIP这样的好模型将导致图像向量及其相关标题向量相对于距离的接近嵌入,或者余弦相似度接近1的高值。这在下面的图像中进行了说明,其中T1是第一个图像标题的嵌入表示,I1是图像本身的编码。这意味着我们希望在训练过程中最大化这个矩阵的对角线,其中我们的图像和文本重合。

作为后处理步骤,作者丢弃了与文本标题余弦相似度小于0.28的图像,从而过滤掉标题和图像不对齐的潜在质量差的结果。通过图像大小、标题长度、可能的非法性和重复项的删除进行的进一步过滤将总数据集从50亿减少到22亿。

信用: https://openai.com/research/clip

LAION数据集可从多个来源下载。选择英文子集,我们使用了拥抱脸托管的版本。该服务依赖于Git大文件存储(LFS),它需要安装客户端才能下载文件。安装后,下载数据需要一个命令。为此,请确保您至少有20TB的可用磁盘空间。

代码语言:javascript
复制
git lfs install
git clone https://huggingface.co/datasets/laion/laion2b-en-vit-l-14-embeddings

下载由三个文件夹组成;其中两个包含图像和标题格式为npy(实际上是多维数组格式)的嵌入,第三个目录包含Parquet文件,其中包含每个图像和标题对的元数据。

代码语言:javascript
复制
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中的向量可以表示为Floats数组,此过程产生的JSON行可能如下所示:

代码语言:javascript
复制
{
 "key": "196060024",
 "url": "https://cdn.shopify.com/s/files/1/1194/1070/products/s-l1600_16_-_Copy_110x110@2x.jpg?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数据集,我们的用户可以下载并用于重现示例。

将生成的Parquet文件加载到ClickHouse需要几个简单的步骤。

下面显示了我们的表模式,其中嵌入存储为Array(Float32) 列。

代码语言:javascript
复制
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键及其频率:

代码语言:javascript
复制
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,用户可以执行以下查询:

代码语言:javascript
复制
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*. parque)。通过确认ClickHouse中的计数和原始Parquet文件中的计数,_file列可用于协调任何加载问题。

对于下面的示例,我们创建了带有表示行数的后缀的各种大小的表;例如,laion_100m包含1亿行。这些是使用适当的blob模式创建的。

代码语言:javascript
复制
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和2b行。

代码语言:javascript
复制
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中的默认值1提高。这提供了大约10%的改进,在1000万行样本上将我们的数据压缩了1.71:

代码语言:javascript
复制
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位。我们将在下面的“改进压缩”部分讨论这一点。

正如我们在本系列的第1部分中所介绍的,执行向量搜索意味着将输入向量与向量存储库进行比较,以找到最接近的匹配项。

输入向量表示感兴趣的概念。在我们的例子中,这是一个编码的图像或标题。向量存储库表示我们希望比较的其他图像及其标题。

执行搜索时,将比较向量的接近度或距离。距离较近的两个向量表示相似的概念。距离最近的两个向量在集合中最相似。

鉴于向量的高维性,有许多方法可以比较距离。这些不同的机制被称为距离函数

ClickHouse支持广泛的距离函数-您可以根据您的用例选择最适合您的距离函数。对于这篇文章,我们将重点关注矢量搜索中非常常用的两个函数:

  • 余弦距离-余弦距离(vector1, vector2)-这为我们提供了2个向量之间的余弦相似性。更具体地说,这测量了两个向量之间角度的余弦,即点积除以长度。这会产生一个介于-1和1之间的值,其中1表示两个嵌入是成比例的,因此在概念上是相同的。可以解析列名和输入嵌入以进行向量搜索。如果向量没有被归一化,以及提供一个可用于过滤的有界范围,则此函数特别相关。
  • L2距离-L2距离(vector1, vector2)-测量2个点之间的L2距离。实际上,这是两个输入向量之间的欧几里德距离,即向量表示的点之间的线的长度。距离越低,源对象在概念上越相似。

这两个函数都计算用于比较向量嵌入的分数。对于我们预先训练的CLIP模型,L2距离表示基于用于官方示例的内部评分的最合适的距离函数。

有关可用距离和矢量归一化函数的完整列表,请参见此处。我们很想知道您如何利用这些来搜索嵌入!

现在我们已经确定了我们将使用哪个距离函数,我们需要将输入(我们想要搜索的图像或标题)转换为向量嵌入。

这需要我们调用CLIP模型。这很容易通过一个简单的Python脚本来实现。安装该脚本依赖项的说明可以在这里找到。我们在下面展示这个脚本:

代码语言:javascript
复制
#!/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.2倍大字幕上为1秒。

作为一个例子,让我们将文本“a瞌睡脊背狗”转换为嵌入。为了简洁起见,我们已经裁剪了完整的嵌入结果,可以在这里找到。

代码语言:javascript
复制
python generate.py --text "a sleepy ridgeback dog"

[0.5736801028251648, 0.2516217529773712, ...,  -0.6825592517852783]

我们现在有一个向量嵌入,它表示文本“一只困倦的脊背狗”这是我们的搜索输入向量。我们现在可以将这个输入向量与我们的向量嵌入库进行比较,以找到代表概念相似事物的图像及其标题。

下面的查询搜索概念上相似的嵌入,并按距离对其进行排序。嵌入存储在image_embedding列中。距离存储为相似性。我们过滤掉任何大于0.2的距离以减少噪音。

代码语言:javascript
复制
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.)

结果表明,我们的输入向量“一只困倦的脊背犬”在概念上最类似于非洲罗得西亚脊背犬数据集中的一张照片,在概念上也非常类似于一只睡着的猎狗的图像。

我的狗基博

为了进一步证明这些模型的实用性,作为使用文本搜索的替代方案,我们可以从睡着的狗的图像开始,以这种方式搜索相似的图像。我们生成一个代表这张照片的输入向量,并搜索概念上相似的结果。

为此,我们使用text_embedding列重复上述查询。完整的嵌入可以在这里找到。

代码语言:javascript
复制
python generate.py --image images/ridgeback.jpg

[0.17179889976978302, 0.6171532273292542, ...,  -0.21313616633415222]
代码语言:javascript
复制
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文件。然后该文件会在本地浏览器中自动打开。上述查询的结果文件如下所示:

代码语言:javascript
复制
python search.py search --image images/ridgeback.jpg --table laion_10m

在这两个示例中,我们都匹配了不同模态的嵌入,即来自图像输入的嵌入与text_embedding列匹配,反之亦然。这与前面描述的原始模型训练一致,并且是预期的应用程序。虽然已经探索了将输入嵌入与相同类型匹配,但之前的尝试导致了混合结果。

通常,在向量搜索的实践中,我们不仅仅是跨嵌入搜索。通常,将搜索与元数据的过滤或聚合相结合还有额外的效用。

例如,假设我们希望对无版权的图像执行矢量搜索。这种查询将矢量搜索与基于版权元数据的过滤相结合。

再举一个例子,假设我们想将搜索限制在大图像上——至少300像素*500像素,其中标题相似度满足更高的余弦相似度分数0.3。对于这个例子,让我们从搜索“伟大的动物迁徙”开始。幸运的是,将其表述为SQL查询很简单。下面,我们对1亿图像执行此查询。

代码语言:javascript
复制
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传递额外过滤器的功能,允许我们验证上述匹配的质量:

代码语言:javascript
复制
python search.py search --filter "(width >= 300) AND (height >= 500) AND (copyright = '') AND simularity > 0.3" --text "great animal migrations"

除了过滤,我们还可以对元数据执行聚合。作为一个面向列的数据库,ClickHouse非常适合这项任务。

例如,假设我们想识别用于“safari图片”的主要相机型号。我们在这里执行搜索:

代码语言:javascript
复制
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.)

显然,佳能应该是你下safari的首选相机。请注意,在这里,我们只使用前1000个结果。与余弦距离不同,余弦距离是无界的,欧几里得距离没有上限——这使得强加阈值具有挑战性。

注意:倒排索引是ClickHouse中的一个实验特性。

ClickHouse的实验性二级索引功能也可以证明对向量处理很有用。

例如,我们可能希望强制执行一个过滤器,将我们的safari图片限制为包含狮子的图片。为此,我们可以施加一个令牌限制——要求标题列包含字符串狮子

如果没有倒排索引,我们的搜索可能如下所示。在这里,我们利用下面图片的嵌入并针对100M向量进行搜索。

代码语言:javascript
复制
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.)

为了加速这种元数据查询,我们可以利用倒排索引,并为标题列添加倒排索引。

代码语言:javascript
复制
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秒。

代码语言:javascript
复制
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.)

此查询的结果如下:

代码语言:javascript
复制
python search.py search --image ./images/safari.jpg --table laion_100m --filter "hasToken(lower(caption), 'lions')"

注意:Annoy索引在ClickHouse中是高度实验性的。

Annoy索引旨在提高大规模最近邻向量搜索的效率。随之而来的是准确性和计算效率之间的权衡。

具体来说,Annoy索引是一种数据结构,用于在高维空间中查找近似最近邻。Annoy的工作原理是将向量组织成树结构。它使用随机超平面(2d空间中的线,3d中的平面等)将高维空间划分为分区。这些超平面将空间拆分为更小的区域,每个区域仅包含数据点的子集。这些分区反过来用于构建树结构(通常是二进制),其中每个节点表示一个超平面,子节点表示拆分平面的区域。树的叶子节点包含实际的数据点。平衡和优化技术,例如随机化插入和使用启发式方法来确定用于分区的最佳超平面,确保树高效且平衡良好。

一旦构建了Annoy索引,它就可以用于搜索。在提供向量时,可以通过将每个向量与每个内部节点的超平面进行比较来遍历树。在树的每个级别,Annoy估计查询向量与子节点表示的区域之间的距离。距离度量确定要进一步探索哪个子节点。到达根节点或指定节点后,返回它遇到的节点集。结果是一组近似的结果,其搜索时间可能比线性扫描快得多。

Annoy分割的超平面图像

在为ClickHouse创建Annoy索引时,我们可以同时指定NumTree和远程名称。后者表示所使用的距离函数,默认为L2距离,适用于我们的LAION数据集。前者表示算法将创建的树的数量。树越大,工作速度越慢(在CREATE和SELECT请求中),但您获得的精度越高(根据随机性进行调整)。默认情况下,NumTree设置为100。

下面,我们展示了LAION数据集的模式,每个嵌入字段都有一个Annoy索引。我们使用索引的默认值并用100m行填充表。

代码语言:javascript
复制
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索引的开销在插入时非常大,100m行的上述插入大约需要27分钟。相比之下,没有这些索引的表需要10分钟。下面,我们重复我们的早期查询,大约需要24秒(热)。

代码语言:javascript
复制
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到2s之间,但牺牲了一些搜索质量。

这里的测试嵌入代表了我们的“一只困倦的脊背狗”文本。我们可以看到下面的图像结果。

代码语言:javascript
复制
python search.py search --text "a sleepy ridgeback dog" --table laion_100m_annoy

在ClickHouse中,重要的是要注意,Annoy索引可用于加快查询速度,这些查询可以利用ORDER BY距离函数(列,向量)WHERE距离函数(列,点)<最大距离,但不能两者兼而有之。必须对查询施加LIMIT,以返回前N个匹配项。要返回前N个匹配项,将使用基于优先级队列的缓冲区来收集匹配的向量。一旦已满,收集停止,缓冲区将被排序。此缓冲区的大小受设置max_limit_for_ann_queries(默认为1000000)的限制。

ClickHouse的用户定义函数或UDF允许用户通过创建可以利用SQL构造和函数的lambda表达式来扩展ClickHouse的行为。然后,这些函数可以像查询中的任何内置函数一样使用。

到目前为止,我们一直依赖于在ClickHouse之外执行向量生成,并在查询时从我们的search.py脚本传递生成的嵌入。虽然这就足够了,但如果我们可以直接在SQL查询中传递文本或图像路径(甚至网址!),那就太好了。

我们可以使用UDF来完成这个任务。下面定义的UDF分别称为embedTextembImage

代码语言:javascript
复制
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

为了定义embTextUDF,我们首先将之前用于生成嵌入的generate.py改编为下面的embed_text.py。

注意:这应该保存在ClickHouse的 user_scripts 文件夹中。

代码语言:javascript
复制
#!/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脚本可以通过自定义函数embText公开。以下配置可以放在ClickHouse配置目录(默认/etc/clickhouse-server)下,名称为embed_text__function. xml

注意:用户应确保已为 clickhouse 用户安装此脚本的依赖项-有关步骤,请参见 此处

代码语言:javascript
复制
<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>

注册函数后,我们现在可以使用它,如前面的示例所示:

代码语言:javascript
复制
SELECT
        url,
        caption,
        L2Distance(image_embedding, embedText('a sleepy ridgeback dog')) AS score
FROM laion_10m
ORDER BY score ASC
LIMIT 10

对于我们类似的embImage函数,我们基于以下python脚本添加另一个UDFembed_image.py。

代码语言:javascript
复制
#!/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()
代码语言:javascript
复制
<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都配置好了,我们可以如下查询:

代码语言:javascript
复制
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脚本和函数嵌入概念公开我们早期的概念数学功能。

代码语言:javascript
复制
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

请注意,上面的示例不包括错误处理和输入验证。我们将此作为读者的练习。希望这些示例为组合用户定义函数、嵌入模型和向量搜索提供了一些灵感!

增强的压缩技术可能有助于满足整体数据大小和存储需求。例如,我们以前的模式和生成的压缩统计信息是基于将向量存储为数组(Float32)类型。尽管对于某些模型,不需要32位浮点精度,并且可以通过将其减少到16位来实现类似的匹配质量。

虽然ClickHouse没有原生的16位浮点类型,但我们仍然可以将精度降低到16位并重用Float32类型,每个值只需用零填充。这些零将使用ZSTD编解码器(ClickHouse云中的标准)有效压缩,从而减少我们压缩的存储需求。

为了实现这一点,我们需要确保16位浮点值的编码正确完成。幸运的是,谷歌的bloat16类型用于机器学习用例运行良好,只需截断32位浮点数的最后16位,假设后者使用IEE-754编码。

信用: https://cloud.google.com/tpu/docs/bfloat16

虽然bfloat16目前不是ClickHouse原生的,但它可以很容易地与其他函数一起复制。我们在下面的image_embeddingtext_embedding列中这样做。

为此,选择表laion_100m中的所有行(包含100m行),并使用INSERT INTO SELECT子句将其插入表laion_100m_bfloat16。在SELECT期间,我们将嵌入中的值转换为BFloat16表示。

这种bfloat16转换是使用arrayMap函数实现的,即arrayMap(x->re解释AsFloat32(bitAnd(re解释AsUInt32(x),4294901760)),image_embedding)

这将遍历向量嵌入中的每个值x,执行转换重新解释AsFloat32(bitAnd(re解释AsUInt32(x),4294901760))-这将使用函数重新解释AsUInt32将二进制序列解释为Int32,并使用值4294901760执行bitAnd。后一个值是二进制序列000000000000000001111111111111111。因此,此操作将尾随的16位归零,执行有效的截断。然后将生成的二进制值重新解释为float32。

我们在下面说明这个过程:

代码语言:javascript
复制
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
代码语言:javascript
复制
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的影响很小。

代码语言:javascript
复制
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 │
└────────────────────────────┴─────────────────┴─────────────────┴───────────────────┴───────┘

除了减少磁盘空间之外,这种压缩的增加还有其他潜在的好处。我们通过对包含10m和100m行的表的查询性能来证明这些好处,使用编码为float32和bfloat16的嵌入。这些结果基于前面使用的相同查询。

编码

冷(秒)

热(秒)

laion_10m

Float32

12.851秒

2.406s

laion_10m

bloat16

7.285s

1.554秒

laion_100m

Float32

111.857s

24.444秒

laion_100m

bloat16

71.362秒

16.271秒

这里我们的线性扫描速度的收益是可观的,bfloat16变体将我们的性能从冷查询100m行数据集的111秒提高到71秒。

一个显而易见的问题可能是这种精度的降低如何影响我们在向量中表示概念的能力,以及它是否会导致搜索质量的降低。毕竟,我们已经减少了编码在多维空间中的信息,并有效地将我们的向量“更接近”在一起。下面,我们使用新的laion_100m_v2表和search.py脚本显示了早期“一只困倦的脊背狗”查询的结果。

代码语言:javascript
复制
python search.py search --text "a sleepy ridgeback dog" --table laion_100m_bfloat16

虽然这并没有明显降低搜索质量,但这可能需要在更广泛的查询样本中进行相关性测试。用户需要在他们的特定模型和数据集上测试这种精度降低技术,结果可能会因具体情况而异。

在阅读了一篇关于如何使用向量数学在高维空间中移动的有趣博客文章后,我们认为看看是否可以将相同的概念应用于我们的CLIP生成的嵌入可能会很有趣。

例如,假设我们有单词柏林德国英国的嵌入。可以对它们各自的向量执行以下数学运算。

(柏林-德国)+('英国'+大桥)

如果我们从逻辑上减去并添加上述概念,我们可以假设结果将代表伦敦的一座桥。

测试这个想法,我们增强了简单的search.py脚本以支持一个可以接受类似上述输入的基本解析器。该解析器支持操作+-*/,以及'来表示多项式输入,并通过concept_math命令公开。

多亏了强大的pyparsing库,为这种语法构建解析器是微不足道的。总之,上面的短语将被解析成以下语法树:

反过来,我们可以递归地计算上述树中文本术语(叶子)的向量。然后,可以使用ClickHouse中指定数学运算符的等效向量函数组合分支。这个过程是深度优先执行的,将整个树解析为单个查询(它应该代表等效的概念)。

最后,使用与标准搜索相同的过程在image_embedding列上匹配此函数。因此,上述内容将解析为以下查询:

代码语言:javascript
复制
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函数向下推动逐点加减法(+-运算符作为逐点运算的支持正在考虑中)。

我们在下面显示结果,匹配10m行示例:

代码语言:javascript
复制
python search.py concept_math —-text "(berlin - germany) + ('united kingdom' + bridge)"

酷!成功了!注意文字没有提到伦敦桥——第一张图片是克劳德·莫奈滑铁卢桥系列画作的一部分。

最后,我们认为增强语法解析器以支持整数常量可能很有用。具体来说,我们想看看两个对比概念之间的中点是否产生了一些有趣的东西。例如,概念立体主义超现实主义之间的艺术可能代表什么?这可以在数学上表示为(立体主义+超现实主义)/2。执行此搜索实际上产生了一些有趣的东西:

我们把它留给我们读者中的艺术家来评论这里的相关性和准确性。

这展示了组合向量的另一种有趣的可能性。毫无疑问,这种基本向量数学在其他情况下也很有用。我们很想听听任何例子!

在这篇博文中,我们展示了如何将20亿行的向量数据集转换为Parquet格式并加载到ClickHouse中。我们已经证明了这可以很好地压缩,线性搜索可以使用CPU进行扩展,并与使用元数据的基于SQL的完整分析相辅相成。最后,我们展示了ClickHouse的一些较新的ANN特性,并探索了如何使用UDF为生成嵌入提供优雅的函数。

🔗 原文链接: https://clickhouse.com/blog/vector-...

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2023-08-10,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 AI科技时讯 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
向量数据库
腾讯云向量数据库(Tencent Cloud VectorDB)是一款全托管的自研企业级分布式数据库服务,专用于存储、检索、分析多维向量数据。该数据库支持多种索引类型和相似度计算方法,单索引支持千亿级向量规模,可支持百万级 QPS 及毫秒级查询延迟。腾讯云向量数据库不仅能为大模型提供外部知识库,提高大模型回答的准确性,还可广泛应用于推荐系统、自然语言处理等 AI 领域。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档