CH3_04_multi_milvus

2039 字
10 分钟
CH3_04_multi_milvus

本节演示多模态图文检索:用 Visual BGE 把图像/文本变成向量,存入 Milvus,再做相似度搜索。

下面的两个 cell 主要是为代码在 Google Colab 上运行设计。

# Google Colab: run this cell first to install notebook dependencies.
import os
import subprocess
import sys
REPO_URL = "https://github.com/datawhalechina/all-in-rag.git"
REPO_DIR = "/content/all-in-rag"
VISUAL_BGE_DIR = os.path.join(REPO_DIR, "code", "C3", "visual_bge")
C3_DIR = os.path.join(REPO_DIR, "code", "C3")
def run(cmd: list[str]) -> None:
print("$", " ".join(cmd))
subprocess.check_call(cmd)
# visual_bge ships with this repo; clone it when running on Colab.
if not os.path.isdir(VISUAL_BGE_DIR):
run(["git", "clone", "--depth", "1", REPO_URL, REPO_DIR])
# Third-party packages used in this notebook.
# Colab already provides torch, torchvision, numpy, requests, and Pillow.
# milvus-lite must match pymilvus 2.5.x, otherwise search() fails with
# MilvusException: function_score (version mismatch in the local DB backend).
run(
[
sys.executable,
"-m",
"pip",
"install",
"-q",
"pymilvus==3.0.0",
"milvus-lite==3.0",
"opencv-python-headless",
"tqdm",
"transformers>=4.40.0",
"huggingface-hub",
"timm",
"einops",
"ftfy",
"regex",
]
)
# Install the local visual_bge package.
run([sys.executable, "-m", "pip", "install", "-q", "-e", VISUAL_BGE_DIR])
# Notebook paths (../../models, ../../data) assume cwd is code/C3.
os.chdir(C3_DIR)
print("Dependencies installed.")
print("Working directory:", os.getcwd())
print("Next: download model weights with `!python download_model.py` if needed.")
print("If you previously hit a Milvus search error, restart the runtime, then re-run all cells.")
!python download_model.py
import os
from tqdm import tqdm
from glob import glob
import torch
from visual_bge.visual_bge.modeling import Visualized_BGE
from pymilvus import MilvusClient, FieldSchema, CollectionSchema, DataType
import numpy as np
import cv2
from PIL import Image

下面按「库类别 → 简介 → 在本 notebook 中的用途」简要说明这些 import.

  • tqdm :进度条工具,主要用于在循环中显示处理进度,这里我们在批量生成图像嵌入并插入 Milvus 时显示进度;
  • cv2 :计算机视觉库,提供缩放、裁剪、绘制文字、保存图像等操作,这里我们用来在 visualized_results 中缩放图像、加边框、写 Query 和排名序号,最后保存图像;
  • PIL :图像读写库,用于打开、转换、保存常见格式图像。这里我们用它的 Image.open() 方法读取查询图和检索结果,转为 RGB,最后用 .show() 展示拼接后的检索结果图。

这些库如何协作:

flowchart LR A[glob + os<br/>收集图像路径] --> B[Visualized_BGE + torch<br/>生成向量] B --> C[pymilvus<br/>存入 Milvus] D[查询图 + 文本] --> E[Visualized_BGE<br/>生成查询向量] E --> C C --> F[检索 Top-K 图像路径] F --> G[PIL + cv2 + numpy<br/>可视化结果]
# 1. 初始化设置
MODEL_NAME = "BAAI/bge-base-en-v1.5"
MODEL_PATH = "../../models/bge/Visualized_base_en_v1.5.pth"
DATA_DIR = "../../data/C3"
COLLECTION_NAME = "multimodal_demo"
# MILVUS_URI = "http://localhost:19530"
MILVUS_URI = "./milvus_demo.db"
# 2. 定义工具 (编码器和可视化函数)
class Encoder:
"""编码器类,用于将图像和文本编码为向量。"""
def __init__(self, model_name: str, model_path: str):
self.model = Visualized_BGE(model_name_bge=model_name, model_weight=model_path)
self.model.eval()
def encode_query(self, image_path: str, text: str) -> list[float]:
with torch.no_grad():
query_emb = self.model.encode(image=image_path, text=text)
return query_emb.tolist()[0] # 去掉 batch 维度
def encode_image(self, image_path: str) -> list[float]:
with torch.no_grad():
query_emb = self.model.encode(image=image_path)
return query_emb.tolist()[0]
  • encode_query :同时传入 imagetext ,返回一个 tensor 对象 query_emb,它的形状为 [1, 768],即 1 条查询的 768 维图文融合向量,且已经经过了 L2 归一化,可以直接用于余弦相似度搜索。
  • encode_image :只传入 image,虽然在底层 Visualized_BGE 里面它也走多模态通路,但文本部分是空字符串,因此只融合了图像。和 encode_query 主要用于检索不同,它主要用于入库,也就是说:
    • 文档侧:仅图像
    • 查询侧:图像和文本

