使用 Redis 构建轻量的向量数据库应用:图片搜索引擎(一)

本篇文章聊聊更轻量的向量数据库方案:Redis。

以及基于 Redis 来快速实现一个高性能的本地图片搜索引擎,在本地环境中,使用最慢的稠密向量检索方式来在一张万图片中查找你想要的图片,总花费时间都不到十分之一秒。

写在前面

Redis, 你这浓眉大眼的家伙也正式支持向量检索啦!

接着上一篇文章的话题,继续聊聊“图片搜索引擎”。给月底即将发生的一场分享中的“命题作文”补充一些详细的实践教程:《使用向量数据库快速构建本地轻量图片搜索引擎》。其实,在一年前,在做 Milvus 开源布道师的时候,我曾经写过一些 Milvus 相关的内容。这篇分享中提到的“图片搜索引擎”的话题,我在一年前就写过啦:《向量数据库入坑:使用 Docker 和 Milvus 快速构建本地轻量图片搜索引擎》。

不过,在这场分享活动中,有来自各种厂商的向量数据库“利益相关”的从业者,举办方站在中立立场上,希望大家的分享内容都更加中立客观的,尤其是厂商之外的分享者,不要表现太多的偏向性,话题百花齐放更好些,朋友的要求,自然是要尊重的

此外,距离我发布上一篇“图片搜索引擎”后,不论是文章中使用的向量数据库 Milvus、还是用来快速做 Embedding 的 Towhee 不论是项目还是团队,都经历了比较多的迭代,面向的目标客户群体和场景也更明确,不太适合再做本地解决方案,更适合云端分布式场景

正巧,在合作中的其中一家朋友的公司,前段时间也在折腾向量数据库,他更倾向先使用“更老牌”一些的技术方案,诸如:Elasticsearch、Mongo、Postgres、ClickHouse、Redis 这类加上向量数据库解决能力的成名久已的传统解决方案。

所以,这篇文章就来聊聊用户群体甚多,大家都很熟悉的老牌开源软件:Redis 的向量数据库场景实践。

准备材料

接下来聊聊本篇实践内容中需要的三个素材:Docker、HuggingFace 上下载的 OpenAI 的 Clip 模型(用于 Embedding)、以及适合我们自己或者业务实际使用的大量的图片数据集(文本、语音、视频、文件等同理)。

本文中使用的相关程序都已经开源在 soulteary/simple-image-search-engine/,欢迎一键三连,😄

Docker 运行环境

容器能够提供标准的、可复现的稳定环境,非常适合折腾 “AI 应用”

本文的所有内容都可以在标准的 Docker 容器环境中复现。

所以,想顺滑的完成实践,我推荐你安装 Docker,不论你的设备是否有显卡,都可以根据自己的操作系统喜好,参考这两篇来完成基础环境的配置《基于 Docker 的深度学习环境:Windows 篇》、《基于 Docker 的深度学习环境:入门篇》。当然,使用 Docker 之后,你还可以做很多事情,比如:之前几十篇有关 Docker 的实践,在此就不赘述啦。

如果你和我一样,使用 Docker 环境折腾、学习和用于生产。那么,我推荐你使用 Nvidia 家提供的深度学习环境 nvcr.io/nvidia/pytorch:23.10-py3 作为基础镜像,其中的 CUDA 版本经常效率比公开的开源社区版本要跑的更快一些:

FROM nvcr.io/nvidia/pytorch:23.10-py3
RUN pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple && \
    pip3 install --upgrade pip  # enable PEP 660 support
WORKDIR /app
RUN pip3 install transformers==4.35.0 "redis[hiredis]==5.0.1"

我们将上面的内容保存为 Dockerfile,然后使用下面的命令来完成稍后使用的镜像的构建(项目中的相关文件保存在 soulteary/simple-image-search-engine/docker/Dockerfile):

docker build -t soulteary/image-search-engine:20231114 .

当然,如果你不使用 Docker 环境,也可以通过手动安装 pytorch 和执行下面的命令,完成 PyPi 相关依赖安装:

pip3 install transformers==4.35.0 "redis[hiredis]==5.0.1"

当然,为了折腾更简单一些,我还做了一个能够让我的读者一键拉起来的运行环境:

version: "2.4"

services:
  embededing-server:
    ipc: host
    ulimits:
      memlock: -1
      stack: 67108864
    stdin_open: true
    tty: true
    volumes:
      - ./make-embededing:/app
    image: soulteary/image-search-engine:20231114
    command: tail -f /etc/hosts
    container_name: embededing-server

  redis-server:
    image: redis/redis-stack-server:7.2.0-v6
    volumes:
      - ./redis-data:/data

将上面的文件保存为 docker-compose.yml。然后使用 docker compose up 启动服务,我们就能够分别使用下面的命令,来访问用来构建向量的容器 embededing-serverredis-server 啦。

# 使用命令行进入 Embededing Server 容器
docker exec -it embededing-server bash
# 使用命令行进入 Redis Server 容器
docker exec -it redis-server bash

HuggingFace 上的 OpenAI Clip 模型

