← 返回投肯智能知识库首页
首页 / 技术教程 / 高级架构

Embedding与向量检索原理解密:从词向量到语义搜索的完整技术演进

📖 阅读时长:55分钟更新:2026-05-28

一、为什么需要Embedding:传统检索的困境

1.1 关键词匹配的局限性

传统搜索引擎(如早期百度、图书馆OPAC系统)使用倒排索引(Inverted Index)技术,本质上是"字符串匹配"。用户搜索"电脑维修",系统只能匹配包含"电脑"或"维修"这两个词的文档。

这种方法的根本问题在于:人类语言充满歧义和表达多样性。同一个意思可以用完全不同的词表达:

关键词检索无法理解语义,只能做表层字符串比对。Embedding的出现,就是为了解决这个问题:让机器理解语义

1.2 Embedding的本质:将语言映射到向量空间

Embedding的核心思想是:将每个词(或句子、文档)编码成一个稠密向量,让语义相似的对象在向量空间中距离相近

举例说明(简化示例,实际向量维度远高于此):

# 这是一个3维词向量的简单示例(真实Embedding维度通常是768/1024/1536)
# 向量空间中的词向量示例(用3维近似)

词汇          向量表示
─────────────────────────────
"国王"   →   [0.8, 0.2, 0.1]
"皇帝"   →   [0.79, 0.21, 0.09]   # 与"国王"距离很近
"男人"   →   [0.6, 0.7, 0.3]
"女人"   →   [0.61, 0.71, 0.29]  # 与"男人"距离很近
"苹果"   →   [0.1, 0.9, 0.4]
"香蕉"   →   [0.12, 0.88, 0.38]  # 水果类,距离近
"手机"   →   [0.1, 0.85, 0.5]    # 科技类,与"苹果"有一定距离

# 使用向量运算验证语义关系:
# "国王" - "男人" + "女人" ≈ "女王"
# 这就是著名的 word2vec 语义算术(king - man + woman ≈ queen)
💡 关键洞察:Embedding将离散的符号(文字)转换为连续的数值(向量),从而可以使用距离度量来表达语义相似度。这是从符号主义到连接主义的根本转变。

二、Word2Vec技术演进:从稀疏表示到稠密向量

2.1 One-Hot编码的困境

在Word2Vec出现之前,语言表示使用的是One-Hot编码

# One-Hot编码示例:假设词表大小为10000
# 每个词表示为一个10000维的向量,只有一个位置是1,其余都是0

"猫"     → [1, 0, 0, 0, 0, 0, ..., 0]  # 第1个位置是1
"狗"     → [0, 1, 0, 0, 0, 0, ..., 0]  # 第2个位置是1
"苹果"   → [0, 0, 1, 0, 0, 0, ..., 0]  # 第3个位置是1

# 问题:
# 1. 向量极其稀疏(10000维中只有1个1),内存浪费严重
# 2. 所有词之间的距离都相同(正交),无法表达语义关系
# 3. 无法处理未知词汇(OOV问题)

# 即使"猫"和"狗"都是动物,在One-Hot表示中它们的相似度为0

2.2 Word2Vec原理:CBOW与Skip-gram

2013年,Mikolov等人提出Word2Vec,通过神经网络从大规模语料中自动学习词向量。其核心有两种模型架构:

2.2.1 CBOW(Continuous Bag-of-Words)

CBOW的目标是:根据上下文预测中心词。给定窗口内的上下文词,预测当前词。

# CBOW模型结构示例
# 句子:"我想买一台新的笔记本电脑"
# 窗口大小:2(左右各2个词)
# 目标:预测中心词"笔记本"

上下文窗口:
位置:    [我] [想] [买] [一] [台] [新] [的] [笔记本] [电脑]
角色:    -2   -1   +1   +2                   目标词

# CBOW的输入是["我","想","买","一"]四个词的向量之和
# 经过隐藏层变换后,输出是"笔记本"的概率分布

# 数学表达:
# input = sum(embedding(context_words))  # 上下文词的向量求和
# hidden = W1 @ input + b1                # 线性变换
# output = softmax(W2 @ hidden + b2)      # 输出概率分布