这样,用户在搜图的时候就能同时给一张图和一句描述,用 encode_query 得到图文联合查询向量,而不是只能以图搜图。

def visualize_results(query_image_path: str, retrieved_images: list, img_height: int = 300, img_width: int = 300, row_count: int = 3) -> np.ndarray:
"""从检索到的图像列表创建一个全景图用于可视化。"""
panoramic_width = img_width * row_count # 创建两张空白画布
panoramic_height = img_height * row_count
panoramic_image = np.full((panoramic_height, panoramic_width, 3), 255, dtype=np.uint8) # 左侧结果区
query_display_area = np.full((panoramic_height, img_width, 3), 255, dtype=np.uint8) # 右侧查询区
# 处理查询图像
query_pil = Image.open(query_image_path).convert("RGB")
query_cv = np.array(query_pil)[:, :, ::-1]
resized_query = cv2.resize(query_cv, (img_width, img_height))
bordered_query = cv2.copyMakeBorder(resized_query, 10, 10, 10, 10, cv2.BORDER_CONSTANT, value=(255, 0, 0))
query_display_area[img_height * (row_count - 1):, :] = cv2.resize(bordered_query, (img_width, img_height))
cv2.putText(query_display_area, "Query", (10, panoramic_height - 20), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 0, 0), 2)
# 处理检索到的图像
for i, img_path in enumerate(retrieved_images):
row, col = i // row_count, i % row_count
start_row, start_col = row * img_height, col * img_width
retrieved_pil = Image.open(img_path).convert("RGB")
retrieved_cv = np.array(retrieved_pil)[:, :, ::-1]
resized_retrieved = cv2.resize(retrieved_cv, (img_width - 4, img_height - 4))
bordered_retrieved = cv2.copyMakeBorder(resized_retrieved, 2, 2, 2, 2, cv2.BORDER_CONSTANT, value=(0, 0, 0))
panoramic_image[start_row:start_row + img_height, start_col:start_col + img_width] = bordered_retrieved
# 添加索引号
cv2.putText(panoramic_image, str(i), (start_col + 10, start_row + 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2)
return np.hstack([query_display_area, panoramic_image])

这里所谓的全景图就是我们最终保存的结果图像,即把多张图拼成一张大图,方便一眼对比查询图和检索结果。

最终输出的图像结构类似:

最终输出 = 左右拼接(np.hstack)
┌─────────┬──────────────────────────────┐
│ │ 检索结果区 (900×900) │
│ Query │ ┌───┬───┬───┐ │
│ 区 │ │ 0 │ 1 │ 2 │ 第 0 行 │
│(300×900)│ ├───┼───┼───┤ │
│ │ │ 3 │ 4 │ │ 第 1 行 │
│ │ └───┴───┴───┘ │
│ [Query] │ (3×3 网格,本例只填 5 张) │
└─────────┴──────────────────────────────┘
300px 900px (3列×300)
总宽 1200px,高 900px
# 3. 初始化客户端
print("--> 正在初始化编码器和Milvus客户端...")
encoder = Encoder(MODEL_NAME, MODEL_PATH)
milvus_client = MilvusClient(MILVUS_URI)
# 4. 创建 Milvus Collection
print(f"\n--> 正在创建 Collection '{COLLECTION_NAME}'")
if milvus_client.has_collection(COLLECTION_NAME):
milvus_client.drop_collection(COLLECTION_NAME)
print(f"已删除已存在的 Collection: '{COLLECTION_NAME}'")
image_list = glob(os.path.join(DATA_DIR, "dragon", "*.png"))
if not image_list:
raise FileNotFoundError(f"在 {DATA_DIR}/dragon/ 中未找到任何 .png 图像。")
dim = len(encoder.encode_image(image_list[0]))
fields = [
FieldSchema(name="id", dtype=DataType.INT64, is_primary=True, auto_id=True),
FieldSchema(name="vector", dtype=DataType.FLOAT_VECTOR, dim=dim),
FieldSchema(name="image_path", dtype=DataType.VARCHAR, max_length=512),
]
# 创建集合 Schema
schema = CollectionSchema(fields, description="多模态图文检索")
print("Schema 结构:")
print(schema)

输出为:

Schema 结构:
{'auto_id': True, 'description': '多模态图文检索', 'fields': [
{'name': 'id', 'description': '', 'type': <DataType.INT64: 5>, 'is_primary': True, 'auto_id': True},
{'name': 'vector', 'description': '', 'type': <DataType.FLOAT_VECTOR: 101>, 'params': {'dim': 768}},
{'name': 'image_path', 'description': '', 'type': <DataType.VARCHAR: 21>, 'params': {'max_length': 512}}
],
'enable_dynamic_field': False, 'enable_namespace': False
}

上面的输出详细展示了刚刚创建的 multimodal_demo Collection Schema 的完整结构。其包含了三个核心字段(Field):一个自增的 id 作为主键,一个 768 维的 vector 向量字段用于存储图像嵌入,以及一个 image_path 标量字段来记录原始图片路径。

# 创建集合
milvus_client.create_collection(collection_name=COLLECTION_NAME, schema=schema)
print(f"成功创建 Collection: '{COLLECTION_NAME}'")
print("Collection 结构:")
print(milvus_client.describe_collection(collection_name=COLLECTION_NAME))

输出为:

成功创建 Collection: 'multimodal_demo'
Collection 结构:
{'collection_name': 'multimodal_demo', 'auto_id': True, 'num_shards': 1, 'description': '', 'fields': [{'field_id': 0, 'name': 'id', 'description': '', 'type': <DataType.INT64: 5>, 'params': {}, 'auto_id': True, 'is_primary': True}, {'field_id': 0, 'name': 'vector', 'description': '', 'type': <DataType.FLOAT_VECTOR: 101>, 'params': {'dim': 768}}, {'field_id': 0, 'name': 'image_path', 'description': '', 'type': <DataType.VARCHAR: 21>, 'params': {'max_length': 512}}], 'functions': [], 'aliases': [], 'collection_id': 0, 'consistency_level': 0, 'consistency_level_name': 'Strong', 'properties': {}, 'num_partitions': 1, 'enable_dynamic_field': False, 'enable_namespace': False}

这里则是 multimodal_demo Collection 的完整结构。

# 5. 准备并插入数据
print(f"\n--> 正在向 '{COLLECTION_NAME}' 插入数据")
data_to_insert = []
for image_path in tqdm(image_list, desc="生成图像嵌入"):
vector = encoder.encode_image(image_path)
data_to_insert.append({"vector": vector, "image_path": image_path})
if data_to_insert:
result = milvus_client.insert(collection_name=COLLECTION_NAME, data=data_to_insert)
print(f"成功插入 {result['insert_count']} 条数据。")

输出为:

生成图像嵌入: 100%|██████████| 7/7 [00:13<00:00, 1.92s/it]
成功插入 7 条数据。
# 6. 创建索引
print(f"\n--> 正在为 '{COLLECTION_NAME}' 创建索引")
index_params = milvus_client.prepare_index_params()
index_params.add_index(
field_name="vector",
index_type="HNSW",
metric_type="COSINE",
params={"M": 16, "efConstruction": 256}
)
milvus_client.create_index(collection_name=COLLECTION_NAME, index_params=index_params)
print("成功为向量字段创建 HNSW 索引。")
print("索引详情:")
print(milvus_client.describe_index(collection_name=COLLECTION_NAME, index_name="vector"))
milvus_client.load_collection(collection_name=COLLECTION_NAME)
print("已加载 Collection 到内存中。")

输出为:

--> 正在为 'multimodal_demo' 创建索引
成功为向量字段创建 HNSW 索引。
索引详情:
{'index_type': 'HNSW', 'metric_type': 'COSINE', 'field_name': 'vector', 'index_name': 'vector', 'total_rows': 7, 'indexed_rows': 7, 'pending_index_rows': 0, 'state': 'Finished'}
已加载 Collection 到内存中。

可以看出,索引创建成功,在 vector 字段上成功创建了 HNSW 索引,并使用 COSINE 作为距离度量。M: '16'efConstruction: '256'HNSW 索引的两个关键参数,分别控制着图中每个节点的最大连接数和索引构建时的搜索范围,这些参数直接影响检索的性能和准确性。state: 'Finished' 状态表明索引已成功构建。

# 7. 执行多模态检索
print(f"\n--> 正在 '{COLLECTION_NAME}' 中执行检索")
query_image_path = os.path.join(DATA_DIR, "dragon", "query.png")
query_text = "一条龙"
query_vector = encoder.encode_query(image_path=query_image_path, text=query_text)
search_results = milvus_client.search(
collection_name=COLLECTION_NAME,
data=[query_vector],
output_fields=["image_path"],
limit=5,
search_params={"metric_type": "COSINE", "params": {"ef": 128}}
)[0]
retrieved_images = []
print("检索结果:")
for i, hit in enumerate(search_results):
print(f" Top {i+1}: ID={hit['id']}, 距离={hit['distance']:.4f}, 路径='{hit['entity']['image_path']}'")
retrieved_images.append(hit['entity']['image_path'])

输出为:

--> 正在 'multimodal_demo' 中执行检索
检索结果:
Top 1: ID=6, 距离=0.0534, 路径='../../data/C3/dragon/query.png'
Top 2: ID=1, 距离=0.2557, 路径='../../data/C3/dragon/dragon02.png'
Top 3: ID=4, 距离=0.3149, 路径='../../data/C3/dragon/dragon06.png'
Top 4: ID=2, 距离=0.3951, 路径='../../data/C3/dragon/dragon03.png'
Top 5: ID=3, 距离=0.4640, 路径='../../data/C3/dragon/dragon05.png'
# 8. 可视化与清理
print(f"\n--> 正在可视化结果并清理资源")
if not retrieved_images:
print("没有检索到任何图像。")
else:
panoramic_image = visualize_results(query_image_path, retrieved_images)
combined_image_path = os.path.join(DATA_DIR, "search_result.png")
cv2.imwrite(combined_image_path, panoramic_image)
print(f"结果图像已保存到: {combined_image_path}")
Image.open(combined_image_path).show()
milvus_client.release_collection(collection_name=COLLECTION_NAME)
print(f"已从内存中释放 Collection: '{COLLECTION_NAME}'")
milvus_client.drop_collection(COLLECTION_NAME)
print(f"已删除 Collection: '{COLLECTION_NAME}'")

输出为:

--> 正在可视化结果并清理资源
结果图像已保存到: ../../data/C3/search_result.png
已从内存中释放 Collection: 'multimodal_demo'
已删除 Collection: 'multimodal_demo'

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

CH3_04_multi_milvus
https://datawhalechina.github.io/all-in-rag/
作者
HAC
发布于
2026-07-05
许可协议
CC BY-NC-SA 4.0

评论区

Profile Image of the Author
HAC
观之非易,行且克难
Greetings
欢迎来到我的博客!这里主要分享我的学习笔记与兴趣爱好。
音乐
封面

音乐

暂未播放

0:00 0:00
暂无歌词
分类
标签
站点统计
文章
32
分类
5
标签
13
总字数
79,889
运行时长
0
最后活动
0 天前

文章目录