OpenAI Clip

本篇文章主要使用的模型是 OpenAI 在两年前开源的 Clip 模型,也是 HuggingFace 上的宝藏模型之一。关于 Clip 的介绍,OpenAI 公开的研究页面有非常详细的资料,如果你感兴趣,可以移步阅读。

在图片搜索这个场景下,我们可以根据自己的情况选择下面两个版本的模型,推荐选择 patch16 版本,相对新一些:

  • openai/clip-vit-base-patch16(第二版发布的版本)
  • openai/clip-vit-base-patch32(第一版发布的版本)

至于更新一些时候发布的两个标记为 large 的版本,好用是好用,但是需要更多的资源(模型尺寸接近之前的三倍):

  • openai/clip-vit-large-patch14-336 (第四版发布版本)
  • openai/clip-vit-large-patch14 (第三版发布版本)

开源项目中的代码,直接执行,会自动下载这两个模型。不过因为一些原因,Huggingface 的模型有时候会下载的特别慢,所以我们可以考虑用下面两个方案来加速模型下载,比如直接使用 Huggingface 新推出的命令行 HF Transfer 和好心网友搭建的 hf-mirror.com 加速器,来完成模型的快速下载:

HF_ENDPOINT=https://hf-mirror.com HF_HUB_ENABLE_HF_TRANSFER=1 huggingface-cli download --resume-download openai/clip-vit-base-patch16 --local-dir clip-vit-base-patch16 --local-dir-use-symlinks False

不过因为上面的加速网站的原理是依赖 CDN,有时候要“看天吃饭”,不甚稳定。所以,你也可以使用百度网盘下载我上传好的两个模型。

模型下载完毕后,将它放在 openai 目录后,我们开始处理图片数据集。

从视频文件中提取图片数据集

因为这篇命题是图片搜索,所以我们还需要一些有趣的、大量的图片数据。在上一篇“图搜实践”的文章里,我用的是从搜索引擎搜索出的第一页原神卡通壁纸,数量不多,只有 60 多张壁纸。

为了更直观的感受 Redis 作为向量数据库的性能优势,我们需要把图片数据整的更多一些。

通常情况下,获取合适的数据集自然是有难度的,但是在学习研究的情况下,或许你可以参考这篇文章《开源软件 FFmpeg 生成模型使用图片数据集》,使用造福了无数视频软件公司、在线直点播公司、无数 CDN 云服务厂商的 FFmpeg 和你喜爱的电影、视频,来手动构建适合你的测试数据集。

选择一部你喜欢的电影,动手拆一套图片数据集出来

通过上面的方法,我把这部电影转换成了每秒 10393 张图片(其实也不多,数量级还是太小了),它们被命名为 ball-001.pngball-002.png … 之所以使用视频中的关键帧作为数据集,主要的原因是:这类数据比较有代表性、画面质量相对较高,包含高质量的多种分类的图片。 目前互联网流量中绝大多数是视频,在“哔哩哔哩”或者各种 PT 爱好者网站、以及各种百度云、阿里云盘等资源站点,视频资源的获取难度相对较低,资源相对充分,比如这篇文章我们可以以科幻电影为例,也可以以纪录片为例、或者用连续剧也没啥问题。

如果你希望获得更大规模大数据集,你可以尝试比如把 “哈利波特系列”、“老友记”、“狂飙” 这种可以转换出图片数量更多的电影、电视剧作为目标,轻轻松松搞出十万级、百万级的图片数据集。

将图片放在名为 images 的目录中,我们要给搜索引擎建立“底库”的数据集准备工作就完毕啦。

一切都准备好之后,我们开始通过编写少量代码,完成这个曾经只有互联网大厂才提供服务的:图片搜索引擎。

设计程序

正常情况下,图片搜索引擎会有两套主要的工作流程。

第一套逻辑是:“制菜和备菜”。使用一些能够解析图片的模型程序,解析海量图片中的特征点,并进行向量化存储,建立合适的数据库索引,方便后续提供服务。

第二套逻辑是:“菜品的售卖”。制作一个用户看着顺眼的界面(网页、客户端)让用户能够通过一些交互方式,来实现用文本搜索图片(文本搜索图片内容或上下文的文本),或者用图片来搜索图片(以图搜图)。

这两套逻辑一般情况下分开处理,各自选择最合适的技术方案性能最好,资源消耗最少,也利于进行水平扩展。第一套逻辑因为数据量通常巨大,适合用“离线、批处理”的方式来做,可以节约大量的成本;而第二套逻辑,则是我们日常使用的搜索引擎,我们在搜索内容的时候,遇到在系统中搜索一个东西超过几秒其实不常见,对于性能要求还是很高的,不然就有极其差的体验或者口碑。

图片搜索引擎的不足之处

目前视频和图片都搜索产品,其实都还不是那么的完善。不论是国内还是海外的产品,目前提供公开的、能够满足大量用户使用的产品还做不到一些看起来很自然的事情:搜索 “连续剧里吃冰糕的小男孩” 就能够快速定位某个影视剧、以及从该影视剧中的小男孩开始吃冰糕的那一秒开始播放。或者搜索“某某电影中男一号第二天起床后,旁边桌面上的闹钟的购物链接”。