# 训练目标:最大化正确中心词的概率(最小化交叉熵损失)

2.2.2 Skip-gram(跳字模型)

Skip-gram与CBOW相反:根据中心词预测上下文。一个词可以预测它周围的词。

# Skip-gram模型结构示例
# 目标词:"笔记本"
# 需要预测:位置-2、-1、+1、+2的词

# Skip-gram的输入是中心词"笔记本"的向量
# 输出是各个位置的上下文词概率分布

# 训练数据构建(以句子"我想买一台新的笔记本电脑"为例):
# (笔记本, 我)
# (笔记本, 想)
# (笔记本, 买)
# (笔记本, 一)
# (笔记本, 台)
# (笔记本, 新)
# (笔记本, 的)
# (笔记本, 电脑)
# 这样每个中心词-上下文词对就是一个训练样本

# Skip-gram的损失函数:
# Loss = -sum(log P(context_i | center_word))
# 目标是让中心词与上下文词的向量点积尽可能大(内积最大化)

2.2.3 两种模型的选择建议

场景推荐模型原因
小规模语料 + 高频词CBOW训练更快,对稀有词有一定平滑效果
大规模语料 + 稀有词为主Skip-gram对稀有词训练更充分,语义关系更精确
短语/成语学习Skip-gram短语的语义更依赖中心词

2.3 Word2Vec的训练细节与代码实现

# 使用Python + Gensim 实现Word2Vec训练
# 完整可运行的训练脚本

from gensim.models import Word2Vec
import jieba

# ============ Step 1: 准备中文语料 ============
# 语料:可以是小说、新闻、百科等中文文本
sentences = [
    "深度学习是机器学习的一个分支,主要使用神经网络模型",
    "卷积神经网络在图像识别领域取得了突破性进展",
    "自然语言处理让计算机能够理解和生成人类语言",
    "循环神经网络适合处理序列数据,如文本和时间序列",
    "Transformer模型通过自注意力机制解决了RNN的长距离依赖问题",
    "预训练语言模型如BERT和GPT大幅提升了NLP任务的效果",
    "向量数据库用于存储和检索高维向量,实现语义搜索",
    "RAG系统结合了检索和生成,提高了问答系统的准确性",
]

# ============ Step 2: 中文分词 ============
# 对每个句子进行分词
tokenized_sentences = [list(jieba.cut(s)) for s in sentences]
print("分词结果示例:", tokenized_sentences[0])
# 输出: ['深度学习', '是', '机器学习', '的', '一个', '分支', ',', '主要', '使用', '神经网络', '模型']

# ============ Step 3: 训练Word2Vec模型 ============
model = Word2Vec(
    sentences=tokenized_sentences,
    vector_size=100,        # 向量维度(实际中文模型通常用256/512/768)
    window=5,              # 上下文窗口大小(左右各5个词)
    min_count=1,            # 词频阈值,低于此频率的词被丢弃
    workers=4,              # 并行训练线程数
    sg=1,                  # 1=Skip-gram, 0=CBOW
    epochs=100,            # 训练轮数
    hs=0,                  # 0=负采样, 1=层次Softmax
    negative=5,            # 负采样数量(推荐5-20)
)

# 保存模型
model.save("word2vec.model")

# ============ Step 4: 使用模型 ============
# 查看词向量
word = "神经网络"
vector = model.wv[word]
print(f"'{word}' 的向量维度: {vector.shape}")
print(f"向量前10维: {vector[:10]}")
# 输出: (100,)   向量前10维: [-0.023, 0.045, -0.012, 0.078, -0.034, ...]

# ============ Step 5: 语义相似度计算 ============
# 计算两个词的余弦相似度
similarity = model.wv.similarity("神经网络", "深度学习")
print(f"'神经网络' 与 '深度学习' 的相似度: {similarity:.4f}")
# 输出: 0.8532

# ============ Step 6: 找最相似的词 ============
similar_words = model.wv.most_similar("神经网络", topn=5)
print(f"与'神经网络'最相似的5个词:")
for word, score in similar_words:
    print(f"  {word}: {score:.4f}")

