Langchain核心组件---文本向量

一、Langchain核心组件(Components)

1. 文本向量

1.1 嵌入与嵌入模型(Embedding and Embedding Models)

计算机天生擅长处理数字,但不理解文字、图片的含义。嵌入(Embedding)的核心思想就是将人类
世界的符号(如单词、句子、产品、用户、图片)转换为计算机能够理解的数值形式(即向量,本质
上是一个数字列表),并且要求这种转换能够保留原始符号的语义和关系。

我们可以把它想象成一个翻译过程,把人类语言“翻译”成计算机的“数学语言”。

说明:我们之前一直用的大语言模型是生成式模型。它理解输入并生成新的文本(回答问题、写文
章)。它内部实际上也使用嵌入技术来理解输入,但最终目标是“创造”。

而嵌入模型(Embedding Models)是表示型模型。它的目标不是生成文本,而是为输入的文本创建
一个最佳的、富含语义的数值表示(向量)。如 OpenAI 的 “text-embedding-3-large” 嵌入模型;
Google 的 “gemini-embedding-001” 嵌入模型;阿里的 “Qwen3-Embedding-8B” 嵌入模型等。

1.1.1 什么是向量?

首先我们要知道,嵌入的结果是就是一个向量,它本质上是一个数字列表(一维数组)。例如:
[0.023, 0.487, -0.129, ..., 0.325] 。对于向量来说,有两个关键概念需要了解:

向量维度

嵌入结果得到的列表长度是固定的,称为向量的“维度”。例如,OpenAI 的 text-embedding- ada-002 模型会生成一个 1536 维的向量, text-embedding-3-large 模型会生成一个 3072 维
的向量。

维度越高,通常能捕捉更细微的语义信息,但也需要更多的计算和存储资源。

向量空间

想象一个无限延伸的、拥有无数个维度的宇宙,这个宇宙就是一个向量空间。这有点抽象,可以想象
一下:

  • 在三维世界里,一个点可以用 (x, y, z) 坐标表示,例如 (2, 5, -1) 。
  • 在机器学习的高维向量空间中,一个点可能是 (0.1, 0.7, -0.2, 0.4, …, 0.02) ,一
    个有几百或几千个数字的坐标。

在这个空间里,每个点(即每个向量)都能代表一个概念。例如在嵌入模型中,一个点可以代表一个
单词、一句话、一张图片、一个用户、一部电影等。

到这里,向量空间的威力就能体现出来:我们可以用数学来度量语义。可以通过计算两个向量之间
的“距离”或“相似度”来实现这一点。

  • 欧氏距离(Euclidean Distance):就是我们高中几何学的两点之间的直线距离。距离越短,相似
    度越高。
  • 余弦相似度(Cosine Similarity):它忽略向量的绝对长度(大小),只关注两个向量在方向上的
    差异。在文本和语义的世界里,“方向”代表“含义”,而“长度”往往只代表“文本的长
    度”或“词汇的多少”。换句话说,余弦相似度关注的是 “你们是否指向同一个方向” / “你们是
    否代表同一个含义”

因此,在捕捉语义上的相似性上,余弦相似度是更常用的度量方式。

我们又能反推出,由于使用向量来绘制向量空间,而向量是有维度的,维度越高,则更能捕捉极其细
微和复杂的语义差别(比如“高兴”和“喜悦”的区别)。

这能干什么?这能解决一个传统数据库(如MySQL)不擅长的问题:基于内容的相似性搜索,而不是
基于精确匹配的查询。

1.1.2 嵌入模型应用场景

对于嵌入模型,实际上在示例选择器部分,我们已经使用过。当时使用的场景就是可以根据语义相似
性完成示例的筛选。

根据嵌入的特性,由此延伸出了许多嵌入模型在 AI 应用的使用场景:

  • 语义搜索(Semantic Search)

传统搜索依赖关键词匹配(搜 “苹果” ,只能找到包含 “苹果” 这个词的文档)。

语义搜索则能将查询(如 “一种红色的水果” )和文档库中的所有文档都转换为向量。然后计算查询
向量与所有文档向量的相似度,返回最相似的文档。这样即使文档里没有 “红色” 和 “水果” 这些
词,但只要它是关于 “苹果” 的,就能被找到。

如下图为我们展示了借助嵌入模型进行文档搜索的过程:

  1. 为多文档生成其各自的向量,
  2. 为搜索查询语句生成向量,
  3. 衡量查询向量与每个文档向量之间的相似性,得到相似度最高的文档。
  • 检索增强生成(Retrieval-Augmented Generation, RAG)

这是当前大语言模型应用的核心模式。当用户向 LLM 提问时,系统首先使用嵌入模型在知识库(如公
司内部文档)中进行语义搜索,找到最相关的内容,然后将这些内容和问题一起交给 LLM 来生成答
案。这极大地提高了答案的准确性和时效性。

  • 推荐系统(Recommendation Systems)

将用户(根据其历史行为、偏好)和物品(商品、电影、新闻)都转换为向量。喜欢相似物品的用
户,其向量会接近;相似的物品,其向量也会接近。通过计算用户和物品向量的相似度,就可以进行
精准推荐。

  • 异常检测(Anomaly Detection)

正常数据的向量通常会聚集在一起。如果一个新数据的向量远离大多数向量的聚集区,它就可能是一
个异常点(如垃圾邮件、欺诈交易)。

1.2 Embeddings 嵌入模型类

在 LangChain 中,有很多的嵌入模型提供方,使用不同的模型提供方,需要安装为其各自包,例如:

  • OpenAI: pip install -U langchain-openai
  • Ollama: pip install -U langchain-ollama
  • Google Gemini: pip install -U langchain-google-genai
  • 更多见这里
1.2.1 定义嵌入模型

在这里我们选择 OpenAI 来进行后续操作。定义 OpenAI 下的嵌入模型使用:
class langchain_openai.embeddings.base.OpenAIEmbeddings ,官方接口介绍见这里