有非常多的搜索引擎,还在依赖着上一代的文本检索、或者基础的语义检索的方式,来针对和图片一起出现的网页文本来进行内容关联。 不少时候,靠的还是搜索到视频编辑运营或用户发布的带有对应描述的文本内容,捎带出来的图片。甚至还有靠文本完全匹配来进行图片推荐的,也是离谱,都 2023 年末啦!

主要依赖文本匹配或者文本语义检索的产品

关于上面提到的“语义检索”,在之前的这篇文章中有提到过:《向量数据库入坑:传统文本检索方式的降维打击,使用 Faiss 实现向量语义检索》,感兴趣可以自行翻阅,自己实现一个试试看。

不过随着算力的发展、越来越多的软件都开始支持向量检索,用户可以被模型宠溺的越来越懒,相信这个状况一定会有所改善。

图片等数据的向量化处理

言归正传,我们先来实现第一套搜索引擎的处理逻辑,将图片进行向量化处理和存储到向量数据库中。

将图片进行向量化处理

为了方便我们测试代码功能,先选择流浪地球2剧作中的一帧画面(随便选就行):

流浪地球中的一幕

下面这段代码,实现了从 HuggingFace 加载 OpenAI 的 Clip 模型,并对电影流浪地球2 中的我们选择的某一帧画面进行向量化处理,生成可以被存储在 Redis 中的数据的逻辑:

import torch
import numpy as np
from transformers import CLIPProcessor, CLIPModel
from PIL import Image
import time

# 默认从 HuggingFace 加载模型,也可以从本地加载,需要提前下载完毕
model_name_or_local_path = "openai/clip-vit-base-patch16"

model = CLIPModel.from_pretrained(model_name_or_local_path)
processor = CLIPProcessor.from_pretrained(model_name_or_local_path)

# 记录处理开始时间
start = time.time()
# 读取待处理图片
image = Image.open("ball-8576.png")
# 处理图片数量,这里每次只处理一张图片
batch_size = 1

with torch.no_grad():
    # 将图片使用模型加载,转换为 PyTorch 的 Tensor 数据类型
    # 你也可以在这里对图片进行一些特殊处理,裁切、缩放、超分、重新取样等等
    inputs = processor(images=image, return_tensors="pt", padding=True)
    # 使用模型处理图片的 Tensor 数据,获取图片特征向量
    image_features = model.get_image_features(inputs.pixel_values)[batch_size-1]
    # 将图片特征向量转换为 Numpy 数组,未来可以存储到数据库中
    embeddings = image_features.numpy().astype(np.float32).tolist()
    print('image_features:', embeddings)
    # 打印向量维度,这里是 512 维
    vector_dimension = len(embeddings)
    print('vector_dimension:', vector_dimension)
    # 计算整个处理过程的时间
    end = time.time()
    print('%s Seconds'%(end-start))

将上面的代码保存为 app.py,执行 python app.py 后,不出意外,我们就能够得到这张图片的向量数据、向量数据的维度、以及处理时间啦:

image_features: [-0.4382634162902832, -0.3964928984642029, 0.23583322763442993, -0.31856775283813477, -0.2937283515930176, 0.13698264956474304, -0.32216179370880127, 0.2034275382757187, 0.11416329443454742, 0.08056379109621048, 
...
    -0.2022382766008377, -0.3622089624404907, 0.14547640085220337, 0.20014266669750214, -0.08147376030683517, 0.24707356095314026, 0.1416967660188675, 0.305078387260437, 0.5607554316520691, -0.005001917481422424]

vector_dimension: 512

0.10873532295227051 Seconds

这部分的代码开源在了 GitHub 的 soulteary/simple-image-search-engine/steps/1.how-to-embededing,有需要可以自取,注释都写的比较详尽啦,就不赘述啦。

获取一万张图片的有序列表

目录中的图片,虽然有序号,但是倘若直接用程序读取图片列表,我们很难保障获取的图片顺序。而有序的存储图片,有利于后续继续拓展这个图片搜索引擎的能力,比如:实现视频搜索引擎,或者实现自动分段视频剪辑工具,连续的内容,一般是连续序列存放。

import os

image_directory = "images"

# 使用列表推导式获取目录中所有的 PNG 图片名称
png_files = [filename for filename in os.listdir(image_directory) if filename.endswith(".png")]

# 根据文件名中的数字部分进行排序
sorted_png_files = sorted(png_files, key=lambda x: int(x.split('-')[-1].split('.')[0]))

# 打印排序后的 PNG 图片名称列表
for idx, png_file in enumerate(sorted_png_files, start=1):
    print(f"{idx}: {png_file}")

将上面的内容保存为 app.py,然后再次使用 python app.py 执行代码,就能够获得有序的文件列表啦:

1: ball-001.png
2: ball-002.png
3: ball-003.png
4: ball-004.png
5: ball-005.png
6: ball-006.png
7: ball-007.png
8: ball-008.png
9: ball-009.png
...
10391: ball-10391.png
10392: ball-10392.png
10393: ball-10393.png

