🚀 완전 관리형 Milvus인 Zilliz Cloud를 무료로 체험해보세요—10배 더 빠른 성능을 경험하세요! 지금 체험하기>>

milvus-logo
LFAI

HomeBlogs시맨틱 검색과 전체 텍스트 검색: Milvus 2.5에서는 어떤 것을 선택해야 할까요?

시맨틱 검색과 전체 텍스트 검색: Milvus 2.5에서는 어떤 것을 선택해야 할까요?

  • Engineering
December 17, 2024
David Wang, Jiang Chen

선도적인 고성능 벡터 데이터베이스인 Milvus는 오랫동안 딥 러닝 모델의 벡터 임베딩을 사용한 시맨틱 검색을 전문으로 해왔습니다. 이 기술은 검색 증강 세대(RAG), 검색 엔진, 추천 시스템과 같은 AI 애플리케이션을 구동합니다. RAG 및 기타 텍스트 검색 애플리케이션의 인기가 높아지면서, 커뮤니티에서는 기존의 텍스트 매칭 방식과 시맨틱 검색을 결합한 하이브리드 검색의 장점을 인식하게 되었습니다. 이 접근 방식은 키워드 매칭에 크게 의존하는 시나리오에서 특히 유용합니다. 이러한 요구를 해결하기 위해 Milvus 2.5는 전체 텍스트 검색(FTS) 기능을 도입하고 버전 2.4부터 이미 제공되던 스파스 벡터 검색 및 하이브리드 검색 기능과 통합하여 강력한 시너지 효과를 창출합니다.

하이브리드 검색은 여러 검색 경로의 결과를 결합하는 방식입니다. 사용자는 다양한 방법으로 서로 다른 데이터 필드를 검색한 다음 결과를 병합하고 순위를 매겨 종합적인 결과를 얻을 수 있습니다. 오늘날 널리 사용되는 RAG 시나리오에서 일반적인 하이브리드 접근 방식은 시맨틱 검색과 전체 텍스트 검색을 결합합니다. 구체적으로, 여기에는 밀도 높은 임베딩 기반 시맨틱 검색과 BM25 기반 어휘 매칭의 결과를 RRF(상호 순위 융합)를 사용하여 병합하여 결과 순위를 높이는 것이 포함됩니다.

이 글에서는 9개 코드 저장소의 코드 스니펫으로 구성된 Anthropic에서 제공하는 데이터 세트를 사용하여 이를 시연해 보겠습니다. 이는 RAG의 인기 있는 사용 사례인 AI 지원 코딩 봇과 유사합니다. 코드 데이터에는 정의, 키워드 및 기타 정보가 많이 포함되어 있기 때문에 텍스트 기반 검색은 이러한 맥락에서 특히 효과적일 수 있습니다. 한편, 대규모 코드 데이터 세트에 대해 학습된 고밀도 임베딩 모델은 더 높은 수준의 의미론적 정보를 캡처할 수 있습니다. 우리의 목표는 실험을 통해 이 두 가지 접근 방식을 결합했을 때의 효과를 관찰하는 것입니다.

구체적인 사례를 분석하여 하이브리드 검색에 대한 보다 명확한 이해를 도모할 것입니다. 대량의 코드 데이터로 학습된 고급 고밀도 임베딩 모델(voyage-2)을 기본으로 사용할 것입니다. 그런 다음 하이브리드 검색이 시맨틱 및 전체 텍스트 검색 결과(상위 5개)보다 성능이 뛰어난 사례를 선택하여 이러한 사례의 특징을 분석할 것입니다.

방법Pass@5
전체 텍스트 검색0.7318
시맨틱 검색0.8096
하이브리드 검색0.8176
하이브리드 검색(중지어 추가)0.8418

사례별로 품질을 분석하는 것 외에도 전체 데이터 세트에 대해 Pass@5 메트릭을 계산하여 평가 범위를 넓혔습니다. 이 지표는 각 쿼리의 상위 5개 결과에서 발견된 관련성 있는 결과의 비율을 측정합니다. 조사 결과, 고급 임베딩 모델은 견고한 기준을 설정하지만, 이를 전체 텍스트 검색과 통합하면 훨씬 더 나은 결과를 얻을 수 있다는 것을 보여줍니다. BM25 결과를 검토하고 특정 시나리오에 대한 매개변수를 미세 조정함으로써 추가적인 개선이 가능하며, 이는 상당한 성능 향상으로 이어질 수 있습니다.

시맨틱 및 전체 텍스트 검색과 하이브리드 검색을 비교하면서 세 가지 다른 검색 쿼리에 대해 검색된 구체적인 결과를 살펴봅니다. 이 리포지토리에서 전체 코드를 확인할 수도 있습니다.

쿼리: 로그 파일은 어떻게 생성되나요?

