LangChain AI应用开发框架核心组件的使用 - Redis 向量存储 : 理解 Redis 向量存储及相关概念, Redis 向量存储的增删查改, Redis向量搜索(相似性搜索与元数据过滤) 目录一、Redis 向量存储基本概念理解 RediSearch理解 Index理解 Index Fields理解 metadata schema二、环境设置基本操作初始化添加文档获取文档删除文档三、向量搜索相似性搜索元数据过滤最大边际相关性搜索四、Pinecone 向量存储Pinecone 介绍环境设置基本操作添加文档删除文档向量搜索一、Redis 向量存储我们还可以使用 Redis 来存储向量。大多数开发者都熟悉 Redis因为它速度快、拥有庞大的客户端库生态系统并且多年来已被众多大型企业采用。从本质上讲Redis 是一种键值型的 NoSQL 数据库除了传统用例之外Redis 还提供了诸如搜索和查询功能等额外能力允许用户在 Redis 内创建二级索引结构。这使得 Redis 能够以缓存的速度充当向量数据库。基本概念理解 RediSearchRediSearch 是 Redis 官方提供的一款高性能【搜索】与【全文索引】引擎模块。它基于 Redis 构建使用户能够直接在 Redis 数据库中执行复杂的【搜索】和【分词查询】无需额外引入外部搜索索引引擎。RediSearch 特别适用于轻量级、响应速度要求较高的分词搜索场景。RediSearch 提供了内置的分词功能既避免 LIKE 的局限性无法支持分词查询。例如数据为 “Apple iPhone 17 Pro Max 256GB 星宇橙色”搜 “苹果 17 橙色” 无法匹配又无需部署 ES 这种大型组件集成简单、性能出色非常适合中等规模的全文检索需求。理解 IndexIndex索引是 RediSearch 模块里的概念用于定义一个查询目录。Index 是一个独立的数据结构它建立在多个 Redis KeysHash 类型之上这专门为了极速执行文本搜索、过滤和聚合而设计。它本身不存储数据而是存储了指向其他 Redis Keys 的指针和这些 Keys 中特定字段的索引信息。理解 Index FieldsIndex Fields索引字段是创建索引时明确指定的那些需要被索引的字段。它们定义了索引的 “结构” 或 “蓝图”告诉 RediSearch“请针对这些字段的内容以其特定的方式为我构建快速搜索的能力。”可以把它想象成在一本书后面制作索引页 (比如人名索引、主题索引)。我们不会把书中的每一个字都做到索引里而是只选择那些重要的关键词 (字段)并记录下它们出现的页码 (文档 ID)。这里的 “关键词” 就是 Index Fields。在 RediSearch 中索引字段是有特定的类型TAG精确匹配的分类/标签可以用来多值分类、过滤、分组。NUMERIC整数/浮点数可以进行数值范围查询排序和统计。TEXT全文搜索的字符串支持分词、词干化、模糊匹配。GEO经纬度坐标用来查询地理空间计算距离。理解 metadata schemaschema 我们讲过就是描述数据结构的声明格式。metadata schema 则用来描述元数据的结构声明。这里的元数据是指我们将来要嵌入文档的元数据。因为对于文档元数据来说它在存入 Redis 后就被定义成了索引字段。对于文档元数据来说里面存放的就是一些文档属性值如 source 表示文档来源。我们还可以手动加入其他元数据这需要设置每个字段的声明name 表示字段名type 表示字段类型。metadata_schema定义元数据模式列表每一项代表一个元数据字段配置。{name: category, type: tag}定义字段名称 category字段类型为 tag用于标签精确过滤。{name: num, type: numeric}定义字段名称 num字段类型为 numeric支持数值范围查询。一张图总结关系如下所示:这里只展示了关系实际的存储我们后续操作时会演示。二、环境设置使用 Redis 来存储向量首先需要将相关环境配置好。1. 第一步启动 Redis 服务端使用 Docker 启动 Redis 实例。这里要说明对于 Redis 版本 8.0使用对于 Redis 版本 8.0使用使用 docker ps 查看是否启动成功。第二步安装 Redis 客户端包以便将来定义客户端以及运行搜索和查询命令。由于我们使用 Python 进行开发则选择 redis‑py 库完成客户端定义。要安装 redis‑py只需第三步在 LangChain 中想要使用 Redis 向量库需要安装 langchain‑redis 包第四步定义 Redis 连接 URL客户端连接 Redis 时需要使用。Redis 连接 URL 的基本结构是这部分根据自己 Redis 服务情况而定。例如redis://localhost:6379第五步测试连接Pingimport redis导入 redis 客户端库。redis_url redis://localhost:6379填写 redis 连接地址。redis_client redis.from_url (redis_url)通过 url 生成 redis 客户端实例。redis_client.ping ()向 redis 服务端发送 ping 指令检测网络连通与服务状态。print (redis_client.ping ())打印 ping 返回值。运行结果:若输出 True则表示连接测试成功如果报错 / 返回 False则代表网络、端口、服务未启动连接失败。基本操作初始化LangChain 中使用 RedisVectorStore 初始化 Redis 向量存储。由于 Redis 需要相关配置如连接 URL 等因此 LangChain 提供了 RedisConfig 配置类供我们使用。如下所示from langchain_openai import OpenAIEmbeddings导入 OpenAI 嵌入模型类用来把文本转为向量。from langchain_redis import RedisConfig, RedisVectorStore导入 Redis 配置类与 Redis 向量存储类。embeddings OpenAIEmbeddings (modeltext‑embedding‑3‑large)实例化嵌入模型指定模型版本。redis_url redis://192.168.100.238:6379填写 Redis 服务的连接地址。config RedisConfig (...)构建 RedisConfig 配置对象。index_nameqa定义 Redis 内部索引名称为 qa。redis_urlredis_url传入 Redis 连接地址。metadata_schema定义元数据字段结构category 为 tag 标签类型num 为 numeric 数值类型。vector_store RedisVectorStore (embeddings, configconfig)传入嵌入模型和配置完成 Redis 向量存储实例初始化。class langchain_redis.vectorstores.RedisVectorStore 类其初始化参数如下embeddings用于此存储的 Embeddings 实例。config可选的 RedisConfig 对象。class langchain_redis.config.RedisConfig 配置类其关键配置参数如下index_nameRedis 中索引的名称。默认为生成的 ULID 唯一标识符。key_prefixRedis Key 的前缀。如果未设置则默认为 index_name。redis_urlRedis 实例的 URL。默认为 redis://localhost:6379。metadata_schema元数据字段的 schema。设置该字段对于将来的元数据过滤有帮助。可以看到 Index Fields 中就包括了text文档文本。embedding向量类型为数组。_index_name索引名。metadata schema文档元数据。其中文档元数据的每个属性还都被当作单独的 Index Fields。添加文档我们可以使用 add_documents 方法向向量库中去添加文档。这次我们可以给被分割的文档添加相关的元数据。如下所示from langchain_community.document_loaders import UnstructuredMarkdown Loader导入 markdown 文档加载器用来读取 md 格式文件。from langchain_text_splitters import CharacterTextSplitter导入文本分割器用于把长文档切分为小块。CharacterTextSplitter.from_tiktoken_encoder基于 tiktoken 编码器做文本切分。encoding_namecl100k_base使用 gpt 系列模型对应的编码器。chunk_size200每一块文档最大 token 数量。chunk_overlap50块与块之间重叠 token避免上下文被截断丢失信息。UnstructuredMarkdownLoader (...).load()读取指定路径下 markdown 文件加载为文档对象。text_splitter.split_documents (data)把完整文档切分成多个 Document 对象。for i, doc in enumerate (documents, start1)遍历分割后的文档从 1 开始计数。doc.metadata [category] QA给每个文档打上 category 标签元数据。doc.metadata [num] i给每个文档设置序号 num 元数据。vector_store.add_documents (documentsdocuments)把文档送入 Redis 向量库生成向量并写入返回每个文档对应的 id 列表。print 打印文档总数量与前三条 id方便查看。运行结果:输出一共存入 96 条文档分片同时返回前三条文档在 Redis 中的唯一索引 id。在 Redis 中存储时embedding 字段转为二进制保存查看会显示乱码同时会完整保存我们设置好的元数据信息。获取文档使用 get_by_ids 方法通过索引列表获取对应的文档列表。如下所示ids [...]传入需要查询的文档 id 数组。vector_store.get_by_ids (ids)根据 id 从 Redis 向量库查询返回对应的 Document 对象列表。列表推导式取出每一个 doc 的 page_content 文档内容并打印。运行结果:返回对应 id 的文档原始文本内容可以拿到文本继续后续业务逻辑。删除文档使用 delete 方法删除传入索引列表对应的文档列表。如下所示vector_store.delete (传入 id 列表)根据文档 id 删除 Redis 向量库中对应单条或多条记录。或者vector_store.index.drop_keys (...)直接操作底层索引删除指定 Redis key。vector_store.index.delete (dropTrue)dropTrue 代表直接把整个索引全部销毁清空全部向量与文档数据。运行结果:删除操作无返回打印报错即代表成功全量删除之后该索引下全部数据不可恢复。三、向量搜索相似性搜索想要获取根据相似性搜索的结果即嵌入单个查询并查找相似的文档并将它们作为文档列表返回。这可以使用 similarity_search 方法来实现。代码如下vector_store.similarity_search向量存储对象的相似搜索方法。query数据库表怎么设计的用户输入查询语句内部自动调用嵌入模型转为向量。k2指定返回相似度最高的 2 条文档分片。for doc in search_docs遍历搜索返回的文档结果列表。print (* * 30)打印分割线方便区分不同文档。print (doc.page_content)打印文档的正文内容。运行结果:输出 k 条匹配的文档片段按相似度从高到低排序返回只返回文档对象不返回相似度分数。除了上面的 similarity_search 方法其实还提供了根据向量搜索方法similarity_search_by_vector根据查询搜索方法并返回相似分值similarity_search_with_score根据向量搜索方法并返回相似分值similarity_search_with_score_by_vector例如我们希望【根据查询语句搜索并返回相似分值】similarity_search_with_score执行相似度检索返回(文档对象,相似度分数)元组列表。query用户查询文本。k4返回 4 条结果。for doc, score in scored_results循环解包拿到文档对象 doc 与对应相似度 score。doc.page_content [:100]截取文档前 100 个字符展示避免输出内容过长。doc.metadata打印该文档保存的元数据。score打印 Redis 向量搜索返回的距离分数。运行结果:分数越低表示相似度越高。元数据过滤对于上述列举的方法在他们搜索之前都可以根据元数据先进行过滤。对于 RedisVectorStore 来说需要使用 Redis 过滤表达式进行筛选。示例如下from redisvl.query.filter import Tag从 redisvl 库导入 Tag 过滤器专门用来过滤 tag 标签类型元数据字段。filter_condition Tag (source) hahahaha构造过滤条件要求元数据中 source 字段等于 hahahaha。similarity_search_with_score带分数相似度检索新增 filter 参数。filterfilter_condition把过滤条件传入向量检索先过滤元数据再做相似度匹配。for doc, score in scored_results遍历返回结果打印文档片段、元数据、相似度分数。运行结果:示例中我们给出的 sourcehahahaha 是无效的因此会将所有的文档都过滤掉导致示例代码运行后结果为空。将 source 换成正确的来源后再次运行则结果正确。对于 Redis 过滤表达式也可以使用 和 | 运算符组合。如我们现根据元数据中的 category 与 num 进行过滤如下所示from redisvl.query.filter import Tag, Num导入 Tag 标签过滤器与 Num 数值过滤器Tag 处理字符串标签Num 处理数字类型元数据。category_is_qa Tag (category) qa条件 1元数据 category 字段等于 qa。num_is_under_50 Num (num) 50条件 2元数据 num 数值小于 50。filter_condition category_is_qa num_is_under_50 代表逻辑与两个条件必须同时满足使用 | 代表逻辑或。filterfilter_condition将组合后的过滤条件传入搜索接口。循环打印输出文档片段、元数据、相似度分数。运行结果:只会返回同时满足category等于qa、num小于50并且向量相似度最高的 k2 条文档先执行元数据过滤再做向量相似度排序。注意分数越低代表向量相似度越高。最大边际相关性搜索回顾一下什么是最大边际相关性它是一种重新排序算法它使用语义相似性作为基础工具从一个候选集中挑选出一组既能代表查询主题又被此多样化的结果。【语义相似性】就像面试官衡量每个应聘者与职位要求的匹配度。他会给每个应聘者打一个分数。【最大边际相关性】就像团队经理MMR 算法要组建一个团队。目标是选出一组 “精华” 结果而不是一个单一结果。MMR 使用场景推荐系统推荐与用户兴趣相关但又不同类型的物品避免 “信息茧房”。文档摘要从长文档中选择能代表主旨又包含不同信息的句子避免摘要内容重复。RAG (检索增强生成)在从知识库检索完一堆相关文档后使用 MMR 进行去重和多样化筛选再交给 LLM 生成答案能有效提升答案质量和减少幻觉。使用 MMR 搜索需要用到 max_marginal_relevance_search 方法代码如下from redisvl.query.filter import Tag, Num导入元数据过滤需要的标签过滤器与数字过滤器。category_is_qa Tag (category) qa构造条件元数据 category 字段等于 qa。num_is_under_50 Num (num) 50构造条件元数据 num 数值小于 50。filter_condition category_is_qa num_is_under_50逻辑与两个条件同时生效。vector_store.max_marginal_relevance_search执行 MMR 最大边际相关性检索。query用户查询文本。k2最终希望返回的文档数量。fetch_k10先从向量库取出 10 条高相似度文档作为候选池MMR 算法在这 10 条里面再挑选 k 条。filterfilter_condition先执行元数据过滤再进行向量检索。for doc in mmr_results遍历 MMR 筛选完成后的结果打印文档片段与元数据。我们需要注意一个参数 fetch_kfetch_k 是 MMR 算法第一步中从向量库中初步获取的候选文档数量。为了理解它我们首先要明白 MMR 搜索是一个两阶段过程初步获取系统首先根据查询的纯向量相似度从庞大的向量库中找出最相似的 fetch_k 个文档。这一步的目标是 “广撒网”先找到一个足够大的相关文档池。重新排序与筛选然后MMR 算法会在这个较小的候选池大小为 fetch_k中运行。它不再只考虑与查询的相似度还会考虑候选文档之间的多样性。它会从这 fetch_k 个文档中挑选出既与查询相关彼此之间又不太相似的 k 个文档作为最终结果。运行结果:说明普通 similarity_search 返回的结果内容会高度接近MMR 返回的 2 条结果既和查询相关同时文档之间内容差异更大减少重复信息适合 RAG 场景。四、Pinecone 向量存储Pinecone 介绍Pinecone 是为机器学习应用量身打造的生产级向量数据库服务适用于高维向量数据的高效存储、索引与查询。它屏蔽了基础设施管理提供无缝扩展、实时数据写入和强大安全保障让开发者和数据科学家能够以极低运维成本快速构建高效的相似度搜索、推荐系统和 AI 应用。Pinecone 是一个全托管的向量数据库平台即负责所有后端维护、扩展、更新和监控让用户专注于应用开发无需担心数据库管理。Pinecone 地址The vector database to build knowledgeable AI | Pinecone魔法上网环境设置1. 首次使用需注册新用户选择个人免费版2. 继续创建账户相关信息或者直接右上角 skip 跳过。这里我们直接跳过。3. 注册成功会生成一个默认的 API Key。注意保存好你的 key。4. 也可以创建新的 API key5. 设置 PINECONE_API_KEY将 Key 添加进环境变量。更新包:基本操作初始化 LangChain 中使用 PineconeVectorStore 类初始化 Pinecone 向量库。我们需要创建索引。使用索引来初始化 PineconeVectorStore。如下所示from langchain_openai import OpenAIEmbeddings导入 OpenAI 嵌入模型用于把文本转为向量。from langchain_pinecone import PineconeVectorStore导入 LangChain 封装好的 Pinecone 向量存储类。from pinecone import Pinecone, ServerlessSpec导入 Pinecone 客户端以及无服务器索引规格类。pc Pinecone ()实例化 Pinecone 客户端读取环境变量 PINECONE_API_KEY 完成鉴权。index_name qa定义索引名称。if not pc.has_index (index_name)判断该索引是否已经存在不存在才执行创建避免重复创建报错。pc.create_index ()创建云端 Pinecone 索引。name索引名字。dimension3072向量维度text‑embedding‑3‑large 输出向量是 3072 维二者必须严格匹配。metriccosine相似度计算方式余弦相似度。ServerlessSpec配置无服务器实例指定云厂商和部署区域。embeddings OpenAIEmbeddings (modeltext‑embedding‑3‑large)实例化嵌入模型。index pc.Index (index_name)获取已经存在的索引对象。vector_store PineconeVectorStore (embeddingembeddings, indexindex)完成 LangChain 向量库实例化绑定嵌入模型与 Pinecone 索引。class langchain_redis.vectorstores.PineconeVectorStore 类其初始化参数如下embeddings用于此存储的 Embeddings 实例。indexPinecone 索引如果我们是第一次创建索引在 Pinecone 控制台则可以看见被创建的索引。添加文档我们可以使用 add_documents 方法向向量库中去添加文档。如下所示UnstructuredMarkdownLoadermarkdown 文档加载器读取本地 md 文件输出 Document 对象。CharacterTextSplitter.from_tiktoken_encoder基于 tiktoken 编码器按照 token 数量做文本切分。chunk_size200每个分片最多 200token。chunk_overlap50分片之间重叠 50token防止上下文被切断丢失信息。.load ()读取文件得到原始文档对象。split_documents (data)把长文档切分成多个小分片 Document 列表。循环遍历分片给每一个 Document 的 metadata 添加 category、num 元数据字段后续可用于元数据过滤。vector_store.add_documents (documentsdocuments)内部自动调用嵌入模型把文本转向量写入 Pinecone 云端返回每一条向量对应的 id 列表。print 打印写入的总数量与前三条 id。运行结果:代表成功向 Pinecone 写入 96 条文档向量控制台页面可以看到新增的记录、元数据、向量信息。页面上可以看到新增的向量记录包含文本内容、metadata、向量得分。删除文档vector_store.delete (delete_allTrue)清空当前索引内全部向量数据测试环境经常使用。vector_store.delete (idsdelete_ids)传入 id 列表只删除指定的若干条向量注释状态需要填入真实 id 才生效。注意Pinecone 是云端向量库删除操作直接作用在云端执行之后数据不可恢复。向量搜索想要获取根据相似性搜索的结果即嵌入单个查询并查找相似的文档并将它们作为文档列表返回。这可以使用 similarity_search 方法来实现。代码如下search_docs vector_store.similarity_search调用向量库的相似度搜索方法完成 RAG 召回。query数据库表怎么设计的用户输入的查询问题方法内部会自动调用嵌入模型把文本转为向量。k2代表返回相似度最高的 2 条文档分片。filter{category: QA}元数据过滤条件只选取元数据里面 category 等于 QA 的文档做筛选缩小召回范围。for doc in search_docs遍历搜索返回得到的文档列表。print (* * 30)打印分隔线方便区分多条召回文档。print (fContent: {doc.page_content [:100]}...)打印文档内容只截取前 100 个字符做预览。print (fMetadata: {doc.metadata})打印这条文档对应的元数据信息。结果打印如下一共返回 2 条结果对应参数 k 等于 2。每一条输出分为 Content 和 Metadata 两部分Content 是召回的文档片段Metadata 保存来源文件、分类、分片编号信息。filter 过滤生效全部返回的文档 category 字段都是 QA不会返回其他分类的数据。输出只展示 page_content 前 100 字符后面内容用省略号截断。