定义 OpenAIEmbeddings 嵌入模型类与定义聊天模型类似,如下所示:

from langchain_openai import OpenAIEmbeddings

embeddings = OpenAIEmbeddings(
    model="text-embedding-3-large", # 是 OpenAI 2024年发布的最新嵌入模型,生成3072维
的高质量向量
)

在 LangChain 框架中基础 Embeddings 类( OpenAIEmbeddings 继承了它)设计了两个核心方法
来处理文本嵌入。

  • .embed_documents() : 用于处理文档 Documents 。它的输入是多个文本。例如要将一个
    知识库里的所有段落都转换成向量后存入数据库,就会使用这个方法。
    • 它返回一个【二维列表】 List[List[float]] 。外层列表的每个元素对应一个输入文档,
      内层列表则是该文档的向量表示。
  • .embed_query() : 用于处理查询 Query 。它的输入是单个文本(一个字符串,str)。例
    如,当用户提出一个问题时,需要将这个问题转换成向量,以便在数据库中搜索相似的文档段落,
    就会使用这个方法。
    • 它返回一个【一维列表】,里面是浮点数( List[float] ),代表单个查询文本的向量。

其实分别对应下图中文档与查询的向量生成:

之所以设计成两个方法,是因为某些嵌入模型提供商(如 OpenAI、Cohere 等)会针对 “被搜索的文
档” 和 “搜索查询本身” 采用不同的优化策略和模型。即使底层是同一个模型,也可能对两者进行不
同的预处理(例如添加不同的指令前缀),以获得更好的搜索效果。

1 .2.2 嵌入文档列表

embed_documents 的语义是 “索引”。它的目的是预处理大量文本,为它们创建向量表示,以便
后续被搜索。这一般是一个离线、批量处理的过程。代码如下:

from langchain_openai import OpenAIEmbeddings
from langchain_community.document_loaders import UnstructuredMarkdownLoader
from langchain_text_splitters import CharacterTextSplitter

markdown_path = "../Docs/Markdown/脚手架级微服务租房平台Q&A.md"

# single 模式加载后,默认只有一个 Document 对象
loader = UnstructuredMarkdownLoader(markdown_path)
data = loader.load()

# 生成分割器
text_splitter = CharacterTextSplitter.from_tiktoken_encoder(
    encoding_name="cl100k_base", chunk_size=200, chunk_overlap=50
)

# 分割文档
documents = text_splitter.split_documents(data)

# 定义嵌入模型
embeddings = OpenAIEmbeddings(model="text-embedding-3-large")

# 嵌入文档列表,生成向量列表
# 注意这里需要提取文档内容为字符串列表,才能传递给嵌入模型
texts = [doc.page_content for doc in documents]
documents_vector = embeddings.embed_documents(texts)

print(f"文档数量为:{len(documents)},生成了{len(documents_vector)}个向量的列表")
print(f"第一个文档向量维度:{len(documents_vector[0])}")
print(f"第二个文档向量维度:{len(documents_vector[1])}")

结果如下:

Created a chunk of size 916, which is longer than the specified 200
Created a chunk of size 916, which is longer than the specified 200
Created a chunk of size 260, which is longer than the specified 200
文档数量为:96,生成了96个向量的列表
第一个文档向量维度:3072
第二个文档向量维度:3072

可以看到 text-embedding-3-large 嵌入模型,生成的便是3072维的高质量向量。共存在96个文
档,因此生成了96个向量的列表。

1.2.3 嵌入单个查询

embed_query 的语义是 “搜索”。它的目的是在用户发起请求时,实时地将一个问题或指令转换
为向量,用于在已索引的文档向量中进行检索。这是一个在线、实时、按需处理的过程。

为单个查询生成向量的代码如下:

from langchain_openai import OpenAIEmbeddings

# 定义嵌入模型
embeddings = OpenAIEmbeddings(model="text-embedding-3-large")

# 嵌入单个查询
query_vector = embeddings.embed_query("项目中遇到了哪些挑战?如何解决?")

print(f"向量维度:{len(query_vector)}")
print(f"向量前五个数值为:{query_vector[:5]}")

结果如下:

向量维度:3072
向量前五个数值为:[-0.01306734886020422, -0.004718094132840633,
-0.005446741823107004, -0.014186487533152103, 0.004673811607062817]

1.3 向量存储(Vector Stores)

1.3.1 向量数据库介绍

在 LangChain 中,实际并不需要我们直接手动调用嵌入模型去生成向量,然后手动去比较向量。在我
们之前提供的 RAG 知识地图中,存在一个 Vector Stores 向量存储,如下图所示:

实际上,向量是被管理在专门的向量存储介质中,如向量数据库。向量存储的核心任务是解决一个传
统数据库(如MySQL)不擅长的问题:基于内容的相似性搜索(Similarity Search),而不是基于精
确匹配的查询。

根据上图,我们想要从向量数据库中进行相似度搜索,首先就需要将向量存储管理起来。想象一下,
一篇文档若转换成一个有1536个浮点数的向量。一百万篇文档就是1536MB(约1.5GB)的纯向量数
据。这只是一个起点,现实中的数据集可能轻松达到千万甚至亿级。如何高效地存储和管理这些向
量?

向量数据库则提供了专门用于高效存储、管理和检索高维向量的能力。其核心就是 “高效地组织和检
索这些数据”。

常见的向量数据库核心机制如下:

  • 专门的索引

这是向量数据库的灵魂。它们不会使用暴力搜索,而是会预先为所有向量构建一种特殊的索引结构。

常见的方法有近似最近邻(ANN)搜索:为了追求极致的速度,它愿意牺牲一点点精度。它不会保证
找到绝对最相似的向量(即最近邻),但能以极高的概率找到非常相似的向量。通过聚类、分层、压
缩等算法技术,将搜索范围从“整个数据库”缩小到“几个最可能的候选集”。