# ============ Step 7: 验证语义算术(类比推理) ============
# Word2Vec最神奇的能力:向量运算能反映语义关系
# king - man + woman ≈ queen
try:
    result = model.wv.most_similar(positive=["神经网络", "深度学习"], negative=["机器学习"], topn=3)
    print(f"神经网络 - 机器学习 + 深度学习 = {result}")
except KeyError as e:
    print(f"词表中缺少: {e}")
⚠️ 中文Word2Vec的局限性:Word2Vec本身无法解决中文的分词歧义问题(如"武汉市长江大桥"可以有两种分词方式)。实际生产环境推荐使用BERT等预训练模型,它们通过子词(Subword)建模,避免了分词错误的影响。

三、技术演进:BERT带来的上下文感知Embedding

3.1 Word2Vec的致命缺陷:多义词问题

Word2Vec学习的是静态词向量——每个词只有一个固定的向量表示。这导致了一个根本问题:无法处理一词多义

# 一词多义的经典例子:"苹果"
# 句子1:"苹果公司发布了新款iPhone"
# 句子2:"每天吃一个苹果对健康有益"

# Word2Vec只能给"苹果"生成一个向量,无法区分两种语义:
"苹果" → [0.1, 0.9, 0.4, ...]  # 无论在哪种语境下,词向量都相同

# BERT通过上下文感知的方式解决此问题:
# 句子1中"苹果" → [0.1, 0.95, 0.02, ...]  # 科技/公司语义
# 句子2中"苹果" → [0.8, 0.15, 0.05, ...]   # 水果语义

# BERT的核心是双向Transformer编码器
# 每个词的最终表示 = 其本身 embedding + 位置 embedding + 上下文加权

3.2 BERT的Embedding机制详解

# BERT的Embedding由三部分相加组成(简化示意):
# final_embedding = token_embedding + position_embedding + segment_embedding

import torch
import torch.nn as nn
from transformers import BertModel, BertTokenizer

# 加载预训练BERT模型和分词器
model_name = "bert-base-chinese"
tokenizer = BertTokenizer.from_pretrained(model_name)
bert_model = BertModel.from_pretrained(model_name)

# 示例句子(包含多义词"苹果")
sentence = "苹果公司发布了新款iPhone手机"
tokens = tokenizer.tokenize(sentence)
print(f"分词结果: {tokens}")
# 输出: ['苹果', '公司', '发布', '了', '新款', 'i', '##Phone', '手机']

# 转换为IDs并获取BERT输出
input_ids = tokenizer.convert_tokens_to_ids(tokens)
input_tensor = torch.tensor([input_ids])

with torch.no_grad():
    outputs = bert_model(input_tensor)
    # last_hidden_state: [batch_size, seq_len, hidden_size]
    # 每一层的隐状态,包含上下文信息
    last_hidden = outputs.last_hidden_state
    print(f"隐状态维度: {last_hidden.shape}")  # [1, 8, 768]

# 取每个token的最终表示(即最后一层Transformer的输出)
token_embeddings = last_hidden[0]  # [seq_len, hidden_size]
print(f"每个token的向量维度: {token_embeddings.shape}")  # [8, 768]

# 取"苹果"这个token的向量
apple_idx = tokens.index("苹果")
apple_embedding = token_embeddings[apple_idx]
print(f"'苹果'的向量维度: {apple_embedding.shape}")  # [768]

3.3 BERT vs Word2Vec 核心对比

维度Word2VecBERT
向量类型静态(每个词一个固定向量)动态上下文感知(依句子而变)
训练目标语言模型(预测词/上下文)掩码语言模型(MLM)+ 下句预测(NSP)
模型架构浅层神经网络(2-3层)深层Transformer(12-24层)
向量维度通常50-300维768-1536维
训练数据无需标注的大规模语料无需标注的大规模语料
推理速度快(简单矩阵运算)慢(需要完整Transformer前向传播)
多义词处理❌ 无法区分✅ 根据上下文动态生成

