Bootstrap

基于milvus的多模态检索


本文包含milvus安装使用、attu 可视化,完整指南启动 Milvus 进行了向量相似度搜索,利用CLIP模型进行多模态检索,附完整代码。

1.Milvus简介(2019)

1.1 什么是向量检索

向量是具有一定大小和方向的量,可以简单理解为一串数字的集合,就像一行多列的矩阵,比如:[2,0,1,9,0,6,3,0]。每一行代表一个数据项,每一列代表一个该数据项的各个属性。特征向量是包含事物重要特征的向量。大家比较熟知的一个特征向量是RGB (红-绿-蓝)色彩。每种颜色都可以通过对红®、绿(G)、蓝(B)三种颜色的比例来得到。这样一个特征向量可以描述为:颜色 = [红,绿,蓝]。向量检索是指从向量库中检索出距离目标向量最近的 K 个向量。一般我们用两个向量间的欧式距离,余弦距离等来衡量两个向量间的距离,一次来评估两个向量的相似度。

2 Milvus安装

2.1 服务器配置

(milvus) [root@ecs-86676-0005 suanfa_jingxiang]# docker --version
Docker version 19.03.9, build 9d988398e7
(milvus) [root@ecs-86676-0005 suanfa_jingxiang]# docker-compose --version
Docker Compose version v2.21.0
(milvus) [root@ecs-86676-0005 suanfa_jingxiang]# uname -a
Linux ecs-86676-0005 4.18.0-348.7.1.el8_5.x86_64 #1 SMP Wed Dec 22 13:25:12 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux

使用Docker Compose安装 Milvus standalone(即单机版),进行一个快速milvus的体验。

2.2 前提条件

  • 系统可以使用centos或者ubuntu
  • 系统已经安装docker和docker-compose
  • milvus版本这里选择2.3.1。由于milvus依赖etcd和minio,因此需要先启动这2个组件。同样也使用docker进行启动。

etcd:用来存储milvus的元数据。
minio:用来存储milvus的向量数据和索引数据。
下载milvus-standalone-docker-compose.yml 文件,保存为docker-compose.yml:

wget https://github.com/milvus-io/milvus/releases/download/v2.3.1/milvus-standalone-docker-compose.yml -O docker-compose.yml

这里经过了一定修改,让其更加方便使用。
这个yml文件里面定义了etcd、minio、milvus的启动参数。
修改后的docker-compose.yml文件内容如下:

version: '3.5'

services:
  etcd:
    container_name: milvus-etcd
    image: quay.io/coreos/etcd:v3.5.5
    environment:
      - ETCD_AUTO_COMPACTION_MODE=revision
      - ETCD_AUTO_COMPACTION_RETENTION=1000
      - ETCD_QUOTA_BACKEND_BYTES=4294967296
      - ETCD_SNAPSHOT_COUNT=50000
    volumes:
      - ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/etcd:/etcd
    ports:
      - "2379:2379"
    command: etcd -advertise-client-urls=http://127.0.0.1:2379 -listen-client-urls http://0.0.0.0:2379 --data-dir /etcd
    healthcheck:
      test: ["CMD", "etcdctl", "endpoint", "health"]
      interval: 5s
      timeout: 3s
      retries: 10

  minio:
    container_name: milvus-minio
    image: minio/minio:RELEASE.2023-03-20T20-16-18Z
    environment:
      MINIO_ACCESS_KEY: minioadmin
      MINIO_SECRET_KEY: minioadmin
    ports:
      - "9001:9001"
      - "9000:9000"
    volumes:
      - ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/minio:/minio_data
    command: minio server /minio_data --console-address ":9001"
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
      interval: 5s
      timeout: 3s
      retries: 10

  standalone:
    container_name: milvus-standalone
    image: milvusdb/milvus:v2.3.1
    command: ["milvus", "run", "standalone"]
    environment:
      ETCD_ENDPOINTS: etcd:2379
      MINIO_ADDRESS: minio:9000
    volumes:
      - ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/milvus:/var/lib/milvus
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:9091/healthz"]
      interval: 30s
      start_period: 90s
      timeout: 20s
      retries: 3
    ports:
      - "19530:19530"
      - "9091:9091"
    depends_on:
      - "etcd"
      - "minio"

networks:
  default:
    name: milvus

然后后台启动这些容器:

docker-compose up -d
-d 代表后台启动
使用ps命令查看容器:

如果看到healthy状态,说明容器内的服务可以正常使用了。
在这里插入图片描述

2.3 启动attu

attu为milvus的一款图形化管理工具,非常方便对milvus的一些管理。
启动attu:

docker pull zilliz/attu:v2.3.0l
docker run -d --name=attu -p 2099:3000 -e MILVUS_URL=your ip zilliz/attu:v2.3.0

在这里插入图片描述

2.4 下载示例代码进行测试

下载 hello_milvus.py 直接或使用以下命令

wget https://raw.githubusercontent.com/milvus-io/pymilvus/v2.2.8/examples/hello_milvus.py
or
wget https://raw.githubusercontent.com/milvus-io/pymilvus/v2.2.x/examples/hello_milvus.py

在这里插入图片描述

3 多模态搜索场景

3.1 文搜图

文搜图是指用自然语言描述来检索相关图片。这种搜索模式结合了文本理解和图像特征匹配。

  • 实现步骤:
  1. 使用 Chinese CLIP 模型将查询文本转换为向量。
  2. 在 Milvus 中搜索与查询向量最相似的图片向量。

3.2 图搜图(相似图片搜索)

图搜图允许用户上传一张图片,然后找到数据库中与之相似的图片。

  • 实现步骤:
  1. 使用 Chinese CLIP 模型提取查询图片的特征向量。
  2. 在 Milvus 中执行向量相似度搜索,找出最相似的图片。

3.3 使用 Milvus Python SDK 实现多模态搜索

下面我们将使用 Milvus Python SDK 来演示如何实现上述搜索场景的核心功能。Milvus 提供了多种 API 和 SDK,包括 RESTful API、Python SDK (PyMilvus)、Go SDK、Java SDK、Node.js SDK,以及由 Microsoft 贡献的 C# SDK。在本例中,我们将使用 PyMilvus 。

  1. 连接Milvus并创建集合
    这里,我们使用了 IVF_FLAT 索引。Milvus 支持多种索引类型,包括 FLAT、IVF_FLAT、IVF_SQ8、IVF_PQ、HNSW、ANNOY 等,您可以根据具体需求选择合适的索引类型。
    首先,安装必要的库:
pip install pymilvus clip torch pillow

完整代码如下:

from pymilvus import connections, Collection, FieldSchema, CollectionSchema, DataType, utility
import os
import torch
from PIL import Image
import clip


