原创 和你一起进步的 Zilliz 2024年12月27日 18:30 上海
Milvus作为一款向量数据库,长期以来专注于基于Embedding的向量检索能力,为RAG等应用提供了高准确率,高性能,高扩展的语义检索功能。随着大模型时代带来各种新型应用探索,社区重新认识到结合传统基于文本匹配的精确检索与混合检索所带来的增益,尤其在一些深度依赖关键字词匹配的场景中,这种需求变得尤为关键。为了满足这一需求,Milvus 2.5引入了全文检索(FTS,Full-Text Search)功能,并将其与2.4版本以来支持的稀疏向量检索能力和混合检索能力结合,从而发挥出强大的协同效应。
混合检索是一项融合了多路搜索结果的搜索方法,用户可以对数据中不同的字段进行多种方式的检索,然后通过混合检索进行融合排序得到一个综合的结果,在当前流行的RAG场景中,典型的混合检索方式是通过结合语义搜索与词汇检索来实现的,具体来说,这种做法会在embedding召回上与基于词汇匹配的bm25检索算法通过RRF的方式融合成一个更优的结果排序。
在本文中我们将使用Anthropic提供的一个RAG数据集来进行展示,这个数据集是一个文本搜索代码的数据集,由9个代码库的片段构成,类似于现在流行的AI辅助编程场景。由于代码数据包含大量定义、关键字等信息,基于文本的检索在这一场景中能够带来更大的增益。同时,经过大量代码数据训练的密集嵌入模型能够理解一些高层次的语义信息。我们希望通过实验观察,二者结合会产生怎样的效果。
为了对混合检索建立起更加具体的认识,我们采样一些具体的案例来进行分析。我们使用一个经过大量代码数据训练过的先进密集嵌入模型(voyage-2)作为基线,分别挑选了混合检索比密集和稀疏结果好的个例结果(top5),看一下能反应出背后的哪些特性。
除了基于个例的微观质量分析外,我们还通过整体评估得出了定量结果,统计了数据集中的Pass@5指标。该指标用于衡量每个查询的Top 5结果中,成功检索到的相关结果占所有相关结果的比例。从这个结果我们可以看出基于先进的embedding模型本身可以达到一个良好的基线效果,但是通过与全文检索方法依然可以带来提升,而通过对于bm25结果进行观察,针对具体场景进行参数调整,可以带来更大的提升。
01.
案例一:混合检索优于语义检索的案例
问题:How is the log file created?
这个问题是希望问一下log file的创建过程,正确答案是一段创建log file的Rust 代码。在语义检索结果中,可以看到了有引入log的头文件,以及c++拿到logger的相关代码,但这个问题关键其实是“logfile”这个变量, 我们在混合检索结果的#hybrid 0发现了这个结果,由于混合检索是融合语义检索和全文检索的结果,自然这个结果就是全文检索出来的。除了这个结果,我们可以在#hybrid 2中发现了很多看起来毫无关系的测试mock代码,尤其是这一句“long string to test how those are handled.”, 反复出现,这就需要理解全文检索算法BM25背后的原理了,全文检索是希望匹配到更多的低频词(因为高频词过于普遍了从而降低了用来甄别检索对象的独特性)。假如在大量自然文本中进行统计,很容易统计出“how”是一个非常常见的词,因此在相关性分数中占很低的比例。然而本文中是一个代码数据,并不会在代码中有很多包含“how”的文本,从而让含有这个词的句子被大量检索出来。
GroundTruth
use {
crate::args::LogArgs,
anyhow::{anyhow, Result},
simplelog::{Config, LevelFilter, WriteLogger},
std::fs::File,
};
pub struct Logger;
impl Logger {
pub fn init(args: &impl LogArgs) -> Result<()> {
let filter: LevelFilter = args.log_level().into();
if filter != LevelFilter::Off {
let logfile = File::create(args.log_file())
.map_err(|e| anyhow!("Failed to open log file: {e:}"))?;
WriteLogger::init(filter, Config::default(), logfile)
.map_err(|e| anyhow!("Failed to initalize logger: {e:}"))?;
}
Ok(())
}
}
语义检索结果:
##dense 0 0.7745316028594971
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "logunit.h"
#include
#include
#include
#include
##dense 1 0.769859254360199
void simple()
{
LayoutPtr layout = LayoutPtr(new SimpleLayout());
AppenderPtr appender = FileAppenderPtr(new FileAppender(layout, LOG4CXX_STR("output/simple"), false));
root->addAppender(appender);
common();
LOGUNIT_ASSERT(Compare::compare(LOG4CXX_FILE("output/simple"), LOG4CXX_FILE("witness/simple")));
}
std::string createMessage(int i, Pool & pool)
{
std::string msg("Message ");
msg.append(pool.itoa(i));
return msg;
}
void common()
{
int i = 0;
// In the lines below, the logger names are chosen as an aid in
// remembering their level values. In general, the logger names
// have no bearing to level values.
LoggerPtr ERRlogger = Logger::getLogger(LOG4CXX_TEST_STR("ERR"));
ERRlogger->setLevel(Level::getError());
##dense 2 0.7591114044189453
log4cxx::spi::LoggingEventPtr logEvt = std::make_shared(LOG4CXX_STR("foo"),
Level::getInfo(),
LOG4CXX_STR("A Message"),