四、向量索引算法:如何在10亿向量中快速检索

4.1 暴力检索的问题

当向量数据库中有1000万条文档时,每次检索需要计算1000万次余弦相似度或欧氏距离。即使每次计算只需要0.1毫秒,1000万次也需要1000秒——完全无法接受。

向量索引算法的核心目标是:用近似方法替代精确计算,将检索时间从O(N)降低到O(log N)或O(1)

4.2 HNSW(Hierarchical Navigable Small World)算法详解

HNSW是目前最流行的向量索引算法,被Qdrant、Milvus、Weaviate等主流向量数据库采用。

4.2.1 HNSW的核心思想

HNSW构建一个多层图结构

# HNSW的多层图结构(ASCII示意)
# Layer 2 (最稀疏):    A ------------------ B
#                          \             /
# Layer 1 (中等):       C ---- D ---- E ---- F
#                          \  |  \  /  \
# Layer 0 (最密集):  G -- H -- I -- J -- K -- L -- M

# 检索示例:找K的最近邻
# Step 1: 从最高层开始(Layer 2),随机选择入口点A
# Step 2: 计算A的邻居(B),如果B比A更接近目标,则移动到B
# Step 3: 重复Step 2直到无法继续优化(局部最优),下沉到Layer 1
# Step 4: 在Layer 1继续上述过程,下沉到Layer 0
# Step 5: 在Layer 0做最精细的搜索,返回最近邻

# 计算复杂度:
# 暴力检索: O(N) — N=1000万时需要1000万次计算
# HNSW: O(log N) — log(10000000) ≈ 23次计算即可

4.2.2 HNSW的关键参数

# Qdrant中HNSW的配置参数详解

# 1. m(每层最大连接数)
#    默认值: 16
#    值越大: 检索精度越高,内存占用越大,构建时间越长
#    建议值: 8-64,推荐16-32
#    公式估算: m >= (ln(N) / ln(score_threshold)),N是向量数量

# 2. ef_construction(构建时的动态列表大小)
#    默认值: 100
#    值越大: 索引精度越高,构建时间越长
#    建议值: 64-512,推荐128-256
#    影响:在插入向量时搜索最近邻候选集的大小

# 3. ef(检索时的动态列表大小)
#    默认值: 100
#    值越大: 检索精度越高,检索时间越长
#    建议值: 64-512,推荐设置为与ef_construction相近
#    注意:ef可以动态调整,不需要重建索引

# 完整的HNSW配置示例(Qdrant API):
curl -X PUT "http://localhost:6333/collections/my_collection" \
  -H "Content-Type: application/json" \
  -d '{
    "hnsw_config": {
      "m": 16,
      "ef_construct": 128,
      "full_scan_threshold": 10000,
      "on_disk": false
    },
    "params": {
      "hnsw_web_cache": true
    }
  }'

4.2.3 IVF+PQ(倒排索引+乘积量化)

另一种常用的索引策略是IVF(倒排索引)与PQ(乘积量化)的组合:

# IVF+PQ的工作原理:

# 1. IVF(倒排索引)
#    步骤:将向量空间预先聚类成K个簇(Cluster)
#    每个向量属于某个簇,用倒排列表存储
#    检索时:先确定查询向量属于哪个簇,只在相关簇内搜索

# 示例(Python伪代码):
from sklearn.cluster import KMeans
import numpy as np

# 假设有100万个128维向量
vectors = np.random.randn(1000000, 128).astype('float32')

# 用K-Means聚类成1024个簇
n_clusters = 1024
kmeans = KMeans(n_clusters=n_clusters, random_state=42)
kmeans.fit(vectors)

# 获取每个向量所属的簇
labels = kmeans.predict(vectors)

# 构建倒排索引:簇ID → 向量ID列表
inverted_index = {}
for vec_id, cluster_id in enumerate(labels):
    if cluster_id not in inverted_index:
        inverted_index[cluster_id] = []
    inverted_index[cluster_id].append(vec_id)

