1. 混合搜索系统概述
在信息检索领域,混合搜索系统正逐渐成为提升搜索质量的主流方案。这个项目实现了一个结合BM25算法和密集向量检索的混合搜索系统,专门针对医疗咨询场景中的症状描述与治疗方案匹配问题。
我最近在实际工作中发现,单一检索方法往往存在明显短板。传统关键词检索(如BM25)擅长处理精确匹配,但对语义相似但用词不同的查询效果不佳;而基于深度学习的向量检索能捕捉语义关系,却可能忽略关键词的重要性。这个项目通过加权融合两种方法,在医疗问答场景中实现了更精准的结果召回。
系统处理的数据结构很明确:每条数据包含"instruction"(症状描述)和"output"(解释或方案)。例如输入"嘴唇肿起来了,怎么办",系统需要从知识库中找到最相关的医疗建议。这种需求在在线医疗咨询、智能分诊等场景中非常普遍。
2. 系统设计与核心组件
2.1 数据处理流程
系统首先加载JSON格式的训练数据,每条数据包含两个关键字段:
- instruction:患者的症状描述(如"持续头痛三天")
- output:对应的医疗建议(如"建议服用布洛芬并观察")
with open('../Data/train.json', 'r', encoding='utf-8') as f: data = [json.loads(line) for line in f.readlines()] instructions = [entry['instruction'] for entry in data] outputs = [entry['output'] for entry in data]实际应用中,建议对原始数据进行清洗和标准化处理,比如统一症状描述中的医学术语(将"发烧"和"发热"归一化),这对提升检索效果至关重要。
2.2 BM25检索模块实现
BM25是基于词频统计的传统检索算法,本项目使用rank_bm25库实现:
from rank_bm25 import BM25Okapi import jieba def bm25_search(query): # 中文分词处理 tokenized_corpus = [jieba.lcut(doc) for doc in instructions] bm25 = BM25Okapi(tokenized_corpus) # 查询处理与评分 tokenized_query = jieba.lcut(query) bm25_scores = bm25.get_scores(tokenized_query) # 分数归一化处理 bm25_scores = np.array(bm25_scores) bm25_scores_normalized = (bm25_scores - bm25_scores.min()) / (bm25_scores.max() - bm25_scores.min()) return bm25_scores_normalized关键细节说明:
- 必须对中文进行分词处理,jieba分词效果直接影响检索质量
- BM25Okapi会计算每个词项的逆文档频率(IDF)和文档内词频(TF)
- 归一化处理确保不同查询间的分数可比性
2.3 向量检索模块设计
向量检索使用ChromaDB向量数据库和阿里通义千问的embedding模型:
class MyVectorDBConnector: def __init__(self, collection_name): self.client = chromadb.Client(Settings(allow_reset=True)) self.collection = self.client.get_or_create_collection(collection_name) self.api_client = get_normal_client() # 自定义API客户端 def get_embeddings_batch(self, texts, model=ALI_TONGYI_EMBEDDING_V4, batch_size=10): all_embeddings = [] for i in range(0, len(texts), batch_size): batch = texts[i:i + batch_size] data = self.api_client.embeddings.create(input=batch, model=model).data all_embeddings.extend([x.embedding for x in data]) return all_embeddings实际使用中发现几个关键点:
- 批量处理文本时需要注意API的速率限制,batch_size=10是个经验值
- 不同embedding模型对医疗文本的编码效果差异很大,需要测试选择
- ChromaDB的collection命名要有明确业务含义,方便后续维护
3. 混合搜索实现细节
3.1 分数融合策略
核心融合算法采用线性加权:
def hybrid_search(query, top_k=3, bm25_weight=0.5): bm25_scores = bm25_search(query) vector_scores = vector_search(query) # 加权融合 combined_scores = bm25_weight * bm25_scores + (1-bm25_weight) * vector_scores # 结果排序 top_index = combined_scores.argsort()[::-1][:top_k] return [outputs[i] for i in top_index]权重选择经验:
- bm25_weight=0.5是常用起点
- 对术语规范的领域(如医学)可适当提高向量权重
- 可通过A/B测试确定最佳权重
3.2 向量距离计算
项目使用欧氏距离衡量向量相似度:
query_embedding = np.array(vector_db.get_embeddings_batch([query])) doc_embeddings = np.array(vector_db.get_embeddings_batch(instructions)) # 计算欧氏距离并转换为相似度分数 vector_scores = np.linalg.norm(query_embedding - doc_embeddings, axis=1) vector_scores_normalized = 1 - (vector_scores - vector_scores.min()) / (vector_scores.max() - vector_scores.min())实际测试发现,对医疗文本,余弦相似度有时比欧氏距离效果更好,建议两种方法都尝试
4. 性能优化与实践经验
4.1 检索效率优化
- 索引预热:系统启动时预加载BM25索引和向量数据
- 结果缓存:对高频查询实现结果缓存
- 批量处理:向量计算采用批处理减少API调用
# 初始化时预加载数据 vector_db = MyVectorDBConnector("medical_knowledge") vector_db.add_documents(instructions, outputs)4.2 医疗场景特殊处理
- 同义词扩展:构建医疗同义词库(如"心梗=心肌梗塞")
- 症状标准化:将口语化描述转为医学术语
- 结果排序优化:危急症状优先排序
4.3 常见问题排查
分词不一致:
- 症状:BM25检索结果不稳定
- 解决:统一使用医疗领域分词词典
向量质量差:
- 症状:语义相近的查询结果差异大
- 解决:尝试不同embedding模型或微调
权重选择困难:
- 症状:混合效果不如单一方法
- 解决:基于测试集进行网格搜索调参
5. 扩展应用与改进方向
这个混合搜索框架可应用于多个领域:
- 法律咨询:法条与案例检索
- 电商客服:商品问题与解决方案匹配
- 教育问答:学习问题与知识点关联
值得尝试的改进方向:
- 动态权重调整:根据查询特点自动调整BM25/向量权重
- 多模态检索:结合图片症状描述等
- 反馈学习:根据用户点击优化排序
我在实际部署中发现,系统对长尾查询(罕见症状)的处理仍需加强。最近尝试用Rerank模型对初步结果进行重排序,准确率提升了约15%。另一个实用技巧是对医疗结果添加可信度评分,帮助用户判断信息可靠性。