这就像在一个大图书馆里找书,你不是从 A 到 Z 遍历所有书架,而是先根据分类(文学、历史、科
学)找到大概区域,再在这个区域内仔细找,这样快得多。

  • 向量相似度计算优化

向量数据库底层使用高度优化的库来进行向量运算。如 FAISS 向量数据库,它是 Facebook AI 研究院
开发的一种高效的相似性搜索和聚类的库。它能够快速处理大规模数据,并且支持在高维空间中进行
相似性搜索。

这些库充分利用了 CPU 的 SIMD 指令集和 GPU 的并行计算能力,让大规模的向量计算速度极快。

SIMD 指令集指 “单指令多数据流” 技术,是一种采用一个控制器来控制多个处理器,本质上非常类
似一个向量处理器,可对控制器上的一组数据(又称“数据向量”) 同时分别执行相同的操作从而
实现空间上的并行。简单来说,就是一个指令能够同时处理多个数据。

  • 数据管理功能

现代的向量数据库不仅有专门的索引,还提供了完整的数据管理功能:

  • CRUD 操作:支持增删改查,可以动态地更新向量数据。
  • 元数据过滤:这是非常关键的功能。除了向量本身,每篇文档还有元数据(Metadata),比如创
    建时间、作者、类别等。向量数据库允许你先用元数据过滤(“找出xxxx年以后的、属于科技类
    别的文档”),再在这个缩小的范围内进行向量相似度搜索,极大地提升了准确性和效率。
  • 可扩展性与持久化:它们可以轻松地分布式部署,处理海量数据;同时保证数据持久化,不像纯
    内存方案一样断电丢失。
  • 集成方便:提供友好的API(如gRPC, RESTful),使得像 LangChain 这样的框架可以轻松地与
    之集成,开发者无需关心底层细节。

因此,我们可以利用专门的向量数据库(如Chroma, Weaviate, Pinecone, Qdrant, Milvus等),通过
构建索引、优化计算流程、并提供丰富的过滤和管理功能,从而实现对海量高维向量数据进行快速、
可扩展的【相似性检索】的一套完整技术方案。

LangChain 框架则通过与这些向量数据库集成,让开发者无需手动处理向量生成、存储和比较的复杂
性,只需关注业务逻辑本身,极大地提高了开发效率和应用性能。

下面我们将演示如何使用 LangChain 提供的内存级存储,与集成的向量数据库。查看 LangChain 支持
的所有向量数据库点击这里。除了课件上的向量数据库使用,其他存储可查阅官方文档自行接入。

1.3.2 内存存储

我们将使用 LangChain 的 InMemoryVectorStore 来实现向量的内存存储。

1.3.2.1 基本操作
  • 初始化

LangChain 中的大多数向量在初始化向量存储时接受嵌入模型作为参数。如下所示:

from langchain_openai import OpenAIEmbeddings
from langchain_core.vectorstores import InMemoryVectorStore

# 定义嵌入模型
embeddings = OpenAIEmbeddings(model="text-embedding-3-large")

# 内存存储初始化
vector_store = InMemoryVectorStore(embedding=embeddings)
  • 添加文档

我们可以使用 add_documents 方法,向内存存储中去添加文档。要注意的是,该方法会为添加的
文档编排索引,索引列表随着该方法返回。

这也就是在前文中,我们一直在提的:当我们想对某文本进行【数据检索】时的两个步骤:

  • 编制索引: 用于从源中摄取数据并为其编制索引。
  • 检索和生成 :接受用户查询并从索引中检索相关数据,然后将其传递给模型。

对于第一步,我们终于要完成了。如下所示:

from langchain_community.document_loaders import UnstructuredMarkdownLoader
from langchain_text_splitters import CharacterTextSplitter

# 生成分割器
text_splitter = CharacterTextSplitter.from_tiktoken_encoder(
    encoding_name="cl100k_base", chunk_size=200, chunk_overlap=50
)