这部分的代码保存在 GitHub 的这个目录:soulteary/simple-image-search-engine/steps/2.get-all-sorted-images。

处理所有的图片数据

将上面的两个代码片段进行合理的组合,我们就能够得到一份依次处理所有图片 embedding 数据的程序啦:

import torch
import numpy as np
from transformers import CLIPProcessor, CLIPModel
from PIL import Image
import time
import os

model_name_or_local_path = "openai/clip-vit-base-patch16"
model = CLIPModel.from_pretrained(model_name_or_local_path)
processor = CLIPProcessor.from_pretrained(model_name_or_local_path)

image_directory = "images"
png_files = [filename for filename in os.listdir(image_directory) if filename.endswith(".png")]
sorted_png_files = sorted(png_files, key=lambda x: int(x.split('-')[-1].split('.')[0]))

batch_size = 1

with torch.no_grad():
    for idx, png_file in enumerate(sorted_png_files, start=1):
        print(f"{idx}: {png_file}")
        start = time.time()
        image = Image.open(f"{image_directory}/{png_file}")
        inputs = processor(images=image, return_tensors="pt", padding=True)
        image_features = model.get_image_features(inputs.pixel_values)[batch_size-1]
        embeddings = image_features.numpy().astype(np.float32).tolist()
        print('image_features:', embeddings)
        vector_dimension = len(embeddings)
        print('vector_dimension:', vector_dimension)
        end = time.time()
        print('%s Seconds'%(end-start))

这部分的代码保存在:soulteary/simple-image-search-engine/steps/3.how-to-embededing-all-images。

使用 Redis 存储图片的向量数据

前面的文章中,我们聊过了如何使用 Clip 模型来对图片进行向量化处理,以及如何批量处理大量文件数据,接下来我们来看看如何操作,以上文中最简单的 Embededing 实现为例:

import torch
import numpy as np
from transformers import CLIPProcessor, CLIPModel
from PIL import Image
import time
import redis

# 连接 Redis 数据库,地址换成你自己的 Redis 地址
client = redis.Redis(host="redis-server", port=6379, decode_responses=True)
res = client.ping()
print("redis connected:", res)

model_name_or_local_path = "openai/clip-vit-base-patch16"
model = CLIPModel.from_pretrained(model_name_or_local_path)
processor = CLIPProcessor.from_pretrained(model_name_or_local_path)

png_file = "ball-8576.png"

start = time.time()
image = Image.open(png_file)
batch_size = 1

# 初始化 Redis Pipeline
pipeline = client.pipeline()
# 初始化 Redis,先使用 PNG 文件名作为 Key 和 Value,后续再更新为图片特征向量
pipeline.json().set(png_file, "$", png_file)
res = pipeline.execute()
print('redis set keys:', res)

with torch.no_grad():
    inputs = processor(images=image, return_tensors="pt", padding=True)
    image_features = model.get_image_features(inputs.pixel_values)[batch_size-1]
    embeddings = image_features.numpy().astype(np.float32).tolist()
    vector_dimension = len(embeddings)
    print('vector_dimension:', vector_dimension)
    end = time.time()
    print('%s Seconds'%(end-start))
    # 将计算出的 Embeddings 更新到 Redis 数据库中
    pipeline.json().set(png_file, "$", embeddings)
    res = pipeline.execute()
    print('redis set:', res)

Redis 的操作方式和我们之前使用并没有太大的区别,还是走“初始化连接”、“初始化键值”、“合适时机塞数据”的路子。

这部分代码保存在 soulteary/simple-image-search-engine/steps/4.save-embededing-to-redis。

处理并保存所有的向量数据

继续调整和优化上面的程序,我们就可以将所有的图片都进行 embedding 处理和存入 Redis 中啦:

import torch
import numpy as np
from transformers import CLIPProcessor, CLIPModel
from PIL import Image
import time
import os
import redis

# 连接 Redis 数据库,地址换成你自己的 Redis 地址
client = redis.Redis(host="redis-server", port=6379, decode_responses=True)
res = client.ping()
print("redis connected:", res)

model_name_or_local_path = "openai/clip-vit-base-patch16"
model = CLIPModel.from_pretrained(model_name_or_local_path)
processor = CLIPProcessor.from_pretrained(model_name_or_local_path)

image_directory = "images"
png_files = [filename for filename in os.listdir(image_directory) if filename.endswith(".png")]
sorted_png_files = sorted(png_files, key=lambda x: int(x.split('-')[-1].split('.')[0]))

# 初始化 Redis Pipeline
pipeline = client.pipeline()
for i, png_file in enumerate(sorted_png_files, start=1):
    # 初始化 Redis,先使用 PNG 文件名作为 Key 和 Value,后续再更新为图片特征向量
    pipeline.json().set(png_file, "$", png_file)

batch_size = 1