# 检索:只搜索查询向量最近的K个簇
query_vector = np.random.randn(128).astype('float32')
query_cluster = kmeans.predict([query_vector])[0]
# 在query_cluster及其相邻簇中搜索,而不是全量100万条

4.2.4 三种索引算法对比

算法适用场景优点缺点
HNSW需要高精度(recall>0.95)检索速度快、精度高、支持GPU加速内存占用大、构建慢
IVF+PQ超大规模数据(>1000万)内存效率高、支持增量更新精度相对较低、检索速度中等
IVF+HNSW超大规模+高精度兼具两者优点实现复杂、参数调优困难

五、相似度计算:代码实现与实战

5.1 余弦相似度(Cosine Similarity)

余弦相似度是向量检索中最常用的距离度量,它衡量两个向量方向的相似程度,取值范围[-1, 1]:

# 余弦相似度计算公式:
# Cosine(A, B) = (A · B) / (||A|| * ||B||)
#            = sum(A[i] * B[i]) / (sqrt(sum(A[i]^2)) * sqrt(sum(B[i]^2)))

import numpy as np

def cosine_similarity(vec_a: np.ndarray, vec_b: np.ndarray) -> float:
    """
    计算两个向量的余弦相似度
    
    Args:
        vec_a: 第一个向量,shape=(d,) 或 (d, 1)
        vec_b: 第二个向量,shape=(d,) 或 (d, 1)
    
    Returns:
        余弦相似度,范围[-1, 1]
    
    Example:
        >>> a = np.array([1.0, 0.0])
        >>> b = np.array([1.0, 0.0])
        >>> cosine_similarity(a, b)
        1.0
    """
    # 将向量展平为1D
    vec_a = np.asarray(vec_a).flatten()
    vec_b = np.asarray(vec_b).flatten()
    
    # 计算点积
    dot_product = np.dot(vec_a, vec_b)
    
    # 计算模长(L2范数)
    norm_a = np.linalg.norm(vec_a)
    norm_b = np.linalg.norm(vec_b)
    
    # 防止除零错误
    if norm_a == 0 or norm_b == 0:
        return 0.0
    
    return dot_product / (norm_a * norm_b)

# 测试:验证"国王-男人+女人≈女王"的语义关系
# 这是word2vec最著名的语义算术例子
king = np.array([0.8, 0.2, 0.1])
man = np.array([0.6, 0.7, 0.3])
woman = np.array([0.61, 0.71, 0.29])
queen = np.array([0.62, 0.70, 0.28])

# 计算 "国王" - "男人" + "女人" 的结果
result = king - man + woman
print(f"语义算术结果: {result}")  # [0.81, 0.21, 0.09]

# 计算与"女王"的相似度
similarity = cosine_similarity(result, queen)
print(f"与女王向量的相似度: {similarity:.4f}")  # ≈ 0.9997
# 结果接近1,说明语义算术非常准确

5.2 点积(Dot Product) vs 余弦相似度

# 在实际向量数据库中,点积(Dot Product)和余弦相似度都有使用
# 两者的区别在于是否对向量做了归一化

def dot_product(vec_a: np.ndarray, vec_b: np.ndarray) -> float:
    """计算点积(内积)"""
    return np.dot(np.asarray(vec_a).flatten(), np.asarray(vec_b).flatten())

# 两种度量方式的选择:
# 1. 如果向量已经归一化(||A|| = ||B|| = 1):点积 = 余弦相似度
# 2. 如果向量未归一化但长度重要:使用点积(长向量表示更强的信号)
# 3. 如果只关心方向相似度:使用余弦相似度

# 实际工程中的经验:
# - OpenAI text-embedding-3-small 输出的向量已归一化,用点积即可
# - BERT类的Embedding通常未归一化,用余弦相似度更准确

# 批量计算:快速找出最相似的K个向量
def find_top_k_similar(query_vec: np.ndarray, 
                       candidate_vectors: np.ndarray, 
                       k: int = 5,
                       metric: str = "cosine") -> list:
    """
    在候选向量中找到与查询向量最相似的K个
    
    Args:
        query_vec: 查询向量,shape=(d,)
        candidate_vectors: 候选向量矩阵,shape=(N, d)
        k: 返回前K个最相似的结果
        metric: "cosine" 或 "dotproduct"
    
    Returns:
        [(index, score), ...] 按相似度降序排列的前K个结果
    """
    # 将查询向量扩展为(N, d)以便批量计算
    query_vec = np.asarray(query_vec).flatten()
    candidates = np.asarray(candidate_vectors)
    N, d = candidates.shape
    
    if metric == "cosine":
        # 归一化
        query_norm = query_vec / np.linalg.norm(query_vec)
        cand_norms = candidates / np.linalg.norm(candidates, axis=1, keepdims=True)
        scores = np.dot(cand_norms, query_norm)
    else:
        scores = np.dot(candidates, query_vec)
    
    # 取最大的K个索引
    top_k_idx = np.argsort(scores)[-k:][::-1]
    return [(int(idx), float(scores[idx])) for idx in top_k_idx]

# 示例:模拟1000条文档的向量检索
np.random.seed(42)
doc_vectors = np.random.randn(1000, 768).astype('float32')  # 1000条768维向量
query = np.random.randn(768).astype('float32')

top5 = find_top_k_similar(query, doc_vectors, k=5, metric="cosine")
print("最相似的5个文档索引和相似度:", top5)

5.3 用NumPy实现完整的简易向量数据库

# 一个简化但完整的向量数据库实现(用于理解原理)
# 不依赖任何外部向量数据库,用NumPy + 暴力检索实现

class SimpleVectorDB:
    """
    简化的向量数据库,支持添加向量和检索最相似的结果
    适合理解原理,生产环境请使用Qdrant/Milvus等
    """
    
    def __init__(self, dimension: int, metric: str = "cosine"):
        self.dimension = dimension
        self.metric = metric
        self.vectors = []       # 存储向量
        self.metadata = []     # 存储元数据(如文档ID、文本内容)
    
    def add(self, vector: np.ndarray, metadata: dict):
        """
        添加向量到数据库
        
        Args:
            vector: 待添加的向量,shape=(dimension,)
            metadata: 与向量关联的元数据(如文档内容)
        """
        vec = np.asarray(vector).flatten()
        assert len(vec) == self.dimension, f"向量维度必须为{self.dimension}"
        
        self.vectors.append(vec)
        self.metadata.append(metadata)
    
    def search(self, query: np.ndarray, top_k: int = 5) -> list:
        """
        检索最相似的top_k个结果
        
        Args:
            query: 查询向量
            top_k: 返回前K个结果
        
        Returns:
            [(metadata, score), ...] 最相似的top_k个结果
        """
        query_vec = np.asarray(query).flatten()
        vectors = np.array(self.vectors)  # shape: (N, dimension)
        
        if len(vectors) == 0:
            return []
        
        # 批量计算相似度
        if self.metric == "cosine":
            # 归一化后计算点积 = 余弦相似度
            query_norm = query_vec / np.linalg.norm(query_vec)
            norms = vectors / np.linalg.norm(vectors, axis=1, keepdims=True)
            scores = np.dot(norms, query_norm)
        else:
            scores = np.dot(vectors, query_vec)
        
        # 取top_k
        top_k = min(top_k, len(scores))
        top_indices = np.argsort(scores)[-top_k:][::-1]
        
        return [(self.metadata[idx], float(scores[idx])) for idx in top_indices]
    
    def count(self) -> int:
        """返回数据库中的向量数量"""
        return len(self.vectors)

# 使用示例
db = SimpleVectorDB(dimension=5, metric="cosine")

# 添加向量及其元数据
db.add(np.array([0.8, 0.2, 0.1, 0.3, 0.5]), {"text": "这是一只猫", "doc_id": "doc_001"})
db.add(np.array([0.79, 0.21, 0.09, 0.31, 0.49]), {"text": "这是一只家猫", "doc_id": "doc_002"})
db.add(np.array([0.1, 0.9, 0.4, 0.2, 0.1]), {"text": "苹果是一种水果", "doc_id": "doc_003"})
db.add(np.array([0.3, 0.7, 0.2, 0.8, 0.1]), {"text": "苹果手机很贵", "doc_id": "doc_004"})

