被分割的文本切片需要转换成向量保存在VertorStore中,才能实现基于自然语言的相似度检索。VectorStore是RAG架构的核心组件。它的主要作用是存储和检索非结构化数据的语义特征。简单来说,它就像一个具备模糊匹配能力的智能仓库。说到向量化,就不得不提一个名为嵌入的概念。
1. 嵌入(Embedding)
之前看到一个问题:“是什么成就了大语言模型?”,我想很多人的答案是深度神经网络。如果答案只有一个的话,我觉得嵌入居功至伟,如果没有嵌入将数据转换成可处理的向量,一切都是空谈。虽然现代的LLM都是通过深度神经网络实现的,但我认为肯定还存在其他的解决方案,但是所有的方案都绕不开基于向量的处理方式。
只要给定一个维度充分且定义合理的语义空间,原则上任何的事物都可以表示成这个空间中的一个点,这个点在语义空间中的位置可以表示成一个向量,该向量可以用来描述这个事物。比如我们描述一张纸,可以从纸张的形状、颜色、厚度、材质、纹理等维度来构建这个N维语义空间,那么任何一张纸都可以转换成基于此空间的一个N维向量。
LLM中的嵌入也是基于这个思想,当处理的文本被分词器转换成相应的词元后,需要将其转换成基于某个语义空间的向量。但是我们没有能力也没有必要确定这个语义空间每个维度具体表示什么,因为我们不关心某个词源对应向量的每个维度的表示,我们只关心两个向量在语义空间中的距离能否反映对应词元的相似度。假设“猫”、“狗”和“菠菜”这三个词元对应的向量分别为下A、B和C,A与B之间的距离必须小于与C的距离。在线性代数中,向量之间的具体可以采用不同的范数来表示,我们最熟悉的欧几里得范数。
对于我们日常获的各种有意义的文本片段,原则上两个词元在同一文本片段一起出现的频率基本能够体现出它们的相似度。那么问题就好解决了,我们只需要确定语义空间的维度数量,然后构建一个用于确定词元对应向量的模型,采用机器学习的方式使用现成的语料(比如爬取的网页内容、出版的图书文本等)训练此模型,训练的目标(损失函数)只有一个:让相似词元映射在语义空间中的点尽量靠近,反之尽量远离。
整个过程相当于通过将词元转换成对应的向量,将其嵌在语义空间的某个位置,这就是嵌入(Embedding)这一慨念的由来。我们称这个对词元实施向量化的过程为嵌入,将这个语义空间成为“嵌入空间”,将词元对应的向量称为嵌入向量,有时直接称为嵌入。
1.1 Embeddings
嵌入在LangChain中通过如下这个名为Embeddings的抽象类表示,确切地说,Embeddings代表的是实现文本向量化的嵌入模型。它定义了embed_documents和embed_query两个抽象方法,在将指定的一个或者多个文本存储在VectorStore之前,我们一般调用前者将其转换成一组通过list[float]表示的嵌入向量(列表的长度就是嵌入空间的维度数量),后者则在对VectorStore实施检索之前将查询文本转换成嵌入向量。
class Embeddings(ABC):
@abstractmethod
def embed_documents(self, texts: list[str]) -> list[list[float]]
@abstractmethod
def embed_query(self, text: str) -> list[float]
async def aembed_documents(self, texts: list[str]) -> list[list[float]]
async def aembed_query(self, text: str) -> list[float]
embed_documents方法所谓的Document更多地是指存储在VectorStore中的记录。除了这两个同步方法,Embeddings还定义了异步方法aembed_documents和aembed_query,它们会默认调用同步方法,大部分子类都会对它们进行重写。
1.2 OpenAIEmbeddings
OpenAIEmbeddings是调用OpenAI官方Embedding API的核心集成类。它继承自Embeddings,自身很多的字段成员都用于调用OpenAI API的客户端(OpenAI)配置,所以与ChatOpenAI的字段成员很相似,我在“OpenAI Chat模型的设计与实现”中对ChatOpenAI的所有字段成员有详细介绍。
class OpenAIEmbeddings(BaseModel, Embeddings):
client: Any = Field(default=None, exclude=True)
async_client: Any = Field(default=None, exclude=True)
model: str = "text-embedding-ada-002"
dimensions: int | None = None
deployment: str | None = model
openai_api_version: str | None = Field(
default_factory=from_env("OPENAI_API_VERSION", default=None),
alias="api_version",
)
openai_api_base: str | None = Field(
alias="base_url", default_factory=from_env("OPENAI_API_BASE", default=None)
)
openai_api_type: str | None = Field(
default_factory=from_env("OPENAI_API_TYPE", default=None)
)
openai_proxy: str | None = Field(
default_factory=from_env("OPENAI_PROXY", default=None)
)
embedding_ctx_length: int = 8191
openai_api_key: (
SecretStr | None | Callable[[], str] | Callable[[], Awaitable[str]]
) = Field(
alias="api_key", default_factory=secret_from_env("OPENAI_API_KEY", default=None)
)
openai_organization: str | None = Field(
alias="organization",
default_factory=from_env(
["OPENAI_ORG_ID", "OPENAI_ORGANIZATION"], default=None
),
)
allowed_special: Literal["all"] | set[str] | None = None
disallowed_special: Literal["all"] | set[str] | Sequence[str] | None = None
chunk_size: int = 1000
max_retries: int = 2
request_timeout: float | tuple[float, float] | Any | None = Field(
default=None, alias="timeout"
)
headers: Any = None
tiktoken_enabled: bool = True
tiktoken_model_name: str | None = None
show_progress_bar: bool = False
model_kwargs: dict[str, Any] = Field(default_factory=dict)
skip_empty: bool = False
default_headers: Mapping[str, str] | None = None
default_query: Mapping[str, object] | None = None
retry_min_seconds: int = 4
retry_max_seconds: int = 20
http_client: Any | None = None
http_async_client: Any | None = None
check_embedding_ctx_length: bool = True
def embed_documents(
self, texts: list[str], chunk_size: int | None = None, **kwargs: Any
) -> list[list[float]]
async def aembed_documents(
self, texts: list[str], chunk_size: int | None = None, **kwargs: Any
) -> list[list[float]]
def embed_query(self, text: str, **kwargs: Any) -> list[float]
async def aembed_query(self, text: str, **kwargs: Any) -> list[float]
从上面的定义可以看出,OpenAIEmbeddings默认使用的模型是text-embedding-ada-002。目前主流使用的是OpenAI的第三代(v3)模型,它们支持动态维度调整,在性能和成本之间提供了更好的平衡。下面的表格提供了三种嵌入模型之间的对比:
| 特性 | text-embedding-3-small | text-embedding-3-large | text-embedding-ada-002 |
|---|---|---|---|
| 默认维度 | 1,536 | 3,072 | 1,536 |
| 可调维度 | 可缩减至 512 | 可缩减至 256 或 1024 | 不支持 |
| 最大输入 | 8,191 Tokens | 8,191 Tokens | 8,191 Tokens |
| 特点 | 高性价比,适合通用搜索 | 精度最高,多语言性能极强 | 兼容性广,已逐渐被v3替代 |
下面的演示程序针对模型text-embedding-3-small创建了两个OpenAIEmbeddings,第一个采用默认的维度(1536),第二个采用指定的维度(512)。我们利用它对指定的文本实施嵌入操作,并验证生成嵌入向量的维度。
from dotenv import load_dotenv
from langchain_openai import OpenAIEmbeddings
load_dotenv()
text = "Sed ut perspiciatis unde omnis iste.Natus error sit voluptatem accusantium doloremque laudantium."
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
vector = embeddings.embed_query(text)
assert len(vector) == 1536
embeddings = OpenAIEmbeddings(model="text-embedding-3-small",dimensions = 512)
vector = embeddings.embed_query(text)
assert len(vector) == 512
2. VectorStore
在RAG的解决流程中,被切割后的文本会通过Embeddings转换成嵌入向量,连同关联的元数据存储到VectorStore中。由于Document是文本和元数据的复合体,所以VectorStore的很多操作都是金基于Document进行的。这也是为什么上面介绍的很多分割器类型直接将Document对象作为分割后的产物,有的还提供方法将指定的文本/对象列表和元数据映射表进行合并生成一组Document。由于需要对文本进行向量化,所以作为向量存储基类的VectorStore必须拥有一个Embeddings对象,它对应着该抽象类的embeddings特性。
class VectorStore(ABC):
@property
def embeddings(self) -> Embeddings | None
2.1 向量记录的添加与删除
VectorStore提供了一系列方法实现针对向量存储的读写。具体的写操作体现在如下所述的三组方法,它们分布实现对存储记录的添加和删除。存储的记录可以通过get_by_ids方法提取出来,方法返回的是Document的列表。
class VectorStore(ABC):
def add_texts(
self,
texts: Iterable[str],
metadatas: list[dict] | None = None,
*,
ids: list[str] | None = None,
**kwargs: Any,
) -> list[str]
async def aadd_texts(
self,
texts: Iterable[str],
metadatas: list[dict] | None = None,
*,
ids: list[str] | None = None,
**kwargs: Any,
) -> list[str]
def add_documents(self, documents: list[Document], **kwargs: Any) -> list[str]
async def aadd_documents(self, documents: list[Document], **kwargs: Any) -> list[str]
def delete(self, ids: list[str] | None = None, **kwargs: Any) -> bool | None
async def adelete(self, ids: list[str] | None = None, **kwargs: Any) -> bool | None
def get_by_ids(self, ids: Sequence[str], /) -> list[Document]
三组方法说明如下:
- add_texts/aadd_texts:需要以可迭代对象和列表形式提供文本和元数据,如果没有通过
ids参数提供输入记录的标识(相当于数据记录的主键),方法会自动创标识。方法返回添加记录的ID列表; - add_documents/aadd_documents:它以
Document的形式提供添加记录的文本和元数据; - delete/adelete:删除指定ID列表对应的记录,方法返回是否成功的布尔值。在生产环境下,如果原始文档更新了,我们需要先根据ID删除旧的分块,再重新插入新的,否则会导致检索到过期信息。
class VectorStore(ABC):
@classmethod
def from_documents(
cls,
documents: list[Document],
embedding: Embeddings,
**kwargs: Any,
) -> Self
@classmethod
async def afrom_documents(
cls,
documents: list[Document],
embedding: Embeddings,
**kwargs: Any,
) -> Self
@classmethod
@abstractmethod
def from_texts(
cls: type[VST],
texts: list[str],
embedding: Embeddings,
metadatas: list[dict] | None = None,
*,
ids: list[str] | None = None,
**kwargs: Any,
) -> VST
@classmethod
async def afrom_texts(
cls,
texts: list[str],
embedding: Embeddings,
metadatas: list[dict] | None = None,
*,
ids: list[str] | None = None,
**kwargs: Any,
) -> Self
上面定义的这两组类方法(from_documents/afrom_documents和from_texts/afrom_texts)同时完成了针对VectorStore的创建和数据的添加,其中from_texts是具体向量存储类型必须实现的抽象方法。
2.2 普通相似度检索
将关联元数据的切片存入VectorStore不是我们的目的,最终目的是能够根据用户指定的基于自然语言的查询文本实施检索,返回相关的记录的元数据。传统关系型和NonSQL提供的文本查询只能完成基于是否包含指定字符检索,VectorStore提供的是基于两个向量的相似度的检索。如果两个对象能够使用同一个描述空间的向量来表示,它们之间的相似度可以利用向量之间的距离来表示。采用不同的范数,向量距离具有不同的计算方式,我们VectorStore通常使用如下两种计算方式:
- 欧氏距离:嵌入空间两个点的物理距离。距离越短,相似度越高;
- 余弦距离:不看物理距离,只看向量的方向。如果将两个向量的夹角表示成θ,可以使用
cosine(θ)的表达式来表示距离。由于cosine是θ的减函数,本着相似度与距离成反向关系的原则,我们将(1 - cosine(θ))作为余弦距离。cosine(θ)表示余弦相似度;
在大规模向量检索中,余弦相似度比欧氏距离更受欢迎,主要有以下几个核心原因。
- 关注语义而非长度:在Embedding 模型中,向量的方向代表了语义特征,而向量的长度往往受文本长度或高频词影响,难以真正反映语义相关性。余弦相似度只看夹角,即使两段话长度不同,只要意思接近,它们的余弦值就会很高;
- 归一化特性:余弦相似度的结果始终在
[-1, 1]之间(在Embedding中通常是[0, 1])。这种标准化的范围让开发者更容易设定阈值来过滤低相关性的结果。相比之下,欧氏距离的数值范围是[0, ∞),很难直观判断多远才算不相关; - 计算效率高:当向量经过归一化处理后,余弦相似度的计算可以简化为两个向量的点积。点积在现代CPU和GPU上都有极高的硬件加速效率,非常适合处理千万级以上的数据检索;
- 高维空间的鲁棒性:在高维空间中,点与点之间的欧氏距离往往会趋于相等(维度灾难),导致区分度下降。而余弦相似度通过对比角度,在处理这种稀疏或高维语义空间时表现更稳定。
所在很多具体的VectorStore(比如InMemoryVectorStore)都默认使用余弦相似度。
2.2.1 基于向量的查询
将查询文本转换成向量后,可以调用如下所示的similarity_search_by_vector/asimilarity_search_by_vector方法实施相似度查询,查询结果以Document列表的形式返回。VectorStore并没有真正实现similarity_search_by_vector方法,asimilarity_search_by_vector仅仅是以Coroutine的形式调用了该方法(以下介绍的异步方法基本采用这样的实现方式),所以具体的子类需自行实现此方法。VectorStore将针对similarity_search_by_vector方法的实现下放给了子类。
class VectorStore(ABC):
def similarity_search_by_vector(self, embedding: list[float], k: int = 4, **kwargs: Any) -> list[Document]:
raise NotImplementedError
async def asimilarity_search_by_vector(self, embedding: list[float], k: int = 4, **kwargs: Any) -> list[Document]:
return await run_in_executor(None, self.similarity_search_by_vector, embedding, k=k, **kwargs)
上述两个方法包含如下的参数:
- embedding:原始查询文本转换成的查询向量;
- k:指定返回最相似的
Document数量。如果k太小(如1),LLM可能拿不到足够的信息;如果k太大(如20),可能会引入无关噪音并浪费Token。通常3-5是平衡点,所有的查询方法都有此参数; - kwargs:通常用来传递元数据过滤器。例如,只在
source='manual_v2.pdf'的范围内进行相似度搜索。
2.2.2 基于文本的查询
similarity_search/asimilarity_search方法的相似性查询直接针对原始的查询文本进行,其内部会利用Embeddings对象对其向量化。similarity_search被定义成必须实现的抽象方法。
class VectorStore(ABC):
@abstractmethod
def similarity_search(self, query: str, k: int = 4, **kwargs: Any) -> list[Document]
async def asimilarity_search(self, query: str, k: int = 4, **kwargs: Any) -> list[Document]
2.2.3 返回相似度分数
如下所示的similarity_search_with_score/asimilarity_search_with_score除了返回Document之外,还会返回一个相似度分数。一般来说,我们使用这个方法返回原始的分数,比如上述的两种距离。由于很多类型的距离是没有范围限制的,基于不同尺度的得分其实没有什么意义,所以我们更希望得到一个归一化的得分,VectorStore为此定义了similarity_search_with_relevance_scores/asimilarity_search_with_relevance_scores。这两个方法返回的相关性得分限定在[0,1]之间,分数越高,相关性越强。
class VectorStore(ABC):
def similarity_search_with_score(
self, *args: Any, **kwargs: Any
) -> list[tuple[Document, float]]
async def asimilarity_search_with_score(
self, *args: Any, **kwargs: Any
) -> list[tuple[Document, float]]
def similarity_search_with_relevance_scores(
self,
query: str,
k: int = 4,
**kwargs: Any,
) -> list[tuple[Document, float]]
async def asimilarity_search_with_relevance_scores(
self,
query: str,
k: int = 4,
**kwargs: Any,
) -> list[tuple[Document, float]]
similarity_search_with_relevance_scores/asimilarity_search_with_relevance_scores默认会调用similarity_search_with_score/asimilarity_search_with_score方法,并使用如下这个_select_relevance_score_fn方法对后者返回的分数实施会归一化。由于_select_relevance_score_fn方法并没有真正被实现,若子类没有重写此方法,调用这两个方法会抛出异常。VectorStore提供了_euclidean_relevance_score_fn和_cosine_relevance_score_fn这两个静态方法对欧式距离和余弦距离进行归一化,我们可以根据当前采用的相似度算法利用它们来实现_select_relevance_score_fn方法。
class VectorStore(ABC):
@staticmethod
def _euclidean_relevance_score_fn(distance: float) -> float:
return 1.0 - distance / math.sqrt(2)
@staticmethod
def _cosine_relevance_score_fn(distance: float) -> float:
return 1.0 - distance
def _select_relevance_score_fn(self) -> Callable[[float], float]:
raise NotImplementedError
2.3 最大边界相关性检索
普通的相似度查询采用K-最近邻算法(KNN:K-Nearest Neighbors)只会找最像的,容易导致搜出来的几条内容高度重复。为了保证相关性的同时让结果更具多样化,可以采用最大边界相关性(MMR:Max Marginal Relevance)算法。举个例子,假设我们的查询条件是:“如何评价周杰伦的音乐?”
- KNN检索:可能给你找回5条都在夸他中国风的段落。信息冗余,浪费了LLM的上下文窗口;
- MMR检索:会先选一条讲中国风的,第二条发现也是讲中国风的(太像了,剔除),转而选一条讲编曲技巧的,第三条选讲社会影响的。
MMR在挑选每一条文档时,会权衡两个指标:
- 相关性 (Relevance):这条文档离我的问题有多近?
- 多样性 (Diversity):这条文档和已经选出来的文档是不是太像了?
MMR公式中有一个关键系数λ(通常在0到1之间),默认值(通常为 0.5)在相关性和多样性之间取得平衡:
- λ = 1:完全等同于普通的相似度搜索(只看相关性);
- λ = 0:完全追求多样性(选出来的东西互不相同,哪怕离问题有点远)。
如果需要使用MMR查询,可以调用VectorStore的如下两组方法:max_marginal_relevance_search/amax_marginal_relevance_search和max_marginal_relevance_search_by_vector/amax_marginal_relevance_search_by_vector。max_marginal_relevance_search和max_marginal_relevance_search_by_vector依然没有被真正实现,提供MMR检索功能的子类需要重写它们。
class VectorStore(ABC):
def max_marginal_relevance_search(
self,
query: str,
k: int = 4,
fetch_k: int = 20,
lambda_mult: float = 0.5,
**kwargs: Any,
) -> list[Document]:
raise NotImplementedError
async def amax_marginal_relevance_search(
self,
query: str,
k: int = 4,
fetch_k: int = 20,
lambda_mult: float = 0.5,
**kwargs: Any,
) -> list[Document]
def max_marginal_relevance_search_by_vector(
self,
embedding: list[float],
k: int = 4,
fetch_k: int = 20,
lambda_mult: float = 0.5,
**kwargs: Any,
) -> list[Document]:
raise NotImplementedError
async def amax_marginal_relevance_search_by_vector(
self,
embedding: list[float],
k: int = 4,
fetch_k: int = 20,
lambda_mult: float = 0.5,
**kwargs: Any,
) -> list[Document]
如下是相关参数说明:
- query/embedding:查询文本或者嵌入向量;
- k: 最终选择记录数;
- fetch_k:MMR检索的候选记录是KNN检索的结果,该参数表示此检索结果的记录数;
- lambda_mult: 上述的系数λ;
2.4 统一收口
对于上面实现的众多形式的相似度查询方法,VectorStore定义了如下的search/asearch方法来统一收口,我们可以利用search_type参数决定具体调用的方法。
class VectorStore(ABC):
def search(self, query: str, search_type: str, **kwargs: Any) -> list[Document]:
if search_type == "similarity":
return self.similarity_search(query, **kwargs)
if search_type == "similarity_score_threshold":
docs_and_similarities = self.similarity_search_with_relevance_scores(
query, **kwargs
)
return [doc for doc, _ in docs_and_similarities]
if search_type == "mmr":
return self.max_marginal_relevance_search(query, **kwargs)
msg = (
f"search_type of {search_type} not allowed. Expected "
"search_type to be 'similarity', 'similarity_score_threshold'"
" or 'mmr'."
)
raise ValueError(msg)
async def asearch(self, query: str, search_type: str, **kwargs: Any) -> list[Document]
search_type参数具有如下三个选项:
- similarity:调用
similarity_search/asimilarity_search方法; - similarity_score_threshold:调用
similarity_search_with_relevance_scores/asimilarity_search_with_relevance_scores方法; - mmr:调用
max_marginal_relevance_search/amax_marginal_relevance_search方法;
3. 自定义VectorStore
在如下的演示程序中,我们继承VectorStore定义了一个SimpleVectorStore类。构造函数除了指定Embeddings对象之外,还利用similarity参数指定了采用的相似度算法(基于欧式距离或者余弦距离)。根据指定的算法,我们定义相应的函数dist_fn用来计算两个嵌入向量的距离,并对用来计算归一化相关分数的_select_relevance_score_fn函数进行了相应设置。
from typing import Any, Iterable,NamedTuple,Callable,Literal, Self,Literal
import uuid
from dotenv import load_dotenv
from langchain_core.documents import Document
from typing_extensions import override
from langchain_openai import OpenAIEmbeddings
from langchain_core.vectorstores import VectorStore,InMemoryVectorStore
from langchain_core.embeddings import Embeddings
from itertools import islice
import math
load_dotenv()
Vector = list[float]
class Record(NamedTuple):
id:str
vector:list[float]
text:str
metadata:dict[str,Any]
class SimpleVectorStore(VectorStore):
store: list[Record]
dist_fn:Callable[[Vector,Vector],float]
def __init__(self, embedding: Embeddings, similarity:Literal["cosine","euclidean"] ="cosine") -> None:
def cosine_dist(v1: Vector, v2: Vector) -> float:
import numpy as np
norm_v1 = np.linalg.norm(v1)
norm_v2 = np.linalg.norm(v2)
return 1- np.dot(v1, v2) / (norm_v1 * norm_v2)
self.store = []
self.embedding = embedding
if similarity == "euclidean":
self.dist_fn = lambda v1, v2: math.dist(v1, v2)
self._select_relevance_score_fn = lambda : self._euclidean_relevance_score_fn
else:
self.dist_fn = cosine_dist
self._select_relevance_score_fn = lambda : self._cosine_relevance_score_fn
def add_texts(
self,
texts: Iterable[str],
metadatas: list[dict] | None = None,
*,
ids: list[str] | None = None,
**kwargs: Any,
) -> list[str]:
count = sum(1 for _ in texts)
metadatas = metadatas or [{}] * count
ids = ids or [str(uuid.uuid4()) for _ in range(count)]
for id, text, metadata in zip(ids, texts, metadatas):
vector = self.embedding.embed_query(text)
self.store.append(Record(id=id, vector=vector, text=text, metadata=metadata))
return ids
def similarity_search(self, query: str, k: int = 4, **kwargs: Any) -> list[Document]:
result = self.similarity_search_with_score(query, k=k, **kwargs)
return [doc for doc, score in result]
@override
def similarity_search_with_score(self, query: str, k: int = 4, **kwargs: Any) -> list[tuple[Document, float]]:
query_embedding = self.embedding.embed_query(query)
filter = lambda it: all(it.metadata.get(k) == v for k, v in kwargs.items())
selected = (it for it in self.store if filter(it))
result = ((it, self.dist_fn(query_embedding, it.vector)) for it in selected)
top_k = islice(sorted(result, key=lambda x: x[1]), k)
return [(Document(page_content=it.text, metadata=it.metadata), score) for it, score in top_k]
@classmethod
@override
def from_texts(
cls,
texts: list[str],
embedding: Embeddings,
metadatas: list[dict] | None = None,
similarity:Literal["cosine","euclidean"] ="cosine",
**kwargs: Any,
) -> Self:
store = cls(
embedding=embedding,
similarity=similarity,
)
store.add_texts(texts=texts, metadatas=metadatas, **kwargs)
return store
实现的add_texts方法将指定的文本、ID和元数据列表转换成对应的Record元组,如果没有指定ID,则使用生成的UUID。Record元组被置于表示存储的store字段,这是一个普通的列表。我们重写了similarity_search_with_score方法,根据指定的关键字参数对记录进行过滤。在为指定的查询文本生成嵌入向量后,我们利用dist_fn字段返回的函数计算相似度距离。我们最后按照此距离对过滤记录进行排序,提取前K条记录生成返回的(Document, Distance)元组列表。实现的similarity_search方法直接调用此方法,将Document提取出来作为返回值。
我们使用如下的程序来验证SimpleVectorStore提供的相似对检索是否合理。我们使用的是OpenAIEmbeddings,添加的两条记录是两条针对“美食”和“运动”喜好的文本,而作为查询的是针对前者的查询,所以我们希望前者与查询的相似度更大。我们分别针对两种相似度算法创建了SimpleVectorStore对象,并调用了similarity_search_with_score和similarity_search_with_relevance_scores方法看看两条记录与查询文本的“相似度距离”和“相关性得分”。
embedding=OpenAIEmbeddings(model="text-embedding-3-small")
texts=[
"I love eating spicy Sichuan food.",
"My favorite sport is swimming."]
query = "What are my food preferences?"
print(f"euclidean similarity:")
store = SimpleVectorStore.from_texts(
texts=texts,
embedding=embedding,
metadatas=[{"category": "food"}, {"category": "sport"}],
similarity = "euclidean"
)
result = store.similarity_search_with_score(query, k=2)
for doc, score in result:
print(f"Raw score: {score:.4f}, Content: {doc.page_content}")
result = store.similarity_search_with_relevance_scores(query, k=2)
for doc, score in result:
print(f"Relevance score: {score:.4f}, Content: {doc.page_content}")
print(f"cosine similarity:")
store = SimpleVectorStore.from_texts(
texts=texts,
embedding=embedding,
metadatas=[{"category": "food"}, {"category": "sport"}],
similarity = "cosine"
)
result = store.similarity_search_with_score(query, k=2)
for doc, score in result:
print(f"Raw score: {score:.4f}, Content: {doc.page_content}")
result = store.similarity_search_with_relevance_scores(query, k=2)
for doc, score in result:
print(f"Relevance score: {score:.4f}, Content: {doc.page_content}")
从如下的输出可以看出,不论是采用欧式距离还是预先距离,第一条记录和查询文本的距离都要小于第二条与查询文本的距离,相关性得分则刚好相反,这符合我们的预期。
euclidean similarity:
Raw score: 1.1316, Content: I love eating spicy Sichuan food.
Raw score: 1.2591, Content: My favorite sport is swimming.
Relevance score: 0.1998, Content: I love eating spicy Sichuan food.
Relevance score: 0.1097, Content: My favorite sport is swimming.
cosine similarity:
Raw score: 0.6401, Content: I love eating spicy Sichuan food.
Raw score: 0.7929, Content: My favorite sport is swimming.
Relevance score: 0.3599, Content: I love eating spicy Sichuan food.
Relevance score: 0.2071, Content: My favorite sport is swimming.
4585



被折叠的 条评论
为什么被折叠?