with torch.no_grad():
    for idx, png_file in enumerate(sorted_png_files, start=1):
        print(f"{idx}: {png_file}")
        start = time.time()
        image = Image.open(f"{image_directory}/{png_file}")
        inputs = processor(images=image, return_tensors="pt", padding=True)
        image_features = model.get_image_features(inputs.pixel_values)[batch_size-1]
        embeddings = image_features.numpy().astype(np.float32).tolist()
        print('image_features:', embeddings)
        vector_dimension = len(embeddings)
        print('vector_dimension:', vector_dimension)
        end = time.time()
        print('%s Seconds'%(end-start))
        # 更新 Redis 数据库中的文件向量
        pipeline.json().set(png_file, "$", embeddings)
        res = pipeline.execute()
        print('redis set:', res)

将程序保存后执行,等待所有数据处理完毕,我们再进行构建索引操作,这里我的电脑大概运行了 10 分钟。

这部分代码保存在 soulteary/simple-image-search-engine/steps/5.save-all-embededing-to-redis。

当一切都执行完毕之后,我们观察 Redis 的容器进程,能够看到类似下面的内容:

8:M 14 Nov 2023 11:39:20.525 * Background saving terminated with success
8:M 14 Nov 2023 11:44:21.093 * 100 changes in 300 seconds. Saving...
8:M 14 Nov 2023 11:44:21.097 * Background saving started by pid 21
21:C 14 Nov 2023 11:44:21.707 * DB saved on disk
21:C 14 Nov 2023 11:44:21.708 * Fork CoW for RDB: current 1 MB, peak 1 MB, average 1 MB
8:M 14 Nov 2023 11:44:21.799 * Background saving terminated with success

我们可以手动执行一个命令,确保 Redis 将所有数据都正确存储了下来。

docker exec -it reids bash -c "echo BGREWRITEAOF | redis-cli"

执行完毕,当看到下面的带有“Background AOF rewrite finished successfully”的提示,数据就都被安全的存储下来啦:

8:M 14 Nov 2023 12:01:58.364 * Background append only file rewriting started by pid 69
69:C 14 Nov 2023 12:01:58.962 * Successfully created the temporary AOF base file temp-rewriteaof-bg-69.aof
69:C 14 Nov 2023 12:01:58.963 * Fork CoW for AOF rewrite: current 1 MB, peak 1 MB, average 1 MB
8:M 14 Nov 2023 12:01:59.022 * Background AOF rewrite terminated with success
8:M 14 Nov 2023 12:01:59.022 * Successfully renamed the temporary AOF base file temp-rewriteaof-bg-69.aof into appendonly.aof.5.base.rdb
8:M 14 Nov 2023 12:01:59.036 * Removing the history file appendonly.aof.4.base.rdb in the background
8:M 14 Nov 2023 12:01:59.050 * Background AOF rewrite finished successfully

查看数据目录,这 135M 的数据里,就包含了上万个图片的特征向量啦。

# du -hs redis-data

135M	redis-data

我们可以编写一段简单的程序,来验证存储的数据是否正确:

import redis

# 连接 Redis 数据库,地址换成你自己的 Redis 地址
client = redis.Redis(host="redis-server", port=6379, decode_responses=True)
res = client.ping()
print("redis connected:", res)

res = client.json().get("ball-1234.png")
print(res)

执行完毕,正常的情况下,我们将得到文件的 embedding 数据。不过,目前这些数据都是以 KEY-VALUE 模式存储在数据库里。想要真正使用上向量化数据查询方式,我们还需要进行最后一步操作:建议向量索引。

构建向量索引

关于向量数据库实现的相似性检索,以及不同向量类型的差异,我在这篇《向量数据库入坑指南:聊聊来自元宇宙大厂 Meta 的相似度检索技术 Faiss》文章中提到过,感兴趣可以自行翻阅。

这里我们使用最简单的平面索引,这种索引方式的内存使用量最低,因为会采取遍历式搜索,所以别名被称为“暴力搜索”。

import redis
from redis.commands.search.field import VectorField
from redis.commands.search.indexDefinition import IndexDefinition, IndexType

# 连接 Redis 数据库,地址换成你自己的 Redis 地址
client = redis.Redis(host="redis-server", port=6379, decode_responses=True)
res = client.ping()
print("redis connected:", res)

# 之前模型处理的向量维度是 512
vector_dimension = 512
# 给索引起个与众不同的名字
vector_indexes_name = "idx:ball_indexes"

# 定义向量数据库的 Schema
schema = (
    VectorField(
        "$",
        "FLAT",
        {
            "TYPE": "FLOAT32",
            "DIM": vector_dimension,
            "DISTANCE_METRIC": "COSINE",
        },
        as_name="vector",
    ),
)
# 设置一个前缀,方便后续查询,也作为命名空间和可能的普通数据进行隔离
# 这里设置为 ball-,未来可以通过 ball-* 来查询所有数据
definition = IndexDefinition(prefix=["ball-"], index_type=IndexType.JSON)
# 使用 Redis 客户端实例根据上面的 Schema 和定义创建索引
res = client.ft(vector_indexes_name).create_index(
    fields=schema, definition=definition
)
print("create_index:", res)