# 加载文档
data = UnstructuredMarkdownLoader("../Docs/Markdown/脚手架级微服务租房平台
Q&A.md").load()

# 分割文档
documents = text_splitter.split_documents(data)

# 添加文档
ids = vector_store.add_documents(documents=documents)

print(f"共编排了{len(ids)}个文档索引")
print(f"前3个文档的索引是:{ids[:3]}")

结果输出:

共编排了96个文档索引
前3个文档的索引是:['3b22073d-8a8f-4f74-b99a-09d6343e7602', '3c2e05ea-1681-4d5ea31c-f01f2e0a0ac8', 'dcc3da40-b1bf-4ae3-9fd4-1f758b47af0f']
  • 获取文档

使用 get_by_ids 方法,通过索引列表获取对应的文档列表。如下所示:

doc_3 = vector_store.get_by_ids(ids[:3])
print(f"{[doc.page_content for doc in doc_3]}")

结果如下:

[
    '通用问题\n\n为什么做这个项目?\n\n回答1:(出于兴趣爱好开发)\n\n大学期间,我和同学
在外合租过一段时间,使用了一些租房平台,于是我有个想法,自己能不能开发一个租房平台,可以让
我将理论知识与实践相结合。我希望通过实际项目来加深对 Java 编程语言和相关技术的理解。于是我
便查找了一些资料,看了一些开源项目,进行了一些改进。\n\n回答2:(开源项目的解释)',
    '回答2:(开源项目的解释)\n\n这个项目其实是在 github 上找到的一个开源项目,主要是可
以支持一些常规的聊天项目,我对于聊天如何实现的比较感兴趣。顺便也想锻炼一下自己工程代码能
力,在网上就找到了这个开源项目和项目的一些比较完善的文档和介绍,再加上找了一个业务场景:租
房。所以就确定了这个项目。\n\n回答3:(学校课设的解释)',
    '回答3:(学校课设的解释)\n\n这是学校的课设题目,然后在课设项目的基础上,查找了一些
资料,进行了一些改进。\n\n这个项目为啥和上个(之前)同学的项目一样?\n\n回答1:(开源项目的
回答)'
]
  • 删除文档

使用 delete 方法,删除传入索引列表对应的文档列表;若不传入索引列表,则认为全量删除。如下
所示:

vector_store.delete(ids=ids[:3])

测试一下删除是否成功:

doc_3 = vector_store.get_by_ids(ids[:3])
print(f"{[doc.page_content for doc in doc_3]}")

最终输出: [] ,可以看到确实删除成功。

1.3.2.2 向量搜索

如果我们传入一个查询,向量存储将嵌入该查询,在所有嵌入的文档中执行相似性搜索,并返回最相
似的文档。

这体现了两个重要的概念:首先,需要有一种方法来衡量查询与任何嵌入文档之间的相似性。其次,
需要有一种算法能够高效地在所有嵌入文档中执行这种相似性搜索。

对于【相似性】这一点,之前已经讲过,再来回顾下:

  • 欧氏距离(Euclidean Distance):就是我们高中几何学的两点之间的直线距离。距离越短,相
    似度越高。
  • 余弦相似度(Cosine Similarity):它忽略向量的绝对长度(大小),只关注两个向量在方向上
    的差异。在文本和语义的世界里,“方向”代表“含义”,而“长度”往往只代表“文本的长
    度”或“词汇的多少”。换句话说,余弦相似度关注的是 “你们是否指向同一个方向” / “你们是
    否代表同一个含义”

因此,在捕捉语义上的相似性上,余弦相似度是更常用的度量方式。

对于【相似性搜索】则可以通过向量存储提供的搜索方法实现。

1.3.2.2.1 相似性搜索

想要获取根据相似性搜索的结果,即嵌入单个查询,并查找相似的文档,并将它们作为文档列表返
回。这可以使用 similarity_search 方法来实现。说明: InMemoryVectorStore 是根据【余
弦相似度】来捕捉语义的。

代码如下:

search_docs = vector_store.similarity_search(query="数据库表怎么设计的?", k=2)

for doc in search_docs:
    print("*" * 30)
    print(doc.page_content)

方法参数解释如下:

  • query :输入的查询str。
  • k :要返回的文档数。默认为 4。

打印结果如下:

******************************
提供两种方案有以下好处:
降低门槛:单机版让用户快速体验核心功能,避免初期陷入复杂部署。
灵活演进:用户可从单机版过渡到集群版,逐步适应生产需求。
业务专属问题
为什么将房源表拆分为水平分表(如按状态分表)?具体如何实现?
原因:
单表数据量过大(如房源状态频繁更新)会导致查询性能下降;
不同状态(上架/已租/下架)的查询频率不同,拆分可减少锁争用;
******************************
数据库:MySQL + MyBatis
MySQL 和 MyBatis 是数据存储和访问的一个常见的做法。MySQL 是一个开源的关系数据库管理系
统,它可以免费使用。MyBatis 提供了一种相对简单的方法来执行 SQL 语句,不需要编写复杂的
ORM 映射文件。
缓存:Redis

可以看见,搜索出来的内容,是较为符合我们的预取。

有的同学可能有疑问了:搜索出来的结果,为什么不直接是问题的答案,而只是相似的文档呢?

那么恭喜你已经具备了构建智能应用基础逻辑了。此时我们可以将 “找到的最相关内容” 和我们 “提
出的问题” 一起交给LLM 来生成答案,这便能极大地提高答案的准确性和时效性。以上流程,叫做检
索增强生成(Retrieval-Augmented Generation, RAG),这是当前大语言模型应用的核心模式。
RAG 我们将会在后续内容进行教学。

1.3.2.2.2 元数据过滤

回顾一下,每个 Document 对象,都包含了以下参数:

  • id :可选的文档标识符。理想情况下,这应该在整个文档集合中是唯一的,并格式化为
    UUID,但不会强制执行。
  • page_content :字符串文本
  • metadata :与内容关联的任意元数据。类型为 dict [Optional]

虽然向量数据库实现了搜索算法,来有效地搜索所有嵌入的文档以找到最相似的文档。但现实场景
中,我们还希望通过先根据元数据进行过滤,来帮助缩小搜索范围。例如从特定来源或日期范围检索
文档。

from langchain_core.documents import Document

def _filter_function(doc: Document) -> bool:
    return doc.metadata.get("source") == "hahaha"

search_docs = vector_store.similarity_search(
    query="数据库表怎么设计的?",
    k=2,
    filter=_filter_function
)

for doc in search_docs:
    print("*" * 30)
    print(doc.page_content)

这次,我们给搜索方法加入了 filter 参数,它接收一个 bool 值,表示我们可以根据条件选择是否
过滤某些文档。因此我们定义了一个 _filter_function 过滤函数,可以根据文档元数据先过滤出
文档,再去进行搜索。

示例中,我们给出的 source=="hahaha" 是无效的,因此会将所有的文档都过滤掉,导致示例代码
运行后结果为空!将 source 换成正确的来源后,再次运行则结果正确。

1.3.3 Redis 向量存储

我们还可以使用 Redis 来存储向量。大多数开发者都熟悉 Redis,因为它速度快、拥有庞大的客户端库
生态系统,并且多年来已被众多大型企业采用。从本质上讲,Redis 是一种键值型的 NoSQL 数据库,
除了传统用例之外,Redis 还提供了诸如搜索和查询功能等额外能力,允许用户在 Redis 内创建二级
索引结构。这使得 Redis 能够以缓存的速度充当向量数据库。

1.3.3.1 基本概念

理解 RediSearch

RediSearch 是 Redis 官方提供的一款高性能【搜索】与【全文索引】引擎模块。它基于 Redis 构建,
使用户能够直接在 Redis 数据库中执行复杂的【搜索】和【分词查询】,无需额外引入外部搜索引
擎。RediSearch 特别适用于轻量级、响应速度要求较高的分词搜索场景。

RediSearch 提供了内置的分词功能,既避免 LIKE 的局限性(无法支持分词查询。例如:数据
为“Apple iPhone 17 Pro Max 256GB 星宇橙色”,搜“苹果17橙色”无法匹配),又无需部署 ES 这
种大型组件,集成简单、性能出色,非常适合中等规模的全文检索需求。

理解 Index

Index(索引)是 RediSearch 模块里的概念,用于定义一个查询目录。Index 是一个独立的数据结
构,它建立在多个 Redis Keys (Hash 类型)之上,这专门为了极速执行文本搜索、过滤和聚合而设
计。它本身不存储数据,而是存储了指向其他 Redis Keys 的指针,和这些 Keys 中特定字段的索引信
息。

理解 Index Fields

Index Fields(索引字段) 是创建索引时,明确指定的那些需要被索引的字段。它们定义了索引的“结
构”或“蓝图”,告诉 RediSearch:“请针对这些字段的内容,以其特定的方式为我构建快速搜索的
能力。”

可以把它想象成在一本书后面制作索引页(比如人名索引、主题索引)。我们不会把书中的每一个字
都做到索引里,而是只选择那些重要的关键词(字段),并记录下它们出现的页码(文档ID)。这里
的“关键词”就是 Index Fields。

在 RediSearch 中,索引字段是有特定的类型:

  • TAG :精确匹配的分类/标签,可以用来多值分类、过滤、分组。
  • NUMERIC :整数/浮点数,可以进行数值范围查询,排序和统计。
  • TEXT :全文搜索的字符串,支持分词、词干化、模糊匹配。
  • GEO :经纬度坐标,用来查询地理空间,计算距离。

理解 metadata schema

schema 我们讲过,就是描述数据结构的声明格式。 metadata schema 则用来描述元数据的结构
声明。

这里的元数据是指我们将来要嵌入文档的元数据。因为对于文档元数据来说,它在存入 Redis 后,就
被定义成了索引字段。

对于文档元数据来说,里面存放的就是一些文档属性值,如 source 表示文档来源。我们还可以手动
加入其他元数据,这需要设置每个字段的声明: name 表示字段名, type 表示字段类型。

metadata_schema=[
    {"name": "category", "type": "tag"},
    {"name": "num", "type": "numeric"},
]

一张图总结关系,如下所示:

这里只展示了关系,实际的存储我们后续操作时会演示。

1.3.3.2 环境设置

使用 Redis 来存储向量,首先需要将相关环境配置好。

  • 第一步:启动 Redis 服务端:使用 Docker 启动 Redis 实例。

这里要说明:对于 Redis 版本 >= 8.0 ,使用:

docker run -d -p 6379:6379 -it redis:latest

对于 Redis 版本 < 8.0 ,使用:

docker run -d -p 6379:6379 redis/redis-stack:latest

使用 docker ps 查看是否启动成功

  • 第二步:安装 Redis 客户端包,以便将来定义客户端,以及运行搜索和查询命令。

由于我们使用 Python 进行开发,则选择 redis-py 库完成客户端定义。要安装 redis-py,只需:

pip install redis
  • 第三步:在 LangChain 中想要使用 Redis 向量库,需要安装 langchain-redis
pip install -qU langchain-redis
  • 第四步:定义 Redis 连接 URL,客户端连接 Redis 时需要使用。

Redis 连接 URL 的基本结构是:

[protocol]://[auth]@[host]:[port]/[database]

这部分根据自己Redis服务情况而定。例如: "redis://localhost:6379"

  • 第五步:测试连接(Ping)
import redis

redis_url = "redis://localhost:6379"

# 定义Redis客户端
redis_client = redis.from_url(redis_url)

# Ping
print(redis_client.ping())

若输出 True ,则表示连接测试成功。

1.3.3.3 基本操作
  • 初始化

LangChain 中使用 RedisVectorStore 初始化 Redis 向量存储。由于 Redis 需要相关配置,如连
接 URL 等,因此 LangChain 提供了 RedisConfig 配置类供我们使用。如下所示:

from langchain_openai import OpenAIEmbeddings
from langchain_redis import RedisConfig, RedisVectorStore

# 定义嵌入模型
embeddings = OpenAIEmbeddings(model="text-embedding-3-large")

# 配置 Redis 客户端
redis_url = "redis://192.168.100.238:6379"
config = RedisConfig(
    index_name="qa",
    redis_url=redis_url,
    metadata_schema=[
        {"name": "category", "type": "tag"},
        {"name": "num", "type": "numeric"},
    ],
)

# Redis 存储初始化
vector_store = RedisVectorStore(embeddings, config=config)

class langchain_redis.vectorstores.RedisVectorStore 类,其初始化参数如下:

  • embeddings :用于此存储的 Embeddings 实例。
  • config :可选的 RedisConfig 对象。

class langchain_redis.config.RedisConfig 配置类,其关键配置参数如下:

  • index_name :Redis 中索引的名称。默认为生成的 ULID 唯一标识符。
  • key_prefix :Redis Key 的前缀。如果未设置,则默认为 index_name
  • redis_url :Redis 实例的 URL。默认为“redis://localhost:6379”。
  • metadata_schema :元数据字段的 schema。设置该字段对于将来的元数据过滤有帮助。

我们可以根据 Index Name 查询其下的所有的 Index Fields ,这需要安装 redisvlpip install -U redisvl ),使用 rvl 命令行工具来检查索引。

