Семантический поиск против полнотекстового поиска: Что выбрать в Milvus 2.5?
Milvus, ведущая высокопроизводительная векторная база данных, давно специализируется на семантическом поиске с использованием векторных вкраплений из моделей глубокого обучения. Эта технология используется в таких приложениях ИИ, как Retrieval-Augmented Generation (RAG), поисковых системах и рекомендательных системах. С ростом популярности RAG и других приложений для поиска текста сообщество осознало преимущества сочетания традиционных методов сопоставления текста и семантического поиска, известного как гибридный поиск. Такой подход особенно полезен в сценариях, которые в значительной степени зависят от сопоставления ключевых слов. Для решения этой задачи в Milvus 2.5 появилась функциональность полнотекстового поиска (FTS), которая интегрируется с возможностями разреженного векторного поиска и гибридного поиска, уже доступными с версии 2.4, создавая мощный синергетический эффект.
Гибридный поиск - это метод, объединяющий результаты нескольких путей поиска. Пользователи могут искать в разных полях данных различными способами, а затем объединять и ранжировать результаты для получения комплексного результата. В популярных сегодня сценариях RAG типичный гибридный подход сочетает семантический поиск с полнотекстовым. В частности, он предполагает объединение результатов семантического поиска на основе плотного встраивания и лексического соответствия на основе BM25 с использованием RRF (Reciprocal Rank Fusion) для улучшения ранжирования результатов.
В этой статье мы продемонстрируем это на примере набора данных, предоставленного компанией Anthropic, который состоит из фрагментов кода из девяти хранилищ кода. Это напоминает популярный вариант использования RAG: бот для кодирования с помощью ИИ. Поскольку данные кода содержат много определений, ключевых слов и другой информации, текстовый поиск может быть особенно эффективным в этом контексте. В то же время модели плотного встраивания, обученные на больших наборах кодовых данных, могут улавливать семантическую информацию более высокого уровня. Наша цель - проследить эффект от сочетания этих двух подходов путем экспериментов.
Мы проанализируем конкретные случаи, чтобы составить более четкое представление о гибридном поиске. В качестве базового уровня мы будем использовать усовершенствованную модель плотного встраивания (voyage-2), обученную на большом объеме кодовых данных. Затем мы выберем примеры, в которых гибридный поиск превосходит результаты как семантического, так и полнотекстового поиска (топ-5), и проанализируем особенности, лежащие в основе этих случаев.
Метод | Pass@5 |
---|---|
Полнотекстовый поиск | 0.7318 |
Семантический поиск | 0.8096 |
Гибридный поиск | 0.8176 |
Гибридный поиск (добавление стоп-слов) | 0.8418 |
В дополнение к анализу качества в каждом конкретном случае мы расширили оценку, рассчитав метрику Pass@5 для всего набора данных. Эта метрика измеряет долю релевантных результатов, найденных в топ-5 по каждому запросу. Наши результаты показывают, что, хотя продвинутые модели встраивания создают надежную основу, их интеграция с полнотекстовым поиском дает еще более высокие результаты. Дальнейшие улучшения возможны путем изучения результатов BM25 и точной настройки параметров для конкретных сценариев, что может привести к значительному росту производительности.
Мы рассмотрели конкретные результаты, полученные по трем различным поисковым запросам, сравнив семантический и полнотекстовый поиск с гибридным поиском. Вы также можете ознакомиться с полным кодом в этом репозитории.
Пример 1: гибридный поиск превосходит семантический поиск
Запрос: Как создается файл журнала?
Этот запрос направлен на создание файла журнала, и правильным ответом должен быть фрагмент кода Rust, который создает файл журнала. В результатах семантического поиска мы увидели код, представляющий заголовочный файл журнала, и код на C++ для получения логгера. Однако ключевым моментом здесь является переменная "logfile". В результатах гибридного поиска #hybrid 0 мы нашли этот релевантный результат, который, естественно, получен из полнотекстового поиска, поскольку гибридный поиск объединяет результаты семантического и полнотекстового поиска.
В дополнение к этому результату в #hybrid 2 мы можем найти несвязанный тестовый код, особенно повторяющуюся фразу "длинная строка, чтобы проверить, как она обрабатывается". Для этого необходимо понять принципы работы алгоритма BM25, используемого в полнотекстовом поиске. Полнотекстовый поиск нацелен на поиск более редких слов (поскольку часто встречающиеся слова снижают различимость текста и мешают распознаванию объектов). Предположим, мы проводим статистический анализ большого корпуса естественных текстов. В этом случае легко сделать вывод, что "как" - очень распространенное слово и вносит очень малый вклад в оценку релевантности. Однако в данном случае набор данных состоит из кода, а в коде слово "как" встречается нечасто, что делает его ключевым поисковым термином в данном контексте.
Истина: Правильный ответ - код Rust, который создает файл журнала.
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 <log4cxx/logger.h>
#include <log4cxx/simplelayout.h>
#include <log4cxx/fileappender.h>
#include <log4cxx/helpers/absolutetimedateformat.h>
##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::spi::LoggingEvent>(LOG4CXX_STR("foo"),
Level::getInfo(),
LOG4CXX_STR("A Message"),
log4cxx::spi::LocationInfo::getLocationUnavailable());
FMTLayout layout(LOG4CXX_STR("{d:%Y-%m-%d %H:%M:%S} {message}"));
LogString output;
log4cxx::helpers::Pool pool;
layout.format( output, logEvt, pool);
##dense 3 0.7562235593795776
#include "util/compare.h"
#include "util/transformer.h"
#include "util/absolutedateandtimefilter.h"
#include "util/iso8601filter.h"
#include "util/absolutetimefilter.h"
#include "util/relativetimefilter.h"
#include "util/controlfilter.h"
#include "util/threadfilter.h"
#include "util/linenumberfilter.h"
#include "util/filenamefilter.h"
#include "vectorappender.h"
#include <log4cxx/fmtlayout.h>
#include <log4cxx/propertyconfigurator.h>
#include <log4cxx/helpers/date.h>
#include <log4cxx/spi/loggingevent.h>
#include <iostream>
#include <iomanip>
#define REGEX_STR(x) x
#define PAT0 REGEX_STR("\\[[0-9A-FXx]*]\\ (DEBUG|INFO|WARN|ERROR|FATAL) .* - Message [0-9]\\{1,2\\}")
#define PAT1 ISO8601_PAT REGEX_STR(" ") PAT0
#define PAT2 ABSOLUTE_DATE_AND_TIME_PAT REGEX_STR(" ") PAT0
#define PAT3 ABSOLUTE_TIME_PAT REGEX_STR(" ") PAT0
#define PAT4 RELATIVE_TIME_PAT REGEX_STR(" ") PAT0
#define PAT5 REGEX_STR("\\[[0-9A-FXx]*]\\ (DEBUG|INFO|WARN|ERROR|FATAL) .* : Message [0-9]\\{1,2\\}")
##dense 4 0.7557586431503296
std::string msg("Message ");
Pool pool;
// These should all log.----------------------------
LOG4CXX_FATAL(ERRlogger, createMessage(i, pool));
i++; //0
LOG4CXX_ERROR(ERRlogger, createMessage(i, pool));
i++;
LOG4CXX_FATAL(INF, createMessage(i, pool));
i++; // 2
LOG4CXX_ERROR(INF, createMessage(i, pool));
i++;
LOG4CXX_WARN(INF, createMessage(i, pool));
i++;
LOG4CXX_INFO(INF, createMessage(i, pool));
i++;
LOG4CXX_FATAL(INF_UNDEF, createMessage(i, pool));
i++; //6
LOG4CXX_ERROR(INF_UNDEF, createMessage(i, pool));
i++;
LOG4CXX_WARN(INF_UNDEF, createMessage(i, pool));
i++;
LOG4CXX_INFO(INF_UNDEF, createMessage(i, pool));
i++;
LOG4CXX_FATAL(INF_ERR, createMessage(i, pool));
i++; // 10
LOG4CXX_ERROR(INF_ERR, createMessage(i, pool));
i++;
LOG4CXX_FATAL(INF_ERR_UNDEF, createMessage(i, pool));
i++;
LOG4CXX_ERROR(INF_ERR_UNDEF, createMessage(i, pool));
i++;
Результаты гибридного поиска
##hybrid 0 0.016393441706895828
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(())
}
}
##hybrid 1 0.016393441706895828
/*
* 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 <log4cxx/logger.h>
#include <log4cxx/simplelayout.h>
#include <log4cxx/fileappender.h>
#include <log4cxx/helpers/absolutetimedateformat.h>
##hybrid 2 0.016129031777381897
"long string to test how those are handled. Here goes more text. "
"long string to test how those are handled. Here goes more text. "
"long string to test how those are handled. Here goes more text. "
"long string to test how those are handled. Here goes more text. "
"long string to test how those are handled. Here goes more text. "
"long string to test how those are handled. Here goes more text. "
"long string to test how those are handled. Here goes more text. "
"long string to test how those are handled. Here goes more text. "
"long string to test how those are handled. Here goes more text. "
"long string to test how those are handled. Here goes more text. "
"long string to test how those are handled. Here goes more text. "
"long string to test how those are handled. Here goes more text. "
"long string to test how those are handled. Here goes more text. "
};
}
##hybrid 3 0.016129031777381897
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());
##hybrid 4 0.01587301678955555
std::vector<std::string> MakeStrings() {
return {
"a", "ab", "abc", "abcd",
"long string to test how those are handled. Here goes more text. "
"long string to test how those are handled. Here goes more text. "
"long string to test how those are handled. Here goes more text. "
"long string to test how those are handled. Here goes more text. "
"long string to test how those are handled. Here goes more text. "
"long string to test how those are handled. Here goes more text. "
Пример 2: гибридный поиск превосходит полнотекстовый поиск
Запрос: Как инициализировать логгер?
Этот запрос очень похож на предыдущий, и правильным ответом также является тот же фрагмент кода, но в данном случае гибридный поиск нашел ответ (с помощью семантического поиска), а полнотекстовый - нет. Причина такого расхождения кроется в статистических весах слов в корпусе, которые не совпадают с нашим интуитивным пониманием вопроса. Модель не смогла распознать, что совпадение со словом "как" здесь не так важно. Слово "логгер" встречалось в коде чаще, чем "как", что привело к тому, что "как" стало более значимым в рейтинге полнотекстового поиска.
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(())
}
}
Результаты полнотекстового поиска
##sparse 0 10.17311954498291
"long string to test how those are handled. Here goes more text. "
"long string to test how those are handled. Here goes more text. "
"long string to test how those are handled. Here goes more text. "
"long string to test how those are handled. Here goes more text. "
"long string to test how those are handled. Here goes more text. "
"long string to test how those are handled. Here goes more text. "
"long string to test how those are handled. Here goes more text. "
"long string to test how those are handled. Here goes more text. "
"long string to test how those are handled. Here goes more text. "
"long string to test how those are handled. Here goes more text. "
"long string to test how those are handled. Here goes more text. "
"long string to test how those are handled. Here goes more text. "
"long string to test how those are handled. Here goes more text. "
};
}
##sparse 1 9.775702476501465
std::vector<std::string> MakeStrings() {
return {
"a", "ab", "abc", "abcd",
"long string to test how those are handled. Here goes more text. "
"long string to test how those are handled. Here goes more text. "
"long string to test how those are handled. Here goes more text. "
"long string to test how those are handled. Here goes more text. "
"long string to test how those are handled. Here goes more text. "
"long string to test how those are handled. Here goes more text. "
##sparse 2 7.638711452484131
// union ("x|y"), grouping ("(xy)"), brackets ("[xy]"), and
// repetition count ("x{5,7}"), among others.
//
// Below is the syntax that we do support. We chose it to be a
// subset of both PCRE and POSIX extended regex, so it's easy to
// learn wherever you come from. In the following: 'A' denotes a
// literal character, period (.), or a single \\ escape sequence;
// 'x' and 'y' denote regular expressions; 'm' and 'n' are for
##sparse 3 7.1208391189575195
/*
* 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 <log4cxx/logger.h>
#include <log4cxx/simplelayout.h>
#include <log4cxx/fileappender.h>
#include <log4cxx/helpers/absolutetimedateformat.h>
##sparse 4 7.066349029541016
/*
* 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 <log4cxx/filter/denyallfilter.h>
#include <log4cxx/logger.h>
#include <log4cxx/spi/filter.h>
#include <log4cxx/spi/loggingevent.h>
#include "../logunit.h"
Результаты гибридного поиска
##hybrid 0 0.016393441706895828
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(())
}
}
##hybrid 1 0.016393441706895828
"long string to test how those are handled. Here goes more text. "
"long string to test how those are handled. Here goes more text. "
"long string to test how those are handled. Here goes more text. "
"long string to test how those are handled. Here goes more text. "
"long string to test how those are handled. Here goes more text. "
"long string to test how those are handled. Here goes more text. "
"long string to test how those are handled. Here goes more text. "
"long string to test how those are handled. Here goes more text. "
"long string to test how those are handled. Here goes more text. "
"long string to test how those are handled. Here goes more text. "
"long string to test how those are handled. Here goes more text. "
"long string to test how those are handled. Here goes more text. "
"long string to test how those are handled. Here goes more text. "
};
}
##hybrid 2 0.016129031777381897
std::vector<std::string> MakeStrings() {
return {
"a", "ab", "abc", "abcd",
"long string to test how those are handled. Here goes more text. "
"long string to test how those are handled. Here goes more text. "
"long string to test how those are handled. Here goes more text. "
"long string to test how those are handled. Here goes more text. "
"long string to test how those are handled. Here goes more text. "
"long string to test how those are handled. Here goes more text. "
##hybrid 3 0.016129031777381897
LoggerPtr INF = Logger::getLogger(LOG4CXX_TEST_STR("INF"));
INF->setLevel(Level::getInfo());
LoggerPtr INF_ERR = Logger::getLogger(LOG4CXX_TEST_STR("INF.ERR"));
INF_ERR->setLevel(Level::getError());
LoggerPtr DEB = Logger::getLogger(LOG4CXX_TEST_STR("DEB"));
DEB->setLevel(Level::getDebug());
// Note: categories with undefined level
LoggerPtr INF_UNDEF = Logger::getLogger(LOG4CXX_TEST_STR("INF.UNDEF"));
LoggerPtr INF_ERR_UNDEF = Logger::getLogger(LOG4CXX_TEST_STR("INF.ERR.UNDEF"));
LoggerPtr UNDEF = Logger::getLogger(LOG4CXX_TEST_STR("UNDEF"));
##hybrid 4 0.01587301678955555
// union ("x|y"), grouping ("(xy)"), brackets ("[xy]"), and
// repetition count ("x{5,7}"), among others.
//
// Below is the syntax that we do support. We chose it to be a
// subset of both PCRE and POSIX extended regex, so it's easy to
// learn wherever you come from. In the following: 'A' denotes a
// literal character, period (.), or a single \\ escape sequence;
// 'x' and 'y' denote regular expressions; 'm' and 'n' are for
В ходе наших наблюдений мы обнаружили, что при поиске по разреженному вектору многие низкокачественные результаты были вызваны совпадением с малоинформативными словами, такими как "Как" и "Что". Изучив данные, мы поняли, что эти слова вызывают помехи в результатах. Один из подходов к решению этой проблемы - добавить эти слова в список стоп-слов и игнорировать их в процессе поиска. Это поможет устранить негативное влияние этих распространенных слов и повысить качество результатов поиска.
Пример 3: гибридный поиск (с добавлением стоп-слов) превосходит семантический поиск
После добавления стоп-слов для отсеивания малоинформативных слов типа "как" и "что" мы проанализировали случай, когда точно настроенный гибридный поиск показал лучшие результаты, чем семантический. Улучшение в этом случае было связано с совпадением термина "RegistryClient" в запросе, что позволило нам найти результаты, не вспоминаемые только семантической моделью поиска.
Кроме того, мы заметили, что гибридный поиск уменьшил количество низкокачественных совпадений в результатах. В данном случае метод гибридного поиска успешно объединил семантический поиск с полнотекстовым, что привело к получению более релевантных результатов с повышенной точностью.
Вопрос: Как создается экземпляр RegistryClient в тестовых методах?
Гибридный поиск эффективно нашел ответ, связанный с созданием экземпляра "RegistryClient", который не смог найти только семантический поиск. Добавление стоп-слов помогло избежать нерелевантных результатов по таким терминам, как "Как", что привело к более качественным совпадениям и уменьшению количества некачественных результатов.
/** Integration tests for {@link BlobPuller}. */
public class BlobPullerIntegrationTest {
private final FailoverHttpClient httpClient = new FailoverHttpClient(true, false, ignored -> {});
@Test
public void testPull() throws IOException, RegistryException {
RegistryClient registryClient =
RegistryClient.factory(EventHandlers.NONE, "gcr.io", "distroless/base", httpClient)
.newRegistryClient();
V22ManifestTemplate manifestTemplate =
registryClient
.pullManifest(
ManifestPullerIntegrationTest.KNOWN_MANIFEST_V22_SHA, V22ManifestTemplate.class)
.getManifest();
DescriptorDigest realDigest = manifestTemplate.getLayers().get(0).getDigest();
Результаты семантического поиска
##dense 0 0.7411458492279053
Mockito.doThrow(mockRegistryUnauthorizedException)
.when(mockJibContainerBuilder)
.containerize(mockContainerizer);
try {
testJibBuildRunner.runBuild();
Assert.fail();
} catch (BuildStepsExecutionException ex) {
Assert.assertEquals(
TEST_HELPFUL_SUGGESTIONS.forHttpStatusCodeForbidden("someregistry/somerepository"),
ex.getMessage());
}
}
##dense 1 0.7346029877662659
verify(mockCredentialRetrieverFactory).known(knownCredential, "credentialSource");
verify(mockCredentialRetrieverFactory).known(inferredCredential, "inferredCredentialSource");
verify(mockCredentialRetrieverFactory)
.dockerCredentialHelper("docker-credential-credentialHelperSuffix");
}
##dense 2 0.7285804748535156
when(mockCredentialRetrieverFactory.dockerCredentialHelper(anyString()))
.thenReturn(mockDockerCredentialHelperCredentialRetriever);
when(mockCredentialRetrieverFactory.known(knownCredential, "credentialSource"))
.thenReturn(mockKnownCredentialRetriever);
when(mockCredentialRetrieverFactory.known(inferredCredential, "inferredCredentialSource"))
.thenReturn(mockInferredCredentialRetriever);
when(mockCredentialRetrieverFactory.wellKnownCredentialHelpers())
.thenReturn(mockWellKnownCredentialHelpersCredentialRetriever);
##dense 3 0.7279614210128784
@Test
public void testBuildImage_insecureRegistryException()
throws InterruptedException, IOException, CacheDirectoryCreationException, RegistryException,
ExecutionException {
InsecureRegistryException mockInsecureRegistryException =
Mockito.mock(InsecureRegistryException.class);
Mockito.doThrow(mockInsecureRegistryException)
.when(mockJibContainerBuilder)
.containerize(mockContainerizer);
try {
testJibBuildRunner.runBuild();
Assert.fail();
} catch (BuildStepsExecutionException ex) {
Assert.assertEquals(TEST_HELPFUL_SUGGESTIONS.forInsecureRegistry(), ex.getMessage());
}
}
##dense 4 0.724872350692749
@Test
public void testBuildImage_registryCredentialsNotSentException()
throws InterruptedException, IOException, CacheDirectoryCreationException, RegistryException,
ExecutionException {
Mockito.doThrow(mockRegistryCredentialsNotSentException)
.when(mockJibContainerBuilder)
.containerize(mockContainerizer);
try {
testJibBuildRunner.runBuild();
Assert.fail();
} catch (BuildStepsExecutionException ex) {
Assert.assertEquals(TEST_HELPFUL_SUGGESTIONS.forCredentialsNotSent(), ex.getMessage());
}
}
Результаты гибридного поиска
##hybrid 0 0.016393441706895828
/** Integration tests for {@link BlobPuller}. */
public class BlobPullerIntegrationTest {
private final FailoverHttpClient httpClient = new FailoverHttpClient(true, false, ignored -> {});
@Test
public void testPull() throws IOException, RegistryException {
RegistryClient registryClient =
RegistryClient.factory(EventHandlers.NONE, "gcr.io", "distroless/base", httpClient)
.newRegistryClient();
V22ManifestTemplate manifestTemplate =
registryClient
.pullManifest(
ManifestPullerIntegrationTest.KNOWN_MANIFEST_V22_SHA, V22ManifestTemplate.class)
.getManifest();
DescriptorDigest realDigest = manifestTemplate.getLayers().get(0).getDigest();
##hybrid 1 0.016393441706895828
Mockito.doThrow(mockRegistryUnauthorizedException)
.when(mockJibContainerBuilder)
.containerize(mockContainerizer);
try {
testJibBuildRunner.runBuild();
Assert.fail();
} catch (BuildStepsExecutionException ex) {
Assert.assertEquals(
TEST_HELPFUL_SUGGESTIONS.forHttpStatusCodeForbidden("someregistry/somerepository"),
ex.getMessage());
}
}
##hybrid 2 0.016129031777381897
verify(mockCredentialRetrieverFactory).known(knownCredential, "credentialSource");
verify(mockCredentialRetrieverFactory).known(inferredCredential, "inferredCredentialSource");
verify(mockCredentialRetrieverFactory)
.dockerCredentialHelper("docker-credential-credentialHelperSuffix");
}
##hybrid 3 0.016129031777381897
@Test
public void testPull_unknownBlob() throws IOException, DigestException {
DescriptorDigest nonexistentDigest =
DescriptorDigest.fromHash(
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
RegistryClient registryClient =
RegistryClient.factory(EventHandlers.NONE, "gcr.io", "distroless/base", httpClient)
.newRegistryClient();
try {
registryClient
.pullBlob(nonexistentDigest, ignored -> {}, ignored -> {})
.writeTo(ByteStreams.nullOutputStream());
Assert.fail("Trying to pull nonexistent blob should have errored");
} catch (IOException ex) {
if (!(ex.getCause() instanceof RegistryErrorException)) {
throw ex;
}
MatcherAssert.assertThat(
ex.getMessage(),
CoreMatchers.containsString(
"pull BLOB for gcr.io/distroless/base with digest " + nonexistentDigest));
}
}
}
##hybrid 4 0.01587301678955555
when(mockCredentialRetrieverFactory.dockerCredentialHelper(anyString()))
.thenReturn(mockDockerCredentialHelperCredentialRetriever);
when(mockCredentialRetrieverFactory.known(knownCredential, "credentialSource"))
.thenReturn(mockKnownCredentialRetriever);
when(mockCredentialRetrieverFactory.known(inferredCredential, "inferredCredentialSource"))
.thenReturn(mockInferredCredentialRetriever);
when(mockCredentialRetrieverFactory.wellKnownCredentialHelpers())
.thenReturn(mockWellKnownCredentialHelpersCredentialRetriever);
Выводы
Из нашего анализа можно сделать несколько выводов о производительности различных методов поиска. В большинстве случаев модель семантического поиска помогает нам получить хорошие результаты, улавливая общий смысл запроса, но она не справляется, когда запрос содержит конкретные ключевые слова, которые мы хотим найти.
В таких случаях модель встраивания не отражает этот замысел в явном виде. С другой стороны, полнотекстовый поиск может решить эту проблему напрямую. Однако при этом возникает проблема нерелевантных результатов, несмотря на совпадение слов, что может ухудшить общее качество результатов. Поэтому очень важно выявлять и устранять эти негативные моменты, анализируя конкретные результаты и применяя целевые стратегии для улучшения качества поиска. Гибридный поиск с такими стратегиями ранжирования, как RRF или взвешенный реранкер, обычно является хорошим базовым вариантом.
С выпуском функции полнотекстового поиска в Milvus 2.5 мы стремимся предоставить сообществу гибкие и разнообразные решения для поиска информации. Это позволит пользователям исследовать различные комбинации методов поиска и решать все более сложные и разнообразные поисковые задачи в эпоху GenAI. Посмотрите пример кода, как реализовать полнотекстовый поиск и гибридный поиск в Milvus 2.5.
- Пример 1: гибридный поиск превосходит семантический поиск
- Пример 2: гибридный поиск превосходит полнотекстовый поиск
- Пример 3: гибридный поиск (с добавлением стоп-слов) превосходит семантический поиск
- Выводы
On This Page
Try Managed Milvus for Free
Zilliz Cloud is hassle-free, powered by Milvus and 10x faster.
Get StartedLike the article? Spread the word