IIWAB BM25 做初排 + 向量 / 深度模型做精排案例 - IIWAB

BM25 做初排 + 向量 / 深度模型做精排案例

IIWAB 5天前 ⋅ 20 阅读

一、BM25初排+向量/深度模型精排 系统架构图(Mermaid可直接渲染)

flowchart TD
    A[用户输入查询Q] --> B[查询预处理]
    B --> B1[分词/去停用词/归一化]
    B1 --> C[多路召回层]
    
    %% 召回层(千万级→千级)
    C --> C1[关键词召回:倒排索引]
    C --> C2[向量召回:FAISS/HNSW]
    C1 & C2 --> D[召回结果合并去重]
    D --> D1[候选集:500~1000条]
    
    %% BM25初排(千级→百级)
    D1 --> E[BM25初排]
    E --> E1[计算Score(Q,D)排序]
    E1 --> E2[初排结果:Top 50~200条]
    
    %% 精排层(百级→十级)
    E2 --> F[精排层]
    F --> F1[轻量版:向量余弦相似度]
    F --> F2[效果版:Cross-Encoder深度模型]
    F1 & F2 --> G[精排打分排序]
    
    %% 最终输出
    G --> H[结果后处理]
    H --> H1[去重/过滤低质结果]
    H1 --> I[返回最终Top10~50]
    I --> J[用户展示]
    
    %% 辅助模块
    subgraph 离线预处理
        K[文档库] --> K1[分词/特征提取]
        K1 --> K2[构建倒排索引(供BM25)]
        K1 --> K3[文档Embedding(供向量召回)]
        K2 --> C1
        K3 --> C2
    end

二、Java/Elasticsearch 落地方案

Elasticsearch(ES)是工业界最主流的检索引擎,天然支持BM25,结合Java客户端+向量插件可快速落地整套架构:

1. 环境准备

  • ES版本:7.x/8.x(推荐8.x,原生支持向量检索)
  • 插件:
    # 向量检索插件(ES 8.x已内置,7.x需安装)
    bin/elasticsearch-plugin install ml-commons
    
  • Java客户端:Elasticsearch High Level REST Client(对应ES版本)

2. 核心步骤(Java代码实现)

步骤1:创建索引(包含文本字段+向量字段)
import org.elasticsearch.action.admin.indices.create.CreateIndexRequest;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentFactory;

public class ESIndexCreator {
    // 创建混合索引(BM25文本+向量)
    public void createMixedIndex(RestHighLevelClient client, String indexName) throws Exception {
        CreateIndexRequest request = new CreateIndexRequest(indexName);
        
        // 构建索引映射:text字段(BM25) + dense_vector字段(向量)
        XContentBuilder mapping = XContentFactory.jsonBuilder()
            .startObject()
                .startObject("properties")
                    // 文本字段:用BM25打分
                    .startObject("content")
                        .field("type", "text")
                        .field("analyzer", "ik_max_word") // 中文分词器(需安装ik插件)
                        .field("similarity", "BM25") // 核心:指定BM25相似度算法
                    .endObject()
                    // 标题字段:权重更高(BM25多字段加权)
                    .startObject("title")
                        .field("type", "text")
                        .field("analyzer", "ik_max_word")
                        .field("similarity", "BM25")
                    .endObject()
                    // 向量字段:用于精排
                    .startObject("content_vector")
                        .field("type", "dense_vector")
                        .field("dims", 768) // 匹配BERT-base输出维度
                        .field("index", true) // 开启向量索引
                        .field("similarity", "cosine") // 余弦相似度
                    .endObject()
                .endObject()
            .endObject();
        
        request.mapping(mapping);
        client.indices().create(request, RequestOptions.DEFAULT);
    }
}
步骤2:BM25初排(Java实现)
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.sort.SortBuilders;
import org.elasticsearch.search.sort.SortOrder;

public class BM25FirstRank {
    // BM25初排:多字段加权,返回Top200
    public SearchResponse bm25Rank(RestHighLevelClient client, String indexName, String query) throws Exception {
        SearchRequest searchRequest = new SearchRequest(indexName);
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        
        // 1. 构建多字段BM25查询(标题权重3倍,正文1倍)
        BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
        boolQuery.should(QueryBuilders.matchQuery("title", query).boost(3.0f));
        boolQuery.should(QueryBuilders.matchQuery("content", query).boost(1.0f));
        
        // 2. 设置BM25排序(默认按BM25得分降序)
        sourceBuilder.query(boolQuery);
        sourceBuilder.sort(SortBuilders.scoreSort().order(SortOrder.DESC));
        
        // 3. 初排只返回Top200,减少后续精排压力
        sourceBuilder.size(200);
        
        searchRequest.source(sourceBuilder);
        return client.search(searchRequest, RequestOptions.DEFAULT);
    }
}
步骤3:向量精排(Java实现)
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.common.unit.Fuzziness;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;

