AI应用开发二:Embedding与向量数据库

Embedding 可以从一个很普通的需求说起:给用户推荐相似酒店,或者在 FAQ 里找到和问题最接近的答案。机器不能直接理解一段文字,第一步就是把文字变成可计算的向量。

向量有了以后,就可以计算相似度,也可以把它们存起来做检索。N-Gram、TF-IDF、Word2Vec、Embedding 模型、FAISS 和向量数据库,对应的正是"向量怎么来、怎么算、怎么存、怎么查"这几个问题。

如果第一次接触这些概念,可以先用一个粗略类比记住它们的关系:Embedding 像是给文本、图片、商品分配坐标;相似度计算是在看两个坐标靠不靠近;向量数据库负责把这些坐标和原始数据一起管起来,方便后面快速查找。

从酒店推荐开始理解 Embedding

假设现在要做一个内容推荐系统:用户选中一家酒店,系统需要推荐描述相似的其他酒店。这个任务的核心就是一句话:把酒店描述变成可以计算的东西,然后找相似。

这里使用的是西雅图酒店数据集,字段主要有三个:nameaddressdescdesc 是酒店的文本描述,也是后面做文本特征和相似度计算的核心字段。

数据集地址:

github.com/susanli2016...

最直接的做法是:先把每家酒店的描述转换成特征向量,再计算目标酒店和其他酒店之间的余弦相似度,最后取相似度最高的 Top 10。

相似度:为什么常用余弦相似度

余弦相似度衡量的是两个向量方向是否接近,而不是单纯看向量长度。对文本来说,这一点很重要。两段文本长度可能不同,但只要语义方向接近,就应该被认为相似。

它的取值范围是 [-1, 1]

  • 越接近 1,说明两个向量越相似
  • 等于 0,说明两个向量基本正交,相关性弱
  • 越接近 -1,说明两个向量方向越相反

公式如下:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> a ⋅ b = ∣ a ∣ ∣ b ∣ cos ⁡ θ a \cdot b = |a| |b| \cos \theta </math>a⋅b=∣a∣∣b∣cosθ
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> s i m i l a r i t y = cos ⁡ ( θ ) = A ⋅ B ∣ A ∣ ∣ B ∣ = ∑ i = 1 n A i B i ∑ i = 1 n A i 2 ∑ i = 1 n B i 2 similarity = \cos(\theta) = \frac{A \cdot B}{|A| |B|} = \frac{\sum_{i=1}^{n} A_i B_i}{\sqrt{\sum_{i=1}^{n} A_i^2} \sqrt{\sum_{i=1}^{n} B_i^2}} </math>similarity=cos(θ)=∣A∣∣B∣A⋅B=∑i=1nAi2 ∑i=1nBi2 ∑i=1nAiBi

用一个很小的中文例子来直观理解。假设有两句话:

句子 A:这个程序代码太乱,那个代码规范

句子 B:这个程序代码不规范,那个更规范

先做分词:

  • 句子 A:这个 / 程序 / 代码 / 太乱,那个 / 代码 / 规范
  • 句子 B:这个 / 程序 / 代码 / 不 / 规范,那个 / 更 / 规范

把两句话里出现过的词统一列出来:

text 复制代码
这个,程序,代码,太乱,那个,规范,不,更

然后统计词频,就可以得到两个向量:

  • 句子 A:(1, 1, 2, 1, 1, 1, 0, 0)
  • 句子 B:(1, 1, 1, 0, 1, 2, 1, 1)

代入余弦相似度公式:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> cos ⁡ ( θ ) = 1 × 1 + 1 × 1 + 2 × 1 + 1 × 0 + 1 × 1 + 1 × 2 + 0 × 1 + 0 × 1 1 2 + 1 2 + 2 2 + 1 2 + 1 2 + 1 2 + 0 2 + 0 2 × 1 2 + 1 2 + 1 2 + 0 2 + 1 2 + 2 2 + 1 2 + 1 2 = 0.738 \cos(\theta) = \frac{1\times1 + 1\times1 + 2\times1 + 1\times0 + 1\times1 + 1\times2 + 0\times1 + 0\times1} {\sqrt{1^2+1^2+2^2+1^2+1^2+1^2+0^2+0^2} \times \sqrt{1^2+1^2+1^2+0^2+1^2+2^2+1^2+1^2}} = 0.738 </math>cos(θ)=12+12+22+12+12+12+02+02 ×12+12+12+02+12+22+12+12 1×1+1×1+2×1+1×0+1×1+1×2+0×1+0×1=0.738

这个结果比较接近 1,说明两句话确实比较相似。

这里不用一开始就纠结公式里的每一项。更重要的是理解流程:先把两段文本放到同一套词表下,变成长度相同的向量,再比较两个向量的方向。词数多不一定更相似,方向接近才更相似。

用 N-Gram 和 TF-IDF 做传统文本特征

在没有深度学习 Embedding 之前,文本特征通常会从词频、N-Gram 和 TF-IDF 入手。

N-Gram 的意思是把连续的 N 个词当作一个特征。比如 A B C D E 这段文本,Bi-Gram 就是:

text 复制代码
A B, B C, C D, D E

如果只看单个词,有些语义会丢失。比如 pikeplace 分开看意义不强,但 pike place 放在一起,就更像一个地点特征。这就是 N-Gram 的价值。

在酒店描述里提取 Top 20 个 Unigram,可以用 CountVectorizer

python 复制代码
plt.rcParams['font.sans-serif'] = ['SimHei']

df = pd.read_csv('Seattle_Hotels.csv', encoding="latin-1")

def get_top_n_words(corpus, n=1, k=None):
    vec = CountVectorizer(ngram_range=(n, n), stop_words='english').fit(corpus)
    bag_of_words = vec.transform(corpus)
    sum_words = bag_of_words.sum(axis=0)
    words_freq = [(word, sum_words[0, idx]) for word, idx in vec.vocabulary_.items()]
    words_freq = sorted(words_freq, key=lambda x: x[1], reverse=True)
    return words_freq[:k]

common_words = get_top_n_words(df['desc'], 1, 20)
df1 = pd.DataFrame(common_words, columns=['desc', 'count'])

df1.groupby('desc').sum()['count'].sort_values().plot(
    kind='barh',
    title='去掉停用词后,酒店描述中的Top20单词'
)
plt.show()

从结果看,seattlehotelcenterdowntownfreestayroombreakfast 这类词出现频率很高。

同样的函数也可以提取 Bi-Gram 和 Tri-Gram:

python 复制代码
# Bi-Gram
common_words = get_top_n_words(df['desc'], 2, 20)

# Tri-Gram
common_words = get_top_n_words(df['desc'], 3, 20)

这时结果就更接近酒店场景本身了,比如 pike placedowntown seattlespace needlefree wi fipike place marketseattle tacoma international 等。

词频本身还不够,因为有些词在所有文档里都很常见,区分度并不高。TF-IDF 正好处理这个问题。

TF 表示词频,一个词在当前文档里出现越多,TF 越高:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> T F = 单词次数 文档中总单词数 TF = \frac{单词次数}{文档中总单词数} </math>TF=文档中总单词数单词次数

IDF 表示逆文档频率,一个词出现在越少的文档里,区分度越高,IDF 越大:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> I D F = log ⁡ 文档总数 单词出现的文档数 + 1 IDF = \log \frac{文档总数}{单词出现的文档数 + 1} </math>IDF=log单词出现的文档数+1文档总数

在酒店推荐里,可以把 1-Gram、2-Gram、3-Gram 一起放进 TF-IDF:

python 复制代码
def clean_text(text):
    text = text.lower()
    ...
    return text

df['desc_clean'] = df['desc'].apply(clean_text)

tf = TfidfVectorizer(
    analyzer='word',
    ngram_range=(1, 3),
    min_df=0,
    stop_words='english'
)

tfidf_matrix = tf.fit_transform(df['desc_clean'])
cosine_similarities = linear_kernel(tfidf_matrix, tfidf_matrix)

这里会得到一个酒店之间的相似度矩阵。之后根据指定酒店名称,取相似度最高的 Top 10 即可:

python 复制代码
def recommendations(name, cosine_similarities=cosine_similarities):
    recommended_hotels = []

    idx = indices[indices == name].index[0]
    score_series = pd.Series(cosine_similarities[idx]).sort_values(ascending=False)
    top_10_indexes = list(score_series.iloc[1:11].index)

    for i in top_10_indexes:
        recommended_hotels.append(list(df.index)[i])

    return recommended_hotels

print(recommendations('Hilton Seattle Airport & Conference Center'))
print(recommendations('The Bacon Mansion Bed and Breakfast'))

清洗文本、提取 N-Gram 和 TF-IDF、计算余弦相似度、返回 Top-K,这套流程可以跑通一个基础的内容推荐。但它也有明显问题:特征矩阵很稀疏,维度高,而且只能表达词面共现,很难处理更深一层的语义。比如"退票"和"退款"在词面上不同,但语义上很接近,传统词频方法就不一定能处理好。

所以这部分更适合理解"文本如何变成向量"的基本思路。真正做 RAG、语义搜索或智能问答时,通常不会手写这么多特征,而是直接用训练好的 Embedding 模型生成向量。

因此需要一种更稠密、更能表达语义的向量表示,这就是 Word Embedding 要解决的问题。

Word Embedding:把词放进语义空间

Embedding 可以理解为一种降维后的表示方式。原来一个离散对象可能只能用 one-hot 表示,维度高、稀疏、彼此之间没有语义关系;Embedding 会把它映射到一个固定维度的稠密向量里。

这些维度不一定都能被人明确命名。它们更像是模型从大量数据里学出来的隐含特征。单独看某一维可能不好解释,但整体距离是有意义的:语义接近的内容会靠得更近,语义差异大的内容会离得更远。

简单说,任何对象只要能被表示成向量,就可以计算相似度,也就可以被检索、推荐、聚类和排序。

一个经典例子是 King。如果用 GloVe 这类方法对它做向量化,King 会被表示成一组数字。每个维度都可以看成模型学到的某种隐含特征,权重有正有负。

有了这样的向量以后,词之间不仅能比较相似度,还能做一些语义方向上的运算。最常见的例子就是:

text 复制代码
king - man + woman ≈ queen

这不是魔法,而是因为模型在训练过程中把"性别""王室身份"等语义关系编码进了向量空间。

Word2Vec 的直觉

Word2Vec 的核心想法是:一个词的意义,可以从它的上下文里学出来。

它会把词从原来的 one-hot 空间映射到一个新的稠密向量空间里。语义相近的词,在这个空间里距离也更近。

从结构上看,Word2Vec 本质上是在学习输入层到隐藏层之间的权重矩阵:

  • 输入层是 one-hot 编码
  • 隐藏层大小就是 Embedding Size
  • 输入层到隐藏层的权重矩阵 W 大小是 [vocab_size, hidden_size]
  • 输出层大小是 [vocab_size],每个位置表示输出某个词的概率

当 one-hot 向量和矩阵相乘时,其实就是从矩阵里取出某一行。这一行就是当前词的向量表示。所以从工程角度看,Word2Vec 也可以理解成一个查找表。

Word2Vec 主要有两种训练方式:

  • Skip-Gram:给定中心词,预测上下文
  • CBOW:给定上下文,预测中心词

用 Gensim 跑一个 Word2Vec 小实验

如果只是学习和实验,Gensim 是一个很方便的工具。它支持 TF-IDF、LDA、LSA、Word2Vec 等主题模型和向量模型。

安装方式:

bash 复制代码
pip install gensim

最基本的调用方式如下:

python 复制代码
from gensim.models import word2vec

model = word2vec.Word2Vec(sentences)

几个常用参数:

  • window:当前词和上下文词之间的最大距离
  • min_count:词出现多少次才参与训练
  • size:词向量维度,老版本 Gensim 用这个参数
  • workers:训练线程数

这个例子使用《西游记》做训练,目标是看看小说人物之间的相似度,比如"孙悟空"和"猪八戒"、"孙悟空"和"孙行者"。

第一步是分词,把原始文本切成训练语料:

python 复制代码
def segment_lines(file_list, segment_out_dir, stopwords=[]):
    for i, file in enumerate(file_list):
        segment_out_name = os.path.join(segment_out_dir, 'segment_{}.txt'.format(i))
        with open(file, 'rb') as f:
            document = f.read()
            document_cut = jieba.cut(document)
            sentence_segment = []

            for word in document_cut:
                if word not in stopwords:
                    sentence_segment.append(word)

            result = ' '.join(sentence_segment)
            result = result.encode('utf-8')

            with open(segment_out_name, 'wb') as f2:
                f2.write(result)

file_list = files_processing.get_files_list('./source', postfix='*.txt')
segment_lines(file_list, './segment')

然后训练 Word2Vec,并计算词之间的相似度:

python 复制代码
from gensim.models import word2vec
import multiprocessing

sentences = word2vec.PathLineSentences('./segment')

model = word2vec.Word2Vec(sentences, size=100, window=3, min_count=1)

print(model.wv.similarity('孙悟空', '猪八戒'))
print(model.wv.similarity('孙悟空', '孙行者'))

model2 = word2vec.Word2Vec(
    sentences,
    size=128,
    window=5,
    min_count=5,
    workers=multiprocessing.cpu_count()
)

model2.save('./models/word2Vec.model')

print(model2.wv.similarity('孙悟空', '猪八戒'))
print(model2.wv.similarity('孙悟空', '孙行者'))
print(model2.wv.most_similar(positive=['孙悟空', '唐僧'], negative=['孙行者']))

Word2Vec 不只适用于自然语言。只要能把某种行为序列看成"句子",也可以训练类似的向量。

比如:

  • 大 V 推荐里,可以把大 V 看作词,把用户关注大 V 的顺序看作句子
  • 商品推荐里,可以把商品看作词,把用户浏览、收藏、购买商品的顺序看作句子

这也是 Embedding 在推荐系统里很常见的原因。

如果继续练习,可以用《三国演义》做同样的事:分析和"曹操"最相近的词,或者尝试类似 曹操 + 刘备 - 张飞 这样的向量运算。

现代 Embedding 模型怎么选

Word2Vec 说明了 Embedding 的基本思想。做应用时,通常会直接使用训练好的 Embedding 模型,把文本、文档甚至图片转成向量。

一个 Embedding 模型的核心价值,是把离散数据转换成低维、稠密、可比较的向量。向量空间里的距离,就可以反映语义上的相似度。

选模型时,MTEB 是一个常见参考。MTEB 全称是 Massive Text Embedding Benchmark,覆盖分类、聚类、检索、重排序等多类任务。它不能替代业务评测,但可以初步反映不同模型的大致能力。

榜单地址:

huggingface.co/spaces/mteb...

榜单示例如下:

排名 模型 Zero-shot Embedding 维度 Max Tokens Mean
1 gemini-embedding-001 99% 3072 2048 68.37
2 Qwen3-Embedding-8B 99% 4096 32768 70.58
3 Qwen3-Embedding-4B 99% 2560 32768 69.45
4 Qwen3-Embedding-0.6B 99% 1024 32768 64.34
5 Linq-Embed-Mistral 99% 4096 32768 61.47
6 gte-Qwen2-7B-instruct N/A 3584 32768 62.51
7 multilingual-e5-large-instruct 99% 1024 514 63.22
8 SFR-Embedding-Mistral 96% 4096 32768 60.90
9 text-multilingual-embedding-002 99% 768 2048 62.16
10 GritLM-7B 99% 4096 32768 60.92

MTEB 的任务类型大致包括:

  • 检索(Retrieval):从文档库里找出和 Query 最相关的文档
  • 语义文本相似度(STS):判断两句话的语义相似程度
  • 重排序(Reranking):对初步召回的结果重新排序
  • 分类(Classification):把文本划分到预定义类别
  • 聚类(Clustering):把语义相近的文本聚在一起
  • 对分类(Pair Classification):判断一对文本是否具有某种关系
  • 双语挖掘(Bitext Mining):找出不同语言中互为翻译的句子
  • 摘要(Summarization):比较生成摘要和参考摘要的语义相似度

选模型不能只看榜单,还要看几个更贴近业务的问题:

  • 任务是检索、分类、聚类,还是 RAG 问答?
  • 文本主要是中文、英文,还是多语言混合?
  • 业务更看重召回率、准确率、NDCG,还是延迟和成本?
  • 模型维度和上下文长度是否适合当前数据?
  • 有没有自己的小规模黄金测试集?

向量维度不是越大越好

向量维度会影响表达能力,也会影响计算和存储成本。

高维向量,比如 1024、4096 维,通常能编码更丰富的语义信息,适合复杂检索和细粒度分类。但维度越高,索引体积越大,检索成本也更高。

低维向量,比如 256、512 维,速度更快、存储更省,更适合资源有限或者实时性要求高的场景。

比如从 768 维升到 1024 维,检索指标只提升不到 1%,但内存多占 35%,这种情况通常不值得。反过来,如果压到 768 维后指标下降超过 5%,那就说明信息损失比较明显,继续使用更高维度更合理。

Jina Embedding 和"俄罗斯套娃"

Jina AI 的 jina-embeddings-v4 是一个多模态、多语言检索模型,适合处理复杂文档检索,尤其是带图表、表格、插图的文档。

模型地址:

modelscope.cn/models/jina...

它一个很实用的点是支持灵活的嵌入大小。默认密集向量是 2048 维,但可以截断到 128、256、512、1024 等维度,同时尽量保持可用的语义质量。

特性 Jina-Embedding-V4
基础模型 Qwen2.5-VL-3B-Instruct
支持的任务 检索、文本匹配、代码
模型数据类型 BFloat 16
最大序列长度 32768
单向量维度 2048
多向量维度 128
套娃维度 128、256、512、1024、2048
池化策略 平均池化
注意力机制 FlashAttention2

这里的"套娃"指的是 Matryoshka Representation Learning,也就是 MRL。它的直觉是:模型先生成一个完整的高维向量,比如 2048 维;这个向量的前 128 维、前 256 维、前 512 维,也都可以作为相对完整的短向量使用。

这样做的好处是可以按场景动态调整维度。

比如社交媒体情感分析,文本短、实时性强、资源有限,用 128 维可能就够了。再比如投资分析报告,文本长、术语多、细节重要,就更适合用 2048 维。

单语言和多语言模型怎么选

如果业务几乎全是中文,比如电商客服 FAQ,那么单语言中文模型通常更合适。它对中文语义、表达习惯、业务词的理解会更深入,比如"七天无理由退货""订单何时送达""如何办理退换货"。