# 检索
query = np.array([0.78, 0.22, 0.10, 0.30, 0.50])
results = db.search(query, top_k=2)

print(f"数据库中共有 {db.count()} 条向量")
print("检索结果:")
for metadata, score in results:
    print(f"  相似度 {score:.4f}: {metadata['text']} ({metadata['doc_id']})")

六、实战:Qdrant Python客户端完整示例

# 使用Qdrant Python客户端进行向量检索
# 安装:pip install qdrant-client

from qdrant_client import QdrantClient
from qdrant_client.models import Distance, VectorParams, PointStruct
from qdrant_client.http.exceptions import UnexpectedResponse
import numpy as np

# ============ 连接Qdrant ============
client = QdrantClient(
    host="localhost",  # Qdrant服务地址
    port=6333          # REST API端口
)

# ============ 创建Collection ============
collection_name = "tech_articles"

try:
    # 如果已存在则先删除
    client.delete_collection(collection_name=collection_name)
    print(f"已删除旧Collection: {collection_name}")
except Exception:
    pass

# 创建新Collection(向量维度=768,对应text-embedding-3-small)
client.create_collection(
    collection_name=collection_name,
    vectors_config=VectorParams(
        size=768,              # 向量维度(必须与Embedding模型输出维度一致)
        distance=Distance.COSINE  # 使用余弦相似度
    )
)
print(f"创建Collection成功: {collection_name}")

# ============ 插入向量数据 ============
# 构造示例数据(实际使用时替换为真实文档的Embedding)
articles = [
    {"id": "1", "text": "RAG系统结合了检索和生成技术,提高了问答准确性", "embedding": np.random.randn(768).astype('float32')},
    {"id": "2", "text": "向量数据库使用HNSW算法实现高速近似检索", "embedding": np.random.randn(768).astype('float32')},
    {"id": "3", "text": "Ollama是一个本地运行LLM的工具,支持多种模型", "embedding": np.random.randn(768).astype('float32')},
    {"id": "4", "text": "Dify是一个开源的LLM应用开发平台", "embedding": np.random.randn(768).astype('float32')},
]

# 归一化Embedding(Qdrant用余弦相似度需要先归一化)
for article in articles:
    vec = article["embedding"]
    article["embedding"] = (vec / np.linalg.norm(vec)).tolist()

# 批量插入
points = [
    PointStruct(
        id=article["id"],
        vector=article["embedding"],
        payload={"text": article["text"]}
    )
    for article in articles
]

client.upsert(
    collection_name=collection_name,
    points=points
)
print(f"插入 {len(points)} 条向量成功")

# ============ 检索 ============
query_text = "如何用本地LLM构建问答系统"
# 实际使用时:用Embedding模型将query_text转换为向量
query_embedding = np.random.randn(768).astype('float32')
query_embedding = (query_embedding / np.linalg.norm(query_embedding)).tolist()

# 执行检索
search_results = client.search(
    collection_name=collection_name,
    query_vector=query_embedding,
    limit=3  # 返回前3个最相似的结果
)

print("\n检索结果:")
for result in search_results:
    print(f"  ID: {result.id}")
    print(f"  相似度: {result.score:.4f}")
    print(f"  内容: {result.payload['text']}")
    print()

# ============ 范围检索(相似度大于阈值) ============
# 只返回相似度 > 0.8 的结果
range_results = client.search(
    collection_name=collection_name,
    query_vector=query_embedding,
    query_filter=None,  # 可以添加过滤条件
    score_threshold=0.8,  # 最小相似度阈值
    limit=10
)
print(f"相似度>0.8的结果数: {len(range_results)}")

结语

从One-Hot到Word2Vec再到BERT,Embedding技术经历了从稀疏到稠密、从静态到动态的演进。理解这些技术背后的数学原理和工程权衡,是构建高质量RAG系统的基础。

核心要点回顾: