Embedding 与向量数据库

0 阅读11分钟

本节目标:理解如何把文字变成数字(Embedding),以及如何高效地存储和搜索这些数字(向量数据库)。这是构建 RAG 系统的基础。


一、什么是 Embedding?

1.1 通俗理解

Embedding 就是把文字变成一组数字(向量),让计算机能理解文字之间的语义关系

人类理解语义:     "猫""狗" 很相似(都是宠物)
                 "猫""汽车" 很不同

计算机怎么理解?  → 把文字变成数字!

"猫"  → [0.2, 0.8, 0.1, 0.9, ...]   ← 一组数字(向量)
"狗"  → [0.3, 0.7, 0.2, 0.8, ...]   ← 和"猫"的数字很接近
"汽车" → [0.9, 0.1, 0.8, 0.2, ...]   ← 和"猫"的数字差很远

1.2 一个直观的比喻

想象一个二维地图(实际的 Embedding 是几百维,但原理一样):

"动物属性"1.0  │    🐱猫    🐶狗
        │
   0.8  │       🐰兔子
        │
   0.5  │
        │              ✈️飞机
   0.2  │    🚗汽车
        │         🚂火车
   0.0  ├───────────────────→ "交通工具属性"
       0.0   0.2   0.5   0.8   1.0

  "猫""狗"在地图上离得很近 → 语义相似
  "猫""汽车"离得很远     → 语义不同

1.3 Embedding 的维度

实际的 Embedding 不是 2 维,而是几百到几千维:

┌─────────────────────┬──────────────────────┐
  Embedding 模型        向量维度             
├─────────────────────┼──────────────────────┤
  OpenAI text-ada-002│  1536              
  OpenAI text-3-small│  1536              
  OpenAI text-3-large│  3072              
  BGE-large-zh         1024              
  Jina Embeddings v3   1024              
  Cohere Embed v3      1024              
└─────────────────────┴──────────────────────┘

维度越高  能表达更细微的语义差异  但计算量更大

二、如何生成 Embedding?

2.1 使用 OpenAI Embedding API

from openai import OpenAI

client = OpenAI(api_key="sk-xxx")

# 生成单个文本的 Embedding
response = client.embeddings.create(
    model="text-embedding-3-small",
    input="今天天气真好"
)

vector = response.data[0].embedding
print(f"维度:{len(vector)}")       # 1536
print(f"前5个数字:{vector[:5]}")    # [0.012, -0.034, 0.056, ...]

2.2 使用开源 Embedding 模型

# 使用 sentence-transformers 库(免费、可本地运行)
# pip install sentence-transformers

from sentence_transformers import SentenceTransformer

# 加载模型(首次运行会自动下载)
model = SentenceTransformer('BAAI/bge-large-zh-v1.5')  # 中文最佳

# 生成 Embedding
sentences = ["今天天气真好", "天气不错适合出门", "我要买一辆汽车"]
embeddings = model.encode(sentences)

print(f"形状:{embeddings.shape}")  # (3, 1024) → 3个句子,每个1024维

三、向量相似度计算

3.1 余弦相似度(最常用)

衡量两个向量的方向是否一致,不关心长度。

余弦相似度的直觉理解:

  两个向量方向完全相同 → 相似度 = 1(完全相似)
  两个向量方向垂直    → 相似度 = 0(不相关)
  两个向量方向相反    → 相似度 = -1(完全相反)

       ↑ B
      ╱
     ╱  θ = 小角度 → cos(θ) 接近 1 → 很相似
    ╱
   ──────→ A
import numpy as np

def cosine_similarity(a, b):
    """计算两个向量的余弦相似度"""
    return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))

# 示例
emb_cat = model.encode("一只可爱的猫咪")
emb_dog = model.encode("一只可爱的狗狗")
emb_car = model.encode("一辆红色的汽车")

print(f"猫 vs 狗:{cosine_similarity(emb_cat, emb_dog):.3f}")   # ~0.85 高相似
print(f"猫 vs 车:{cosine_similarity(emb_cat, emb_car):.3f}")   # ~0.25 低相似

3.2 其他距离度量

┌──────────────────┬──────────────────────────────────────────┐
│  度量方式          │  适用场景                                 │
├──────────────────┼──────────────────────────────────────────┤
│  余弦相似度        │  最通用,推荐默认使用                       │
│  (Cosine)        │  不受向量长度影响                           │
├──────────────────┼──────────────────────────────────────────┤
│  欧氏距离         │  需要考虑"绝对距离"时                       │
│  (Euclidean)     │  值越小越相似                              │
├──────────────────┼──────────────────────────────────────────┤
│  内积             │  已归一化的向量                            │
│  (Inner Product) │  等价于余弦相似度                           │
└──────────────────┴──────────────────────────────────────────┘