如果业务天然是多语言,比如国际酒店评论分析,那就需要多语言模型。用户可能用英文查询 Loud music at night,系统需要同时找出日文的"夜に音楽がうるさい"和中文的"晚上音乐很吵"。这要求不同语言的表达能映射到同一个语义空间。

text 复制代码
"clean room"、"部屋が綺麗"、"干净的房间"

这些表达的向量足够接近,跨语言检索和聚类才有意义。

Embedding 模型选型不能只看"榜单排名最高"。比较稳的做法是:先明确业务任务和指标,再构建一套小规模黄金测试集,然后挑几款候选模型做 Benchmark。最终选型要同时考虑效果、延迟、成本和部署复杂度。

向量数据库:给大模型一个可检索的外部记忆

Embedding 解决的是"怎么把内容变成向量",向量数据库解决的是"怎么把这些向量存好、查快、管起来"。

这里容易误会:向量数据库不是用来替代 MySQL、PostgreSQL 这类传统数据库的。它更像一个专门负责"按语义相似度找东西"的检索层。用户、订单、权限、状态这些业务数据,通常仍然要放在传统数据库里管理。

在 RAG、知识库问答、推荐系统里,大模型不可能把所有私有知识都放进上下文窗口。更常见的做法是:把文档提前切分、向量化、存入向量数据库;用户提问时,先检索相关内容,再把检索结果交给大模型生成回答。

向量数据库主要有三点价值:

  • 给大模型提供长期记忆,弥补上下文窗口长度和知识更新延迟的问题
  • 支撑私有知识库问答和语义搜索,把内部文档、产品信息转成可检索向量
  • 支撑推荐、以图搜图等应用,通过向量相似度找到相近对象

常见向量数据库可以这样看:

  • FAISS:更像一个高性能向量检索算法库,不是完整数据库,适合研究和需要自己搭服务层的场景
  • Elasticsearch:传统搜索能力强,适合关键词搜索和向量搜索结合的混合搜索场景
  • Milvus:开源向量数据库,功能完整,适合大规模向量数据和企业级部署
  • Pinecone:托管云服务,部署简单,适合快速上线和少运维团队

向量数据库和传统数据库的差异可以简单对比一下:

对比项 传统数据库 向量数据库
数据类型 存储结构化数据,如表格、行、列 存储高维向量数据,适合非结构化数据
查询方式 依赖精确匹配,如 =<> 基于相似度或距离度量,如欧几里得距离、余弦相似度
应用场景 适合事务记录和结构化信息管理 适合语义搜索、内容推荐等需要相似性计算的场景

数据怎么导入向量数据库

向量数据库不是直接吃原始文档的。导入数据通常是这个流程:

  1. 清洗原始数据,比如文档、网页、图片、音视频
  2. 使用 Embedding 模型把数据转换成向量
  3. 把向量、唯一 ID、元数据一起写入存储系统
  4. 查询时把 Query 也转成向量,再做相似度检索

如果是文本,可以使用 bge-mQwen3-EmbeddingJina-Embeddingtext-embedding-v4 等模型;如果是图片,可以使用 CLIPResNet 等模型。

下面是一个用百炼兼容 OpenAI SDK 计算 Embedding 的例子:

python 复制代码
import os
from openai import OpenAI

client = OpenAI(
    api_key=os.getenv("DASHSCOPE_API_KEY"),
    base_url="https://dashscope.aliyuncs.com/compatible-mode/v1"
)

completion = client.embeddings.create(
    model="text-embedding-v4",
    input='我想知道迪士尼的退票政策',
    dimensions=1024,
    encoding_format="float"
)

print(completion.model_dump_json())

返回结果里会包含一个 embedding 数组:

json 复制代码
{
  "data": [
    {
      "embedding": [
        0.00954423751682043,
        -0.11166470497846603,
        0.0002610872033983469,
        -0.04448245093226433,
        0.018730206415057182,
        "...省略后续向量..."
      ],
      "index": 0,
      "object": "embedding"
    }
  ],
  "model": "text-embedding-v4",
  "object": "list",
  "usage": {
    "prompt_tokens": 23,
    "total_tokens": 23
  },
  "id": "84faa227-2672-94df-87d3-a1648dc7aea7"
}

还有一点要单独记下来:向量本身不够用,还必须保存元数据。 向量数据库里通常不是只存向量,而是会同时存三类东西:

类型 示例 作用
原文内容 "迪士尼门票一经售出......" 最后给大模型参考
向量 [0.012, -0.034, ...] 用来做相似度检索
元数据 source=官网, page=3, title=退票政策 用来过滤、溯源、引用

比如一条向量背后对应哪篇文档、哪个章节、哪个 URL、哪个商品、哪个作者,这些都不是向量能直接提供的信息。实际系统里,检索返回的是向量 ID,还要根据 ID 找回原始文本和元数据。

小规模练习时,用一个 Python 列表保存元数据已经够了。到了真实项目里,向量 ID、文档 ID、业务 ID 之间的关系要提前设计清楚。否则即使找到了最相似的向量,系统也不知道该把哪段原文、哪个商品或哪篇文档返回给用户。

用 FAISS 存 Embedding 和元数据

FAISS 本身只负责向量索引和检索,不负责存元数据。所以要把 FAISS 和一个外部元数据存储搭配起来。最简单的方式是维护一个 metadata_store,并通过向量 ID 关联。

下面准备 4 条迪士尼相关文档,每条文档都有 textmetadata

python 复制代码
documents = [
    {
        "id": "doc1",
        "text": "迪士尼乐园的门票一经售出,原则上不予退换。但在特殊情况下,如恶劣天气导致园区关闭,可在官方指引下进行改期或退款。",
        "metadata": {
            "source": "official_faq_v1.pdf",
            "category": "退票政策",
            "author": "Admin"
        }
    },
    {
        "id": "doc2",
        "text": "购买"奇妙年卡"的用户,可以享受一年内多次入园的特权,并且在餐饮和购物时有折扣。",
        "metadata": {
            "source": "annual_pass_rules.docx",
            "category": "会员权益",
            "author": "MarketingDept"
        }
    },
    {
        "id": "doc3",
        "text": "对于在线购买的迪士尼门票,如果需要退票,必须在票面日期前48小时通过原购买渠道提交申请,并可能收取手续费。",
        "metadata": {
            "source": "online_policy.html",
            "category": "退票政策",
            "author": "E-commerceTeam"
        }
    },
    {
        "id": "doc4",
        "text": "园区内餐厅支持提前预约,热门餐厅建议至少提前一周预约。",
        "metadata": {
            "source": "restaurant_guide.pdf",
            "category": "餐饮服务",
            "author": "OpsTeam"
        }
    }
]

然后逐条生成向量,同时把元数据和向量 ID 存下来:

python 复制代码
metadata_store = []
vectors_list = []
vector_ids = []

print("正在为文档生成向量...")

for i, doc in enumerate(documents):
    try:
        completion = client.embeddings.create(
            model="text-embedding-v4",
            input=doc["text"],
            dimensions=1024,
            encoding_format="float"
        )

        vector = completion.data[0].embedding
        vectors_list.append(vector)

        metadata_store.append(doc)
        vector_ids.append(i)

        print(f" - 已处理文档 {i + 1}/{len(documents)}")

    except Exception as e:
        print(f"处理文档 '{doc['id']}' 时出错: {e}")
        continue

vectors_np = np.array(vectors_list).astype('float32')
vector_ids_np = np.array(vector_ids)

接下来创建 FAISS 索引。这里用 IndexIDMap 包一层,是为了给每个向量指定自定义 ID。这个 ID 就是向量和元数据之间的桥。

python 复制代码
dimension = 1024
k = 3

index_flat_l2 = faiss.IndexFlatL2(dimension)
index = faiss.IndexIDMap(index_flat_l2)

index.add_with_ids(vectors_np, vector_ids_np)

print(f"\nFAISS 索引已成功创建,共包含 {index.ntotal} 个向量。")

查询时,把用户问题也转成向量,再到 FAISS 里搜索最相似的向量:

python 复制代码
query_text = "我想了解一下迪士尼门票的退款流程"
print(f"\n正在为查询文本生成向量: '{query_text}'")

try:
    query_completion = client.embeddings.create(
        model="text-embedding-v4",
        input=query_text,
        dimensions=1024,
        encoding_format="float"
    )

    query_vector = np.array(
        [query_completion.data[0].embedding]
    ).astype('float32')

    distances, retrieved_ids = index.search(query_vector, k)

except Exception as e:
    print(f"执行搜索时发生错误: {e}")

最后根据返回的 ID,从 metadata_store 里取回原始文本和元数据:

python 复制代码
print("\n--- 搜索结果 ---")

for i in range(k):
    doc_id = retrieved_ids[0][i]

    if doc_id == -1:
        print(f"\n排名 {i + 1}: 未找到更多结果。")
        continue

    retrieved_doc = metadata_store[doc_id]

    print(f"\n--- 排名 {i + 1} (相似度得分/距离: {distances[0][i]:.4f}) ---")
    print(f"ID: {doc_id}")
    print(f"原始文本: {retrieved_doc['text']}")
    print(f"元数据: {retrieved_doc['metadata']}")

示例结果里,和"退款流程"最相似的是在线退票政策,其次是官方 FAQ 里的退换说明:

text 复制代码
--- 排名 1 (相似度得分/距离: 0.3222) ---
ID: 2
原始文本: 对于在线购买的迪士尼门票,如果需要退票,必须在票面日期前48小时通过原购买渠道提交申请,并可能收取手续费。
元数据: {'source': 'online_policy.html', 'category': '退票政策', 'author': 'E-commerceTeam'}

--- 排名 2 (相似度得分/距离: 0.3312) ---
ID: 0
原始文本: 迪士尼乐园的门票一经售出,原则上不予退换。但在特殊情况下,如恶劣天气导致园区关闭,可在官方指引下进行改期或退款。
元数据: {'source': 'official_faq_v1.pdf', 'category': '退票政策', 'author': 'Admin'}

--- 排名 3 (相似度得分/距离: 1.0135) ---
ID: 1
原始文本: 购买"奇妙年卡"的用户,可以享受一年内多次入园的特权,并且在餐饮和购物时有折扣。
元数据: {'source': 'annual_pass_rules.docx', 'category': '会员权益', 'author': 'MarketingDept'}

到这里,完整检索链路已经跑通:准备文档,生成向量,建立 FAISS 索引,查询时生成 Query 向量,FAISS 返回最相似的向量 ID,再通过 ID 找回原始文本和元数据。

在真实生产环境里,metadata_store 不会一直用 Python 列表。更常见的选择是:

  • Redis:适合通过 ID 快速查询,性能高
  • PostgreSQL:适合结构化元数据和复杂查询
  • MongoDB:适合存 JSON 结构的元数据

FAISS 继续专注高速向量检索,元数据交给更适合的数据库,这样架构会更清晰。

常见向量数据库怎么选

几个常见方案的对比如下:

数据库 核心特点 性能表现 适用场景
FAISS 核心算法库,非数据库。由 Meta AI 开发,提供最广泛、最前沿的 ANN 索引算法,支持 CPU 和 GPU 纯粹的性能标杆。在内存中进行裸向量检索时速度极快,尤其是 GPU 版本,是衡量其他数据库性能的基准 算法研究者;需要将向量检索能力深度集成到现有系统中的团队;可以自行构建服务层,如 API、元数据管理
Milvus 开源领导者,功能全面。云原生架构,高度可扩展。支持多种 ANN 索引和丰富的调优参数 在多个公开 ANN 基准测试中,尤其是在大规模数据集上表现优异,吞吐量和延迟控制得很好 需要处理海量数据、对性能和扩展性有高要求的企业级应用。适合有一定运维能力的团队进行私有化部署
Pinecone 全托管的商业先驱。主打 Serverless 和易用性,API 设计简洁。性能稳定,专注于提供极致的低延迟检索 性能非常出色,尤其在低延迟方面有优势。由于是闭源托管服务,其内部优化细节不透明,但用户体验顺滑 追求快速上线、希望将运维负担降至最低的团队。适合初创公司和需要快速验证 AI 应用可行性的场景
Weaviate 开源,内置数据向量化模块。能够连接各种 Embedding 模型,实现数据自动向量化,简化开发流程 性能良好,易用性强。自动向量化功能在开发效率上是一大亮点 希望简化 ETL 流程,快速构建从数据到向量再到检索的全链路应用的开发者
Qdrant 开源,以性能和内存安全著称,使用 Rust 开发。过滤查询能力强大且高效 性能强劲,尤其在过滤条件复杂的混合查询场景下表现突出。内存使用效率高 对检索性能和资源利用率有极致要求的场景。金融、电商等需要复杂过滤规则的应用
Elasticsearch 通用搜索巨头,扩展向量检索能力。将向量检索 KNN 作为功能之一,可以无缝结合全文检索、聚合分析能力 作为通用数据库,其向量检索性能通常弱于专门的向量数据库。但在混合搜索(关键词 + 向量)场景下表现优异 业务需求是"以文本搜索为主,向量搜索为辅",希望在一个统一平台中解决所有搜索问题,而非追求极致向量检索性能

如果只是学习和做算法实验,FAISS 很合适。如果要做企业级向量检索服务,可以重点看 Milvus、Qdrant、Weaviate。想快速上线、少做运维,Pinecone 这种托管服务会更省事。如果团队原本就大量使用 Elasticsearch,并且关键词搜索仍然是主需求,那么在 ES 上叠加向量检索也很自然。

小结

