从零构建LangChain自定义Retriever:手把手实现电商评论语义搜索
如果你正在处理海量的电商评论数据,想快速找到用户对某款手机“拍照功能”的真实评价,或者想分析过去三个月内“差评”主要集中在哪些产品特性上,传统的数据库关键词匹配往往力不从心。它可能漏掉“成像效果”、“夜景拍摄”这些语义相近但措辞不同的反馈,也无法将评分、时间、情感倾向这些丰富的元数据灵活地组合进搜索逻辑里。这正是LangChain的Retriever接口大显身手的地方。
今天,我们不满足于使用现成的检索器,而是要深入核心,从零开始构建一个专为电商评论分析场景设计的自定义Retriever。这个检索器将深度融合语义搜索与多维度元数据过滤,让你能像搭积木一样,自由组合“查找关于电池续航的负面评论”、“搜索最近一个月评分高于4星的高频关键词”等复杂查询。无论你是希望提升客服效率的产品经理,还是需要从用户反馈中挖掘痛点的数据分析师,或是正在构建智能问答系统的开发者,这套从数据处理、混合检索策略到对接LCEL链的完整实战指南,都将为你提供一套可直接复用的高阶解决方案。我们将绕过简单的API调用,直击BaseRetriever基类的实现原理,让你真正掌握定制化检索能力的主动权。
1. 理解核心:为什么需要自定义Retriever?
在LangChain的生态中,Retriever(检索器)扮演着“智能记忆库”的角色。它的核心接口极其简洁:接收一个字符串查询,返回一个相关的文档列表。然而,正是这种简洁性赋予了它巨大的灵活性。市面上常见的向量数据库检索器(如FAISS.as_retriever())提供了开箱即用的语义搜索,但在真实的业务场景,尤其是像电商评论这样的垂直领域,我们面临的需求往往更加立体和复杂。
想象一下这几个场景:
- 场景A:市场团队想了解新品发布后第一周,用户对“屏幕”提及的负面评价有哪些,以便快速定位品控问题。
- 场景B:算法工程师需要为推荐系统收集“好评”中关于“手感”和“材质”的描述,用于优化商品标签。
- 场景C:客服系统需要实时检索与用户当前问题(如“手机发烫”)最相似的历史投诉,并优先展示已解决的案例。
这些场景的共同点是,它们都超越了简单的“文本相似度”匹配。它们要求检索系统能够同时理解查询的语义,并精准地应用基于评分(rating)、时间戳(timestamp)、情感极性(sentiment) 等元数据的过滤条件。而LangChain内置的SelfQueryRetriever虽然能处理部分元数据过滤,但其查询构造依赖于LLM对自然语言的解析,在复杂、确定性的业务过滤逻辑面前,有时显得笨重且不可控。
因此,构建自定义Retriever的本质,是将领域知识(电商评论的元数据 schema)和业务规则(复杂的过滤逻辑)深度编码到检索过程中。我们获得的不再是一个黑盒工具,而是一个完全透明、可调试、可扩展的检索引擎。它的优势显而易见:
- 精准控制:过滤逻辑完全由代码定义,避免了LLM解析可能带来的偏差或错误。
- 性能优化:可以针对特定的数据模式和查询模式进行底层优化,例如利用数据库索引加速元数据过滤。
- 灵活扩展:可以轻松集成新的检索源(如关键词搜索、业务数据库)、新的过滤维度(如用户等级、商品类目)或新的排序策略(如结合相关性与热度)。
理解了“为什么”,接下来我们就进入“怎么做”的实战环节。首先,我们需要一份高质量的数据。
2. 实战准备:构建电商评论数据集与向量化管道
任何强大的检索系统都建立在坚实的数据基础之上。对于电商评论语义搜索,我们的数据至少应包含两部分:评论文本本身(用于语义理解)和丰富的元数据(用于结构化过滤)。
2.1 设计数据模型与模拟数据生成
我们先定义一个清晰的数据模型。一份电商评论数据可能包含以下字段:
| 字段名 | 类型 | 描述 | 示例 |
|---|---|---|---|
review_id | str | 评论唯一标识 | “rev_001” |
product_id | str | 商品ID | “prod_手机_X100” |
review_text | str | 评论文本内容 | “拍照效果真的很惊艳,尤其是夜景模式,细节保留得很好。” |
rating | int | 用户评分(1-5星) | 5 |
sentiment | str | 情感倾向(正面/中性/负面) | “正面” |
timestamp | datetime | 评论发表时间 | “2024-05-10 14:30:00” |
helpful_votes | int | 有用投票数 | 24 |
category | str | 商品类目 | “电子产品/手机” |
为了后续演示,我们使用Python快速生成一个模拟数据集。
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
import random
def generate_sample_reviews(num=500):
"""生成模拟电商评论数据"""
products = ['智能手机 Alpha', '无线耳机 Beta', '智能手表 Gamma', '笔记本电脑 Delta']
categories = ['电子产品/手机', '电子产品/音频', '电子产品/可穿戴', '电子产品/电脑']
features = {
'智能手机 Alpha': ['拍照', '电池续航', '屏幕显示', '系统流畅度', '充电速度'],
'无线耳机 Beta': ['音质', '降噪效果', '佩戴舒适度', '续航', '连接稳定性'],
'智能手表 Gamma': ['健康监测', '运动模式', '屏幕亮度', '续航', '消息提醒'],
'笔记本电脑 Delta': ['运行速度', '散热性能', '屏幕素质', '便携性', '键盘手感']
}
sentiments = ['正面', '负面', '中性']
reviews = []
base_date = datetime.now() - timedelta(days=90)
for i in range(num):
product_idx = i % len(products)
product = products[product_idx]
category = categories[product_idx]
# 生成评论文本:结合商品、特性、情感
feature = random.choice(features[product])
sentiment = random.choice(sentiments)
text_templates = {
'正面': [f"这款{product}的{feature}非常出色,超出了我的预期。",
f"必须夸一下{product}的{feature},体验太棒了!",
f"{feature}是这款{product}最大的亮点,强烈推荐。"],
'负面': [f"{product}的{feature}让人失望,存在明显缺陷。",
f"对{product}的{feature}不太满意,希望后续能改进。",
f"{feature}方面{product}做得不好,影响了整体使用感受。"],
'中性': [f"{product}的{feature}中规中矩,符合这个价位的表现。",
f"关于{product}的{feature},我的感受比较一般。",
f"{feature}算是{product}的一个标准配置吧,没什么惊喜。"],
}
review_text = random.choice(text_templates[sentiment])
# 评分与情感大致关联,但不完全一致
if sentiment == '正面':
rating = random.choices([4, 5], weights=[0.3, 0.7])[0]
elif sentiment == '负面':
rating = random.choices([1, 2], weights=[0.7, 0.3])[0]
else:
rating = random.choice([3, 4])
# 生成时间戳(在过去90天内随机分布)
days_ago = random.randint(0, 90)
hours_ago = random.randint(0, 23)
review_time = base_date + timedelta(days=days_ago, hours=hours_ago)
reviews.append({
'review_id': f'rev_{i:04d}',
'product_id': product.replace(' ', '_').lower(),
'product_name': product,
'review_text': review_text,
'rating': rating,
'sentiment': sentiment,
'timestamp': review_time,
'helpful_votes': random.randint(0, 50),
'category': category,
'feature': feature
})
return pd.DataFrame(reviews)
# 生成并查看数据
df_reviews = generate_sample_reviews(500)
print(df_reviews[['product_name', 'review_text', 'rating', 'sentiment', 'feature']].head())
提示:在实际项目中,你的数据可能来自数据库导出、API接口或日志文件。关键是将原始数据清洗并组织成类似上述结构化的
DataFrame或文档列表,并为每个文档附加正确的元数据。
2.2 文档分割、嵌入与向量存储
评论文本可能长短不一。为了更精细的检索,我们通常需要将长文本分割成更小的“块”(chunks)。同时,为了进行语义搜索,我们需要将文本块转化为向量(嵌入)。
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import Chroma # 这里以Chrona为例,可替换为FAISS, Pinecone等
from langchain_core.documents import Document
import os
# 假设已设置环境变量 OPENAI_API_KEY
# os.environ["OPENAI_API_KEY"] = "your-api-key"
# 1. 将DataFrame转换为LangChain Document对象
documents = []
for _, row in df_reviews.iterrows():
# 将元数据存储在metadata中
metadata = {
'review_id': row['review_id'],
'product_id': row['product_id'],
'product_name': row['product_name'],
'rating': row['rating'],
'sentiment': row['sentiment'],
'timestamp': row['timestamp'].isoformat(), # 序列化时间
'helpful_votes': row['helpful_votes'],
'category': row['category'],
'feature': row['feature']
}
# 核心文本内容
doc = Document(page_content=row['review_text'], metadata=metadata)
documents.append(doc)
# 2. 文本分割(根据评论特点,块可以较小)
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=200, # 每个块大约200字符
chunk_overlap=50, # 块间重叠50字符以保持上下文
separators=["\n\n", "\n", "。", "!", "?", ",", " ", ""] # 中文分隔符
)
split_docs = text_splitter.split_documents(documents)
print(f"原始文档数: {len(documents)}, 分割后文档块数: {len(split_docs)}")
# 3. 初始化嵌入模型
embeddings = OpenAIEmbeddings(model="text-embedding-3-small") # 使用较小、性价比较高的模型
# 4. 创建向量存储并存入文档块
vectorstore = Chroma.from_documents(
documents=split_docs,
embedding=embeddings,
collection_name="ecommerce_reviews",
persist_directory="./chroma_db" # 可选:持久化到磁盘
)
print("向量数据库构建完成。")
至此,我们已经拥有了一个支持语义搜索的向量数据库。但它的检索能力还仅限于文本相似度。接下来,我们将进入最核心的部分:打造一个能理解复杂业务过滤逻辑的智能检索器。
3. 核心构建:实现支持多维度过滤的自定义Retriever
现在,我们基于LangChain的BaseRetriever基类,来创建我们的EcommerceReviewRetriever。这个检索器将支持以下功能:
- 语义检索:利用底层向量存储进行核心的语义相似度搜索。
- 多维度元数据过滤:允许用户指定
rating范围、sentiment列表、时间区间、product_id等过滤条件。 - 混合检索策略:可选地结合关键词匹配(如BM25),以提升对特定术语的召回率。
3.1 定义检索器类与初始化
首先,我们定义检索器的骨架。它将封装一个向量存储,并接受过滤参数。
from langchain_core.retrievers import BaseRetriever
from langchain_core.callbacks import CallbackManagerForRetrieverRun
from langchain_core.documents import Document
from typing import List, Optional, Dict, Any, Union
from datetime import datetime
class EcommerceReviewRetriever(BaseRetriever):
"""面向电商评论的自定义检索器,支持语义搜索与多维度元数据过滤。"""
def __init__(
self,
vectorstore: Any, # 底层向量存储,如Chroma, FAISS实例
rating_filter: Optional[tuple] = None, # 评分范围,如 (4, 5)
sentiment_filter: Optional[List[str]] = None, # 情感列表,如 [“正面”, “中性”]
product_filter: Optional[List[str]] = None, # 商品ID列表
time_filter: Optional[tuple] = None, # 时间范围,如 (start_datetime, end_datetime)
feature_filter: Optional[str] = None, # 产品特性关键词
use_keyword_search: bool = False, # 是否启用关键词检索混合
keyword_search_kwargs: Optional[Dict] = None,
search_kwargs: Optional[Dict] = None, # 向量搜索参数
):
super().__init__()
self.vectorstore = vectorstore
self.rating_filter = rating_filter
self.sentiment_filter = sentiment_filter
self.product_filter = product_filter
self.time_filter = time_filter
self.feature_filter = feature_filter
self.use_keyword_search = use_keyword_search
self.keyword_search_kwargs = keyword_search_kwargs or {}
self.search_kwargs = search_kwargs or {"k": 10} # 默认返回10个结果
# 如果启用关键词搜索,初始化一个简单的BM25检索器(这里需要安装rank_bm25)
# 注意:这是一个简化示例,实际生产环境可能需要更健壮的实现
self.keyword_retriever = None
if use_keyword_search:
# 这里仅为示意,实际需准备文档库
# from rank_bm25 import BM25Okapi
# corpus = [doc.page_content for doc in split_docs]
# self.bm25 = BM25Okapi(corpus)
pass
3.2 实现元数据过滤逻辑
核心的_get_relevant_documents方法需要完成以下步骤:1) 执行初始检索;2) 应用元数据过滤器;3) (可选)融合关键词检索结果。
def _get_relevant_documents(
self,
query: str,
*,
run_manager: CallbackManagerForRetrieverRun = None,
) -> List[Document]:
"""根据查询和预设过滤器检索相关文档。"""
# 步骤1:从向量存储进行初始语义搜索
try:
vector_results = self.vectorstore.similarity_search(
query, **self.search_kwargs
)
except Exception as e:
# 处理向量搜索错误,例如连接失败
print(f"向量搜索失败: {e}")
vector_results = []
# 步骤2:应用元数据过滤器
filtered_docs = self._apply_metadata_filters(vector_results)
# 步骤3:(可选)执行关键词搜索并融合结果
if self.use_keyword_search and query.strip():
keyword_docs = self._keyword_search(query)
# 简单的结果融合:去重后合并,优先保留向量搜索结果中的文档
filtered_docs = self._merge_results(filtered_docs, keyword_docs)
return filtered_docs
def _apply_metadata_filters(self, docs: List[Document]) -> List[Document]:
"""应用评分、情感、时间等元数据过滤器。"""
filtered = []
for doc in docs:
meta = doc.metadata
include = True
# 1. 评分过滤
if self.rating_filter:
min_rating, max_rating = self.rating_filter
if not (min_rating <= meta.get('rating', 0) <= max_rating):
include = False
# 2. 情感过滤
if self.sentiment_filter and include:
if meta.get('sentiment') not in self.sentiment_filter:
include = False
# 3. 商品过滤
if self.product_filter and include:
if meta.get('product_id') not in self.product_filter:
include = False
# 4. 时间过滤 (需将字符串时间转回datetime对象进行比较)
if self.time_filter and include:
start_time, end_time = self.time_filter
doc_time_str = meta.get('timestamp')
if doc_time_str:
try:
doc_time = datetime.fromisoformat(doc_time_str)
if not (start_time <= doc_time <= end_time):
include = False
except ValueError:
# 时间格式解析失败,跳过此过滤条件或记录日志
pass
# 5. 产品特性过滤(在元数据中匹配)
if self.feature_filter and include:
if self.feature_filter.lower() not in meta.get('feature', '').lower():
include = False
if include:
filtered.append(doc)
return filtered
def _keyword_search(self, query: str) -> List[Document]:
"""基于关键词的检索(简化版BM25示例)。"""
# 此处为概念演示。实际应用中,你需要一个与vectorstore同步的文档索引。
# 例如,可以预先构建一个 {doc_id: Document} 的映射和BM25索引。
# 这里返回空列表,示意流程。
# 真实实现可能如下:
# tokenized_query = query.split()
# doc_scores = self.bm25.get_scores(tokenized_query)
# top_indices = np.argsort(doc_scores)[-self.search_kwargs.get("k", 10):][::-1]
# return [self.all_docs[i] for i in top_indices if doc_scores[i] > 0]
return []
def _merge_results(self, vector_docs: List[Document], keyword_docs: List[Document]) -> List[Document]:
"""融合语义检索和关键词检索的结果。"""
# 简单的基于文档ID的去重与合并
seen_ids = set()
merged = []
for doc in vector_docs + keyword_docs:
doc_id = doc.metadata.get('review_id')
if doc_id not in seen_ids:
seen_ids.add(doc_id)
merged.append(doc)
# 可以在此处实现更复杂的排序逻辑,如加权分数
return merged
3.3 让检索器“动态”可配置
上面的实现将过滤条件写死在初始化参数里,不够灵活。更优雅的方式是允许在每次调用get_relevant_documents时动态指定过滤条件。我们可以通过重载invoke或使用Runnable协议来实现。这里展示一种通过扩展_get_relevant_documents参数来实现动态过滤的方法(需注意保持与基类兼容性,实践中可能需封装自定义方法)。
# 在类中添加一个公共方法,支持动态过滤
def search_with_filters(
self,
query: str,
rating_filter: Optional[tuple] = None,
sentiment_filter: Optional[List[str]] = None,
product_filter: Optional[List[str]] = None,
time_filter: Optional[tuple] = None,
feature_filter: Optional[str] = None,
**search_kwargs
) -> List[Document]:
"""动态指定过滤条件进行搜索。"""
# 临时覆盖实例的过滤条件
original_filters = {
'rating_filter': self.rating_filter,
'sentiment_filter': self.sentiment_filter,
'product_filter': self.product_filter,
'time_filter': self.time_filter,
'feature_filter': self.feature_filter,
}
self.rating_filter = rating_filter
self.sentiment_filter = sentiment_filter
self.product_filter = product_filter
self.time_filter = time_filter
self.feature_filter = feature_filter
try:
# 使用更新后的过滤器执行检索
results = self.get_relevant_documents(query, **search_kwargs)
finally:
# 恢复原始过滤条件
for key, value in original_filters.items():
setattr(self, key, value)
return results
现在,我们的自定义检索器已经具备了核心能力。让我们实例化它并进行测试。
# 实例化我们的自定义检索器
custom_retriever = EcommerceReviewRetriever(
vectorstore=vectorstore,
rating_filter=(4, 5), # 默认只检索4-5星好评
search_kwargs={"k": 5}
)
# 测试1:基础语义搜索
print("=== 测试1:搜索‘拍照’ ===")
docs = custom_retriever.get_relevant_documents("拍照")
for doc in docs[:2]: # 展示前2个结果
print(f"内容: {doc.page_content[:60]}... | 评分: {doc.metadata.get('rating')} | 情感: {doc.metadata.get('sentiment')}")
# 测试2:使用动态过滤方法,搜索“电池”但限定负面情感
print("\n=== 测试2:动态过滤搜索‘电池’且情感为负面 ===")
negative_battery_reviews = custom_retriever.search_with_filters(
query="电池",
sentiment_filter=["负面"],
rating_filter=None # 覆盖默认的(4,5)评分过滤
)
for doc in negative_battery_reviews[:2]:
print(f"内容: {doc.page_content[:60]}... | 评分: {doc.metadata.get('rating')} | 情感: {doc.metadata.get('sentiment')}")
通过以上测试,你应该能看到检索器能够根据语义和元数据过滤返回精确的结果。这个检索器已经是一个功能强大的独立工具了。但LangChain的魅力在于链式组合,接下来我们看看如何将它无缝集成到更复杂的LCEL链中。
4. 集成与进阶:对接LCEL链与性能优化
构建一个独立的Retriever只是第一步,让它成为智能应用流水线中的一环,才能发挥最大价值。LangChain Expression Language (LCEL) 提供了声明式的链组合方式,我们的自定义Retriever可以轻松融入。
4.1 将自定义Retriever接入LCEL链
假设我们要构建一个电商评论分析助手:用户输入一个自然语言问题,系统首先检索相关评论,然后让LLM基于这些评论生成一个总结性回答。
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough, RunnableLambda
# 1. 定义提示模板
template = """你是一个专业的电商数据分析助手。请根据以下用户评论片段,回答用户的问题。
请保持回答客观、简洁,并引用评论中的具体观点。
相关评论片段:
{context}
用户问题:{question}
请基于以上评论进行回答:"""
prompt = ChatPromptTemplate.from_template(template)
# 2. 初始化LLM
llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0)
# 3. 定义一个格式化检索结果的函数
def format_review_docs(docs):
"""将检索到的文档格式化为字符串上下文。"""
formatted = []
for i, doc in enumerate(docs):
meta = doc.metadata
formatted.append(
f"[片段{i+1}] 产品:{meta.get('product_name', 'N/A')} | "
f"评分:{meta.get('rating', 'N/A')}星 | "
f"情感:{meta.get('sentiment', 'N/A')}\n"
f"评论内容:{doc.page_content}\n"
)
return "\n".join(formatted)
# 4. 创建LCEL链
# 注意:我们的custom_retriever本身就是一个Runnable(因为继承自BaseRetriever)
review_qa_chain = (
{
"context": custom_retriever | RunnableLambda(format_review_docs),
"question": RunnablePassthrough()
}
| prompt
| llm
| StrOutputParser()
)
# 5. 测试链式调用
print("=== LCEL链测试:分析‘智能手机 Alpha的屏幕问题’ ===")
response = review_qa_chain.invoke("智能手机 Alpha的屏幕主要有哪些负面评价?")
print(response)
这个链的工作流程非常清晰:RunnablePassthrough()将用户问题直接传递;custom_retriever根据该问题检索相关评论;RunnableLambda(format_review_docs)将检索结果格式化为LLM易于理解的文本;最后,prompt和llm基于格式化的上下文生成答案。
4.2 实现更复杂的混合检索策略
之前我们简单提到了关键词检索。在实际电商场景中,用户可能搜索非常具体的型号或术语(如“骁龙8 Gen 3”),这些专有名词的语义嵌入可能不够精确。此时,结合关键词检索(如BM25)能显著提升召回率。下面我们实现一个更完整的混合检索器。
from rank_bm25 import BM25Okapi
import jieba # 用于中文分词
class HybridEcommerceRetriever(EcommerceReviewRetriever):
"""增强版混合检索器,结合语义搜索与BM25关键词搜索。"""
def __init__(self, vectorstore, all_documents: List[Document], **kwargs):
"""
Args:
all_documents: 用于构建BM25索引的完整文档列表。
"""
super().__init__(vectorstore, **kwargs)
self.all_docs = all_documents
# 构建BM25索引
self._build_bm25_index()
def _build_bm25_index(self):
"""为所有文档构建BM25索引。"""
# 对中文进行分词
tokenized_corpus = [list(jieba.cut(doc.page_content)) for doc in self.all_docs]
self.bm25 = BM25Okapi(tokenized_corpus)
# 建立文档ID到索引位置的映射
self.doc_id_to_index = {doc.metadata['review_id']: i for i, doc in enumerate(self.all_docs)}
def _keyword_search(self, query: str) -> List[Document]:
"""使用BM25进行关键词检索。"""
if not hasattr(self, 'bm25'):
return []
# 对查询进行分词
tokenized_query = list(jieba.cut(query))
# 获取文档得分
doc_scores = self.bm25.get_scores(tokenized_query)
# 获取top k个结果的索引
k = self.keyword_search_kwargs.get("k", self.search_kwargs.get("k", 10))
top_n_indices = np.argsort(doc_scores)[-k:][::-1]
keyword_docs = []
for idx in top_n_indices:
if doc_scores[idx] > 0: # 只返回有相关性的
keyword_docs.append(self.all_docs[idx])
return keyword_docs
def _merge_results(self, vector_docs: List[Document], keyword_docs: List[Document]) -> List[Document]:
"""基于加权分数融合结果。"""
# 为每篇文档赋予来源和初始分数
scored_docs = {}
# 向量搜索结果的分数(假设相似度分数在metadata中,这里简化处理)
for i, doc in enumerate(vector_docs):
doc_id = doc.metadata['review_id']
# 简化的排名分数:越靠前分数越高
vector_score = 1.0 / (i + 1)
scored_docs[doc_id] = {'doc': doc, 'vector_score': vector_score, 'keyword_score': 0.0}
# BM25搜索结果的分数
for i, doc in enumerate(keyword_docs):
doc_id = doc.metadata['review_id']
keyword_score = 1.0 / (i + 1)
if doc_id in scored_docs:
# 如果文档同时出现在两个结果中,合并分数
scored_docs[doc_id]['keyword_score'] = keyword_score
else:
scored_docs[doc_id] = {'doc': doc, 'vector_score': 0.0, 'keyword_score': keyword_score}
# 计算综合分数(这里采用简单加权平均,权重可调)
alpha = 0.7 # 向量搜索权重
beta = 0.3 # 关键词搜索权重
for info in scored_docs.values():
info['combined_score'] = alpha * info['vector_score'] + beta * info['keyword_score']
# 按综合分数排序
sorted_docs = sorted(scored_docs.values(), key=lambda x: x['combined_score'], reverse=True)
final_k = self.search_kwargs.get("k", 10)
return [info['doc'] for info in sorted_docs[:final_k]]
# 使用混合检索器
print("\n=== 初始化混合检索器 ===")
hybrid_retriever = HybridEcommerceRetriever(
vectorstore=vectorstore,
all_documents=split_docs, # 传入所有文档块用于BM25索引
use_keyword_search=True, # 启用关键词搜索
rating_filter=None,
search_kwargs={"k": 8}
)
# 测试混合检索:搜索一个可能包含专有名词的查询
print("测试查询:‘续航时间长’")
hybrid_results = hybrid_retriever.get_relevant_documents("续航时间长")
print(f"混合检索返回 {len(hybrid_results)} 条结果。")
for doc in hybrid_results[:3]:
print(f" - {doc.page_content[:50]}... (评分: {doc.metadata.get('rating')})")
这个HybridEcommerceRetriever在继承原有元数据过滤能力的基础上,增加了基于BM25的关键词检索,并通过加权分数融合两种检索策略的结果。这对于处理包含品牌、型号、特定参数等“硬匹配”关键词的查询尤为有效。
4.3 性能考量与优化建议
在真实生产环境中部署自定义检索器时,性能是必须考虑的因素:
- 向量搜索优化:确保你的向量数据库(如Chroma, Weaviate, Pinecone)有合适的索引。对于大规模数据,考虑使用
HNSW或IVF等索引类型来加速近似最近邻搜索。 - 元数据过滤下推:如果底层向量数据库支持(如Weaviate, Qdrant),尽量将元数据过滤逻辑“下推”到数据库查询层面,而不是在Python内存中过滤。这可以大幅减少网络传输和数据反序列化的开销。
# 伪代码示例:使用支持过滤的向量库客户端 # results = vectorstore.similarity_search( # query, # k=50, # filter={"rating": {"$gte": 4}, "sentiment": {"$in": ["正面"]}} # 过滤条件直接下推 # ) - 缓存策略:对于频繁出现的相同或相似查询,可以引入缓存层(如
Redis或LRU内存缓存),存储检索结果,避免重复的向量计算和数据库查询。 - 异步处理:如果检索流程涉及多个IO操作(如查询多个数据源),使用异步(
async/await)可以显著提升吞吐量。确保你的_get_relevant_documents方法或相关组件支持异步调用。 - 索引更新策略:电商评论是动态增长的。需要设计一个稳健的流程来定期或实时地将新评论添加到向量存储和关键词索引中,同时考虑数据的一致性。
构建自定义Retriever是一个迭代过程。从满足核心功能开始,然后根据实际查询日志和性能监控,不断调整过滤逻辑、混合策略的权重、分块大小等参数。最终,你将获得一个完全贴合你业务需求的、高性能的智能检索核心。
338



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



