014、索引高级实战:当单一向量库不够用的时候
上周排查一个线上问题,用户反馈"找相似文档"功能时灵时不灵。跟踪日志发现,当用户查询涉及多个专业领域时,返回结果总是偏向某一个方向。打开向量库一看------好家伙,我们把所有类型的文档(技术手册、用户反馈、产品介绍)全塞进了一个ChromaDB集合里。
这就像把小说、菜谱、维修手册混在一个书架上,然后指望读者能快速找到想要的东西。今天我们就聊聊LangChain索引系统的高级玩法:多索引协同、元数据精准过滤、混合检索策略。
多索引架构:分而治之的智慧
先看我们重构后的方案:
python
from langchain.vectorstores import Chroma, FAISS
from langchain.embeddings import OpenAIEmbeddings
from langchain.text_splitter import RecursiveCharacterTextSplitter
# 以前是这么干的(别学)
# all_docs = load_all_documents() # 各种类型混在一起
# vectorstore = Chroma.from_documents(all_docs, embedding)
# 现在按业务域拆分索引
embeddings = OpenAIEmbeddings()
# 创建三个独立的向量库
tech_docs = load_technical_manuals() # 技术文档
feedback_docs = load_user_feedback() # 用户反馈
product_docs = load_product_intros() # 产品介绍
# 分别构建索引,注意这里加了元数据标签
tech_store = Chroma.from_documents(
tech_docs,
embeddings,
collection_name="tech_knowledge", # 集合名要明确
metadatas=[{"source": "manual", "domain": "technical"} for _ in tech_docs]
)
feedback_store = Chroma.from_documents(
feedback_docs,
embeddings,
collection_name="user_feedback",
metadatas=[{"source": "feedback", "domain": "user"} for _ in feedback_docs]
)
# FAISS也可以混着用,有些场景性能更好
product_store = FAISS.from_documents(
product_docs,
embeddings
)
# 给FAISS加元数据需要额外处理
product_store.metadata = [{"source": "product", "domain": "marketing"} for _ in product_docs]
为什么要这么麻烦?实战中发现几个关键点:
- 更新频率不同:用户反馈每天更新,技术文档每月更新,分开维护更省资源
- 相似度阈值不同:技术文档需要高精度匹配(阈值0.9),用户反馈可以宽松些(阈值0.7)
- Embedding模型可以不同:技术文档用text-embedding-ada-002,用户反馈用多语言模型
元数据过滤:给检索装上GPS
有了多索引,下一步是精准定位。LangChain的元数据过滤语法需要适应不同向量库的实现差异:
python
# Chroma的过滤语法(注意这个坑:不同版本API有变化)
from langchain.vectorstores import Chroma
# 正确写法:用filter参数
results = tech_store.similarity_search_with_score(
query="API限流如何配置",
k=5,
filter={"source": "manual", "version": "v2.3"} # 同时过滤多个字段
)
# 错误写法:直接传字符串(早期版本支持,现在会报错)
# results = tech_store.similarity_search("query", filter="source='manual'")
# 更复杂的过滤:结合日期范围
import datetime
last_month = datetime.datetime.now() - datetime.timedelta(days=30)
# 假设metadata里有created_at字段
recent_docs = feedback_store.similarity_search(
"登录问题",
filter={
"source": "feedback",
"created_at": {"$gte": last_month.isoformat()} # Chroma支持的操作符
}
)
实际项目中,我们给每个文档片段添加了丰富的元数据:
python
metadata_template = {
"doc_id": "唯一文档ID",
"doc_type": "manual/feedback/product",
"department": "技术部/市场部/客服部",
"security_level": "public/internal/confidential", # 权限控制用
"language": "zh/en/ja",
"updated_at": "2024-01-15T10:30:00",
"version": "v2.1",
"relevance_weight": 1.0 # 可以动态调整的权重
}
混合检索策略:向量搜索只是开始
纯向量搜索在以下场景会翻车:
- 精确术语查询(如"错误码 ERR-402")
- 版本号匹配("v2.3.1")
- 日期范围("上周的反馈")
这时候需要混合检索:
python
from langchain.retrievers import BM25Retriever, EnsembleRetriever
from langchain.retrievers.self_query.base import SelfQueryRetriever
from langchain.chains.query_constructor.base import AttributeInfo
# 1. 先做元数据自查询(让LLM自己分析查询中的过滤条件)
metadata_field_info = [
AttributeInfo(name="source", description="文档来源", type="string"),
AttributeInfo(name="created_at", description="创建时间", type="date"),
AttributeInfo(name="department", description="所属部门", type="string"),
]
self_query_retriever = SelfQueryRetriever.from_llm(
llm=llm,
vectorstore=tech_store,
document_contents="技术文档内容",
metadata_field_info=metadata_field_info,
verbose=True # 调试时打开,看LLM如何解析查询
)
# 2. 传统关键词检索(BM25还是香)
from langchain.retrievers import BM25Retriever
from langchain.schema import Document
# 准备BM25的文档集
bm25_docs = [Document(page_content=doc.page_content, metadata=doc.metadata)
for doc in tech_docs]
bm25_retriever = BM25Retriever.from_documents(bm25_docs)
# 3. 集成检索器 - 加权混合
ensemble_retriever = EnsembleRetriever(
retrievers=[
(self_query_retriever, 0.3), # 元数据过滤优先
(tech_store.as_retriever(), 0.5), # 向量搜索主权重
(bm25_retriever, 0.2) # 关键词检索兜底
],
weights=[0.3, 0.5, 0.2] # 权重可基于查询类型动态调整
)
# 4. 重排序(Reranking) - 提升精度
from langchain.retrievers.document_compressors import LLMChainReranker
from langchain.retrievers.document_compressors.chain_filter import LLMChainFilter
# 用LLM对初筛结果重新打分
reranker = LLMChainReranker.from_llm(llm=llm, top_n=5)
compressed_docs = reranker.compress_documents(
documents=initial_results,
query=user_query
)
动态路由:智能选择索引
当查询过来时,怎么知道该查哪个索引?我们实现了一个路由层:
python
class IndexRouter:
def __init__(self, llm):
self.llm = llm
self.index_map = {
"technical": tech_store,
"feedback": feedback_store,
"product": product_store
}
def route_query(self, query: str):
"""让LLM判断查询意图,返回目标索引"""
prompt = f"""
分析用户查询所属的领域:
查询:{query}
可选领域:
- technical: 技术问题、API使用、错误排查、配置方法
- feedback: 用户反馈、投诉、建议、使用体验
- product: 产品功能、价格、套餐、购买咨询
只返回领域关键词,不要解释。
"""
response = self.llm.predict(prompt)
domain = response.strip().lower()
# 兜底策略:如果LLM判断不准,三个索引都查
if domain not in self.index_map:
return list(self.index_map.values())
return [self.index_map[domain]]
def hybrid_search(self, query: str, top_k: int = 10):
"""完整的混合检索流程"""
# 1. 路由
target_stores = self.route_query(query)
all_results = []
# 2. 并行查询多个索引
for store in target_stores:
# 向量相似度搜索
vector_results = store.similarity_search_with_relevance_scores(query, k=top_k)
# 如果有元数据过滤条件
if self._has_metadata_filter(query):
filtered = store.similarity_search(
query,
filter=self._extract_filters(query),
k=top_k
)
vector_results.extend(filtered)
all_results.extend(vector_results)
# 3. 去重和排序(按分数)
seen_content = set()
unique_results = []
for doc, score in sorted(all_results, key=lambda x: x[1], reverse=True):
if doc.page_content[:100] not in seen_content: # 简单去重
seen_content.add(doc.page_content[:100])
unique_results.append((doc, score))
return unique_results[:top_k]
性能优化实战经验
在压力测试中我们踩过几个坑:
坑1:元数据过滤性能
python
# 慢:过滤条件太多太细
filter={"department": "tech", "version": "v2.1", "language": "zh", "status": "active"}
# 优化:建立复合索引或分层过滤
# 第一层:按部门分区存储
# 第二层:在部门内过滤其他条件
坑2:向量库连接数
python
# 错误:每次查询都新建连接
def search(query):
store = Chroma(persist_directory="./chroma_db") # 每次都重新连接
return store.search(query)
# 正确:全局连接池
class VectorStorePool:
def __init__(self):
self.stores = {} # 缓存已加载的向量库
def get_store(self, store_id):
if store_id not in self.stores:
self.stores[store_id] = Chroma(
persist_directory=f"./chroma_db_{store_id}",
embedding_function=embeddings
)
return self.stores[store_id]
坑3:混合检索的延迟
并行查询确实快,但要注意资源消耗。我们最终实现了基于查询复杂度的自适应策略:
- 简单查询:只查主索引
- 中等复杂度:向量+关键词
- 复杂查询:全链路混合检索+重排序
个人经验建议
索引系统不是建完就完事的,它需要持续"运维"。我们团队现在每周会做这些事:
- 索引健康检查:监控每个向量库的查询延迟、命中率、内存占用
- 元数据质量审计:随机抽查文档的元数据是否准确完整
- 查询日志分析:统计哪些查询效果差,针对性优化
- Embedding模型评估:定期用测试集检查embedding质量是否下降
关于选型,如果数据量小于100万条,用Chroma足够;超过这个量级考虑Weaviate或PGVector。但记住,没有银弹------我们生产环境是Chroma+Elasticsearch混合部署,前者负责语义搜索,后者处理精确过滤和聚合。
最后说一个反直觉的经验:有时候加索引不如删索引。我们曾经给每个产品线建独立索引,结果维护成本爆炸。后来合并了相似领域,反而因为数据量足够大,embedding的表征能力更强了。
索引设计的本质是在精度、召回率、性能、维护成本之间找平衡点。下次当你看到"相似度0.78的结果"时,不妨想想背后是哪些索引在协同工作,又是哪些元数据在默默过滤------好的索引系统应该像熟练的图书管理员,不仅知道每本书在哪,还知道什么时候该推荐哪一本。