整理到最后,几个关键点是:

  1. 原始文本不能直接做语义计算,需要先变成向量。
  2. 传统的 N-Gram、TF-IDF 可以做基础推荐,但语义表达能力有限。
  3. Word Embedding 把词放进语义空间,让相似度和向量运算变得可行。
  4. 现代 Embedding 模型进一步把句子、文档、图片等内容变成高质量向量。
  5. 向量数据库负责高效存储和检索这些向量。
  6. 业务落地时,向量和元数据必须一起管理,否则检索结果很难回到业务数据。

Embedding 解决"怎么表示",向量数据库解决"怎么检索和管理"。这两块接起来,才是 RAG、语义搜索、推荐系统这类 AI 应用的基础设施。

总结

怎么把文本、图片、文档这些原始内容变成向量,然后用向量做相似度检索,最后用向量数据库支撑搜索、推荐和 RAG。

简单说,不是单独讲 Embedding,也不是单独讲向量数据库,而是在讲完整链路:

js 复制代码
准备文档
  ↓
文档切分 chunk
  ↓
调用 Embedding 模型,把 chunk 转成向量
  ↓
向量 + 原文 + 元数据 存入向量数据库
  ↓
用户提问
  ↓
用户问题转成向量
  ↓
去向量数据库检索相似内容
  ↓
取回相关原文片段
  ↓
把"原文片段 + 用户问题"发给大模型
  ↓
大模型生成答案

外部知识库 → 检索 → 拼进 Prompt → 大模型回答

按内容看,大概分成几块:

  1. 从酒店推荐开始

    用西雅图酒店数据集举例:如果想推荐相似酒店,就要先把酒店描述文本变成可计算的特征,再计算酒店之间的相似度,最后取最相似的 Top 10。

  2. 传统文本特征怎么做

    先讲 N-Gram、词频、TF-IDF。

    这些方法可以把文本变成向量,但它们主要看词面,比如词有没有出现、出现几次。问题是语义理解比较弱,比如"退票"和"退款"意思接近,但词面不同。

  3. 为什么需要 Word Embedding

    Word Embedding 是把词映射到稠密向量空间里。

    这样语义相近的词距离更近,还能做类似:

    king - man + woman ≈ queen

    Word2Vec、Skip-Gram、CBOW、Gensim 训练实验

  4. 现代 Embedding 模型怎么选

    后面从 Word2Vec 过渡到现在常用的 Embedding 模型,比如看 MTEB 榜单、模型维度、最大 token、中文/多语言能力、成本和延迟。

    Jina Embedding 和"俄罗斯套娃"向量,也就是同一个向量可以截成不同维度来用。

  5. 向量数据库是干什么的

    Embedding 解决"怎么表示内容",向量数据库解决"怎么存、怎么查、怎么管理"。

    在 RAG、知识库问答、语义搜索里,流程通常是:先把文档切块、向量化、存进去;用户提问时,再把问题向量化,查出最相关的内容。

  6. FAISS 怎么和元数据一起用

    FAISS 本身更像高性能向量检索库,不是完整数据库。

    它负责查"哪个向量最相似",但不知道这个向量对应哪篇文档、哪个章节、哪个 URL。

    所以还要维护 metadata_store,把向量 ID 和原始文本、来源、分类等元数据关联起来。

  7. 常见向量数据库怎么选

    对比 FAISS、Milvus、Pinecone、Weaviate、Qdrant、Elasticsearch。

    学习和实验可以用 FAISS;企业级向量检索可以看 Milvus/Qdrant/Weaviate;想少运维可以用 Pinecone;如果原本就重度依赖 ES,也可以做混合搜索。

AI 应用里,原始内容不能直接被检索和推理,必须先变成向量;向量能表达语义相似度;向量数据库负责把这些向量高效存起来、查出来,并通过元数据回到真实业务数据。

相关推荐
日取其半万世不竭1 小时前
Ollama + Open WebUI 部署教程:本地运行大语言模型,自建私有 AI 助手
人工智能·语言模型·自然语言处理
weixin_446260851 小时前
本地部署与实践指南:构建免费的AI开发助手系统(Claude Code + Ollama)
人工智能
Momo__1 小时前
Vue3 v-memo:长列表渲染的性能核武器
前端·vue.js
南宫萧幕1 小时前
Simulink 从零搭建 HEV ECMS 环境:模块解析、排坑指南与智能算法接口预留
人工智能·算法·matlab·汽车·控制
Forever7_1 小时前
弃用 Canvas!高性能2D WebGL 引擎性能提升几十倍!
前端·canvas
李白的天不白1 小时前
webpack 压缩文件
前端·webpack·node.js
倒流时光三十年1 小时前
Java String.split() 方法陷阱:为什么你应该始终使用 split(regex, -1)
后端
YuanDaima20481 小时前
Docker 工程化安装与核心命令实战
运维·人工智能·docker·微服务·容器·bash
杰之行1 小时前
Fast-DDS 接收数据完整时序分析
c++·人工智能