class MilvusImageSearch:
    def __init__(self, host="0.0.0.0", port="19530", top_k=2, save_folder="output", image_folder_path="data"):
        self.top_k = top_k
        self.save_folder = save_folder
        self.image_folder_path = image_folder_path

        # 连接到 Milvus 服务器
        connections.connect("default", host=host, port=port)
        self.collection_name = "multimodal_image_collection"

        if utility.has_collection(self.collection_name):
            utility.drop_collection(self.collection_name)

        # 定义 Collection Schema
        fields = [
            FieldSchema(name="id", dtype=DataType.INT64, is_primary=True, auto_id=False),
            FieldSchema(name="embedding", dtype=DataType.FLOAT_VECTOR, dim=512),
            FieldSchema(name="image_path", dtype=DataType.VARCHAR, max_length=512)
        ]
        schema = CollectionSchema(fields, "多模态图片集合")
        self.collection = Collection(self.collection_name, schema)

        # 创建索引以加快搜索速度
        index_params = {"metric_type": "L2", "index_type": "IVF_FLAT", "params": {"nlist": 1024}}
        self.collection.create_index("embedding", index_params)

        # 加载 CLIP 模型
        self.device = "cuda" if torch.cuda.is_available() else "cpu"
        self.model, self.preprocess = clip.load("ViT-B/32", device=self.device)

    def image_to_vector(self, image_path):
        image = Image.open(image_path)
        image = self.preprocess(image).unsqueeze(0).to(self.device)
        with torch.no_grad():
            image_features = self.model.encode_image(image)
        return image_features.cpu().numpy()[0]

    def batch_insert_images(self, batch_size=10):
        image_files = [os.path.join(self.image_folder_path, img) for img in os.listdir(self.image_folder_path)
                       if img.lower().endswith(('.png', '.jpg', '.jpeg'))]

        ids, embeddings, image_paths = [], [], []
        for i, image_file in enumerate(image_files):
            try:
                vector = self.image_to_vector(image_file)
                ids.append(i + 1)
                embeddings.append(vector)
                image_paths.append(image_file)
            except Exception as e:
                print(f"处理图片 {image_file} 出错: {e}")
                continue

            if len(embeddings) == batch_size:
                try:
                    self.collection.insert([ids, embeddings, image_paths])
                    print(f"成功插入 {len(ids)} 条记录")
                    ids, embeddings, image_paths = [], [], []
                except Exception as e:
                    print(f"插入错误: {e}")

        if embeddings:
            try:
                self.collection.insert([ids, embeddings, image_paths])
                print(f"成功插入剩余的 {len(ids)} 条记录")
            except Exception as e:
                print(f"插入错误: {e}")

        self.collection.flush()
        self.collection.load()

    def search_images_by_text(self, query_text):
        try:
            text_inputs = clip.tokenize([query_text]).to(self.device)
            with torch.no_grad():
                text_features = self.model.encode_text(text_inputs)
            query_vector = text_features.cpu().numpy()[0]
            search_params = {"metric_type": "L2", "params": {"nprobe": 10}}
            results = self.collection.search([query_vector], "embedding", search_params, limit=self.top_k,
                                             output_fields=["id", "image_path"])
            return results
        except Exception as e:
            print(f"搜索错误: {e}")
            return []

    def search_similar_images(self, query_image_path):
        try:
            query_vector = self.image_to_vector(query_image_path)
            search_params = {"metric_type": "L2", "params": {"nprobe": 10}}
            results = self.collection.search([query_vector], "embedding", search_params, limit=self.top_k,
                                             output_fields=["id", "image_path"])
            return results
        except Exception as e:
            print(f"搜索错误: {e}")
            return []

    def display_results(self, results):
        os.makedirs(self.save_folder, exist_ok=True)  # 创建保存图片的目录
        for i, result in enumerate(results[0]):
            print(f"Top {i + 1} 匹配图片ID: {result.id}, 距离: {result.distance}")
            image_path = result.entity.get("image_path")
            print(f"图片路径: {image_path}")
            try:
                image = Image.open(image_path)
                image.show()
                # 保存图片到指定目录
                save_path = os.path.join(self.save_folder, f"result_{i + 1}.jpg")
                image.save(save_path)
                print(f"图片已保存到: {save_path}")
            except Exception as e:
                print(f"显示图片出错: {e}")


# 使用示例
search_system = MilvusImageSearch(top_k=3, save_folder="output", image_folder_path="data")

# 插入图片
search_system.batch_insert_images()

# 根据文本搜索图片
query = "dog"
results = search_system.search_images_by_text(query)
print(f"查询文本: '{query}'")
search_system.display_results(results)

# 根据图片搜索相似图片
query_image = "./data/4.jpeg"
results = search_system.search_similar_images(query_image)
print(f"查询图片: '{query_image}'")
search_system.display_results(results)

结果展示
在这里插入图片描述
参考:https://blog.csdn.net/weixin_44839084/article/details/142828988?ops_request_misc=&request_id=&biz_id=102&utm_term=%E5%9F%BA%E4%BA%8Emilvus%E7%9A%84%E5%A4%9A%E6%A8%A1%E6%80%81%E6%A3%80%E7%B4%A2&utm_medium=distribute.pc_search_result.none-task-blog-2allsobaiduweb~default-0-142828988.142v100pc_search_result_base4&spm=1018.2226.3001.4187

;