文章目录
  1. 1. 万物皆向量
    1. 1.1. 什么是向量
    2. 1.2. Embedding 模型
  2. 2. 向量检索
    1. 2.1. 向量相似度计算
    2. 2.2. 向量数据库
  3. 3. 进一步优化
    1. 3.1. 主体识别和图片裁切
    2. 3.2. 重排序(rerank)

搜索引擎提供的以图搜图功能,电商软件里拍照就能搜同款,音乐软件中的听歌识曲,视频网站上一张截图就能搜出来源于哪部电影或电视剧,乃至人脸识别和检索,以及智能体中常用的 RAG,底层都普遍采用同一项关键技术:向量检索。
电商“搜同款”功能

万物皆向量

什么是向量

在数学中,向量(vector)指具有大小和方向的量。它可以形象化地表示为带箭头的线段:

  • 箭头所指:代表向量的方向
  • 线段长度:代表向量的大小(即模长)

一个二维向量

在编程语言中,向量可以用一个数组来表示,向量的维度即为这个数组中元素的个数。例如上图是一个二维向量,可以用一个包含两个元素的数组表示:v = [3, 4]

如果数组中有 512 个元素,那么向量就是 512 维的。虽然 512 维向量无法用图片直观画出来(因为我们所在的世界是三维空间,512 维对于三维生物来说太抽象了),但是,在数学中,它确实存在。以下是一个多维向量实例:

1
2
3
4
5
# 用一个 512 个元素的数组表示一个 512 维向量
v = [-0.013052909, 0.020387933, -0.007869, -0.11111383,
-0.030188112, -0.0053388323, 0.0010654867, 0.072027855,
-0.04167721, 0.014839341, -0.032948174, -0.062975034,
-0.024837125, ...]

Embedding 模型

Embedding 模型(即嵌入模型)可将文本、图像、音频、视频等数据转换为数值向量,也就是所谓的“万物皆可向量化”。

Embedding 模型

优秀的 embedding 模型能提取数据的特征,让内容相似的数据生成距离相近的向量。例如:

  • “香蕉”和“Banana”在语义上接近,会被 embedding 模型转化为接近的向量;
  • “我爱苹果手机”和“我爱吃苹果”虽然都包含“苹果”,但是语义相差甚远,优秀的 embedding 模型会把二者区分开来,二者向量距离也会较远。

在相似图片检索场景下,两只相似的猫在不同环境、角度、光线下的图片,其向量相似度远比同样环境下相同颜色的一只猫和一条狗的图片相似度更高。相对于传统的图片相似度算法(例如颜色直方图对比、明暗对比),embedding 模型能“看懂”图像里的事物,而非只对比像素级色彩明暗,匹配逻辑更贴近人类看待事物的方式。

此外,部分 embedding 模型支持将一张香蕉的图片和“香蕉”这个字符串转化为接近的向量,这就让“文搜图”更加精准。例如电商场景下搜索“红色的连衣裙”,更容易匹配到符合要求的图片。

使用多模态 embedding 模型,语义接近的内容(无论是图片还是文本)向量化后其空间距离也非常接近,图片来源:https://odsc.medium.com/getting-started-with-vector-based-search-23c22da43266

由于本文主题是“以图搜图”,所以需要用到多模态 embedding 模型(而非纯文本 embedding 模型),以下列出一些可选的模型:

示例代码(使用 qwen3-vl-embedding 模型计算一张图片的向量):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import dashscope

image = "https://xxgblog.com/img/image-vector-search-apple.jpg"

input_data = [
{"image": image}
]

resp = dashscope.MultiModalEmbedding.call(
api_key="sk-xxx",
model="qwen3-vl-embedding",
input=input_data,
# 可选参数:指定向量维度
dimension=1024
)

embedding = resp.output['embeddings'][0]['embedding']

print(resp.output['embeddings'][0]['embedding'])
print("向量维度:" + str(len(embedding)))

输出:

1
2
[0.00017150086932815611, -0.013744989410042763, 0.13095951080322266, ...]
向量维度:1024

向量检索

以图搜图应用分为离线入库与在线检索两大环节:

  • 离线环节将全量图片(如电商平台的商品图)通过多模态 embedding 模型转为向量并存储至向量数据库;
  • 用户在线检索时,上传的图片经相同模型生成查询向量,在向量库中执行相似向量匹配,召回对应原图,实现相似图像检索。

离线入库与在线检索流程

向量相似度计算

需要判断两个图片是否相似,就需要计算两个向量的相似度。通常有如下算法可选择:

  • 余弦相似度(cosine similarity):计算两个向量的夹角,本质上只和向量的角度有关,和向量的长度无关;
  • 内积(inner product, IP)或点积(dot product) :当向量已归一化时,内积的计算结果与余弦相似度等价;
  • 欧氏距离 (L2) :测量两个向量之间线段的长度。

这里引入一个概念:向量归一化。归一化本质上就是将向量的模长缩放为 1,方向不变。

很多 embedding 模型本身已经做过归一化(例如本文使用的 qwen3-vl-embedding 模型),则首选内积(IP)作为相似度计算算法,相对于余弦相似度更加简单,性能也会更好。

代码示例(使用内积计算向量相似度,从图库中获取最相似的图片):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import dashscope
import numpy as np


def embedding(image):
input_data = [
{"image": image}
]
resp = dashscope.MultiModalEmbedding.call(
api_key="sk-xxx",
model="qwen3-vl-embedding",
input=input_data,
dimension=1024
)
return resp.output['embeddings'][0]['embedding']


# 使用内积/点积计算向量相似度
def similarity(vector1, vector2):
return np.dot(vector1, vector2)


if __name__ == '__main__':
# 模拟离线环节:准备 5 张图片作为被检索图片,获取图片向量
image_data = {
"苹果": "https://xxgblog.com/img/image-vector-search-apple.jpg",
"香蕉": "https://xxgblog.com/img/image-vector-search-banana.jpg",
"蓝莓": "https://xxgblog.com/img/image-vector-search-blueberry.jpg",
"柠檬": "https://xxgblog.com/img/image-vector-search-lemon.jpg",
"芒果": "https://xxgblog.com/img/image-vector-search-mango.jpg",
}
vectors = {}
for key, value in image_data.items():
v = embedding(value)
vectors[key] = v

# 模拟在线检索:准备一张图片作为用户上传的图片,获取图片向量
user_image = "https://xxgblog.com/img/image-vector-search-user-image.jpg"
print(f"用户上传的图片:{user_image}")
user_image_vector = embedding(user_image)

# 逐个对比相似度并检索出最相似的图片
top1 = {}
for key, value in vectors.items():
image_similarity = similarity(user_image_vector, value)
print(f"{key}({image_data[key]}) 和用户图片的相似度:{similarity(user_image_vector, value)}")
if not top1 or image_similarity > top1["similarity"]:
top1["similarity"] = image_similarity
top1["image"] = image_data[key]
top1["name"] = key

print(f"查找到最相似的图片是[{top1['name']}]:{top1['image']},相似度:{top1['similarity']}")

输出:

1
2
3
4
5
6
7
用户上传的图片:https://xxgblog.com/img/image-vector-search-user-image.jpg
苹果(https://xxgblog.com/img/image-vector-search-apple.jpg) 和用户图片的相似度:0.20179038343485586
香蕉(https://xxgblog.com/img/image-vector-search-banana.jpg) 和用户图片的相似度:0.5099913278397701
蓝莓(https://xxgblog.com/img/image-vector-search-blueberry.jpg) 和用户图片的相似度:0.20387121238504502
柠檬(https://xxgblog.com/img/image-vector-search-lemon.jpg) 和用户图片的相似度:0.2078659394638562
芒果(https://xxgblog.com/img/image-vector-search-mango.jpg) 和用户图片的相似度:0.24791952668519704
查找到最相似的图片是[香蕉]:https://xxgblog.com/img/image-vector-search-banana.jpg,相似度:0.5099913278397701

向量数据库

在大量向量的场景下,全表扫描式遍历所有向量并逐一计算相似度的方法效率极低。因此需要为向量数据构建专用索引,通过近似最近邻(ANN)算法大幅减少计算量,这套技术统称为向量索引算法,作用等价于关系数据库的 B+ 树索引。典型的索引实现方式有:

  • HNSW (Hierarchical Navigable Small World)
  • IVF (Inverted File Index)

支持向量检索的数据库就是所谓的向量数据库。目前流行的向量数据库有:

此外,很多传统的数据库也扩展并支持向量检索:

代码示例(使用 Chroma 作为向量数据库实现相似图片检索):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
import dashscope
import chromadb


def embedding(image):
input_data = [
{"image": image}
]
resp = dashscope.MultiModalEmbedding.call(
api_key="sk-xxx",
model="qwen3-vl-embedding",
input=input_data,
dimension=1024
)
return resp.output['embeddings'][0]['embedding']


if __name__ == '__main__':
# 模拟离线环节:准备 5 张图片作为被检索图片并入库
image_data = {
"苹果": "https://xxgblog.com/img/image-vector-search-apple.jpg",
"香蕉": "https://xxgblog.com/img/image-vector-search-banana.jpg",
"蓝莓": "https://xxgblog.com/img/image-vector-search-blueberry.jpg",
"柠檬": "https://xxgblog.com/img/image-vector-search-lemon.jpg",
"芒果": "https://xxgblog.com/img/image-vector-search-mango.jpg",
}
vectors = []
metadatas = []
for key, value in image_data.items():
v = embedding(value)
vectors.append(v)
metadatas.append({"name": key, "image": value})

# 获取图片向量写入向量数据库
# 这里为了演示,使用内存数据库,实际使用中请使用持久化数据库:client = chromadb.PersistentClient(path="/path/to/save/to")
chroma_client = chromadb.Client()
collection = chroma_client.create_collection(
name="my_products",
embedding_function=None, # 由于图片向量是自行调用 embedding 模型获取,不需要 ChromaDB 提供 embedding 功能
configuration={
"hnsw": {
"space": "ip" # Chroma 默认使用 L2,这里改为使用内积(IP)
}
}
)
collection.add(
ids=["1", "2", "3", "4", "5"],
embeddings=vectors,
metadatas=metadatas,
)

# 模拟在线检索:准备一张图片作为用户上传的图片,获取图片向量
user_image = "https://xxgblog.com/img/image-vector-search-user-image.jpg"
print(f"用户上传的图片:{user_image}")
user_image_vector = embedding(user_image)

# 从向量数据库中查询最相似的记录
results = collection.query(
query_embeddings=[user_image_vector], # 查询条件
n_results=3 # 查询几条记录
)

for i in range(len(results['metadatas'][0])):
print(f"[{i+1}] {results['metadatas'][0][i]['name']}{results['metadatas'][0][i]['image']},相似度:{1 - results['distances'][0][i]}")

top1 = results['metadatas'][0][0]
top1_similarity = 1 - results['distances'][0][0]
print(f"查找到最相似的图片是[{top1['name']}]:{top1['image']},相似度:{top1_similarity}")

输出:

1
2
3
4
5
用户上传的图片:https://xxgblog.com/img/image-vector-search-user-image.jpg
[1] 香蕉:https://xxgblog.com/img/image-vector-search-banana.jpg,相似度:0.5099915862083435
[2] 芒果:https://xxgblog.com/img/image-vector-search-mango.jpg,相似度:0.247919499874115
[3] 柠檬:https://xxgblog.com/img/image-vector-search-lemon.jpg,相似度:0.2078658938407898
查找到最相似的图片是[香蕉]:https://xxgblog.com/img/image-vector-search-banana.jpg,相似度:0.5099915862083435

进一步优化

目前我们已经基于 embedding 模型和向量数据库搭建了一个简易的以图搜图应用,但是在生产级应用中,通常还需要做更多优化,让检索结果更加精准。

主体识别和图片裁切

针对图片中杂乱的背景可能会影响检索结果的问题,通常会先将图片进行主体识别后裁切,再用 embedding 模型向量化后检索。很多公有云平台提供了图像主体检测 API,参考:

重排序(rerank)

向量检索系统优先保证速度,因此结果的精确度可能不够理想。重排序模型对检索到的文档重新打分,可将最相关的结果排在前面,显著提升搜索精度。

Embedding 模型和重排序模型通常结合使用,互为补充:

  • Embedding 模型对被检索的数据和用户实时数据分别独立计算向量,可基于被检索数据向量建立索引,便于快速检索,但结果往往没那么精准,可能更相似的结果反而排在后面;
  • 重排序模型通常更加精确,但它需要将两个数据同时输入模型对比相似度并打分,无法建立索引快速检索,适合针对向量检索出的少量结果进行相似性排序。

随着技术的发展,embedding 模型的结果也越来越精准,重排序模型也不是必选项,毕竟重排序过程会导致最终响应更慢,可根据实际情况来选择。

代码示例(使用 qwen3-vl-rerank 模型对向量数据库检索结果重排序):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
import dashscope
import chromadb


def embedding(image):
input_data = [
{"image": image}
]
resp = dashscope.MultiModalEmbedding.call(
api_key="sk-xxx",
model="qwen3-vl-embedding",
input=input_data,
dimension=1024
)
return resp.output['embeddings'][0]['embedding']


if __name__ == '__main__':
# 准备 5 张图片作为被检索图片
image_data = {
"苹果": "https://xxgblog.com/img/image-vector-search-apple.jpg",
"香蕉": "https://xxgblog.com/img/image-vector-search-banana.jpg",
"蓝莓": "https://xxgblog.com/img/image-vector-search-blueberry.jpg",
"柠檬": "https://xxgblog.com/img/image-vector-search-lemon.jpg",
"芒果": "https://xxgblog.com/img/image-vector-search-mango.jpg",
}

# 获取图片向量
vectors = []
metadatas = []
for key, value in image_data.items():
v = embedding(value)
vectors.append(v)
metadatas.append({"name": key, "image": value})

# 获取图片向量写入向量数据库
chroma_client = chromadb.Client()
collection = chroma_client.create_collection(
name="products",
embedding_function=None,
configuration={
"hnsw": {
"space": "ip"
}
}
)
collection.add(
ids=["1", "2", "3", "4", "5"],
embeddings=vectors,
metadatas=metadatas,
)

# 准备一张图片作为用户上传的图片,获取图片向量
user_image = "https://xxgblog.com/img/image-vector-search-user-image.jpg"
print(f"用户上传的图片:{user_image}")
user_image_vector = embedding(user_image)

# 从向量数据库中查询最相似的记录
results = collection.query(
query_embeddings=[user_image_vector],
n_results=3
)

images = []
for i in range(len(results['metadatas'][0])):
images.append({"image": results['metadatas'][0][i]['image']})

# 使用 qwen3-vl-rerank 模型对向量数据库检索结果重排序(rerank)
resp = dashscope.TextReRank.call(
api_key="sk-xxx",
model="qwen3-vl-rerank",
query={"image": user_image},
documents=images,
top_n=3, # 返回重排序后结果数量
return_documents=True
)
for item in resp.output.results:
print(f"{item['document']['image']},相关度:{item.relevance_score}")

输出:

1
2
3
4
用户上传的图片:https://xxgblog.com/img/image-vector-search-user-image.jpg
https://xxgblog.com/img/image-vector-search-banana.jpg,相关度:0.7338192024181801
https://xxgblog.com/img/image-vector-search-mango.jpg,相关度:0.4842462474210706
https://xxgblog.com/img/image-vector-search-lemon.jpg,相关度:0.4773846354376837

文章目录
  1. 1. 万物皆向量
    1. 1.1. 什么是向量
    2. 1.2. Embedding 模型
  2. 2. 向量检索
    1. 2.1. 向量相似度计算
    2. 2.2. 向量数据库
  3. 3. 进一步优化
    1. 3.1. 主体识别和图片裁切
    2. 3.2. 重排序(rerank)