Semantische Suche vs. Volltextsuche: Wofür entscheide ich mich in Milvus 2.5?
Milvus, eine führende Hochleistungs-Vektordatenbank, hat sich seit langem auf die semantische Suche mit Vektoreinbettungen aus Deep-Learning-Modellen spezialisiert. Diese Technologie unterstützt KI-Anwendungen wie Retrieval-Augmented Generation (RAG), Suchmaschinen und Empfehlungssysteme. Mit der zunehmenden Beliebtheit von RAG und anderen Textsuchanwendungen hat die Community die Vorteile der Kombination traditioneller Text-Matching-Methoden mit semantischer Suche erkannt, die als hybride Suche bezeichnet wird. Dieser Ansatz ist besonders vorteilhaft in Szenarien, die stark auf den Abgleich von Schlüsselwörtern angewiesen sind. Um diesem Bedarf gerecht zu werden, führt Milvus 2.5 die Funktionalität der Volltextsuche (FTS) ein und integriert sie mit der spärlichen Vektorsuche und den hybriden Suchfunktionen, die bereits seit Version 2.4 zur Verfügung stehen, wodurch eine leistungsstarke Synergie entsteht.
Die hybride Suche ist eine Methode, die Ergebnisse aus mehreren Suchpfaden kombiniert. Benutzer können verschiedene Datenfelder auf unterschiedliche Weise durchsuchen und die Ergebnisse dann zusammenführen und bewerten, um ein umfassendes Ergebnis zu erhalten. In den heute gängigen RAG-Szenarien kombiniert ein typischer hybrider Ansatz die semantische Suche mit der Volltextsuche. Konkret geht es dabei um die Zusammenführung von Ergebnissen aus der auf Dense Embedding basierenden semantischen Suche und dem BM25-basierten lexikalischen Abgleich unter Verwendung von RRF (Reciprocal Rank Fusion), um das Ranking der Ergebnisse zu verbessern.
In diesem Artikel werden wir dies anhand eines von Anthropic bereitgestellten Datensatzes demonstrieren, der aus Codeschnipseln aus neun Code-Repositories besteht. Dies ähnelt einem beliebten Anwendungsfall von RAG: ein KI-gestützter Coding-Bot. Da die Codedaten eine Vielzahl von Definitionen, Schlüsselwörtern und anderen Informationen enthalten, kann die textbasierte Suche in diesem Kontext besonders effektiv sein. Gleichzeitig können dichte Einbettungsmodelle, die auf großen Codedatensätzen trainiert wurden, semantische Informationen auf höherer Ebene erfassen. Unser Ziel ist es, die Auswirkungen der Kombination dieser beiden Ansätze durch Experimente zu beobachten.
Wir werden spezifische Fälle analysieren, um ein klareres Verständnis der hybriden Suche zu entwickeln. Als Basis verwenden wir ein fortschrittliches Dense Embedding Modell (voyage-2), das auf einer großen Menge von Codedaten trainiert wurde. Anschließend werden wir Beispiele auswählen, bei denen die hybride Suche sowohl semantische als auch Volltextsuchergebnisse übertrifft (Top 5), um die Merkmale dieser Fälle zu analysieren.
Methode | Durchgang@5 |
---|---|
Volltextsuche | 0.7318 |
Semantische Suche | 0.8096 |
Hybride Suche | 0.8176 |
Hybride Suche (Stoppwort hinzufügen) | 0.8418 |
Zusätzlich zur Analyse der Qualität auf Einzelfallbasis haben wir unsere Bewertung erweitert, indem wir die Pass@5-Metrik für den gesamten Datensatz berechnet haben. Diese Metrik misst den Anteil relevanter Ergebnisse, die unter den ersten 5 Ergebnissen jeder Suchanfrage zu finden sind. Unsere Ergebnisse zeigen, dass fortgeschrittene Einbettungsmodelle zwar eine solide Basis darstellen, aber durch die Integration mit der Volltextsuche noch bessere Ergebnisse erzielt werden können. Weitere Verbesserungen sind durch die Untersuchung der BM25-Ergebnisse und die Feinabstimmung der Parameter für bestimmte Szenarien möglich, was zu erheblichen Leistungssteigerungen führen kann.
Wir untersuchen die spezifischen Ergebnisse für drei verschiedene Suchanfragen und vergleichen die semantische und die Volltextsuche mit der hybriden Suche. Sie können auch den vollständigen Code in diesem Repo einsehen.
Fall 1: Hybride Suche übertrifft die semantische Suche
Abfrage: Wie wird die Protokolldatei erstellt?
Diese Abfrage zielt darauf ab, die Erstellung einer Protokolldatei zu erfragen, und die richtige Antwort sollte ein Ausschnitt aus dem Rust-Code sein, der eine Protokolldatei erstellt. In den semantischen Suchergebnissen sahen wir etwas Code, der die Log-Header-Datei einführt, und den C++-Code, um den Logger zu erhalten. Der Schlüssel ist hier jedoch die Variable "logfile". In dem hybriden Suchergebnis #hybrid 0 fanden wir dieses relevante Ergebnis, das natürlich aus der Volltextsuche stammt, da die hybride Suche semantische und Volltextsuchergebnisse zusammenführt.
Zusätzlich zu diesem Ergebnis finden wir in #hybrid 2 unzusammenhängenden Test-Mockcode, insbesondere die wiederholte Phrase "long string to test how those are handled". Dies erfordert ein Verständnis der Grundsätze des BM25-Algorithmus, der bei der Volltextsuche verwendet wird. Die Volltextsuche zielt darauf ab, seltenere Wörter zu finden (da häufige Wörter die Unterscheidbarkeit des Textes verringern und die Unterscheidung der Objekte erschweren). Nehmen wir an, wir führen eine statistische Analyse an einem großen Korpus von natürlichem Text durch. In diesem Fall kann man leicht zu dem Schluss kommen, dass "wie" ein sehr häufiges Wort ist und nur wenig zur Relevanzbewertung beiträgt. In diesem Fall besteht der Datensatz jedoch aus Code, und das Wort "wie" kommt im Code nur selten vor, so dass es in diesem Kontext ein wichtiger Suchbegriff ist.
Grundwahrheit: Die richtige Antwort ist der Rust-Code, der eine Protokolldatei erstellt.
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(())
}
}
Semantische Suchergebnisse
##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++;
Ergebnisse der hybriden Suche
##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. "
Fall 2: Hybride Suche übertrifft die Volltextsuche
Abfrage: Wie initialisiert man den Logger?
Diese Anfrage ist der vorherigen sehr ähnlich, und die richtige Antwort ist auch derselbe Codeschnipsel, aber in diesem Fall fand die hybride Suche die Antwort (über die semantische Suche), während die Volltextsuche dies nicht tat. Der Grund für diese Diskrepanz liegt in der statistischen Gewichtung der Wörter im Korpus, die nicht mit unserem intuitiven Verständnis der Frage übereinstimmt. Das Modell hat nicht erkannt, dass die Übereinstimmung mit dem Wort "wie" hier nicht so wichtig war. Das Wort "logger" kam im Code häufiger vor als "how", was dazu führte, dass "how" in der Rangliste der Volltextsuche eine größere Bedeutung erhielt.
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(())
}
}
Volltext-Suchergebnisse
##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"
Hybride Suchergebnisse
##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
Bei unseren Beobachtungen stellten wir fest, dass bei der spärlichen Vektorsuche viele Ergebnisse von geringer Qualität durch übereinstimmende Wörter mit geringem Informationsgehalt wie "Wie" und "Was" verursacht wurden. Bei der Untersuchung der Daten haben wir festgestellt, dass diese Wörter die Ergebnisse stören. Ein Ansatz zur Entschärfung dieses Problems besteht darin, diese Wörter zu einer Stoppwortliste hinzuzufügen und sie während des Abgleichs zu ignorieren. Dies würde dazu beitragen, die negativen Auswirkungen dieser häufigen Wörter zu beseitigen und die Qualität der Suchergebnisse zu verbessern.
Fall 3: Hybride Suche (mit Hinzufügung von Stoppwörtern) schneidet besser ab als die semantische Suche
Nach dem Hinzufügen von Stoppwörtern zum Herausfiltern von Wörtern mit geringem Informationsgehalt wie "Wie" und "Was" analysierten wir einen Fall, in dem eine fein abgestimmte hybride Suche besser abschnitt als eine semantische Suche. Die Verbesserung in diesem Fall war darauf zurückzuführen, dass der Begriff "RegistryClient" in der Anfrage enthalten war, wodurch wir Ergebnisse finden konnten, die mit dem semantischen Suchmodell allein nicht gefunden werden konnten.
Außerdem konnten wir feststellen, dass die hybride Suche die Anzahl der Treffer von geringer Qualität in den Ergebnissen reduzierte. In diesem Fall hat die hybride Suchmethode die semantische Suche erfolgreich mit der Volltextsuche verknüpft, was zu relevanteren Ergebnissen mit höherer Genauigkeit führte.
Abfrage: Wie wird die RegistryClient-Instanz in den Testmethoden erstellt?
Mit der hybriden Suche konnte die Antwort auf die Frage nach der Erstellung der Instanz "RegistryClient" gefunden werden, die mit der semantischen Suche allein nicht gefunden werden konnte. Durch die Hinzufügung von Stoppwörtern konnten irrelevante Ergebnisse von Begriffen wie "Wie" vermieden werden, was zu qualitativ besseren Übereinstimmungen und weniger Ergebnissen von geringer Qualität führte.
/** 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();
Semantische Suchergebnisse
##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());
}
}
Hybride Suchergebnisse
##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);
Schlussfolgerungen
Aus unserer Analyse können wir mehrere Schlussfolgerungen über die Leistung der verschiedenen Suchmethoden ziehen. In den meisten Fällen hilft uns das semantische Suchmodell, gute Ergebnisse zu erzielen, indem es die allgemeine Intention der Anfrage erfasst, aber es greift zu kurz, wenn die Anfrage spezifische Schlüsselwörter enthält, die wir abgleichen möchten.
In diesen Fällen bildet das Einbettungsmodell diese Absicht nicht explizit ab. Andererseits kann die Volltextsuche dieses Problem direkt angehen. Sie bringt jedoch auch das Problem mit sich, dass trotz übereinstimmender Wörter irrelevante Ergebnisse gefunden werden, was die Gesamtqualität der Ergebnisse beeinträchtigen kann. Daher ist es von entscheidender Bedeutung, diese negativen Fälle zu erkennen und zu behandeln, indem man bestimmte Ergebnisse analysiert und gezielte Strategien zur Verbesserung der Suchqualität anwendet. Eine hybride Suche mit Ranking-Strategien wie RRF oder Weighted Reranker ist in der Regel eine gute Basisoption.
Mit der Freigabe der Volltextsuche in Milvus 2.5 möchten wir der Community flexible und vielfältige Lösungen für das Information Retrieval bieten. Dies wird es den Nutzern ermöglichen, verschiedene Kombinationen von Suchmethoden zu erforschen und die zunehmend komplexen und vielfältigen Suchanforderungen im Zeitalter von GenAI zu erfüllen. Sehen Sie sich das Code-Beispiel für die Implementierung von Volltextsuche und hybrider Suche mit Milvus 2.5 an.
- Fall 1: Hybride Suche übertrifft die semantische Suche
- Fall 2: Hybride Suche übertrifft die Volltextsuche
- Fall 3: Hybride Suche (mit Hinzufügung von Stoppwörtern) schneidet besser ab als die semantische Suche
- Schlussfolgerungen
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