第3章:核心原理 - 向量表示、距离计算与索引结构¶
3.1 向量表示¶
什么是向量¶
向量(Vector) 是数学中的基本概念,可以理解为一个"有方向和大小"的数值数组。在向量数据库中,每个数据都被表示为一个高维数值数组。
通俗解释:想象你在描述一篇文章的特征: - 第一个数表示"科技含量"(0.8表示很高) - 第二个数表示"娱乐性"(0.2表示不太娱乐) - 第三个数表示"政治敏感度"(-0.3表示略微负面) - ...
这篇文章就可以用 [0.8, 0.2, -0.3, ...] 这样一个向量来表示。
# 向量示例:不同文本的向量表示
text_vectors = {
"今天天气真好": [0.1, 0.9, 0.3, -0.1],
"人工智能将改变世界": [0.8, 0.2, 0.7, 0.5],
"看了一场精彩的电影": [0.2, 0.8, 0.4, 0.3],
}
# 维度:每个向量有4个数值,就是4维向量
# [0.1, 0.9, 0.3, -0.1] # 4维向量
向量的直观理解¶
graph TD
subgraph 低维到高维的映射
A[1维: 点在线上] --> B[2维: 点在平面上]
B --> C[3维: 点在空间中]
C --> D[高维: 无法直观想象
但数学上完全一致]
end
subgraph 向量表示示例
E["文本: '苹果'"] --> F["向量: [0.8, 0.1, 0.3, ...]"]
G["图片: 猫的照片"] --> H["向量: [0.2, 0.7, 0.4, ...]"]
H --> J["所有数据都可以
表示为数值向量"]
end
style D fill:#e1f5fe
style J fill:#c8e6c9
向量的数学性质¶
| 性质 | 说明 | 示例 |
|---|---|---|
| 方向 | 向量有方向,不同方向代表不同的语义 | [1,0] 和 [0,1] 方向完全不同 |
| 距离 | 方向越接近的向量,语义越相似 | [0.9, 0.1] 和 [0.8, 0.2] 很接近 |
| 可计算 | 向量之间可以做加减运算 | 著名的 king - man + woman ≈ queen |
3.2 距离计算¶
什么是向量距离¶
向量距离(Distance)是衡量两个向量之间"相似程度"的指标。距离越小,两个向量越相似。
常见距离度量方法¶
1. 欧氏距离(L2 Distance)¶
定义:两点之间的直线距离,即各维度差值平方和的平方根。
通俗理解:就像你在地图上计算两点之间的直线距离。
import numpy as np
def euclidean_distance(v1, v2):
"""计算欧氏距离"""
return np.sqrt(np.sum((v1 - v2) ** 2))
# 示例
vector_a = np.array([1, 2, 3])
vector_b = np.array([4, 6, 3])
distance = euclidean_distance(vector_a, vector_b)
print(f"欧氏距离: {distance:.2f}") # 输出: 4.47
几何解释:
graph TD
subgraph 二维空间中的欧氏距离
A["A点 (1,2)"] --> B["B点 (4,6)"]
A --> C["(4,2)"]
C --> B
style A fill:#e1f5fe
style B fill:#ffccbc
style C fill:#b3e5fc
end
NOTE["欧氏距离 = √[(4-1)² + (6-2)²] = √[9 + 16] = √25 = 5"]
适用场景: - 图像/音频特征向量 - 当各维度重要性相同时 - 需要考虑向量大小(magnitude)的场景
2. 余弦相似度(Cosine Similarity)¶
定义:两个向量夹角的余弦值,取值范围 [-1, 1]。
通俗理解:比较两个向量的方向是否一致,而不考虑它们的长度。
import numpy as np
def cosine_similarity(v1, v2):
"""计算余弦相似度"""
dot_product = np.dot(v1, v2)
norm_v1 = np.linalg.norm(v1)
norm_v2 = np.linalg.norm(v2)
return dot_product / (norm_v1 * norm_v2)
# 示例
vector_a = np.array([1, 1, 0])
vector_b = np.array([1, 1, 1])
vector_c = np.array([-1, -1, 0])
print(f"a与b相似度: {cosine_similarity(vector_a, vector_b):.3f}") # 0.816
print(f"a与c相似度: {cosine_similarity(vector_a, vector_c):.3f}") # -1.000
几何解释:
graph TD
subgraph 余弦相似度示意
O["原点"]
A["A [1,1]"]
B["B [1,1,1]"]
C["C [-1,-1]"]
O --> A
O --> B
O --> C
AA["夹角45°
相似度0.816"]
CC["夹角180°
相似度-1.0"]
end
style A fill:#b3e5fc
style B fill:#c8e6c9
style C fill:#ffccbc
适用场景: - 文本语义相似度(最常用!) - 文档主题相似度 - 忽略向量大小,只关心方向
3. 点积相似度(Dot Product)¶
定义:两个向量对应元素相乘后求和。
def dot_product_similarity(v1, v2):
"""计算点积"""
return np.dot(v1, v2)
# 示例
vector_a = np.array([1, 2, 3])
vector_b = np.array([4, 5, 6])
dot = dot_product_similarity(vector_a, vector_b)
print(f"点积: {dot}") # 输出: 32 (1*4 + 2*5 + 3*6)
距离度量方法对比¶
graph LR
subgraph 距离度量对比
A[向量A] -->|相同方向| B[向量B]
A -->|相反方向| C[向量C]
A -->|垂直| D[向量D]
end
subgraph 相似度值
B --> E["余弦: 0.95 (很相似)"]
C --> F["余弦: -0.95 (相反)"]
D --> G["余弦: 0 (正交)"]
end
subgraph 距离值
B --> H["欧氏: 0.3 (很近)"]
C --> I["欧氏: 2.5 (很远)"]
D --> J["欧氏: 1.4 (中等)"]
end
style E fill:#c8e6c9
style F fill:#ffccbc
style G fill:#fff3e0
如何选择距离度量¶
| 场景 | 推荐方法 | 原因 |
|---|---|---|
| 文本语义搜索 | 余弦相似度 | 文本向量方向比长度更重要 |
| 图像特征匹配 | 欧氏距离 | 需要考虑特征强度 |
| 推荐系统 | 点积 | 综合考虑用户偏好强度和物品属性 |
| 人脸识别 | 余弦相似度 | 关注特征方向,不受光照影响 |
3.3 索引结构¶
为什么需要索引¶
如果没有索引,向量数据库在查询时需要与所有向量逐一计算距离,时间复杂度是 O(N),这在海量数据下是不可接受的。
# 无索引的线性搜索
def linear_search(query_vector, all_vectors, top_k=10):
distances = []
for vector in all_vectors:
dist = euclidean_distance(query_vector, vector)
distances.append((dist, vector))
distances.sort(key=lambda x: x[0])
return distances[:top_k]
# 问题:100万条数据 = 100万次距离计算 = 几秒钟
# 解决:建立索引,将计算次数降低到几百次
近似最近邻搜索(ANN)¶
近似最近邻搜索(Approximate Nearest Neighbor, ANN) 是向量数据库的核心技术。它通过建立索引结构,在"可接受的精度损失范围内",大幅提升搜索速度。
flowchart LR
subgraph 精确 vs 近似
A[精确搜索] -->|100%准确| B["时间: O(N)
N=10亿时不可用"]
C[近似搜索] -->|99%准确| D["时间: O(log N)
毫秒级响应"]
end
style A fill:#ffccbc
style C fill:#c8e6c9
style B fill:#ffccbc
style D fill:#c8e6c9
主流索引算法¶
1. HNSW(分层导航小世界图)¶
HNSW(Hierarchical Navigable Small World) 是目前最流行的高性能向量索引算法。
核心思想: 1. 构建多层图结构,上层稀疏、下层密集 2. 查询时从最上层开始,通过"贪心搜索"快速定位 3. 利用"小世界"特性,确保搜索路径很短
HNSW索引结构:
graph TB
subgraph Layer 2 [L2层 - 顶层导航]
L2_1(( )) --> L2_2(( ))
L2_2(( )) --> L2_3(( ))
L2_3(( )) --> L2_1(( ))
end
subgraph Layer 1 [L1层 - 中层]
L1_1(( )) --> L1_2(( ))
L1_2(( )) --> L1_3(( ))
L1_3(( )) --> L1_4(( ))
L1_4(( )) --> L1_1(( ))
L1_2(( )) --> L1_5(( ))
end
subgraph Layer 0 [L0层 - 底层数据]
L0_1(( )) --> L0_2(( ))
L0_2(( )) --> L0_3(( ))
L0_3(( )) --> L0_4(( ))
L0_4(( )) --> L0_5(( ))
L0_5(( )) --> L0_6(( ))
L0_6(( )) --> L0_7(( ))
L0_7(( )) --> L0_8(( ))
L0_1(( )) --> L0_4(( ))
L0_3(( )) --> L0_6(( ))
L0_5(( )) --> L0_8(( ))
end
subgraph 查询路径
Q((Query)) -->|1. 进入L2| L2_2
L2_2 -->|2. 下降到L1| L1_3
L1_3 -->|3. 下降到L0| L0_4
L0_4 -->|4. 局部搜索| L0_5
end
style L2_1 fill:#90caf9
style L2_2 fill:#90caf9
style L2_3 fill:#90caf9
style Q fill:#ff5722,color:#fff
HNSW搜索过程示例:
# HNSW搜索伪代码
def hnsw_search(query_vector, index, top_k=10):
# 1. 从最高层开始
current_layer = index.max_layer
# 2. 在当前层贪心搜索,找到最近的入口点
entry_point = index.get_entry_point(current_layer)
while current_layer > 0:
neighbors = index.get_neighbors(entry_point, current_layer)
best_next = find_nearest(query_vector, neighbors)
if distance(query_vector, best_next) < distance(query_vector, entry_point):
entry_point = best_next
current_layer -= 1
# 3. 在底层进行精细搜索
results = layer0_search(query_vector, entry_point, top_k)
return results
HNSW的参数说明:
| 参数 | 说明 | 推荐值 |
|---|---|---|
| M | 每个节点的最大连接数 | 16-64 |
| efConstruction | 构建时的动态列表大小 | 100-400 |
| efSearch | 搜索时的动态列表大小 | 100-500 |
| max_level | 最大层数 | log(N) |
2. IVF(倒排索引)¶
IVF(Inverted File Index) 是一种基于聚类的索引方法。
核心思想: 1. 对所有向量进行聚类(如K-Means) 2. 每个聚类维护一个倒排列表 3. 查询时只搜索目标向量所在的聚类及其邻近聚类
graph TD
subgraph 聚类空间
C1["聚类中心1"]
C2["聚类中心2"]
C3["聚类中心3"]
C1 -.->|倒排列表| I1["向量1,5,8,12..."]
C2 -.->|倒排列表| I2["向量2,3,7,15..."]
C3 -.->|倒排列表| I3["向量4,6,9,11..."]
end
subgraph 查询过程
Q["查询向量"] -->|找到最近中心| C2
C2 -->|搜索该聚类| I2
I2 --> R["候选向量"]
end
style C1 fill:#e1f5fe
style C2 fill:#c8e6c9
style C3 fill:#fff3e0
style I2 fill:#c8e6c9
3. PQ(乘积量化)- 有损压缩¶
PQ(Product Quantization) 将高维向量分割成多个子向量,分别进行聚类压缩。
graph TD
subgraph 原始向量
O["原始向量 [0.23, -0.45, 0.89, -0.12, 0.56, -0.78]"]
end
subgraph 分段
S1["段1: [0.23, -0.45]"]
S2["段2: [0.89, -0.12]"]
S3["段3: [0.56, -0.78]"]
end
subgraph 量化
Q1["→ 聚类中心ID: 42"]
Q2["→ 聚类中心ID: 17"]
Q3["→ 聚类中心ID: 89"]
end
O --> S1
O --> S2
O --> S3
S1 --> Q1
S2 --> Q2
S3 --> Q3
Q1 --> R["压缩后: [42, 17, 89]"]
Q2 --> R
Q3 --> R
style O fill:#e1f5fe
style R fill:#c8e6c9
索引算法对比¶
| 索引算法 | 查询速度 | 精度 | 内存占用 | 适用场景 |
|---|---|---|---|---|
| HNSW | 极快 | 高 | 中等 | 通用场景,推荐首选 |
| IVF | 快 | 中高 | 低 | 大规模数据 |
| PQ | 快 | 中 | 很低 | 内存受限场景 |
3.4 最近邻搜索¶
最近邻搜索示意图¶
graph TD
subgraph 二维向量空间示意
Q["查询向量 Q"]
A["A [0.2, 0.3]"]
B["B [0.8, 0.7]"]
C["C [0.5, 0.9]"]
D["D [0.9, 0.2]"]
E["E [0.3, 0.8]"]
end
subgraph 距离计算
Q -->|与A距离| DA["0.36"]
Q -->|与B距离| DB["0.79"]
Q -->|与C距离| DC["0.28"]
Q -->|与D距离| DD["0.70"]
Q -->|与E距离| DE["0.22"]
end
subgraph 排序结果
DE -->|最近| R1["1. E (0.22)"]
DC -->|次近| R2["2. C (0.28)"]
DA -->|第三| R3["3. A (0.36)"]
end
style Q fill:#ff5722,color:#fff
style E fill:#4caf50
style C fill:#4caf50
ANN vs KNN¶
graph LR
subgraph KNN精确搜索
A[K=5] -->|找到| B["5个精确最近邻
100%准确
但很慢"]
end
subgraph ANN近似搜索
C[K=5] -->|找到| D["5个近似最近邻
99%准确
但很快"]
end
style A fill:#e1f5fe
style C fill:#c8e6c9
style B fill:#ffccbc
style D fill:#c8e6c9
| 对比项 | KNN | ANN |
|---|---|---|
| 精确度 | 100% | 95%-99% |
| 速度 | 慢 | 快 |
| 适用数据量 | <10万 | 10万-10亿+ |
| 召回率 | 1.0 | 0.95-0.99 |
3.5 过滤与混合搜索¶
什么是过滤搜索¶
在实际应用中,我们常常需要在搜索时加入额外的过滤条件,如"只搜索2024年发布的文章"。
# 过滤搜索示例
results = vector_db.search(
query_vector=query_embedding,
top_k=10,
filter={
"category": "technology", # 只搜索科技类
"year": {"$gte": 2024}, # 发布时间>=2024
"is_published": True # 已发布的
}
)
混合搜索架构¶
flowchart TD
subgraph 查询处理
Q["查询向量"] -->|1| FE[过滤器表达式]
Q -->|2| ANN[ANN索引搜索]
end
subgraph 过滤阶段
FE -->|构建| FM["过滤掩码
[T,F,T,T,F...]"]
ANN -->|返回| CR["候选结果"]
end
subgraph 最终筛选
CR -->|应用过滤| FR["过滤后的结果"]
end
style ANN fill:#e1f5fe
style FE fill:#fff3e0
style FR fill:#c8e6c9
本章小结¶
本章要点:
- 向量表示:所有数据(文本、图像、音频)都可以转换为高维数值向量
- 距离计算:
- 欧氏距离:适合图像/音频,需要考虑大小
- 余弦相似度:适合文本语义,最常用
- 点积:适合推荐系统
- 索引结构:
- HNSW:目前最主流,多层图结构
- IVF:基于聚类的倒排索引
- PQ:有损压缩,适合内存受限场景
- ANN搜索:用少量精度损失换取大规模提速
下一章预告:第4章:基础概念解析 - 深入理解嵌入、维度、相似度度量等核心术语 →