rvl index info -i qa --host 192.168.100.238 --port 6379

可以看到 Index Fields 中就包括了:

  • text :文档文本。
  • embedding :向量,类型为数组。
  • _index_name :索引名。
  • meteData schema :文档元数据。

其中文档元数据的每个属性还都被当作单独的 Index Fields 。

  • 添加文档

我们可以使用 add_documents 方法,向向量库中去添加文档。这次我们可以给被分割的文档添加相
关的元数据。如下所示:

from langchain_community.document_loaders import UnstructuredMarkdownLoader
from langchain_text_splitters import CharacterTextSplitter

# 生成分割器
text_splitter = CharacterTextSplitter.from_tiktoken_encoder(
    encoding_name="cl100k_base", chunk_size=200, chunk_overlap=50
)

# 加载文档
data = UnstructuredMarkdownLoader("../Docs/Markdown/脚手架级微服务租房平台
Q&A.md", category="QA").load()

# 分割文档
documents = text_splitter.split_documents(data)

# 为文档添加元数据
for i, doc in enumerate(documents, start=1):
    doc.metadata["category"] = "QA"
    doc.metadata["num"] = i

ids = vector_store.add_documents(documents=documents)

print(f"共编排了{len(ids)}个文档索引")
print(f"前3个文档的索引是:{ids[:3]}")

结果输出:

共编排了96个文档索引
前3个文档的索引是:['qa::01K4Q0A3DSQVZBRFKJD5MS25HG',
'qa::01K4Q0A3DSQVZBRFKJD5MS25HH', 'qa::01K4Q0A3DSQVZBRFKJD5MS25HJ']

在 Redis 中的存储如下所示,下面的 embedding 字段被转换为二进制存储,因此展示了乱码。除此
之外,还存放了我们给文档设置的元数据信息。

  • 获取文档

使用 get_by_ids 方法,通过索引列表获取对应的文档列表。如下所示:

ids = [":01K4Q0A3DSQVZBRFKJD5MS25HJ", ":01K4Q0A3DSQVZBRFKJD5MS25HK",
":01K4Q0A3DSQVZBRFKJD5MS25HM"]
doc_3 = vector_store.get_by_ids(ids)
print(f"{[doc.page_content for doc in doc_3]}")

结果如下:

[
    '回答3:(学校课设的解释)\n\n这是学校的课设题目,然后在课设项目的基础上,查找了一些
资料,进行了一些改进。\n\n这个项目为啥和上个(之前)同学的项目一样?\n\n回答1:(开源项目的
回答)',
    '这个项目为啥和上个(之前)同学的项目一样?\n\n回答1:(开源项目的回答)\n\n因为这个项
目本身就是开源项目,网上一搜都是好多都是这个项目的博客分析,写得优质的主要就是那几篇,可能
我们参考了一样的博客讲解梳理框架吧。抽奖系统虽然听起来简单,但实际上涉及到数据存储、状态转
换、异常处理等多个技术点。\n\n回答2:(学校课设的回答)',
    '回答2:(学校课设的回答)\n\n这个问题面试官可能会直接的指出:上个同学和你学校不同,
但是跟你的项目完全一样。\n\n这个项目确实是我们学校的开源项目,但是我在实现的时候也在网上找
了跟课设要求类似的开源项目,而且发现网上针对这个类型的项目文档还听完善的,可能其他的同学也
是在网上跟我一样找到了类似的博客或开源项目进行借鉴的吧。'
]
  • 删除文档

使用 delete 方法,删除传入索引列表对应的文档列表。如下所示:

vector_store.delete([":01K4Q0A3DSQVZBRFKJD5MS25HJ"])

或者:

# 删除指定内容
vector_store.index.drop_keys(["qa::01K4Q0A3DSQVZBRFKJD5MS25HJ"])

# 全量删除,删除索引
vector_store.index.delete(drop=True)
1.3.3.4 向量搜索
1.3.3.4.1 相似性搜索

想要获取根据相似性搜索的结果,即嵌入单个查询,并查找相似的文档,并将它们作为文档列表返
回。这可以使用 similarity_search 方法来实现。代码如下:

search_docs = vector_store.similarity_search(query="数据库表怎么设计的?", k=2)

for doc in search_docs:
    print("*" * 30)
    print(doc.page_content)

方法参数解释如下:

  • query :输入的查询str。
  • k :要返回的文档数。默认为 4。

打印结果如下:

******************************
提供两种方案有以下好处:
降低门槛:单机版让用户快速体验核心功能,避免初期陷入复杂部署。
灵活演进:用户可从单机版过渡到集群版,逐步适应生产需求。
业务专属问题
为什么将房源表拆分为水平分表(如按状态分表)?具体如何实现?
原因:
单表数据量过大(如房源状态频繁更新)会导致查询性能下降;
不同状态(上架/已租/下架)的查询频率不同,拆分可减少锁争用;
******************************
数据库:MySQL + MyBatis
MySQL 和 MyBatis 是数据存储和访问的一个常见的做法。MySQL 是一个开源的关系数据库管理系
统,它可以免费使用。MyBatis 提供了一种相对简单的方法来执行 SQL 语句,不需要编写复杂的
ORM 映射文件。
缓存:Redis

除了上面的 similarity_search 方法,其实还提供了:

  • 根据向量搜索方法: similarity_search_by_vector
  • 根据查询搜索方法,并返回相似分值: similarity_search_with_score
  • 根据向量搜索方法,并返回相似分值: similarity_search_with_score_by_vector

例如,我们希望【根据查询语句搜索,并返回相似分值】:

scored_results = vector_store.similarity_search_with_score(query="数据库表怎么设
计的?", k=4)

for doc, score in scored_results:
    print("*" * 30)
    print(f"Content: {doc.page_content[:100]}...")
    print(f"Metadata: {doc.metadata}")
    print(f"Score: {score}")

结果如下:

******************************
Content: 提供两种方案有以下好处:
降低门槛:单机版让用户快速体验核心功能,避免初期陷入复杂部署。
灵活演进:用户可从单机版过渡到集群版,逐步适应生产需求。
业务专属问题
为什么将房源表拆分为水平分表(...
Metadata: {'source': '../Docs/Markdown/脚手架级微服务租房平台Q&A.md', 'category':
'QA', 'num': 79}
Score: 0.580324351788

******************************
Content: 数据库:MySQL + MyBatis
MySQL 和 MyBatis 是数据存储和访问的一个常见的做法。MySQL 是一个开源的关系数据库管理系
统,它可以免费使用。MyBatis 提供了一种相对简...
Metadata: {'source': '../Docs/Markdown/脚手架级微服务租房平台Q&A.md', 'category':
'QA', 'num': 23}
Score: 0.582783222198

注意:分数越低表示相似度越高。

1.3.3.4.2 元数据过滤

对于上述列举的方法,在他们搜索之前,都可以根据元数据先进行过滤。对于 RedisVectorStore
来说,需要使用 Redis 过滤表达式进行筛选。示例如下:

from redisvl.query.filter import Tag

# 过滤表达式
filter_condition = Tag("source") == "hahahaha"

scored_results = vector_store.similarity_search_with_score(
    query="数据库表怎么设计的?",
    k=2,
    filter=filter_condition
)

for doc, score in scored_results:
    print("*" * 30)
    print(f"Content: {doc.page_content[:100]}...")
    print(f"Metadata: {doc.metadata}")
    print(f"Score: {score}")

示例中,我们给出的 source=="hahahaha" 是无效的,因此会将所有的文档都过滤掉,导致示例
代码运行后结果为空!将 source 换成正确的来源后,再次运行则结果正确。

对于 Redis 过滤表达式,也可以使用 &| 运算符组合。如我们现根据元数据中的 category
num 进行过滤,如下所示:

from redisvl.query.filter import Tag, Num

# 过滤表达式
category_is_qa = Tag("category") == "qa"
num_is_under_50 = Num("num") < 50

filter_condition = category_is_qa & num_is_under_50

scored_results = vector_store.similarity_search_with_score(
    query="数据库表怎么设计的?",
    k=2,
    filter=filter_condition
)

for doc, score in scored_results:
    print("*" * 30)
    print(f"Content: {doc.page_content[:100]}...")
    print(f"Metadata: {doc.metadata}")
    print(f"Score: {score}")

结果如下:

******************************
Content: 数据库:MySQL + MyBatis
MySQL 和 MyBatis 是数据存储和访问的一个常见的做法。MySQL 是一个开源的关系数据库管理系
统,它可以免费使用。MyBatis 提供了一种相对简...
Metadata: {'source': '../Docs/Markdown/脚手架级微服务租房平台Q&A.md', 'category':
'QA', 'num': 23}
Score: 0.582783222198

******************************
Content: 咨询消息的持久化优化
分库分表:按user_id哈希分库,按月份分表(如chat_msg_2023_10);
读写分离:
写操作:主库(高并发下队列缓冲);
读操作:从库 + Redis缓存最...
Metadata: {'source': '../Docs/Markdown/脚手架级微服务租房平台Q&A.md', 'category':
'QA', 'num': 42}
Score: 0.657752811909
1.3.3.4.3 最大边际相关性搜索

回顾一下什么是最大边际相关性?它是一种重新排序算法,它使用语义相似性作为基础工具,从一个
候选集中挑选出一组既能代表查询主题又彼此多样化的结果。

  • 【语义相似性】就像面试官衡量每个应聘者与职位要求的匹配度。他会给每个应聘者打一个分数。
  • 【最大边际相关性】就像团队经理(MMR算法)要组建一个团队。目标是选出一组“精华”结果,
    而不是一个单一结果:

MMR 使用场景:

  • 推荐系统:推荐与用户兴趣相关但又不同类型的物品,避免“信息茧房”。
  • 文档摘要:从长文档中选择能代表主旨又包含不同信息的句子,避免摘要内容重复。
  • RAG (检索增强生成):在从知识库检索完一堆相关文档后,使用 MMR 进行去重和多样化筛选,再
    交给LLM生成答案,能有效提升答案质量和减少幻觉。

使用 MMR 搜索,需要用到 max_marginal_relevance_search 方法,代码如下:

from redisvl.query.filter import Tag, Num

# 过滤表达式
category_is_qa = Tag("category") == "qa"
num_is_under_50 = Num("num") < 50

filter_condition = category_is_qa & num_is_under_50

mmr_results = vector_store.max_marginal_relevance_search(
    query="数据库表怎么设计的?",
    k=2,
    fetch_k=10,
    filter=filter_condition
)

for doc in mmr_results:
    print("*" * 30)
    print(f"Content: {doc.page_content[:100]}...")
    print(f"Metadata: {doc.metadata}")

我们需要注意一个参数 fetch_kfetch_k 是 MMR 算法第一步中,从向量库中初步获取的候选
文档数量。

为了理解它,我们首先要明白 MMR 搜索是一个两阶段过程:

  1. 初步获取:系统首先根据查询的纯向量相似度,从庞大的向量库中找出最相似的 fetch_k 个文
    档。这一步的目标是“广撒网”,先找到一个足够大的相关文档池。
  2. 重新排序与筛选:然后,MMR 算法会在这个较小的候选池(大小为 fetch_k )中运行。它不再
    只考虑与查询的相似度,还会考虑候选文档之间的多样性。它会从这 fetch_k 个文档中,挑选
    出既与查询相关,彼此之间又不太相似的 k 个文档作为最终结果。

因此,其目的就是在保证相关性的前提下,提升结果的多样性。打印结果如下:

******************************
Content: 数据库:MySQL + MyBatis
MySQL 和 MyBatis 是数据存储和访问的一个常见的做法。MySQL 是一个开源的关系数据库管理系
统,它可以免费使用。MyBatis 提供了一种相对简...
Metadata: {'source': '../Docs/Markdown/脚手架级微服务租房平台Q&A.md', 'category':
'QA', 'num': 23}

******************************
Content: 生产环境搭建
develop合并master
流水线部署
线上验证
你是如何优化项目性能的?你有哪些性能调优的经验?
房源搜索的筛选+排序用设计模式优化
热点数据存放缓存的优化
没有性能...
Metadata: {'source': '../Docs/Markdown/脚手架级微服务租房平台Q&A.md', 'category':
'QA', 'num': 40}
1.3.4 Pinecone 向量存储
1.3.4.1 Pinecone 介绍

Pinecone 是为机器学习应用量身打造的生产级向量数据库服务,适用于高维向量数据的高效存储、索
引与查询。它屏蔽了基础设施管理,提供无缝扩展、实时数据写入和强大安全保障,让开发者和数据
科学家能够以极低运维成本,快速构建高效的相似度搜索、推荐系统和 AI 应用。

Pinecone 是一个全托管的向量数据库平台,即负责所有后端维护、扩展、更新和监控,让用户专注于
应用开发,无需担心数据库管理。

Pinecone 地址:https://www.pinecone.io/(魔法上网)

1.3.4.2 环境设置
  • 首次使用需注册新用户,选择个人免费版
  • 继续创建账户相关信息,或者直接右上角 skip 跳过。这里我们直接跳过。
  • 注册成功会生成一个默认的 API Key。注意保存好你的 key。
  • 也可以创建新的 API key:
  • 设置 PINECONE_API_KEY ,将 Key 添加进环境变量。
  • 更新包
pip install -qU pinecone langchain-pinecone
1.3.4.3 基本操作
  • 初始化

LangChain 中使用 PineconeVectorStore 类初始化 Pinecone 向量库。我们需要:

  1. 创建索引,参考这里
  2. 使用索引来初始化 PineconeVectorStore

如下所示:

from langchain_openai import OpenAIEmbeddings
from langchain_pinecone import PineconeVectorStore
from pinecone import Pinecone, ServerlessSpec

# 建立索引
pc = Pinecone()
index_name = "qa"

if not pc.has_index(index_name):
    pc.create_index(
        name=index_name, # 索引名称
        dimension=3072, # 尺寸,表示向量维度,需要和嵌入模型维度一致
        metric="cosine", # 度量方式,cosine 表示余弦相似度
        spec=ServerlessSpec(
            cloud="aws", # 亚马逊云
            region="us-east-1" # 区域
        ),
    )

# 定义嵌入模型
embeddings = OpenAIEmbeddings(model="text-embedding-3-large")

# 获取索引
index = pc.Index(index_name)

# 定义 Pinecone 向量存储
vector_store = PineconeVectorStore(embedding=embeddings, index=index)

class langchain_redis.vectorstores.PineconeVectorStore 类,其初始化参数如
下:

  • embeddings :用于此存储的 Embeddings 实例。
  • index :Pinecone 索引

如果我们是第一次创建索引,在 Pinecone 控制台 则可以看见被创建的索引。

  • 添加文档

我们可以使用 add_documents 方法,向向量库中去添加文档。如下所示:

from langchain_community.document_loaders import UnstructuredMarkdownLoader
from langchain_text_splitters import CharacterTextSplitter

# 生成分割器
text_splitter = CharacterTextSplitter.from_tiktoken_encoder(
    encoding_name="cl100k_base", chunk_size=200, chunk_overlap=50
)

# 加载文档
data = UnstructuredMarkdownLoader("../Docs/Markdown/脚手架级微服务租房平台
Q&A.md", category="QA").load()

# 分割文档
documents = text_splitter.split_documents(data)

# 为文档添加元数据
for i, doc in enumerate(documents, start=1):
    doc.metadata["category"] = "QA"
    doc.metadata["num"] = i

ids = vector_store.add_documents(documents=documents)

print(f"共编排了{len(ids)}个文档索引")
print(f"前3个文档的索引是:{ids[:3]}")

结果如下:

共编排了96个文档索引
前3个文档的索引是:['eee6f5f6-696b-4088-94d0-d0511cc92623', '36b8f351-35e5-4096-
81ea-b29701a9b4c8', '8c133041-e2a4-4040-8b11-98b4d408a752']

控制台更新数据:

  • 删除文档
# 全量删除
vector_store.delete(delete_all=True)

# 删除指定id的文档列表
delete_ids = []
vector_store.delete(ids=delete_ids)
1.3.4.4 向量搜索

想要获取根据相似性搜索的结果,即嵌入单个查询,并查找相似的文档,并将它们作为文档列表返
回。这可以使用 similarity_search 方法来实现。代码如下:

search_docs = vector_store.similarity_search(
    query="数据库表怎么设计的?",
    k=2,
    filter={"category": "QA"},
)

for doc in search_docs:
    print("*" * 30)
    print(f"Content: {doc.page_content[:100]}...")
    print(f"Metadata: {doc.metadata}")

结果打印如下:

******************************
Content: 提供两种方案有以下好处:
降低门槛:单机版让用户快速体验核心功能,避免初期陷入复杂部署。
灵活演进:用户可从单机版过渡到集群版,逐步适应生产需求。
业务专属问题
为什么将房源表拆分为水平分表(...
Metadata: {'category': 'QA', 'num': 79.0, 'source': '../Docs/Markdown/脚手架级微
服务租房平台Q&A.md'}

******************************
Content: 数据库:MySQL + MyBatis
MySQL 和 MyBatis 是数据存储和访问的一个常见的做法。MySQL 是一个开源的关系数据库管理系
统,它可以免费使用。MyBatis 提供了一种相对简...
Metadata: {'category': 'QA', 'num': 23.0, 'source': '../Docs/Markdown/脚手架级微
服务租房平台Q&A.md'}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

tew_gogogo

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值