当程序执行完毕之后,我们将得到 create_index: OK 的结果。这个过程可能会需要几秒钟,当我们看到 Redis 后台日志出现下面的内容时,索引就构建完毕啦:

8:M 14 Nov 2023 13:04:24.834 * <module> Scanning index idx:ball_indexes in background: done (scanned=10393)

如果你不放心,还可以手动查询下向量索引的数量,看看和你的原始图片数量对不对的上。

import redis

# 连接 Redis 数据库,地址换成你自己的 Redis 地址
client = redis.Redis(host="redis-server", port=6379, decode_responses=True)
res = client.ping()
print("redis connected:", res)

vector_indexes_name = "idx:ball_vss"

# 从 Redis 数据库中读取索引状态
info = client.ft(vector_indexes_name).info()
# 获取索引状态中的 num_docs 和 hash_indexing_failures
num_docs = info["num_docs"]
indexing_failures = info["hash_indexing_failures"]
print(f"{num_docs} documents indexed with {indexing_failures} failures")

当我们执行完上面的代码后,将得到下面的日志输出:

10393 documents indexed with 0 failures

嗯,和我们的图片素材一致。引构建完毕后,我们就能够使用程序来进行向量查询检索啦。

实现以图搜图功能

图片搜索引擎,可以有很多能力,我们先来实现相对技术含量最高的一种:以图搜图。

import torch
import numpy as np
from transformers import CLIPProcessor, CLIPModel
from PIL import Image
import time
import redis
from redis.commands.search.query import Query

model_name_or_local_path = "openai/clip-vit-base-patch16"
model = CLIPModel.from_pretrained(model_name_or_local_path)
processor = CLIPProcessor.from_pretrained(model_name_or_local_path)

vector_indexes_name = "idx:ball_indexes"

client = redis.Redis(host="redis-server", port=6379, decode_responses=True)
res = client.ping()
print("redis connected:", res)

start = time.time()
image = Image.open("ball-8576.png")
batch_size = 1

with torch.no_grad():
    inputs = processor(images=image, return_tensors="pt", padding=True)
    image_features = model.get_image_features(inputs.pixel_values)[batch_size-1]
    embeddings = image_features.numpy().astype(np.float32).tobytes()
    print('image_features:', embeddings)

# 构建请求命令,查找和我们提供图片最相近的 30 张图片
query_vector = embeddings
query = (
    Query("(*)=>[KNN 30 @vector $query_vector AS vector_score]")
    .sort_by("vector_score")
    .return_fields("$")
    .dialect(2)
)

# 定义一个查询函数,将我们查找的结果的 ID 打印出来(图片名称)
def create_query_table(query, query_vector, extra_params={}):
    result_docs = (
        client.ft(vector_indexes_name)
        .search(
            query,
            {
                "query_vector": query_vector
            }
            | extra_params,
        )
        .docs
    )
    print(result_docs)
    for doc in result_docs:
        print(doc['id'])

create_query_table(query, query_vector, {})

end = time.time()
print('%s Seconds'%(end-start))

在上面的代码里,其实最关键的有三个细节。

第一个是,在之前的对图片进行向量化的过程中,我们是将向量数据从 Tensor 类型数据使用 tolist 转换为 list 数据。

在这里,因为要进行 Redis 查询,我们需要将数据使用 tobytes 进行转换,如果我们将数据打印出来,大概是这样样子:

image_features: 

b'\xc7\n\x80>\xec\xd4+\xbfK\xee:\xbe\xd3\r\x10>\x8d\xe1\x80\xbeI\xed\xdd\xbef\xc5\xaf\xbd@*\x8f\xbd\xd8\x04\x8e>\x8e\xf32?V&\x04?\xf4P\xc0=O\xac\x07\xbe\xbc\xa5$>\xa9\xf6\xf0\xbe\xc9\xb7i>
... 
... 
... 
>HG\xf4\xbc\xbfA\x8d>\x06\xfcL\xbdh\xe4\r\xbd\xc6\x9a\xaa\xbc\x99&E>\xe2Sn?w\xf6\xa0>M\x8d\x88?oa\x1d\xbeXO\xc6>\xa2\x10\x0c\xbe\xff\xd7\xfb='

第二个细节是我们需要在 query 中实现,关于 Redis 能够支持的搜索方式,在官方文档里有非常详细的记录,可以移步:

from redis.commands.search.query import Query

...

# 构建请求命令,查找和我们提供图片最相近的 30 张图片
query_vector = embeddings
query = (
    Query("(*)=>[KNN 30 @vector $query_vector AS vector_score]")
    .sort_by("vector_score")
    .return_fields("$")
    .dialect(2)
)

最后,我们需要使用这个封装的函数,来获取我们找到的最接近的图片的名称(字段 ID),这主要借助了 Redis 的 commands/ft.search/:

# 定义一个查询函数,将我们查找的结果的 ID 打印出来(图片名称)
def dump_query(query, query_vector, extra_params={}):
    result_docs = (
        client.ft(vector_indexes_name)
        .search(
            query,
            {
                "query_vector": query_vector
            }
            | extra_params,
        )
        .docs
    )
    print(result_docs)
    for doc in result_docs:
        print(doc['id'])