이 쿼리는 로그 파일 생성에 대해 문의하는 것을 목표로 하며, 정답은 로그 파일을 생성하는 Rust 코드 스니펫이어야 합니다. 시맨틱 검색 결과에서 로그 헤더 파일을 소개하는 코드와 로거를 가져오는 C++ 코드를 볼 수 있었습니다. 하지만 여기서 핵심은 'logfile' 변수입니다. 하이브리드 검색 결과 #hybrid 0에서 이 관련 결과를 발견했는데, 이는 하이브리드 검색이 시맨틱 검색과 전체 텍스트 검색 결과를 병합하므로 당연히 전체 텍스트 검색에서 나온 결과입니다.

이 결과 외에도 #hybrid 2에서 관련 없는 테스트 모의 코드, 특히 "긴 문자열이 어떻게 처리되는지 테스트하기 위해"라는 반복되는 문구를 찾을 수 있습니다. 이를 위해서는 전체 텍스트 검색에 사용되는 BM25 알고리즘의 원리를 이해해야 합니다. 전체 텍스트 검색은 자주 사용하지 않는 단어를 일치시키는 것을 목표로 합니다(일반적인 단어는 텍스트의 고유성을 떨어뜨리고 대상 식별을 방해하기 때문입니다). 대규모 자연어 텍스트 코퍼스에 대해 통계 분석을 수행한다고 가정해 보겠습니다. 이 경우 '어떻게'는 매우 일반적인 단어로 연관성 점수에 거의 기여하지 않는다는 결론을 내리기 쉽습니다. 그러나 이 경우 데이터 세트는 코드로 구성되어 있고 코드에서 'how'라는 단어가 많이 등장하지 않기 때문에 이 맥락에서 핵심 검색어가 될 수 있습니다.

실체적진실: 정답은 로그 파일을 생성하는 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. "

질문: 로거를 어떻게 초기화하나요?

이 쿼리는 이전 쿼리와 매우 유사하며 정답도 동일한 코드 스니펫이지만, 이 경우 하이브리드 검색은 시맨틱 검색을 통해 답을 찾은 반면, 전체 텍스트 검색은 그렇지 못했습니다. 이러한 불일치의 이유는 질문에 대한 직관적인 이해와 일치하지 않는 말뭉치 내 단어의 통계적 가중치 때문입니다. 이 모델에서는 '어떻게'라는 단어의 일치 여부가 그다지 중요하지 않다는 것을 인식하지 못했습니다. 코드에서 '어떻게'보다 '로거'라는 단어가 더 자주 등장했기 때문에 전체 텍스트 검색 순위에서 '어떻게'가 더 중요해졌습니다.

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

관찰 결과, 희소 벡터 검색에서 "How" 및 "What"과 같이 정보가 적은 단어를 일치시킴으로써 품질이 낮은 결과가 많이 발생하는 것을 발견했습니다. 데이터를 검토한 결과, 이러한 단어들이 결과에 간섭을 일으킨다는 사실을 알게 되었습니다. 이 문제를 완화하기 위한 한 가지 방법은 이러한 단어를 제외어 목록에 추가하고 검색 과정에서 이를 무시하는 것입니다. 이렇게 하면 이러한 일반적인 단어의 부정적인 영향을 제거하고 검색 결과의 품질을 개선하는 데 도움이 됩니다.

'어떻게', '무엇'과 같이 정보가 적은 단어를 필터링하기 위해 제외어를 추가한 후, 미세 조정된 하이브리드 검색이 시맨틱 검색보다 더 나은 성능을 보인 사례를 분석했습니다. 이 경우의 개선은 쿼리에서 "RegistryClient"라는 용어를 일치시킴으로써 시맨틱 검색 모델만으로는 기억하지 못하는 결과를 찾을 수 있었기 때문입니다.

또한, 하이브리드 검색이 결과에서 품질이 낮은 일치의 수를 줄인다는 사실도 발견했습니다. 이 사례에서 하이브리드 검색 방식은 시맨틱 검색과 전체 텍스트 검색을 성공적으로 통합하여 정확도가 향상되고 관련성이 높은 결과를 도출했습니다.

쿼리: 테스트 메서드에서 RegistryClient 인스턴스는 어떻게 생성되나요?

하이브리드 검색은 시맨틱 검색만으로는 찾을 수 없었던 "RegistryClient" 인스턴스 생성과 관련된 답변을 효과적으로 검색했습니다. 중지어를 추가하면 'How'와 같은 용어에서 관련 없는 결과를 피할 수 있어 더 나은 품질의 일치 결과를 얻고 품질이 낮은 결과를 줄일 수 있었습니다.

/** 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로 전체 텍스트 검색과 하이브리드 검색을 구현하는 방법에 대한 코드 예제를 확인해 보세요.

Like the article? Spread the word

계속 읽기