public class VectorReRank {
    // 向量精排:基于BM25初排结果,用向量余弦相似度重排
    public SearchResponse vectorReRank(RestHighLevelClient client, String indexName, String query, float[] queryVector) throws Exception {
        SearchRequest searchRequest = new SearchRequest(indexName);
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        
        // 1. 先通过BM25初排拿到候选集(复用上面的BM25查询)
        BM25FirstRank bm25Rank = new BM25FirstRank();
        SearchResponse bm25Response = bm25Rank.bm25Rank(client, indexName, query);
        
        // 2. 提取BM25初排的文档ID,限定精排范围
        String[] docIds = extractDocIds(bm25Response); // 自定义方法:提取Top200文档ID
        sourceBuilder.query(QueryBuilders.idsQuery().addIds(docIds));
        
        // 3. 向量精排:按余弦相似度排序
        sourceBuilder.sort(
            SortBuilders.scriptSort(
                "cosineSimilarity(params.queryVector, doc['content_vector']) + 1.0", // 余弦相似度+1避免负数
                org.elasticsearch.script.ScriptType.INLINE
            )
            .param("queryVector", queryVector)
            .order(SortOrder.DESC)
        );
        
        // 4. 精排后返回Top10
        sourceBuilder.size(10);
        
        searchRequest.source(sourceBuilder);
        return client.search(searchRequest, RequestOptions.DEFAULT);
    }
    
    // 辅助方法:提取BM25结果的文档ID
    private String[] extractDocIds(SearchResponse response) {
        return response.getHits().getHits().stream()
                .map(hit -> hit.getId())
                .toArray(String[]::new);
    }
}
步骤4:完整链路封装(Java)
public class SearchPipeline {
    private RestHighLevelClient esClient;
    private EmbeddingModel embeddingModel; // 自定义Embedding模型(如BERT)
    
    // 完整检索链路:BM25初排 → 向量精排
    public SearchResponse search(String indexName, String query) throws Exception {
        // 1. 查询预处理
        String processedQuery = preprocessQuery(query); // 分词、去停用词
        
        // 2. 生成查询向量(调用BERT等模型)
        float[] queryVector = embeddingModel.getEmbedding(processedQuery);
        
        // 3. BM25初排
        BM25FirstRank bm25Rank = new BM25FirstRank();
        SearchResponse bm25Response = bm25Rank.bm25Rank(esClient, indexName, processedQuery);
        
        // 4. 向量精排
        VectorReRank vectorReRank = new VectorReRank();
        return vectorReRank.vectorReRank(esClient, indexName, processedQuery, queryVector);
    }
    
    // 预处理:分词、去停用词(对接IK分词器)
    private String preprocessQuery(String query) {
        // 实现自定义预处理逻辑
        return query;
    }
}

3. 关键配置与优化(ES落地必看)

  1. BM25参数调优(elasticsearch.yml):
    index.similarity.bm25.k1: 1.2  # 词频饱和参数
    index.similarity.bm25.b: 0.75   # 长度归一化参数
    
  2. 向量索引优化
    • 维度选择:768(BERT-base)/ 384(MiniLM)
    • 索引类型:hnsw(高效近似近邻)
    • 参数:m: 16, ef_construction: 100(平衡速度/精度)
  3. 性能优化
    • 初排数量:建议50~200(太多耗性能,太少丢相关文档)
    • 缓存:缓存热门查询的BM25结果和向量
    • 分片:按数据量合理分片(建议每分片10~30GB)

总结

  1. 架构核心:先通过BM25从千万级文档快速筛选出200条左右候选集,再用向量/深度模型对小候选集做语义精排,兼顾速度与效果。
  2. ES落地关键:文本字段指定BM25相似度算法,新增dense_vector字段存储文档向量,通过ID限定精排范围,最后按余弦相似度重排。
  3. 参数原则:BM25初排数量控制在50~200,向量维度选384/768,BM25默认k1=1.2、b=0.75即可满足大部分场景。

全部评论: 0

    我有话说: