大模型RAG技术学习——索引构建
本文主要对面向RAG的索引构建进行了介绍,包括向量嵌入(文本嵌入、多模态嵌入)、向量数据库以及索引优化等内容。作为大模型RAG的基础和前提,索引构建的品质至关重要,需要精心设计与构建。
索引构建
1 向量嵌入
1.1 向量嵌入基础
1.1.1 向量嵌入的概念
向量嵌入(Embedding) 是一种将真实世界中复杂、高维的数据对象(如文本、图像、音频、视频等)转换为数学上易于处理的、低维、稠密的连续数值向量的技术。Embedding 产生的向量不是随机数值的堆砌,而是对数据语义的数学编码。
向量嵌入一般是将数据从高维的稀疏编码空间压缩到较低维度的致密编码空间,并保留其在语义上的关键特征,而对无关特征进行压缩。例如对500*600分辨率的图像数据嵌入到500维空间,即是将一个500*600*3维的高维数据压缩至500维的向量空间。
- 核心原则:在 Embedding 构建的向量空间中,语义上相似的对象,其对应的向量在空间中的距离会更近;而语义上不相关的对象,它们的向量距离会更远。
- 关键度量:我们通常使用以下数学方法来衡量向量间的“距离”或“相似度”:
- 余弦相似度 (Cosine Similarity):计算两个向量夹角的余弦值。值越接近 1,代表方向越一致,语义越相似。这是最常用的度量方式。
- 点积 (Dot Product):计算两个向量的乘积和。在向量归一化后,点积等价于余弦相似度。
- 欧氏距离 (Euclidean Distance):计算两个向量在空间中的直线距离。距离越小,语义越相似。
在RAG流程中,向量嵌入实际上是衔接知识库与用户查询的桥梁:
-
一方面,离线的知识(如非结构化的文档等)通过分块和嵌入转换形成嵌入向量存入向量数据库。
-
另一方面,当用户进行查询时,通过将用户查询采用相同方法(相同的嵌入模型)进行嵌入,可得到用户查询的嵌入向量,通过将其与向量数据库中的向量进行相似匹配,可以对前期录入的知识根据用户查询进行召回,实现相近、相关知识的检索与增强。
因此,通用流程如下:
- 离线索引构建:将知识库内文档切分后,使用 Embedding 模型将每个文档块(Chunk)转换为向量,存入专门的向量数据库中。
- 在线查询检索:当用户提出问题时,使用同一个 Embedding 模型将用户的问题也转换为一个向量。
- 相似度计算:在向量数据库中,计算“问题向量”与所有“文档块向量”的相似度。
- 召回上下文:选取相似度最高的 Top-K 个文档块,作为补充的上下文信息,与原始问题一同送给大语言模型(LLM)生成最终答案。
由上述RAG通用流程可知,向量嵌入的质量直接决定了 RAG 检索召回内容的准确性与相关性。一个优秀的向量嵌入模型能够精准捕捉问题和文档之间的深层语义联系。即使用户的提问和原文的表述不完全一致,也能够对相关的文档内容进行召回。反之,一个劣质的向量嵌入模型可能会因为无法理解语义而召回不相关或错误的信息,从而“污染”提供给 LLM 的上下文,导致最终生成的答案质量低下。[1]
1.1.2 向量嵌入技术发展
(1)静态词嵌入:上下文无关的表示
该方法的主要原理是为词汇表中的每个单词生成一个固定的、与上下文无关的向量。该类方法的典型代表包括Word2Vec
(2013)、GloVe
(2014)等。其中,Word2Vec
通过 Skip-gram 和 CBOW 架构,利用局部上下文窗口学习词向量,并验证了向量运算的语义能力(如 国王 - 男人 + 女人 ≈ 王后
)。GloVe
则融合了全局词-词共现矩阵的统计信息。
Word2Vec是从大量文本语料中以无监督的方式学习语义知识的一种模型,它被大量地用在自然语言处理(NLP)中。实际上,Word2Vec是通过学习文本来用词向量的方式表征词的语义信息的,即通过一个嵌入空间使得语义上相似的单词在该空间内距离很近,但这种映射是静态的,也就是上下文无关的(或者说是有限上下文相关的)。
Word2Vec模型中,主要有Skip-Gram和CBOW两种模型,从直观上理解,Skip-Gram是给定input word来预测上下文。而CBOW是给定上下文,来预测input word。
Skip-Gram(跳字模型):Skip-Gram模型通过给定的中心词来预测其上下文中的单词。具体来说,对于文本中的每一个单词,Skip-Gram模型将其视为中心词,并尝试预测该词周围一定窗口大小内的其他单词(即上下文单词)。Skip-Gram的模型结构如下图所示[2]
输入层:输入中心词的one-hot编码。
嵌入层:将one-hot编码转换为词向量。这一层的权重矩阵即为我们要学习的词嵌入矩阵。这层模型就是我们最终想要得到的向量嵌入模型。
线性层:主要是用作输出维数的控制。
输出层:对于每一个上下文单词,输出层会输出一个概率分布,表示中心词预测为该上下文单词的概率。通常,输出层使用softmax函数进行归一化处理。
Skip-Gram模型通过遍历文本中的中心词及其上下文,使用中心词词向量预测上下文单词分布,计算预测与真实分布间的损失,并利用反向传播更新模型参数与词嵌入矩阵,以优化词向量表示。[3]
CBOW(Continuous Bag of Words) 中文译为“连续词袋模型”。它是一种用于生成词向量的神经网络模型,由Tomas Mikolov等人于2013年提出 。词向量是一种将单词表示为固定长度的实数向量的方法,可以捕捉单词之间的语义和语法关系[4]。CBOW的模型结构如下图所示[5]
其主要包含:
- 输入层:输入上下文单词的one-hot编码。(不包含当前单词,当前单词是要预测的。)
- 嵌入层(图中蓝色部分):将上下文单词的one-hot编码转换为词向量,并将这些词向量进行平均或求和,得到上下文向量。这层模型就是我们最终想要得到的向量嵌入模型。
- 线性层(图中橙色部分):一般情况下,它是一个不设置激活函数的线性层。它的作用主要是用作输出维数的控制。
- 输出层:输出层同样使用softmax函数输出中心词的概率分布。
CBOW通过遍历文本中的中心词及其上下文,使用中心词词向量预测上下文单词分布,计算预测与真实分布间的损失,并利用反向传播更新模型参数与词嵌入矩阵,以优化词向量表示。
无论是CBOW还是Skip-Gram,它们的最终目标都是迭代出词向量字典,也就是嵌入矩阵。
该类方法的局限在于无法处理一词多义问题,限制了其在复杂语境下的语义表达能力。
(2)动态上下文嵌入
2017年,Transformer
架构的诞生带来了自注意力机制(Self-Attention),它允许模型在生成一个词的向量时,动态地考虑句子中所有其他词的影响。基于此,2018年 BERT
模型利用 Transformer
的编码器,通过掩码语言模型(MLM)等自监督任务进行预训练,生成了深度上下文相关的嵌入。同一个词在不同语境中会生成不同的向量,这有效解决了静态嵌入的一词多义难题。
(3)RAG 对嵌入技术的新要求
2020年,RAG 框架的提出,旨在解决大型语言模型知识固化(其内部知识难以更新)和幻觉(生成的内容可能不符合事实且无法溯源)的问题。RAG 通过“检索-生成”范式,动态地为 LLM 注入外部知识。这一过程的核心是语义检索,它完全依赖于高质量的向量嵌入。
RAG 的兴起对嵌入技术提出了更高、更具体的要求:
- 领域自适应能力:通用的嵌入模型在专业领域(如法律、医疗)可能表现不佳。因此,能够通过微调或使用指令(如 INSTRUCTOR 模型)来适应特定领域术语和语义的嵌入模型变得至关重要。
- 多粒度与多模态支持:RAG 系统需要处理的不仅仅是短句,还可能包括长文档、代码,甚至是图像和表格。这就要求嵌入模型能够处理不同长度和类型的输入数据。
- 检索效率与混合检索:嵌入向量的维度和模型大小直接影响存储成本和检索速度。同时,为了结合语义相似性(密集检索)和关键词匹配(稀疏检索)的优点,支持混合检索的嵌入模型(如 BGE-M3)应运而生,在某些任务中成为提升召回率的关键。
1.2 嵌入模型训练原理(基于BERT模型)
BERT 的成功很大程度上归功于其巧妙的自监督学习策略,它允许模型从海量的、无标注的文本数据中学习知识。基于BERT的嵌入模型训练通常包括两种任务(这实际上也是BERT语言模型预训练的任务)[6],具体如下。
1.2.1 任务一:掩码语言模型 (Masked Language Model, MLM)
- 过程:
- 随机地将输入句子中 15% 的词元(Token)替换为一个特殊的
[MASK]
标记。 - 让模型去预测这些被遮盖住的原始词元是什么。
- 随机地将输入句子中 15% 的词元(Token)替换为一个特殊的
- 目标:通过这个任务,模型被迫学习每个词元与其上下文之间的关系,从而掌握深层次的语境语义。
1.2.2 任务二:下一句预测 (Next Sentence Prediction, NSP)
-
过程:
- 构造训练样本,每个样本包含两个句子 A 和 B。
- 其中 50% 的样本,B 是 A 的真实下一句(IsNext);另外 50% 的样本,B 是从语料库中随机抽取的句子(NotNext)。
- 让模型判断 B 是否是 A 的下一句。
-
目标:这个任务让模型学习句子与句子之间的逻辑关系、连贯性和主题相关性。
-
重要说明:后续的研究(如 RoBERTa)发现,NSP 任务可能过于简单,甚至会损害模型性能。因此,许多现代的预训练模型(如 RoBERTa、SBERT)已经放弃了 NSP 任务。
与 BERT 不同,RoBERTa 在预训练期间不使用下一句预测 (NSP) 丢失。这使得 RoBERTa 能够专注于掩码语言建模目标,从而获得更具表现力的语言表示。
We find that using individual sentences hurts performance on downstream tasks, which we hypothesize is because the model is not able to learn long-range dependencies. … We next compare training without the NSP loss and training with blocks of text from a single document (DOC-SENTENCES). We find that this setting outperforms the originally published
B E R T B A S E BERT_{BASE} BERTBASE results and that removing the NSP loss matches or slightly improves downstream task performance, in contrast to Devlin et al. (2019).[7]
1.2.3 效果增强策略
虽然 MLM 和 NSP 赋予了模型强大的基础语义理解能力,但为了在检索任务中表现更佳,现代嵌入模型通常会引入更具针对性的训练策略。
-
度量学习 (Metric Learning):
- 思想:直接以“相似度”作为优化目标。
- 方法:收集大量相关的文本对(例如,(问题,答案)、(新闻标题,正文))。训练的目标是优化向量空间中的相对距离:让“正例对”的向量表示在空间中被“拉近”,而“负例对”的向量表示被“推远”。关键在于优化排序关系,而非追求绝对的相似度值(如 1 或 0),因为过度追求极端值可能导致模型过拟合。
-
对比学习 (Contrastive Learning):
- 思想:在向量空间中,将相似的样本“拉近”,将不相似的样本“推远”。
- 方法:构建一个三元组(Anchor, Positive, Negative)。其中,Anchor 和 Positive 是相关的(例如,同一个问题的两种不同问法),Anchor 和 Negative 是不相关的。训练的目标是让
distance(Anchor, Positive)
尽可能小,同时让distance(Anchor, Negative)
尽可能大。
总体上讲,上述训练任务的核心还是在于构建数据集,训练模型基于嵌入后的相似度区分正负样本。
1.3 嵌入模型选型
嵌入模型的选取可以参考MTEB 排行榜。MTEB (Massive Text Embedding Benchmark) 排行榜[9]是一个由 Hugging Face 维护的、全面的文本嵌入模型评测基准。它涵盖了分类、聚类、检索、排序等多种任务,并提供了公开的排行榜,为评估和选择嵌入模型提供了重要的参考依据。
下图是网站中的模型评估图像,非常直观地展示了在选择开源嵌入模型时需要权衡的四个核心维度:
- 横轴 - 模型参数量 (Number of Parameters):代表了模型的大小。通常,参数量越大的模型(越靠右),其潜在能力越强,但对计算资源的要求也越高。
- 纵轴 - 平均任务得分 (Mean Task Score):代表了模型的综合性能。这个分数是模型在分类、聚类、检索等一系列标准 NLP 任务上的平均表现。分数越高(越靠上),说明模型的通用语义理解能力越强。
- 气泡大小 - 嵌入维度 (Embedding Size):代表了模型输出向量的维度。气泡越大,维度越高,理论上能编码更丰富的语义细节,但同时也会占用更多的存储和计算资源。
- 气泡颜色 - 最大处理长度 (Max Tokens):代表了模型能处理的文本长度上限。颜色越深,表示模型能处理的 Token 数量越多,对长文本的适应性越好。
通常,对于嵌入模型的选择,我们可以依据上述四个核心维度进行选取。
在查看榜单时,除了分数,你还需要关注以下几个关键维度:
- 任务 (Task):对于 RAG 应用,需要重点关注模型在
Retrieval
(检索) 任务下的排名。 - 语言 (Language):模型是否支持你的业务数据所使用的语言?对于中文 RAG,应选择明确支持中文或多语言的模型。
- 模型大小 (Size):模型越大,通常性能越好,但对硬件(显存)的要求也越高,推理速度也越慢。需要根据你的部署环境和性能要求来权衡。
- 维度 (Dimensions):向量维度越高,能编码的信息越丰富,但也会占用更多的存储空间和计算资源。
- 最大 Token 数 (Max Tokens):这决定了模型能处理的文本长度上限。这个参数是你设计文本分块(Chunking)策略时必须考虑的重要依据,块大小不应超过此限制。
- 得分与机构 (Score & Publisher):结合模型的得分排名和其发布机构的声誉进行初步筛选。知名机构发布的模型通常质量更有保障。
- 成本 (Cost):如果是使用 API 服务的模型,需要考虑其调用成本;如果是自部署开源模型,则需要评估其对硬件资源的消耗(如显存、内存)以及带来的运维成本。
当然,上述选择也只能作为参考,最终的模型选择还是要通过具体实验进行测试对比,包括:
- 确定基线 (Baseline):根据上述维度,选择几个符合要求的模型作为你的初始基准模型。
- 构建私有评测集:根据真实业务数据,手动创建一批高质量的评测样本,每个样本包含一个典型用户问题和它对应的标准答案(或最相关的文档块)。
- 迭代优化:
- 使用基线模型在你的私有评测集上运行,评估其召回的准确率和相关性。
- 如果效果不理想,可以尝试更换模型,或者调整 RAG 流程的其他环节(如文本分块策略)。
- 通过几轮的对比测试和迭代优化,最终选出在你的特定场景下表现最佳的那个“心仪”模型。
2 多模态嵌入
2.1 多模态嵌入概念
上述介绍的向量嵌入主要针对文本信息进行嵌入,但仅有文本的世界是不完整的。现实世界的信息是多模态的,包含图像、音频、视频等。然而,传统的文本嵌入无法理解其他模态的信息,文本向量和图像向量处于相互隔离的空间,存在一堵“模态墙”。
多模态嵌入 (Multimodal Embedding) 的目标正是为了打破这堵墙。其目的是将不同类型的数据(如图像和文本)映射到同一个共享的向量空间。在这个统一的空间里,一段描述“一只奔跑的狗”的文字,其向量会非常接近一张真实小狗奔跑的图片向量。
实现这一目标的关键,在于解决 跨模态对齐 (Cross-modal Alignment) 的挑战。以对比学习、视觉 Transformer(ViT) 等技术为代表的突破,让模型能够学习到不同模态数据之间的语义关联,最终催生了像 CLIP(Contrastive Language-Image Pre-training)[11]这样的模型。
CLIP(对比语言-图像预训练)是一种多模态学习模型,旨在通过对大量的图像和文本数据进行联合训练,理解图像和文本之间的关系。CLIP 是由 OpenAI 提出的,通过对图像和文本数据的联合学习,CLIP 能够对视觉内容进行语言描述,同时也能够通过自然语言查询来理解图像内容。这种方式使得 CLIP 模型在多个视觉任务上表现出色。其核心特征如下[10]:
多模态预训练: CLIP 是通过对大规模的图像和文本对进行训练来学习图像和文本之间的关系。这些图像-文本对是来自互联网上的公开数据集,通常包含图像和描述该图像的文本描述。模型在训练过程中,通过最大化图像与文本的相似度来实现多模态的预训练。
对比学习: CLIP 使用了一种对比学习方法,其中的关键思想是通过训练模型使得相关图像和文本的表示距离较近,而不相关的图像和文本的表示距离较远。这个过程通过最大化相关图像和文本之间的相似度(即最小化它们的对比损失)来实现。CLIP 模型通过一个图像编码器和一个文本编码器来分别提取图像和文本的特征。然后,模型计算图像特征和文本特征之间的相似度,以此来进行匹配,其训练过程如下图所示。
对比学习,通俗来说,就是定义一些正样本和一些负样本,然后训练网络能够去区分这些样本。但是定义正负样本的方式是无需手工进行的,因此属于无监督学习。
图像编码与文本编码: CLIP 使用深度神经网络(如 ResNet、Vision Transformer(ViT))作为图像编码器,以及一个基于 Transformer 的文本编码器(如 GPT 模型)来处理输入的文本。这两个编码器分别将图像和文本转化为固定维度的向量表示。图像编码器将图像转化为一组高维特征,而文本编码器将文本转化为一个等长的向量,二者的相似度通过计算它们的余弦相似度来度量。
这种大规模的对比学习赋予了 CLIP 有效的零样本(Zero-shot)识别能力。它能将一个传统的分类任务,转化为一个“图文检索”问题——例如,要判断一张图片是不是猫,只需计算图片向量与“a photo of a cat”文本向量的相似度即可。这使得 CLIP 无需针对特定任务进行微调,就能实现对视觉概念的泛化理解。[1]
2.2 多模态嵌入实践
虽然 CLIP 为图文预训练提供了重要基础,但多模态领域的研究迅速发展,涌现了许多针对不同目标和场景进行优化的模型。
在众多优秀的模型中,由北京智源人工智能研究院(BAAI)开发的 BGE-M3[12] 是一个很有代表性的现代多模态嵌入模型。它在多语言、多功能和多粒度处理上都表现出色,体现了当前技术向“更统一、更全面”发展的趋势。
BGE-M3 的核心特性包括:
- 多语言性 (Multi-Linguality):原生支持超过 100 种语言的文本与图像处理,能够轻松实现跨语言的图文检索。
- 多功能性 (Multi-Functionality):在单一模型内同时支持密集检索(Dense Retrieval)、多向量检索(Multi-Vector Retrieval)和稀疏检索(Sparse Retrieval),为不同应用场景提供了灵活的检索策略。
- 多粒度性 (Multi-Granularity):能够有效处理从短句到长达 8192 个 token 的长文档,覆盖了更广泛的应用需求。
在技术架构上,BGE-M3 采用了基于 XLM-RoBERTa 优化的联合编码器,并对视觉处理机制进行了创新。它不同于 CLIP 对整张图进行编码的方式,而是采用网格嵌入 (Grid-Based Embeddings),将图像分割为多个网格单元并独立编码。这种设计显著提升了模型对图像局部细节的捕捉能力,在处理多物体重叠等复杂场景时更具优势。
BGE-M3模型训练分为三个阶段:
1)RetroMAE预训练: 在105种语言的网页数据和wiki数据上进行,提供一个可以支持8192长度和面向表示任务的基座模型;
2)无监督对比学习: 在194种单语言和1390种翻译对数据共1.1B的文本对上进行的大规模对比学习;
3)多检索方式统一优化: 在高质量多样化的数据上进行多功能检索优化,使模型具备多种检索能力。
不同检索方式的介绍:
- 稠密检索: 常用的向量检索方式,将文本映射为单个向量,通过向量相似度判断文本间的相关性。无需词汇匹配通用性强。
- 稀疏检索: 例如经典的BM25检索算法,向量维度为整个词表,其中大部分为0,仅对文本中出现的单词计算出一个权重。有着更强的泛化能力和长文本建模能力。
- 多向量检索: 对每个文本使用多个向量进行表示,代表性工作有Colbert。BGE-M3中采用了Colbert的交互机制计算相关性。多向量检索可以用于细粒度的检索和重排。
下面,我们用bge-base-en-v1.5
模型来进行图文嵌入。
(1)安装必要的环境
安装visual_bge模块,此处我们从源码进行安装,采用pip install -e .
的方式安装,如下
# 进入 visual_bge 目录
cd XX/XX/visual_bge
# 安装 visual_bge 模块及其依赖
pip install -e .
(2)模型下载
模型下载方面,推荐国内采用modelscope进行下载,如下
modelscope download --model BAAI/bge-visualized
下载后将模型移动至所需的路径。
(3)代码运行
用bge-base-en-v1.5
模型来进行图文嵌入代码如下
import os
os.environ["HF_ENDPOINT"] = "https://hf-mirror.com"
os.environ["TRANSFORMERS_CACHE"] = 'XXX'
import torch
from visual_bge.visual_bge.modeling import Visualized_BGE
# Set model path
model_weight_file = '../../models/BAAI/bge-visualized/Visualized_base_en_v1.5.pth'
model = Visualized_BGE(model_name_bge="BAAI/bge-base-en-v1.5",
model_weight=model_weight_file)
model.eval()
with torch.no_grad():
text_emb = model.encode(text="datawhale开源组织的logo")
img_emb_1 = model.encode(image="../../data/C3/imgs/datawhale01.png")
multi_emb_1 = model.encode(image="../../data/C3/imgs/datawhale01.png", text="datawhale开源组织的logo")
img_emb_2 = model.encode(image="../../data/C3/imgs/datawhale02.png")
multi_emb_2 = model.encode(image="../../data/C3/imgs/datawhale02.png", text="datawhale开源组织的logo")
# 计算相似度 (Matrix multiplication)
sim_1 = img_emb_1 @ img_emb_2.T
sim_2 = img_emb_1 @ multi_emb_1.T
sim_3 = text_emb @ multi_emb_1.T
sim_4 = multi_emb_1 @ multi_emb_2.T
print("=== 相似度计算结果 ===")
print(f"纯图像 vs 纯图像: {sim_1}")
print(f"图文结合1 vs 纯图像: {sim_2}")
print(f"图文结合1 vs 纯文本: {sim_3}")
print(f"图文结合1 vs 图文结合2: {sim_4}")
# 向量信息分析
print("\n=== 嵌入向量信息 ===")
print(f"多模态向量维度: {multi_emb_1.shape}")
print(f"图像向量维度: {img_emb_1.shape}")
print(f"多模态向量示例 (前10个元素): {multi_emb_1[0][:10]}")
print(f"图像向量示例 (前10个元素): {img_emb_1[0][:10]}")例 (前10个元素): {img_emb_1[0][:10]}")
其中,添加TRANSFORMERS_CACHE
环境变量是将Huggingface的缓存地址更换为当前工程下的地址路径;添加HF_ENDPOINT
环境变量则是设定采用Huggingface的镜像源进行模型资源获取。可能是由于visual-bge
库中加载模型方式,以及下载的模型信息不完整等原因,首次运行程序需要访问Huggingface网站下载缓存文件。首次下载后,仅需指定缓存路径的环境变量,即TRANSFORMERS_CACHE
,而无需再给出镜像源链接环境变量HF_ENDPOINT
。
运行上述代码,可得如下结果输出
=== 相似度计算结果 ===
纯图像 vs 纯图像: tensor([[0.8318]])
图文结合1 vs 纯图像: tensor([[0.8291]])
图文结合1 vs 纯文本: tensor([[0.7627]])
图文结合1 vs 图文结合2: tensor([[0.9058]])
=== 嵌入向量信息 ===
多模态向量维度: torch.Size([1, 768])
图像向量维度: torch.Size([1, 768])
多模态向量示例 (前10个元素): tensor([ 0.0360, -0.0032, -0.0377, 0.0240, 0.0140, 0.0340, 0.0148, 0.0292,
0.0060, -0.0145])
图像向量示例 (前10个元素): tensor([ 0.0407, -0.0606, -0.0037, 0.0073, 0.0305, 0.0318, 0.0132, 0.0442,
-0.0380, -0.0270])
Visual BGE提供了编码多模态数据的多样性,支持纯文本、纯图像或图文组合的格式:
- 纯文本编码: 保持原始BGE模型的强大文本嵌入能力。
- 纯图像编码: 使用基于EVA-CLIP的视觉编码器处理图像。
- 图文联合编码: 将图像和文本特征融合到统一的向量空间。
3 向量数据库
嵌入模型的功能在于将文本、图像等非结构化数据转换为高维向量。这些向量是 RAG 系统能够进行语义理解的基础。有了这些嵌入向量构成的集合后,随之而来的问题便是:如何快速、准确地从海量向量中找到与用户查询最相似的那几个? 这时就需要引入向量数据库来帮我们解决这个问题。
3.1 向量数据库主要功能
向量数据库的核心价值在于其高效处理海量高维向量的能力。其主要功能可以概括为以下几点:
此处的高维向量即为嵌入向量。
- 高效的相似性搜索:这是向量数据库最重要的功能。它利用专门的索引技术(如 HNSW,IVF),能够在数十亿级别的向量中实现毫秒级的近似最近邻(Approximate Nearest Neighbor,ANN)查询,快速找到与给定查询最相似的数据。
向量相似性搜索是搜索领域的游戏规则改变者。它使我们能够有效地搜索从 GIF 到文章的各种媒体——在亚秒级时间尺度上以令人难以置信的精度搜索数十亿+ 大小的数据集。
无论采用什么向量数据库(如Faiss、Milvus等),其索引方法大致是接近的,其中一些重要的索引方式包括[13,14]:
(1)Flat: FLAT(也称为 IDMap)是最简单的索引方法,采用暴力搜索(Brute Force),直接计算查询向量与所有存储向量的相似度,并返回最近的匹配项。
Flat 索引是最简单且直接的索引类型,它不对输入的向量进行任何修改,因此提供了最准确的结果。这种索引在搜索质量上追求完美,但代价是较慢的搜索速度。
特点:
检索精度:100%(因为它是完整的暴力计算)。
查询速度:慢,时间复杂度为 O(n)。
内存占用:高,因为所有向量都需要加载到内存中。
适用场景:
小规模数据集(<100K)。
需要绝对精确的相似度匹配。
测试阶段,作为基准来评估其他索引算法的效果。
(2)局部敏感哈希(Locality Sensitive Hashing,LSH): 局部敏感哈希通过使用哈希函数将向量分组到桶中,这些函数旨在最大化哈希冲突,而非像传统哈希函数那样最小化冲突。这种方法允许相似的向量被分组在一起,便于搜索时快速找到最接近的匹配。
为什么LSH要最大化冲突?对于搜索,使用LSH将相似的对象分组在一起。当引入一个新的查询对象(或向量)时,LSH算法可以用来找到最接近的匹配组。
LSH 提供了广泛的性能范围,其性能严重依赖于参数设置: 高分辨率(高
nbits
)可以提高召回率,但会增加内存使用和搜索时间。对于高维数据,LSH 的性能可能不佳,尤其是当向量维度较大时。随着向量维度d
的增加,存储的向量变得更大,这可能导致搜索时间过长。
nbits
参数指的是哈希向量的“分辨率”。更高的值意味着更高的准确性,但代价是更多的内存和更慢的搜索速度。例如,下图为
d
为128时,LSH的召回率得分。注意,要获得更高的召回性能,需要大幅度增加num_bits
的值。例如要达到90%的召回率,使用64d
,即64×128=8192。
因此,当处理大向量维度(如 128)时,LSH 可能不再适用。(3)分层可导航小世界网络(Hierarchical Navigable Small World Graphs,HNSW): HNSW 是一种基于 图(Graph) 结构的索引算法,可实现快速高精度的 ANN搜索。它使用 分层小世界图(Hierarchical Small World Graph) 来高效连接和搜索向量,是一种高性能的索引类型,适用于大型数据集,特别是在高维度下。
工作原理:
- 通过构造多层图,使高层节点连接更远的点,底层节点连接更近的点。
- 查询时,首先从高层搜索,再逐层向下递归找到最近的向量。
特点:
检索精度:高,接近 FLAT。
查询速度:快,通常比 IVF 更快。
存储占用:较高,需要存储图结构。
适用场景:
高并发实时搜索(如 AI 聊天机器人、智能客服)。
需要高精度 ANN 检索的应用(如人脸识别、语义搜索)。
(4)倒排文件(Inverted File Index,IVF): IVF(倒排文件索引)通过将向量划分到多个“簇”中,仅搜索最相关的簇以加快查询速度。(Milvus 提供多种 IVF 变体,包括
IVF_FLAT
、IVF_PQ
和IVF_SQ8
。)
特点:
检索精度:较高,但受聚类质量影响。
查询速度:比 FLAT 快很多,复杂度为 O(log n) + O(√n)(依赖簇数量)。
存储占用:较低,可结合量化(PQ/SQ8)进一步压缩数据。
适用场景:
中等规模数据(>100K)。
在查询速度和精度之间寻找平衡的场景(如推荐系统、搜索引擎)。
适用于离线索引构建的情况(IVF 需要预先训练索引)。
- 高维数据存储与管理:专门为存储高维向量(通常维度成百上千)而优化,支持对向量数据进行增、删、改、查等基本操作。
- 丰富的查询能力:除了基本的相似性搜索,还支持按标量字段过滤查询、范围查询和聚类分析等,满足复杂业务需求。
- 可扩展与高可用:现代向量数据库通常采用分布式架构,具备良好的水平扩展能力和容错性,能够通过增加节点来应对数据量的增长,并确保服务的稳定可靠。
- 数据与模型生态集成:与主流的 AI 框架(如 LangChain, LlamaIndex)和机器学习工作流无缝集成,简化了从模型训练到向量检索的应用开发流程。
3.2 向量数据库与传统数据库
向量数据库和传统数据库并非相互替代的关系,而是互补关系。在构建现代 AI 应用时,通常会将两者结合使用:利用传统数据库存储业务元数据和结构化信息,而向量数据库则专门负责处理和检索由 AI 模型产生的海量向量数据。
3.3 向量数据库工作原理
向量数据库通常采用四层架构,通过以下技术手段实现高效相似性搜索:
- 存储层:存储向量数据和元数据,优化存储效率,支持分布式存储。
- 索引层:维护索引算法(HNSW、LSH、PQ等),创建和优化索引,支持索引调整。
- 查询层:处理查询请求,支持混合查询,实现查询优化。
- 服务层:管理客户端连接,提供监控和日志,实现安全管理。
主要技术手段包括:
- 基于树的方法:如 Annoy 使用的随机投影树,通过树形结构实现对数复杂度的搜索。
- 基于哈希的方法:如 LSH(局部敏感哈希),通过哈希函数将相似向量映射到同一“桶”。
- 基于图的方法:如 HNSW(分层可导航小世界图),通过多层邻近图结构实现快速搜索。
- 基于量化的方法:如 Faiss 的 IVF 和 PQ,通过聚类和量化压缩向量。
3.4 主流向量数据库
当前主流的向量数据库产品包括[1]:
Pinecone 是一款完全托管的向量数据库服务,采用Serverless架构设计。它提供存储计算分离、自动扩展和负载均衡等企业级特性,并保证99.95%的SLA。Pinecone支持多种语言SDK,提供极高可用性和低延迟搜索(<100ms),特别适合企业级生产环境、高并发场景和大规模部署。
Milvus 是一款开源的分布式向量数据库,采用分布式架构设计,支持GPU加速和多种索引算法。它能够处理亿级向量检索,提供高性能GPU加速和完善的生态系统。Milvus特别适合大规模部署、高性能要求的场景,以及需要自定义开发的开源项目。
Qdrant 是一款高性能的开源向量数据库,采用Rust开发,支持二进制量化技术。它提供多种索引策略和向量混合搜索功能,能够实现极高的性能(RPS>4000)和低延迟搜索。Qdrant特别适合性能敏感应用、高并发场景以及中小规模部署。
Weaviate 是一款支持GraphQL的AI集成向量数据库,提供20+AI模块和多模态支持。它采用GraphQL API设计,支持RAG优化,特别适合AI开发、多模态处理和快速开发场景。Weaviate具有活跃的社区支持和易于集成的特点。
Chroma 是一款轻量级的开源向量数据库,采用本地优先设计,无依赖。它提供零配置安装、本地运行和低资源消耗等特性,特别适合原型开发、教育培训和小规模应用。Chroma的部署简单,适合快速原型开发。
选择建议:
- 新手入门/小型项目:从
ChromaDB
或FAISS
开始是最佳选择。它们与 LangChain/LlamaIndex 紧密集成,几行代码就能运行,且能满足基本的存储和检索需求。 - 生产环境/大规模应用:当数据量超过百万级,或需要高并发、实时更新、复杂元数据过滤时,应考虑更专业的解决方案,如
Milvus
、Weaviate
或云服务Pinecone
。
3.5 本地向量存储(基于FAISS)
FAISS (Facebook AI Similarity Search) 是一个由 Facebook AI Research 开发的高性能库,专门用于高效的相似性搜索和密集向量聚类。当与 LangChain 结合使用时,它可以作为一个强大的本地向量存储方案,非常适合快速原型设计和中小型应用。
Faiss的工作,就是把我们自己的候选向量集封装成一个index数据库,它可以加速我们检索相似向量Top K的过程,一些最有用的算法是在 GPU 上实现的。它主要由 Meta 的基础 AI 研究团队 FAIR 开发。
如果有 GPU,可以安装
faiss-gpu
以获得更好的性能,否则,可以安装CPU版本faiss-cpu
,本文使用CPU版本faiss-cpu
。
FAISS 本质上是一个算法库,它将索引直接保存为本地文件(一个 .faiss
索引文件和一个 .pkl
映射文件,两个均为二进制文件),而非运行一个数据库服务。这种方式轻量且高效。
下面的代码演示了使用 LangChain 和 FAISS 完成一个完整的“创建 -> 保存 -> 加载 -> 查询”流程。
from langchain_community.vectorstores import FAISS
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_core.documents import Document
# 1. 示例文本和嵌入模型
texts = [
"张三是法外狂徒",
"FAISS是一个用于高效相似性搜索和密集向量聚类的库。",
"LangChain是一个用于开发由语言模型驱动的应用程序的框架。"
]
docs = [Document(page_content=t) for t in texts]
# 加载本地嵌入模型
embeddings = HuggingFaceEmbeddings(
model_name="../../models/BAAI/bge-small-zh-v1.5",
model_kwargs={'device': 'cpu'},
encode_kwargs={'normalize_embeddings': True}
)
# 2. 创建向量存储并保存到本地
vectorstore = FAISS.from_documents(docs, embeddings)
local_faiss_path = "./faiss_index_store"
vectorstore.save_local(local_faiss_path)
print(f"FAISS index has been saved to {local_faiss_path}")
# 3. 加载索引并执行查询
# 加载时需指定相同的嵌入模型,并允许反序列化
loaded_vectorstore = FAISS.load_local(
local_faiss_path,
embeddings,
allow_dangerous_deserialization=True
)
# 执行相似性搜索
query = "FAISS是做什么的?"
results = loaded_vectorstore.similarity_search(query, k=1)
print(f"\n查询: '{query}'")
print("相似度最高的文档:")
for doc in results:
print(f"- {doc.page_content}")
上述过程中,我们使用langchain_community.vectorstores
中的FAISS
库,通过如下部分代码,借助LangChain的工具链,便捷地结合文档加载器和嵌入模型构建了向量数据库vectorstore
。
# 2. 创建向量存储并保存到本地
vectorstore = FAISS.from_documents(docs, embeddings)
进一步,在使用时通过如下代码完成向量数据库地加载和基于该向量数据库的相似性检索
# 3. 加载索引并执行查询
# 加载时需指定相同的嵌入模型,并允许反序列化
loaded_vectorstore = FAISS.load_local(
local_faiss_path,
embeddings,
allow_dangerous_deserialization=True
)
# 执行相似性搜索
query = "FAISS是做什么的?"
results = loaded_vectorstore.similarity_search(query, k=1)
上述两个方法的具体参数如下:
def load_local(
cls,
folder_path: str,
embeddings: Embeddings,
index_name: str = "index",
*,
allow_dangerous_deserialization: bool = False,
**kwargs: Any,
) -> FAISS:
"""Load FAISS index, docstore, and index_to_docstore_id from disk.
Args:
folder_path: folder path to load index, docstore,
and index_to_docstore_id from.
embeddings: Embeddings to use when generating queries
index_name: for saving with a specific index file name
allow_dangerous_deserialization: whether to allow deserialization
of the data which involves loading a pickle file.
Pickle files can be modified by malicious actors to deliver a
malicious payload that results in execution of
arbitrary code on your machine.
"""
if not allow_dangerous_deserialization:
raise ValueError(
"The de-serialization relies loading a pickle file. "
"Pickle files can be modified to deliver a malicious payload that "
"results in execution of arbitrary code on your machine."
"You will need to set `allow_dangerous_deserialization` to `True` to "
"enable deserialization. If you do this, make sure that you "
"trust the source of the data. For example, if you are loading a "
"file that you created, and know that no one else has modified the "
"file, then this is safe to do. Do not set this to `True` if you are "
"loading a file from an untrusted source (e.g., some random site on "
"the internet.)."
)
path = Path(folder_path)
# load index separately since it is not picklable
faiss = dependable_faiss_import()
index = faiss.read_index(str(path / f"{index_name}.faiss"))
# load docstore and index_to_docstore_id
with open(path / f"{index_name}.pkl", "rb") as f:
(
docstore,
index_to_docstore_id,
) = pickle.load( # ignore[pickle]: explicit-opt-in
f
)
return cls(embeddings, index, docstore, index_to_docstore_id, **kwargs)
def similarity_search(
self,
query: str,
k: int = 4,
filter: Optional[Union[Callable, Dict[str, Any]]] = None,
fetch_k: int = 20,
**kwargs: Any,
) -> List[Document]:
"""Return docs most similar to query.
Args:
query: Text to look up documents similar to.
k: Number of Documents to return. Defaults to 4.
filter: (Optional[Dict[str, str]]): Filter by metadata. Defaults to None.
fetch_k: (Optional[int]) Number of Documents to fetch before filtering.
Defaults to 20.
Returns:
List of Documents most similar to the query.
"""
...
从load_local
方法的代码可以看到,当从本地加载FAISS向量数据库时,FAISS会把整个.faiss
索引文件和 .pkl
映射文件加载到内存,如果向量数据库比较大,会占用比较多的内存空间。
4 Milvus向量数据库
Milvus 是一个开源的、专为大规模向量相似性搜索和分析而设计的向量数据库。Milvus 从设计之初就瞄准了生产环境。其采用云原生架构,具备高可用、高性能、易扩展的特性,能够处理十亿、百亿甚至更大规模的向量数据。
Milvus 官方不支持 Windows 直接安装,而是需要Docker。因此,为部署Milvus,需首先安装Docker。对于Windows系统,可以使Docker Desktop进行部署,而后者则依赖 WSL 2 作为后端运行环境。Linux系统则可以直接使用Docker进行部署。
4.1 核心组件
4.1.1 集合(Collection)
Collection 是 Milvus 中最基本的数据组织单位,类似于关系型数据库中的一张表 (Table)。是我们存储、管理和查询向量及相关元数据的容器。所有的数据操作,如插入、删除、查询等,都是围绕 Collection 展开的。Collection(集合) 相当于一个图书馆,是所有数据的顶层容器。一个 Collection 可以包含多个 Partition(分区),每个 Partition 可以包含多个 Entity(实体):
-
Partition (分区) 是 Collection 内部的一个逻辑划分,相当于图书馆里的不同区域(如“小说区”、“科技区”),将数据物理隔离,让检索更高效。每个 Collection 在创建时都会有一个名为
_default
的默认分区。我们可以根据业务需求创建更多的分区,将数据按特定规则(如类别、日期等)存入不同分区。 -
Schema(模式) 规定了 Collection 的数据结构,定义了其中包含的所有字段 (Field) 及其属性,相当于图书馆的图书卡片规则,定义了每本书(数据)必须登记哪些信息(字段)。一个设计良好的 Schema 对于保证数据一致性和提升查询性能至关重要。
-
Entity(实体) 相当于一本具体的书,是数据本身。
-
Alias(别名) 是为 Collection 提供的一个“昵称”,相当于一个动态的推荐书单(如“本周精选”),它可以指向某个具体的 Collection,方便应用层调用,实现数据更新时的无缝切换。
通过为一个 Collection 设置别名,我们可以在应用程序中使用这个别名来执行所有操作,而不是直接使用真实的 Collection 名称。
当需要对一个在线服务的 Collection 进行大规模的数据更新或重建索引。直接在原 Collection 上操作风险很高。正确的做法是:
- 创建一个新的 Collection (
collection_v2
) 并导入、索引好所有新数据。 - 将指向旧 Collection (
collection_v1
) 的别名(例如my_app_collection
)原子性地切换到新 Collection (collection_v2
) 上。
- 创建一个新的 Collection (
4.1.2 索引(Index)
从宏观上看,索引本身就是一种为了加速查询而设计的复杂数据结构。对向量数据创建索引后,Milvus 可以极大地提升向量相似性搜索的速度,代价是会占用额外的存储和内存资源。
Milvus 向量索引的内部组件及其工作流程:
- 数据结构:这是索引的骨架,定义了向量的组织方式(如 HNSW 中的图结构)。
- 量化(可选):数据压缩技术,通过降低向量精度来减少内存占用和加速计算。
- 结果精炼(可选):在找到初步候选集后,进行更精确的计算以优化最终结果。
Milvus 支持对标量字段和向量字段分别创建索引。
- 标量字段索引:主要用于加速元数据过滤,常用的有
INVERTED
、BITMAP
等。通常使用推荐的索引类型即可。 - 向量字段索引:这是 Milvus 的核心。选择合适的向量索引是在查询性能、召回率和内存占用之间做出权衡的艺术。
Milvus 提供了多种向量索引算法,以适应不同的应用场景,包括:FLAT (精确查找)、IVF 系列(倒排文件索引)、HNSW (基于图的索引)、DiskANN (基于磁盘的索引)等。
选择索引没有唯一的“最佳答案”,需要根据业务场景在数据规模、内存限制、查询性能和召回率之间进行权衡。
场景 | 推荐索引 | 备注 |
---|---|---|
数据可完全载入内存,追求低延迟 | HNSW | 内存占用较大,但查询性能和召回率都很优秀。 |
数据可完全载入内存,追求高吞吐 | IVF_FLAT / IVF_SQ8 | 性能和资源消耗的平衡之选。 |
数据量巨大,无法载入内存 | DiskANN | 在 SSD 上性能优异,专为海量数据设计。 |
追求 100% 准确率,数据量不大 | FLAT | 暴力搜索,确保结果最精确。 |
在实际应用中,通常需要通过测试来找到最适合自己数据和查询模式的索引类型及其参数。
4.1.3 检索
(1)基础向量检索 (ANN Search)
这是 Milvus 的核心功能之一,近似最近邻 (Approximate Nearest Neighbor, ANN) 检索,利用预先构建好的索引,能够极速地从海量数据中找到与查询向量最相似的 Top-K 个结果。
(2)增强检索
在基础的 ANN 检索之上,Milvus 提供了多种增强检索功能,以满足更复杂的业务需求,具体包括:
-
过滤检索
-
范围检索
-
多向量混合检索
-
分组检索
5 索引优化
基于LlamaIndex的高性能生产级RAG构建方案,可进一步进行索引优化。
5.1 上下文扩展
在RAG系统中,常常面临一个权衡问题:使用小块文本进行检索可以获得更高的精确度,但小块文本缺乏足够的上下文,可能导致大语言模型(LLM)无法生成高质量的答案;而使用大块文本虽然上下文丰富,却容易引入噪音,降低检索的相关性。为了解决这一矛盾,LlamaIndex 提出了一种实用的索引策略——句子窗口检索(Sentence Window Retrieval)[1]。
句子窗口检索的思想可以概括为:为检索精确性而索引小块,为上下文丰富性而检索大块。
其工作流程如下:
-
索引阶段:在构建索引时,文档被分割成单个句子。每个句子都作为一个独立的“节点(Node)”存入向量数据库。同时,每个句子节点都会在元数据(metadata)中存储其上下文窗口,即该句子原文中的前N个和后N个句子。这个窗口内的文本不会被索引,仅仅是作为元数据存储。
-
检索阶段:当用户发起查询时,系统会在所有单一句子节点上执行相似度搜索。因为句子是表达完整语义的最小单位,所以这种方式可以非常精确地定位到与用户问题最相关的核心信息。
-
后处理阶段:在检索到最相关的句子节点后,系统会使用一个名为
MetadataReplacementPostProcessor
的后处理模块。该模块会读取到检索到的句子节点的元数据,并用元数据中存储的完整上下文窗口来替换节点中原来的单一句子内容。 -
生成阶段:最后,这些被替换了内容的、包含丰富上下文的节点被传递给LLM,用于生成最终的答案。
5.2 结构化索引
随着知识库的规模不断扩大(例如,包含数百个PDF文件),传统的RAG方法(即对所有文本块进行top-k相似度搜索)会遇到瓶颈。当一个查询可能只与其中一两个文档相关时,在整个文档库中进行无差别的向量搜索,不仅效率低下,还容易被不相关的文本块干扰,导致检索结果不精确。
为了解决这个问题,一个有效的方法是利用结构化索引。其原理是在索引文本块的同时,为其附加结构化的元数据(Metadata)。这些元数据可以是任何有助于筛选和定位信息的标签,例如:
- 文件名
- 文档创建日期
- 章节标题
- 作者
- 任何自定义的分类标签
通过这种方式,可以在检索时实现“元数据过滤”和“向量搜索”的结合。例如,当用户查询“请总结一下2023年第二季度财报中关于AI的论述”时,系统可以:
- 元数据预过滤:首先通过元数据筛选,只在
document_type == '财报'
、year == 2023
且quarter == 'Q2'
的文档子集中进行搜索。 - 向量搜索:然后,在经过滤的、范围更小的文本块集合中,执行针对查询“关于AI的论述”的向量相似度搜索。
这种“先过滤,再搜索”的策略,能够极大地缩小检索范围,显著提升大规模知识库场景下RAG应用的检索效率和准确性。LlamaIndex 提供了包括“自动检索”(Auto-Retrieval)在内的多种工具来支持这种结构化的检索范式。
参考资料
[2] Word2vec之CBOW和Skip-gram_skipgram算法-CSDN博客
[3] 大模型 | 搞懂Embedding - Word2Vec(Skip-Gram和CBOW)_word2vec cbow-CSDN博客
[4] 连续词袋模型(CBOW) - emanlee - 博客园
[5] word2vec连续词袋模型CBOW详解,使用Pytorch实现 - 知乎
[6] [1810.04805v2] BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding
[7] [1907.11692] RoBERTa: A Robustly Optimized BERT Pretraining Approach
[8] 一文理解Ranking Loss/Margin Loss/Triplet Loss - 知乎
[9] https://huggingface.co/spaces/mteb/leaderboard
[10] 【超详细版】CLIP(Contrastive Language-Image Pre-Training)模型概述_clip 模型-CSDN博客
[11] [2103.00020] Learning Transferable Visual Models From Natural Language Supervision
[13] Faiss:选择合适的索引Index本文探讨了在Faiss中选择合适的索引的重要性,并详细介绍了四种主要索引类型:Fla - 掘金
更多推荐
所有评论(0)