四、向量数据库

4.1 为什么需要向量数据库?

场景:你有 100 万篇文档的 Embedding,用户问了一个问题。
     需要快速找到最相关的 10 篇文档。

暴力搜索(逐一比较):
  比较 100 万次 → 太慢了!(几分钟)

向量数据库(用索引加速):
  用特殊的数据结构 → 毫秒级返回结果!

4.2 主流向量数据库对比

┌──────────────┬──────────┬──────────┬──────────┬─────────────────┐
│  数据库       │  类型     │  特点    │  难度     │  适合场景        │
├──────────────┼──────────┼──────────┼──────────┼─────────────────┤
│  Chroma      │  嵌入式   │ 最简单    │ ★☆☆☆☆    │ 学习、原型验证     │
│  FAISS       │  库(Meta)│ 最快      │ ★★★☆☆    │ 研究、单机大规模   │
│  Milvus      │  分布式   │ 功能全    │ ★★★★☆    │ 企业级生产环境     │
│  Qdrant      │  服务端   │ Rust写的  │ ★★★☆☆    │ 性能敏感场景      │
│  Weaviate    │  服务端   │ 多模态    │ ★★★☆☆    │ 图+文混合搜索     │
│  Pinecone    │  全托管   │ 免运维    │ ★★☆☆☆    │ 不想管运维        │
│  PgVector    │  PG扩展   │ 复用PG   │ ★★☆☆☆    │ 已有PostgreSQL   │
└──────────────┴──────────┴──────────┴──────────┴─────────────────┘

4.3 Chroma 快速上手(最简单)

# pip install chromadb

import chromadb

# 1. 创建客户端和集合
client = chromadb.Client()
collection = client.create_collection("my_docs")

# 2. 添加文档(Chroma 自动生成 Embedding!)
collection.add(
    documents=[
        "Python 是一种简单易学的编程语言",
        "Java 是一种面向对象的编程语言",
        "今天股市大涨,上证指数突破3500点",
        "机器学习是人工智能的一个子领域",
    ],
    ids=["doc1", "doc2", "doc3", "doc4"]
)

# 3. 搜索最相关的文档
results = collection.query(
    query_texts=["什么编程语言适合初学者?"],
    n_results=2  # 返回最相关的2条
)

print(results["documents"])
# [['Python 是一种简单易学的编程语言',
#   'Java 是一种面向对象的编程语言']]
# → 准确找到了编程相关的文档,没有返回股市新闻!

4.4 Milvus 生产级使用

# pip install pymilvus

from pymilvus import MilvusClient

# 1. 连接 Milvus
client = MilvusClient("http://localhost:19530")

# 2. 创建集合
client.create_collection(
    collection_name="articles",
    dimension=1024,  # 向量维度,需要与 Embedding 模型匹配
)

# 3. 插入数据
data = [
    {"id": 1, "vector": [0.1, 0.2, ...], "text": "Python 入门教程"},
    {"id": 2, "vector": [0.3, 0.4, ...], "text": "Java 设计模式"},
    # ...
]
client.insert(collection_name="articles", data=data)

# 4. 搜索
results = client.search(
    collection_name="articles",
    data=[query_embedding],  # 查询向量
    limit=5,                 # 返回 top 5
    output_fields=["text"]   # 同时返回文本
)

五、向量索引类型

5.1 为什么需要索引?

没有索引(暴力搜索 Flat):
  ┌──┐ ┌──┐ ┌──┐ ┌──┐ ┌──┐ ... ┌──┐
  │  │ │  │ │  │ │  │ │  │     │  │
  └──┘ └──┘ └──┘ └──┘ └──┘     └──┘
  逐一比较所有向量 → 准确但慢

有索引(近似搜索):
  先把向量分组/建树,搜索时只看相关的组 → 快但可能遗漏

5.2 常见索引类型

┌───────────┬──────────────────────────────────────────────┐
│  索引类型  │  通俗解释                                      │
├───────────┼──────────────────────────────────────────────┤
│  Flat     │  暴力搜索,逐一比较                             │
│  (平坦)    │  最准确但最慢。数据少时可以用                    │
│           │                                              │
│  IVF      │  先把向量分成若干个"簇"                         │
│  (倒排)    │  搜索时只在最相近的几个簇中找                    │
│           │  类比:先确定在哪个书架,再在书架上找书            │
│           │                                              │
│  HNSW     │  建一个多层图结构(像跳表)                      │
│  (层次图)  │  从粗到细逐层搜索                              │
│           │  类比:先看世界地图,再看国家地图,再看城市地图     │
│           │  目前最流行的索引类型                          │
│           │                                             │
│  PQ       │  把向量压缩(量化),用更少的空间存储              │
│  (乘积量化)│  牺牲一点精度换取大幅减少内存占用                 │
└───────────┴──────────────────────────────────────────────┘
HNSW 索引示意图:

Layer 2 (最顶层,最稀疏):    A ─────────── D
                             │              │
Layer 1 (中间层):       A ── B ──── C ── D
                        │    │      │    │
Layer 0 (最底层,最密集): ABEFCGHDI

搜索过程:
1. 从顶层开始,找到大致方向(AD2. 下一层细化(ABC3. 底层精确搜索(找到最近邻)

六、混合检索

6.1 向量检索的局限

问题:用户搜索 "Python 3.12 新特性"

纯向量搜索可能返回:
  1. "Python 最新版本的功能介绍"  ← 相关但不精确
  2. "编程语言的新特性总结"       ← 相关但太宽泛
  ✗ 可能没有精确匹配 "3.12" 这个版本号

纯关键词搜索(BM25)可能返回:
  1. "Python 3.12 发布说明"  ← 精确匹配 ✓
  2. "Python 3.12 变更日志"  ← 精确匹配 ✓
  ✗ 但如果用户搜 "最新Python有啥变化",关键词匹配不到

6.2 混合检索 = 向量 + 关键词

┌────────────────────────────────────────────────────────┐
│                    混合检索流程                          │
│                                                        │
│  用户查询:"Python 3.12 新特性"                           │
│        │                                               │
│   ┌────┴────┐                                          │
│   ▼         ▼                                          │
│ 向量检索    BM25 关键词检索                               │
│ (语义理解)  (精确匹配)                                    │
│   │         │                                          │
│   ▼         ▼                                          │
│ 结果集A     结果集B                                      │
│   │         │                                          │
│   └────┬────┘                                          │
│        ▼                                               │
│   融合排序(RRF / 加权融合)                              │
│        │                                               │
│        ▼                                               │
│   最终排序结果                                           │
└────────────────────────────────────────────────────────┘
# 混合检索的简化实现
def hybrid_search(query, documents, alpha=0.7):
    """
    alpha: 向量检索的权重(0-1)
    1-alpha: 关键词检索的权重
    """
    # 向量检索得分
    vector_scores = vector_search(query, documents)

    # BM25 关键词检索得分
    bm25_scores = bm25_search(query, documents)

    # 融合得分
    final_scores = {}
    for doc_id in set(vector_scores) | set(bm25_scores):
        v_score = vector_scores.get(doc_id, 0)
        b_score = bm25_scores.get(doc_id, 0)
        final_scores[doc_id] = alpha * v_score + (1 - alpha) * b_score

    return sorted(final_scores.items(), key=lambda x: x[1], reverse=True)

七、Reranker 重排序

7.1 为什么需要重排序?

向量检索是"粗筛",可能前 10 名的排序不够精准。Reranker 是"精排",对粗筛结果进行更精确的重新排序。

检索流程:

  百万文档 ──向量检索──→ Top 20 候选 ──Reranker──→ Top 5 精排结果
  (粗筛:快但粗略)                    (精排:慢但精准)

7.2 使用 Reranker

# 使用 Cohere Reranker(API 方式)
import cohere

co = cohere.Client(api_key="xxx")

results = co.rerank(
    query="什么是向量数据库?",
    documents=[
        "向量数据库用于存储和检索高维向量数据",
        "关系数据库使用SQL进行查询",
        "向量搜索引擎可以进行语义搜索",
        "NoSQL数据库包括MongoDB等"
    ],
    top_n=2
)

for r in results.results:
    print(f"排名 {r.index}: 分数 {r.relevance_score:.3f}")
# 排名 0: 分数 0.95  → "向量数据库用于存储..."
# 排名 2: 分数 0.82  → "向量搜索引擎可以..."
# 使用开源 Reranker(本地运行,免费)
from sentence_transformers import CrossEncoder

reranker = CrossEncoder('BAAI/bge-reranker-v2-m3')

pairs = [
    ["什么是向量数据库?", "向量数据库用于存储和检索高维向量数据"],
    ["什么是向量数据库?", "关系数据库使用SQL进行查询"],
]

scores = reranker.predict(pairs)
# [0.95, 0.12] → 第一个文档和查询更相关

八、Embedding 模型选择指南

┌──────────────────────────────────────────────────────────────┐
│                  Embedding 模型选择决策树                      │
│                                                              │
│  你的数据是什么语言?                                           │
│  │                                                           │
│  ├─ 中文为主 → BGE-large-zh / Jina Embeddings v3              │
│  │                                                           │
│  ├─ 英文为主 → OpenAI text-embedding-3-small                  │
│  │             (付费但效果好)                                  │
│  │             或 BGE-large-en (免费开源)                      │
│  │                                                           │
│  └─ 多语言   → Jina Embeddings v3 / Cohere Embed v3           │
│                                                              │
│  数据能否发送到云端?                                           │
│  │                                                           │
│  ├─ 可以 → OpenAI / Cohere API(简单省事)                      │
│  └─ 不行 → BGE / Jina(本地部署)                               │
│                                                              │
│  数据规模多大?                                                │
│  │                                                           │
│  ├─ < 10万条 → 维度大一点没关系(1024-3072)                     │
│  └─ > 100万条 → 考虑较小维度(512-768)节省存储                   │
└──────────────────────────────────────────────────────────────┘

九、实战练习

完整示例:构建一个简易语义搜索引擎

"""
用 Chroma + Sentence Transformers 构建语义搜索引擎
无需 API Key,完全本地运行
"""
import chromadb
from sentence_transformers import SentenceTransformer

# 1. 初始化
embedding_model = SentenceTransformer('BAAI/bge-small-zh-v1.5')
chroma_client = chromadb.Client()

# 2. 创建集合,使用自定义 Embedding 函数
collection = chroma_client.create_collection(
    name="knowledge_base",
    metadata={"hnsw:space": "cosine"}  # 使用余弦相似度
)

# 3. 准备文档
documents = [
    "Python 是一种解释型、面向对象的高级编程语言",
    "Java 是一种广泛使用的编程语言,具有跨平台特性",
    "JavaScript 是网页开发的核心语言,可以在浏览器中运行",
    "机器学习是人工智能的一个分支,通过数据来学习规律",
    "深度学习使用多层神经网络来处理复杂的模式识别任务",
    "Docker 是一种容器化技术,可以将应用打包成轻量级容器",
    "Kubernetes 用于自动化部署、扩展和管理容器化应用",
    "Git 是一种分布式版本控制系统,用于跟踪代码变更",
    "REST API 是一种基于 HTTP 协议的接口设计风格",
    "微服务架构将应用拆分为多个独立的小服务",
]

# 4. 生成 Embedding 并存入数据库
embeddings = embedding_model.encode(documents).tolist()

collection.add(
    documents=documents,
    embeddings=embeddings,
    ids=[f"doc_{i}" for i in range(len(documents))]
)

# 5. 搜索
def search(query: str, top_k: int = 3):
    query_embedding = embedding_model.encode([query]).tolist()
    results = collection.query(
        query_embeddings=query_embedding,
        n_results=top_k
    )
    print(f"\n查询:{query}")
    print("=" * 50)
    for i, (doc, distance) in enumerate(
        zip(results["documents"][0], results["distances"][0])
    ):
        similarity = 1 - distance  # Chroma 返回的是距离,转为相似度
        print(f"  {i+1}. [{similarity:.2f}] {doc}")

# 测试
search("什么编程语言好学")
# 1. [0.78] Python 是一种解释型、面向对象的高级编程语言
# 2. [0.65] Java 是一种广泛使用的编程语言...
# 3. [0.61] JavaScript 是网页开发的核心语言...

search("如何部署应用")
# 1. [0.82] Docker 是一种容器化技术...
# 2. [0.75] Kubernetes 用于自动化部署...
# 3. [0.52] 微服务架构将应用拆分为...

search("怎么管理代码")
# 1. [0.80] Git 是一种分布式版本控制系统...
# 2. [0.45] ...

十、本章小结

┌────────────────────────────────────────────────────────┐
│                    本章知识地图                          │
│                                                        │
│  Embedding:把文字变成数字向量                            │
│  ├── 语义相似的文字 → 向量距离近                           │
│  ├── 余弦相似度 = 最常用的相似度计算方式                    │
│  └── 主流模型:OpenAI / BGE / Jina                      │
│                                                        │
│  向量数据库:高效存储和搜索向量                             │
│  ├── 入门首选:Chroma(嵌入式,零配置)                     │
│  ├── 生产首选:Milvus / Qdrant                           │
│  └── 索引类型:Flat / IVF / HNSW / PQ                    │
│                                                        │
│  检索增强:                                              │
│  ├── 混合检索 = 向量 + 关键词(互补)                       │
│  └── Reranker = 对初步结果精排                            │
└────────────────────────────────────────────────────────┘

十一、扩展学习资源

必读

推荐

动手实践

  • 用 Chroma 构建一个简单的文档搜索系统
  • 对比不同 Embedding 模型在中文搜索上的效果
  • 试试在同一数据集上,纯向量检索 vs 混合检索的效果差异

下一篇章预告:将讲解 RAG(检索增强生成)——把 Embedding 和向量数据库用起来,让大模型能够基于你的私有文档来回答问题!


觉得有用的话,点个关注吧!大模型方面你想看什么?留言区说,我来写。

声明:本博客内容素材来源于网络,文章由AI技术辅助生成。如有侵权或不当引用,请联系作者进行下架或删除处理。