dump_query(query, query_vector, {})

当我们执行代码之后,将能够得到一串结果,包含了和我们提交查询图片最接近的图片:

...

ball-8576.png
ball-8595.png
ball-8596.png
ball-8591.png
ball-8592.png
ball-8305.png
ball-8579.png
ball-8310.png
ball-8161.png
ball-2818.png

>>>
>>> end = time.time()
>>> print('%s Seconds'%(end-start))
0.08090639114379883 Seconds

目前,我们还在设计第一阶段的程序,还没有方便用户使用的界面。所以,我们手动找到这些图片,来进行对比,看看程序通过模型找的图片像不像?

看起来,还是蛮准的嘛

似乎还挺靠谱的嘛。

当然,这只是图片搜索引擎的一部分能力,下一篇文章,我们来探索更多的内容,包括实现图片搜索引擎中的第二个部分,用户交互流程部分。

最后

原本以为,我把上一篇文章单独拆出来之后,这篇文章一整篇就能把图片搜索引擎架构中,常见的两个主要部分都讲完,没想到还需要再拆一篇。

那么,下一篇文章见。

—EOF


我们有一个小小的折腾群,里面聚集了一些喜欢折腾、彼此坦诚相待的小伙伴。

我们在里面会一起聊聊软硬件、HomeLab、编程上、生活里以及职场中的一些问题,偶尔也在群里不定期的分享一些技术资料。

关于交友的标准,请参考下面的文章:

致新朋友:为生活投票,不断寻找更好的朋友

当然,通过下面这篇文章添加好友时,请备注实名和公司或学校、注明来源和目的,珍惜彼此的时间 😄

关于折腾群入群的那些事


本文使用「署名 4.0 国际 (CC BY 4.0)」许可协议,欢迎转载、或重新修改使用,但需要注明来源。 署名 4.0 国际 (CC BY 4.0)

本文作者: 苏洋

创建时间: 2023年11月15日
统计字数: 20431字
阅读时间: 41分钟阅读
本文链接: https://soulteary.com/2023/11/15/use-redis-to-build-a-lightweight-vector-database-application-image-search-engine-part-1.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/147304.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

⑨【MySQL事务】事务开启、提交、回滚,事务特性ACID,脏读、幻读、不可重复读。

个人简介&#xff1a;Java领域新星创作者&#xff1b;阿里云技术博主、星级博主、专家博主&#xff1b;正在Java学习的路上摸爬滚打&#xff0c;记录学习的过程~ 个人主页&#xff1a;.29.的博客 学习社区&#xff1a;进去逛一逛~ MySQL事务 ⑨【事务】1. 事务概述2. 操作事务3…

2023年咨询实务速记突破【专题总结】

需要完整资料的可以联系我获取

xshell连接云服务器(保姆级教程)

文章目录 1. 前言2. 查看云服务器的信息3. xshell7连接云服务器 1. 前言 云服务器&#xff0c;也被称为Elastic Compute Service (ECS)&#xff0c;是一种简单高效、安全可靠、处理能力可弹性伸缩的计算服务。它源于物理服务器集群资源池&#xff0c;可以像从大海中取水一样&am…

【软考篇】中级软件设计师 第二部分(二)

中级软件设计师 第二部分&#xff08;二&#xff09; 十三. 死锁问题十四. 段页式存储14.1 页式存储14.1.1 缺页中断14.1.2 页面置换算法 14.2 段式存储14.3 段页式存储 十五. 索引文件十六. 文件目录16.1 树形目录结构16.2 位示图 十三. 死锁问题 多刷题 系统不可能发生死锁的…

【Linux】Linux基础IO(下)

​ ​&#x1f4dd;个人主页&#xff1a;Sherry的成长之路 &#x1f3e0;学习社区&#xff1a;Sherry的成长之路&#xff08;个人社区&#xff09; &#x1f4d6;专栏链接&#xff1a;Linux &#x1f3af;长路漫漫浩浩&#xff0c;万事皆有期待 上一篇博客&#xff1a;【Linux】…

智能供应链中的预测算法:理论与实践

&#x1f482; 个人网站:【工具大全】【游戏大全】【神级源码资源网】&#x1f91f; 前端学习课程&#xff1a;&#x1f449;【28个案例趣学前端】【400个JS面试题】&#x1f485; 寻找学习交流、摸鱼划水的小伙伴&#xff0c;请点击【摸鱼学习交流群】 引言 智能供应链已经成…

linux高级篇基础理论(详细文档)二

♥️作者&#xff1a;小刘在C站 ♥️个人主页&#xff1a; 小刘主页 ♥️不能因为人生的道路坎坷,就使自己的身躯变得弯曲;不能因为生活的历程漫长,就使求索的 脚步迟缓。 ♥️学习两年总结出的运维经验&#xff0c;以及思科模拟器全套网络实验教程。专栏&#xff1a;云计算技…

技术贴 | SQL 执行 - 执行器优化

本期技术贴主要介绍查询执行引擎的优化。查询执行引擎负责将 SQL 优化器生成的执行计划进行解释&#xff0c;通过任务调度执行从存储引擎里面把数据读取出来&#xff0c;计算出结果集&#xff0c;然后返回给客户。 在关系型数据库发展的早期&#xff0c;受制于计算机 IO 能力的…

Live800:客服行业的发展历程及未来前景

随着信息技术和互联网的高速发展&#xff0c;客服行业也在不断变革和发展。客服行业是一个服务型的行业&#xff0c;其发展历程也与人们对服务需求的变化密切相关。本文将介绍客服行业的发展历程和未来前景。 客服行业的发展历程 20世纪70年代&#xff0c;客服行业主要以电话服…

SAM分割模型的5个典型用例

Meta AI 于2023 年推出的分割任意模型 (SAM) 彻底改变了我们对图像分割的质量标准。 给定输入图像&#xff0c;SAM 尝试分割图像中的所有对象并生成分割掩模。 使用 SAM&#xff0c;你可以分割对象&#xff0c;然后&#xff0c;可以使用模型来利用该信息&#xff0c;例如用于为…

【开源】基于Vue.js的校园二手交易系统的设计和实现

目录 一、摘要1.1 项目介绍1.2 项目详细录屏 二、功能模块2.1 数据中心模块2.2 二手商品档案管理模块2.3 商品预约管理模块2.4 商品预定管理模块2.5 商品留言板管理模块2.6 商品资讯管理模块 三、实体类设计3.1 用户表3.2 二手商品表3.3 商品预约表3.4 商品预定表3.5 留言表3.6…

μC/OS-II---时间管理(os_time.c)

目录 时间管理相关&#xff08;os_time.c&#xff09;Task延迟按时、分、秒、毫秒延时恢复被延时的Task返回系统当前的Tick计数值设置系统的Tick计数值 时间管理相关&#xff08;os_time.c&#xff09; Task延迟 void OSTimeDly (INT32U ticks) {INT8U y; #if OS_CRITI…

Kibana:使用 “链接” 面板简化 Kibana 仪表板导航 - Links panel

作者&#xff1a;Teresa Alvarez Soler 我们很高兴地宣布 Kibana 仪表板的最新功能版本&#xff1a;链接面板&#xff08;Links panel&#xff09;&#xff0c;这是在仪表板之间组织和导航的简单方法。 此功能在 Kibana 8.11 的技术预览版中提供。 有时你可能希望创建多个主题…

Rust实战教程:构建您的第一个应用

大家好&#xff01;我是lincyang。 今天&#xff0c;我们将一起动手实践&#xff0c;通过构建一个简单的Rust应用来深入理解这门语言。 我们的项目是一个命令行文本文件分析器&#xff0c;它不仅能读取和显示文件内容&#xff0c;还会提供一些基础的文本分析&#xff0c;如计算…

IDEA-git commit log 线

一、本地代码颜色标识 红色&#xff1a;新建的文件&#xff0c;没有add到git本地仓库蓝色&#xff1a;修改的文件&#xff0c;没有提交到git远程仓库绿色&#xff1a;已添加到git本地仓库&#xff0c;没有提交到git远程仓库灰色&#xff1a;删除的文件&#xff0c;没有提交到g…

常见限流算法解读

目录 前言 固定窗口&#xff08;计算器法&#xff09; 滑动窗口 漏桶算法 令牌桶算法 总结 前言 在现在的互联网系统中有很多业务场景&#xff0c;比如商品秒杀、下单、数据查询详情&#xff0c;其最大特点就是高并发&#xff0c;但是我们的系统通常不能承受这么大的流…

【Azure 架构师学习笔记】-Azure Storage Account(6)- File Layer

本文属于【Azure 架构师学习笔记】系列。 本文属于【Azure Storage Account】系列。 接上文 【Azure 架构师学习笔记】-Azure Storage Account&#xff08;5&#xff09;- Data Lake layers 前言 上一文介绍了存储帐户的概述&#xff0c;还有container的一些配置&#xff0c;在…

Kibana:作为非设计师设计直观的 Kibana 仪表板

作者&#xff1a;Carly Richmond, Marco Vettorello, Giovanni Magni 开发人员、SRE 工程师和才华横溢的技术人员通常需要构建快速仪表板来展示有关其应用程序状态的重要信息&#xff0c;这些信息可供混合受众使用。 如果你不是前端开发人员或设计师&#xff0c;那么构建所有人…

vue echart 立体柱状图 带阴影

根据一个博主代码改编而来 <template><div class"indexBox"><div id"chart"></div></div> </template><script setup> import * as echarts from "echarts"; import { onMounted } from "vue&…

二叉树-堆(9.10)

接上节内容 目录 3.3 堆的实现 3.2.1 堆向下调整算法 3.2.2大堆的创建 3.4 堆的应用 3.4.1 堆排序 3.4.2 TOP-K问题 ​编辑 二叉树的性质 练习 4.二叉树链式结构的实现 4.1 前置说明 4.2二叉树的遍历 4.2.1 前序、中序以及后序遍历 4.3 节点个数以及高度等 4.3…