know-engine

混合检索如何实现?

因为我们采用了ElasticSearch来做知识的存储,而且ES同时支持向量检索和关键词检索,我们只需要依赖一个扩展包langchain4j elasticsearch的话,就能使用他的检索器做检索了。 为什么要做混合检索? 只所以…

TL;DR

因为我们采用了ElasticSearch来做知识的存储,而且ES同时支持向量检索和关键词检索,我们只需要依赖一个扩展包langchain4j elasticsearch的话,就能使用他的检索器做检索了。 为什么要做混合检索? 只所以…

因为我们采用了ElasticSearch来做知识的存储,而且ES同时支持向量检索和关键词检索,我们只需要依赖一个扩展包langchain4j-elasticsearch的话,就能使用他的检索器做检索了。

为什么要做混合检索?

只所以要用,是因为单独使用向量检索和关键词检索都会有问题: 1、只用了向量检索效果不行 核心问题:向量检索依赖语义相似度,对专有名词、精确短语、编号等缺乏精确匹配能力;且在面对极短查询时,因语义上下文有限,容易导致召回内容过于宽泛。 比如我们课程讲的内容中,针对『超级桌面』这个内容的检索。超级桌面本身就是一个汽车领域的专有名词,而和他相似的词有很多,比如典型的3D桌面,手机桌面,车机桌面,桌面。 单纯靠向量检索的话,包含这些关键词的文档得分都挺高的。使得检索效果差。比如这个case:

✅解决中文分词不准确导致召回效果差的问题

我们虽然在ES中安装了IK分词器,但是并没有用上,所以导致中文分词是每个中文字单独作为一个分词了,就导致召回结果不好,明明相关的内容却召不回。 所以我们需要让IK分词器生效。 创建组件模板 创建索引模板 LLMentor 2、只用bm25关键词检索效果不行 核心问题:关键词匹配是死板的字面匹配,无法理解同义词、语义表达变化或复杂的自然语言意图。 这个就很好理解了,关键词检索要求一定要字面匹配,比如我们想要问关于手机能否启动汽车等问题的时候,其实在问的是汽车如何做启停、如何无钥匙进入、智能座舱功能等。 单纯按照分词后的关键词检索的话,就会检索不到想要的内容,就算检索到,得分也不高。

ES中接入混合检索

接入方式参考:

✅接入ElasticSearch做向量存储

我们项目中选择使用ES做向量数据库,方便后面我们基于ES做混合检索。这一篇介绍下如何在LangChain4J中接入ES。 增加依赖 首先,langchain4j-elasticsearch是一个针对langchain4j的es的包, 里面提 LLMentor 前面的课程中我们讲过,这个包中提供了ElasticsearchEmbeddingStore、ElasticsearchContentRetriever两个类。 - ElasticsearchEmbeddingStore:EmbeddingStore 接口的实现,使用 Elasticsearch 来存储和检索嵌入向量。 - ElasticsearchContentRetriever:ContentRetriever 接口的实现,使用 Elasticsearch 基于向量相似度搜索来检索相关文档。 而ElasticsearchContentRetriever就是我们要用的ContentRetriever的实现。那我们就可以直接用它来做检索,即:

public String retriever(String query, String chatMessageId) {
    ElasticsearchConfiguration configuration = ElasticsearchConfigurationHybrid.builder().build();

    ElasticsearchContentRetriever contentRetriever = ElasticsearchContentRetriever.builder()
            .restClient(restClient)
            .embeddingModel(openAiEmbeddingModel)
            .configuration(configuration)
            .maxResults(5)
            .indexName("know-engine")
            .minScore(0.5)
            .build();

    KnowEngineQueryTransformer queryTransformer = new KnowEngineQueryTransformer(chatModel, chatMessageId);

    // 创建检索增强器
    RetrievalAugmentor retrievalAugmentor = DefaultRetrievalAugmentor.builder()
            .contentRetriever(contentRetriever)
            .queryTransformer(queryTransformer)
            .build();

    KnowEngineChatAiService aiService = AiServices.builder(KnowEngineChatAiService.class)
            .chatModel(chatModel)
            .streamingChatModel(streamingChatModel)
            .chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(10))
            .retrievalAugmentor(retrievalAugmentor)
            .build();

    Result<String> result = aiService.chatWithRag(UUID.randomUUID(), query);

    return result.content();
}

这里面比较关键的就是这个ElasticsearchConfiguration,为了使用混合检索,我们用到的是ElasticsearchConfigurationHybrid,即支持混合检索的配置。 但是,以上代码我在实际运行中报错如下:

[security_exception] current license is non-compliant for [Reciprocal Rank Fusion (RRF)]

主要原因是我的ES用的是开源版,而开源版默认是不支持RRF做重排序的,只有企业版才支持。而ElasticsearchContentRetriever会默认使用内置的RRF做重排序,那么就会导致报错。 所以,直接使用ElasticsearchContentRetriever+ElasticsearchConfigurationHybrid这条路走不通了。于是我们需要自己来实现混合检索了。 什么是混合检索不展开说了,请看:

✅RAG优化技术:混合检索

在检索增强生成流程中,混合检索(Hybrid Search)是提高检索阶段质量的关键高级技术。它确保了大模型在生成答案之前,能够获得语义最相关、关键词最精确的上下文信息。 传统检索方式 传统的单一检索方式存在以下局限: 稠密检索/向量检索 LLMentor

自定义实现混合检索

既然直接使用自带的ElasticsearchConfigurationHybrid走不通,那我们就要自己来实现这个混合检索的逻辑了,我们看看ElasticsearchConfiguration其实有多个实现类: 其中ElasticsearchConfigurationFullText、ElasticsearchConfigurationKnn分别是用来做全文检索和向量检索的配置。 我们可以借助这两个配置来实现混合检索,即基于KNN做相似度检索,基于全文做关键词检索。 先定义一个fullTextRetriever:

fullTextRetriever = ElasticsearchContentRetriever.builder()
        .restClient(restClient)
       .configuration(ElasticsearchConfigurationFullText.builder().build())
        .maxResults(MAX_RESULT)
        .indexName(INDEX_NAME)
        .minScore(MIN_SCORE)
        .build();

再定义一个embeddingRetriever:

embeddingRetriever = ElasticsearchContentRetriever
.builder()
        .restClient(restClient)
        .embeddingModel(openAiEmbeddingModel)
        .configuration(ElasticsearchConfigurationKnn.builder().build())
        .maxResults(MAX_RESULT)
        .indexName(INDEX_NAME)
        .minScore(MIN_SCORE)
        .build();

然后把这两个Retriver都设置给一个Router即可:

KnowEngineChatAiService chatService = AiServices.builder(KnowEngineChatAiService.class)
        .chatModel(chatModel)
        .streamingChatModel(streamingChatModel)
        .chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(10))
        .retrievalAugmentor(DefaultRetrievalAugmentor.builder()
                .queryRouter(new KnowEngineQueryRouter(List.of(embeddingRetriever, fullTextRetriever), chatModel)
                .contentInjector(contentInjector)
                .queryTransformer(queryTransformer)
                .contentAggregator(contentAggregator)
                .build())
        .build();

但是,实际的代码中,我们还需要给我们的向量检索的检索器做一些定制,如: 1、如何选择精确KNN 还是 近似KNN检索?

✅自定义基于ElasticSearch的向量检索器

ES的语义相似度检索,采用的是KNN的方式,在ES的官网中(https://www.elastic.co/docs/solutions/search/vector/knn ) 提到,ES支持两种 kNN 搜索方法 分别是: 近似 kNN( LLMentor 2、如何实现检索过滤 3、涉及到父子分段/兄弟分段时,需要把分段做替换和合并 4、针对混合检索到的结果做重排序 这几个我们会分开在讲解。

版本提示

模型、框架与接口会持续变化。涉及版本号、参数与生产配置时,请在实践前对照对应官方文档。

LLMentor系统化学习大模型应用工程

内容来自个人课程知识库备份,并经过结构化整理。技术版本持续演进,生产使用前请结合官方文档验证。