데보션앱 소개페이지 바로가기
로그인 선택

신고하기

CLOSE
신고사유 (대표 사유 1개)
상세내용 (선택)
0/200
  • 신고한 게시글은 더 이상 보이지 않습니다.
  • 이용약관과 운영정책에 따라 신고사유에 해당하는지 검토 후 조치됩니다.
  • 허위 신고인 경우, 신고자의 서비스 이용이 제한될 수 있으니 유의하시어 신중하게 신고해 주세요.
(이 회원이 작성한 모든 댓글과 커뮤니티 게시물이 보이지 않고, 알림도 오지 않습니다.)

미리보기

커뮤니티

      1,234

      badge 23.06.15

      글 등록

      카테고리를 선택해주세요.

      DEVOTEE를 활성화 시키면
      지금 작성한 커뮤니티 글에 대해 1개의 댓글을 달아줍니다.

      버튼을 누르면 글 수정 시 ChatGPT가 작성한 댓글이 수정됩니다.

      임시저장함에 저장되었습니다. 저장일시 : 2022.5.17 14:29:08

      임시저장함

      제목을 선택하시면 이어서 작성이 가능하며,
      최대 20건까지 저장합니다.
      컨텐츠 유형, 제목, 저장일시, 삭제로 이뤄진 임시저장 목록
      컨텐츠 유형 제목 저장일 삭제

      데보션 블로그 게재 요청

      CLOSE
      • *
      • *

      본인인증

      효율적인 데보션 서비스 이용 및
      고객님의 소중한 개인정보보호를 위해
      본인인증을 진행해주세요. 본인인증 미 진행 시 로그인이 제한됩니다.
      본인인증 실패

      본인인증 로그인에 실패하였습니다.
      회원이 아니시거나 본인인증 등록이
      완료되지 않은 사용자입니다.

      회원정보 연결

      OpenSearch의 하이브리드 검색 소개

      코난신 25.05.30
      5,773 7 1
      DEVOTEE 요약
      Auto 서비스팀이 Retrieval-Augmented Generation(RAG) 기반 서비스 개발을 위해 OpenSearch의 하이브리드 검색을 도입한 사례를 소개했습니다. OpenSearch는 키워드(BM25), 시맨틱(벡터), 하이브리드 검색 방식을 모두 지원하며, 검색 정확도와 효율성을 높이기 위해 의미 기반 검색과 키워드 검색을 결합하여 다양한 자동차 정비 데이터에서 문맥에 맞는 정보를 제공합니다. 또한, OpenSearch는 완전 오픈소스 솔루션으로 높은 확장성과 AWS 기반 연동, 보안 기능 등을 제공하며, 도커(Docker)를 활용한 설치 및 Python 라이브러리를 통한 데이터 인덱싱·검색 활용 방법이 설명되었습니다.
      DEVOTEE 추천 블로그

      OpenSearch 소개

      image

      안녕하세요. Auto서비스개발팀 신철민입니다.

      에이닷 오토는 RAG(Retrieval-Augmented Generation) 기반 서비스를 통해 고객에게 더 정확하고 상황 인지형 답변을 제공할 예정입니다.

      RAG 시스템의 핵심은 사용자 질문의 의도를 정확히 이해하고, 이에 맞는 컨텍스트를 신속하게 검색하는 데 있습니다.

      하지만 초기 테스트에서 키워드 검색만으로는 “ELF-7W30” 같은 전문 용어 외에 “겨울용 오일” 같은 일상적 표현을 포착하지 못하는 한계가 있었고,

      의미 기반 검색은 “TPMS 경고등” 같은 정형화된 키워드 매칭에서 누락을 일으켰습니다.

      자동차 정비라는 도메인의 특수성상, 두 검색 방식을 유기적으로 결합한 전략이 필수적이었습니다.

      이러한 요구사항을 해결하기 위해 OpenSearch의 하이브리드 검색을 도입했습니다. 하이브리드 검색은 키워드 검색의 정밀도와 의미 검색의 맥락 이해력을 하나의 파이프라인에서 통합합니다.

      예를 들어 “배터리 수명”이라는 질문에는 정확한 매뉴얼 항목을, “차에서 끼릭거리는 소리가 나요”에는 구동축 베어링 마모 관련 진단 가이드를 동시에 추출합니다.

      이는 단일 검색 방식으로는 달성하기 어려운 수준의 종합적 결과입니다.

      OpenSearch는 이러한 하이브리드 검색을 구현하기에 가장 적합한 솔루션으로, 신경망 기반 의미 분석과 BM25 알고리즘을 실시간으로 병행 실행합니다.

      점수 정규화 기술을 통해 서로 다른 검색 결과의 신뢰도를 공정하게 평가하고, 자동차 정비 매뉴얼, 부품 코드, 고객 문의 기록 등 반정형 데이터를 JSON 기반으로 유연하게 관리할 수 있습니다.

      Amazon이 주도하는 오픈소스 프로젝트인 점도 중요한 선택 이유였는데,

      Apache 2.0 라이선스로 상용 제약 없이 AWS 인프라와의 긴밀한 연동이 가능했으며, RBAC 기반 보안과 Dashboards 모니터링으로 안정성을 확보할 수 있었습니다.

      이제 본론에서는 OpenSearch 환경에서 키워드, 시맨틱, 하이브리드 검색을 실제로 구현하고 성능을 비교 평가했던 구체적인 과정을 단계별로 설명드리겠습니다.


      OpenSearch Benchmark

      OpenSearch는 Splunk, Elasticsearch, Graylog 등과 같은 대표적인 로그 모니터링 및 검색 솔루션들과 다음과 같은 차별점을 보입니다.

      항목

      OpenSearch

      Splunk

      Elasticsearch

      라이선스/비용

      완전 오픈소스, 무료 사용 가능

      상용, 볼륨 기반 라이선스

      최근 버전은 상용, 7.10까지 오픈소스

      커뮤니티/지원

      AWS 및 오픈 커뮤니티 주도

      벤더 중심, 공식 지원

      Elastic사 중심

      배포/운영 방식

      자체/클라우드/관리형 모두 지원

      자체/클라우드

      자체/클라우드/관리형

      검색 방식

      키워드, 벡터(시맨틱), 하이브리드

      키워드(SPL), 일부 ML 지원

      키워드, 벡터(일부)

      시각화 도구

      OpenSearch Dashboards

      Splunk Dashboard

      Kibana

      보안/인증

      기본 제공, RBAC, 암호화 지원

      강력한 보안, SIEM 기능

      일부 유료, 오픈소스 제한적

      확장성/플러그인

      다양한 플러그인(ML, SQL 등)

      앱 마켓, 확장성 높음

      플러그인 다양

      • 오픈소스와 비용: OpenSearch는 완전 오픈소스이며, 커뮤니티 주도로 발전하고 있어 라이선스 비용 부담이 없습니다. Splunk는 상용 제품으로, 데이터 볼륨에 따라 비용이 크게 증가할 수 있습니다.

      • 검색 방식의 다양성: OpenSearch는 키워드(BM25), 시맨틱(벡터), 하이브리드 검색을 모두 지원합니다. 특히 시맨틱·하이브리드 검색은 최근 AI 기반 검색 수요에 부합하는 강점입니다.

      • 운영의 유연성: 자체 구축, 클라우드, AWS 관리형 서비스 등 다양한 운영 방식을 지원합니다.

      • 보안 및 확장성: RBAC, 암호화, 감사 로그 등 엔터프라이즈급 보안 기능을 기본 제공하며, 다양한 플러그인으로 확장성이 높습니다.

      OpenSearch의 검색 방식인 키워드, 시맨틱, 하이브리드 검색에 대해 간단히 설명 드리겠습니다.

      • 키워드 검색: BM25 알고리즘 기반으로, 입력된 키워드와 문서 내 용어 빈도 및 길이 등을 고려해 연관성 점수를 산출합니다. 전통적인 로그 검색, 모니터링에 적합합니다.

      • 시맨틱(의미 기반) 검색: 텍스트 임베딩 모델(딥러닝 기반)을 활용해 쿼리와 문서를 벡터로 변환, 의미적 유사성을 기준으로 검색합니다. 자연어 질의, 유사 문서 추천 등에서 뛰어난 성능을 보입니다.

      • 하이브리드 검색: 키워드와 시맨틱 검색 결과를 결합해 검색 정확도와 관련성을 더욱 높입니다. 예를 들어, BM25 점수와 벡터 유사도 점수를 정규화·가중평균하여 최종 결과를 산출합니다.

      SIEM·IT 운영 등 특화된 엔터프라이즈 기능이 필요하다면 Splunk와 ElasticSearch 같은 상용 솔루션이 적합할 수 있습니다.

      하지만 OpenSearch는 오픈소스 기반의 강력한 검색 및 로그 분석 솔루션으로, 키워드, 시맨틱, 하이브리드 등 다양한 검색 방식을 지원하며,

      AWS 관리형 서비스와의 연계, 높은 확장성, 엔터프라이즈급 보안 기능 등에서 차별화된 강점을 갖고 있습니다.

      따라서 비용 효율성과 최신 AI 검색, 커스터마이징이 중요하다면 OpenSearch가 매우 매력적인 선택이 될 수 있습니다.


      OpenSearch 설치

      https://docs.opensearch.org/docs/latest/install-and-configure/install-opensearch/docker/#sample-docker-composeyml

      위 사이트에서는 간단하게 OpenSearch를 시작할 수 있도록 docker-compose 파일을 제공하고 있습니다.

      로컬에서 OpenSearch를 사용하기 위해서는 몇가지 수정사항이 있습니다.

      1. OpenSearch 설치 버전을 환경변수값으로 설정할수 있도록 수정합니다.

        • opensearch-node1 & opensearch-node2

        image: opensearchproject/opensearch:latest # 이 값을
        image: opensearchproject/opensearch:${OPENSEARCH_VERSION} # 이 값으로 수정
        • opensearch-dashboards

        image: opensearchproject/opensearch-dashboards:latest # 이 값을
        image: opensearchproject/opensearch-dashboards:${OPENSEARCH_VERSION} # 이 값으로 수정
      2. Local 환경에서는 OpenSearch API 등록시 Https 통신이 지원되지 않을 수 있기 때문에 아래 환경 변수들을 추가해 줍니다.

        • opensearch-node1 & opensearch-node2

        environment:
            ...
            - plugins.security.disabled=true # ssl 비활성화
        • opensearch-dashboards

        environment:
          # OPENSEARCH_HOSTS들을 https에서 http로 수정
          OPENSEARCH_HOSTS: '["http://opensearch-node1:9200","http://opensearch-node2:9200"]' 
          DISABLE_SECURITY_DASHBOARDS_PLUGIN: true # 추가
      3. 이제 해당 docker-compose.yml파일과 동일 디렉토리 내에 .env 파일을 생성해서 아래와 같이 변수를 정의합니다.

        OPENSEARCH_VERSION=2.18.0
        OPENSEARCH_INITIAL_ADMIN_PASSWORD=Platttae1r!!

        여기에서 비밀번호를 충분히 복잡하게 정의해주지 않으면 "[ConnectionError]: getaddrinfo ENOTFOUND opensearch-node1" 에러가 발생할 수 있으니 주의합니다.

      4. docker-compose를 실행합니다.

        docker-compose up -d
        [+] Running 14/14
         ✔ opensearch-node2 Pulled                                                                                                                                                                            30.7s
         ✔ opensearch-dashboards Pulled                                                                                                                                                                       28.5s
           ✔ 2dc99809e331 Download complete                                                                                                                                                                    4.8s
           ✔ 1a7487881ebe Download complete                                                                                                                                                                   15.6s
           ✔ 4f4fb700ef54 Already exists                                                                                                                                                                       1.4s
           ✔ 09269bd91141 Download complete                                                                                                                                                                    1.4s
           ✔ 1fb528a57184 Download complete                                                                                                                                                                    4.8s
           ✔ a2fc91cba926 Download complete                                                                                                                                                                    5.0s
         ✔ opensearch-node1 Pulled                                                                                                                                                                            30.6s
           ✔ d948a2b87b57 Download complete                                                                                                                                                                    1.4s
           ✔ 78679551b9df Download complete                                                                                                                                                                    4.7s
           ✔ e9a6a1c3c5f3 Download complete                                                                                                                                                                   22.1s
           ✔ 7999b2541daa Download complete                                                                                                                                                                    4.8s
           ✔ 666773a8bf15 Download complete                                                                                                                                                                    4.7s
        [+] Running 6/6
         ✔ Network opensearch_opensearch-net     Created                                                                                                                                                       0.0s
         ✔ Volume "opensearch_opensearch-data2"  Created                                                                                                                                                       0.0s
         ✔ Volume "opensearch_opensearch-data1"  Created                                                                                                                                                       0.0s
         ✔ Container opensearch-dashboards       Started                                                                                                                                                       0.4s
         ✔ Container opensearch-node2            Started                                                                                                                                                       0.4s
         ✔ Container opensearch-node1            Started

        도커에서 container들이 정상적으로 실행되고 있는지 확인합니다.

        image

        http://localhost:5601 접속해서 대시보드를 확인합니다.

        (본 경험기는 OpenSearch의 Search 기능을 공유하는 목적이어서 Dashboard 기능에 대해서는 포함하고 있지 않습니다.)

        image


      테스트를 위한 데이터 준비

      본격적으로 OpenSearch Search 테스트를 진행하기 앞서 테스트를 진행하기 위한 사전 작업을 진행하겠습니다.

      1. 데이터 준비

        각종 search 테스트의 대상이 될 데이터 입니다. 저는 차량 가이드 문서를 기반으로 데이터를 수집했습니다.

        chunk_id    context pages   images
        9965    하이브리드 차량: 소개\n1 엔진\n2 12볼트 배터리\n3 오렌지색 전기 배선\n4 구동 배터리\n5 전기 모터\n하이브리드 차량은 전기 모터를 이용하여 가속, 시동 등 엔진의 성능을 향상시킵니다. 본 차량은 더 많은 가속 토크를 제공하면서 연료를 덜 소모합니다. 본 차량은 고전압 구동 배터리에 저장되어 있는 에너지를 이용합니다. ■ 배터리\n하이브리드 차량에는 다음 두 가지 유형의 배터리가 장착되어 있습니다. – 고전압 구동 배터리\n– 12볼트 배터리\n▶ 고전압 구동 배터리\n이 배터리는 차량 하부에 위치해 있으며, 전기 모터가 원활하게 작동하는데 필요한 전기 에너지를 저장합니다. 다른 배터리와 마찬가지로 사용을 하면 방전이 됩니다. 다음과 같은 경우 충전됩니다. – 차량이 감속할 때\n– 엔진이 발전기 역할을 하기 위해 자동으로 시동될 때\n전기 모터가 작동할 때의 차량 주행 가능 거리는 구동 배터리의 충전량 뿐 아니라 주행 스타일 그리고 에너지 소비 장치(에어컨, 히터 등)의 사용에 따라 달라집니다. 구동 배터리가 방전된 경우 구동 배터리가 충분히 충전될 때까지 차량이 엔진만으로 움직입니다. ▶ 12볼트 배터리\n12볼트 배터리는 트렁크 하부에 위치해 있으며, 차량을 열고 닫고 차량 장치를 작동시키는데 필요한 에너지를 공급해 줍니다. 참고: 차량 시동을 걸 때 12볼트 배터리는 개입하지 않습니다. 차량 시동은 하이브리드 시스템에 의해 진행됩니다. 경고\n본 차량의 하이브리드 전기 시스템은 약 232~361볼트의 고전압 직류 전압을 이용합니다. 운행 중과 시동이 꺼진 후에도 고온의 열이 구동 시스템에 남아 있을 수 있습니다. 차량에 부착된 라벨의 경고 문구를 준수하십시오. 위험 상황이 발생할 수 있으므로 구성품, 전선, 커넥터, 구동 배터리 등의 고전압 전기 시스템에 대한 어떤 작업과 개조도 엄격하게 금지되어 있습니다. 문제 발생 시 지정 정비업소로 문의하십시오. 심각한 화상, 전기 충격 등의 사고로 사망에 이를 위험이 있습니다. 경고\n표시 A는 신체 등에 감전 위험을 줄 수 있는 차량의 전기 부품을 나타냅니다. ■ 고전압 전기 회로\n차량 내 고전압 전기 회로는 오렌지색 배선 6과 여러 부품에 부착된 기호로 알아볼 수 있습니다. 10,11   http://9965.1,http://9965.2
        9966   ...
      2. 라이브러리 준비

        pip==24.3.1
        opensearch-py==2.8.0
        sentence-transformers==3.3.1
        pandas==2.2.3
        • pip==24.3.1

          Python의 표준 패키지 관리 도구로, PyPI 등에서 다양한 패키지를 설치, 업그레이드, 삭제, 관리할 수 있습니다.

          최신 버전에서는 대규모 의존성 트리 해석 성능이 개선되고, 캐싱 및 시작 속도 최적화, 다양한 버그 수정 및 기능 개선이 적용되었습니다.

        • opensearch-py==2.8.0

          OpenSearch와 통신하기 위한 공식 Python 클라이언트 라이브러리입니다.

          오픈소스이며, OpenSearch 클러스터에 데이터 색인, 검색, 관리 등 다양한 작업을 Python 코드로 수행할 수 있게 해줍니다.

        • sentence-transformers==3.3.1

          문장 임베딩, 의미 기반 검색, 유사도 계산, 재정렬(reranking) 등에 사용되는 라이브러리입니다.

          최신 트랜스포머 기반 모델을 쉽게 활용하거나, 직접 파인튜닝하여 텍스트 임베딩을 생성할 수 있습니다. 자연어 처리, 시맨틱 검색, 클러스터링 등 다양한 AI 응용에 활용됩니다.

        • pandas==2.2.3

          Python에서 데이터 분석과 조작을 위한 대표적인 라이브러리로, DataFrame, Series 등 강력한 데이터 구조와 풍부한 기능(결측치 처리, 그룹 연산, 병합, 시계열 처리 등)을 제공합니다.

          대용량 데이터의 실무 분석에 널리 사용됩니다


      Keyword Search Test

      키워드 검색은 OpenSearch의 기본 검색 방식으로, BM25 알고리즘을 기반으로 동작합니다.

      사용자가 입력한 키워드와 문서 내의 단어가 얼마나 자주 등장하는지(용어 빈도, Term Frequency)와,

      그 단어가 전체 문서 집합에서 얼마나 희귀한지(역문서 빈도, Inverse Document Frequency)를 함께 고려해 각 문서의 연관성 점수를 산출합니다.

      이 방식은 입력한 단어와 정확히 일치하는 텍스트를 빠르게 찾는 데 강점을 가지며, 로그 검색이나 모니터링처럼 구조화된 데이터와 명확한 키워드 기반 탐색이 필요한 환경에서 널리 사용됩니다.

      하지만 쿼리와 문서가 다른 표현이나 동의어를 사용할 경우, 혹은 오타가 있을 때는 관련성 높은 결과를 놓칠 수 있습니다.

      1. 데이터 프로세서

        사전 준비 단계에서 마련한 데이터를 기반으로 데이터 전처리를 진행합니다.

        class DataProcessor:
            def load_and_process_data(self, file_path):
                """
                TSV 파일 로드 및 전처리
                """
                df = pd.read_csv(file_path, sep="\t")
        
                # 결측치 제거 및 리스트 변환
                df["pages"] = df["pages"].apply(lambda x: x.split(",") if isinstance(x, str) else [])
                df["images"] = df["images"].apply(lambda x: x.split(",") if isinstance(x, str) else [])
                df = df.dropna()
        
                print(f"Processed {len(df)} rows of data.")
                return df

        여기에서 키포인트는 TSV파일을 data frame 객체로 전환하는 과정에서 pages 필드와 images 필드는 문자열을 리스트로 변환하며 dropna() 함수로 값이 없는 데이터는 제거합니다.

      2. 인덱서

        class Indexer:
            def __init__(self, host, port, username, password):
                self.client = OpenSearch(
                    hosts=[{"host": host, "port": port}],
                    http_auth=(username, password),
                    use_ssl=False,
                    verify_certs=False
                )
                print("Connected to OpenSearch:", self.client.info())
        
            def create_index(self, index_name):
                index_body = {
                    "settings": {
                        "index": {
                            "number_of_shards": 1,
                            "number_of_replicas": 0
                        }
                    },
                    "mappings": {
                        "properties": {
                            "chunk_id": {"type": "keyword"},
                            "context": {"type": "text"},
                            "pages": {"type": "keyword"},
                            "images": {"type": "keyword"}
                        }
                    }
                }
        
                response = self.client.indices.create(index=index_name, body=index_body, ignore=400)
                print(f"Index creation response for '{index_name}':", response)
        
            def index_data(self, index_name, data):
                """
                데이터 인덱싱
                """
                for i, row in data.iterrows():
                    doc = {
                        "chunk_id": row["chunk_id"],
                        "context": row["context"],
                        "pages": row["pages"],
                        "images": row["images"]
                    }
                    response = self.client.index(index=index_name, body=doc, id=str(i), refresh=True)
                    print(f"Indexed document {i}: {response}")
        
            def search_query(self, index_name, keyword_query, top_k):
                """
                키워드 검색 쿼리 실행
                """
                query_body = {
                    "query": {
                        "match": {
                            "context": keyword_query
                        }
                    },
                    "_source": ["chunk_id", "context", "pages", "images"],
                    "size": top_k
                }
        
                response = self.client.search(body=query_body, index=index_name)
                print(f"Keyword search response for '{index_name}':", json.dumps(response, ensure_ascii=False, indent=4, sort_keys=True))
        • _init_(self, host, port, username, password)

          Indexer 클래스의 초기화를 담당하며, OpenSearch 서버와의 연결을 설정합니다. 호스트(host)와 포트(port) 정보를 통해 서버 위치를 지정하고, 사용자 이름(username)과 비밀번호(password)로 인증합니다. SSL 암호화를 사용하지 않도록 설정(use_ssl=False)하며, 인증서 검증도 생략합니다(verify_certs=False). 이는 주로 테스트 환경에서 편의성을 위해 적용되는 설정입니다. 연결이 성공하면 client.info()를 호출해 서버 정보를 출력하여 연결 상태를 확인할 수 있습니다. 이 과정을 통해 클라이언트 객체가 준비되며, 이후 모든 OpenSearch 작업은 이 객체를 통해 수행됩니다.

        • create_index(self, index_name)

          OpenSearch에 새로운 인덱스를 생성하는 역할을 합니다. 인덱스 이름(index_name)을 입력받아 설정(settings)과 매핑(mappings) 정보를 정의합니다. 샤드(shard) 수는 데이터 분산 저장을 위해 number_of_shards=1로 설정되며, 복제본(replica)은 number_of_replicas=0으로 지정되어 백업 기능이 비활성화됩니다. 매핑에서는 필드별 데이터 타입을 명시합니다: chunk_id, pages, images는 키워드(keyword) 타입으로 정확한 일치 검색에 적합하고, context는 텍스트(text) 타입으로 풀텍스트 검색이 가능합니다. self.client.indices.create를 호출할 때 ignore=400 옵션을 추가해 이미 인덱스가 존재하는 경우에도 오류를 방지합니다. 이 메서드는 인덱스 생성 결과를 응답(response)으로 반환하며, 콘솔에 출력합니다.

        • index_data(self, index_name, data)

          pandas 데이터프레임(data)의 내용을 OpenSearch 인덱스에 문서(document) 형태로 저장합니다. data.iterrows()를 사용해 데이터프레임의 각 행(row)을 순회하며, chunk_id, context, pages, images 필드 값을 추출해 문서(doc)를 구성합니다. self.client.index 메서드를 호출할 때 문서 ID(id)를 문자열로 변환해 지정하며, refresh=True를 설정해 문서가 즉시 검색 가능하도록 강제합니다. 각 문서의 인덱싱 결과는 응답(response)으로 반환되며, 반복문 내에서 처리된 문서 번호와 함께 콘솔에 출력됩니다. 이 과정을 통해 대량의 데이터를 효율적으로 인덱싱할 수 있습니다.

        • search_query(self, index_name, keyword_query, top_k)

          지정된 인덱스(index_name)에서 키워드(keyword_query) 기반 검색을 수행합니다. 쿼리 본문(query_body)은 match 쿼리를 사용해 context 필드에서 키워드를 검색하도록 정의되며, size 파라미터로 상위 top_k개의 결과만 반환받습니다. _source 필드를 통해 반환될 문서의 필드를 제한함으로써 네트워크 대역폭을 절약합니다. self.client.search를 실행한 후, json.dumps를 활용해 응답(response)을 가독성 있게 포맷팅하여 출력합니다. 이 메서드는 검색 결과를 신속하게 확인할 수 있도록 설계되었으며, 복잡한 쿼리로 확장하기 위한 기반 구조를 제공합니다.

      3. 실행 결과

        위에서 선언된 함수들을 기반으로 main 함수를 구성합니다.

        여기에서 password 값은 위 docker-compose 파일에 선언된 값을 사용하면 되고, index_name 값은 opensearch에 등록될 인덱스 명입니다.

        def main():
            host = "localhost"
            port = 9200
            username = "admin"
            password = "Platttae1r!!"
            file_path = 'chunk_data.tsv'
            index_name = 'keyword-test'
            top_k = 3  # 검색할 후보지 수
        
            # open search 핸들러 및 데이터 프로세서 초기화
            os_handler = Indexer(host=host, port=port, username=username, password=password)
            data_processor = DataProcessor()
        
            processed_data = data_processor.load_and_process_data(file_path) # 데이터 로드 및 전처리
            os_handler.create_index(index_name=index_name)# 인덱스 생성
            os_handler.index_data(index_name=index_name, data=processed_data) # 데이터 인덱싱
        
            # 키워드 검색 실행
            search_query = "하이브리드 차량 정보 알려줘"
            os_handler.search_query(index_name=index_name, keyword_query=search_query, top_k=top_k)

        위 처럼 키워드 검색을 진행하면 실행 결과는 다음과 같습니다.

        {
        "_shards": {
            "failed": 0,
            "skipped": 0,
            "successful": 1,
            "total": 1
        },
        "hits": {
            "hits": [
                {
                    "_id": "2",
                    "_index": "rkm-keyword-test",
                    "_score": 1.3337513,
                    "_source": {
                        "chunk_id": 9967,
                        "context": "#####\\n하이브리드 차량: 주의 사항\\n경고\\n다음 사항을 준수하십시오. 이 지침을 준수하지 않으면 화재, ...",
                        "images": [],
                        "pages": [
                            "13"
                        ]
                    }
                },
                {
                    "_id": "0",
                    "_index": "rkm-keyword-test",
                    "_score": 1.3307788,
                    "_source": {
                        "chunk_id": 9965,
                        "context": "하이브리드 차량: 소개\\n1 엔진\\n2 12볼트 배터..."
                        "images": [
                            "http://9965.1",
                            "http://9965.2"
                        ],
                        "pages": [
                            "10",
                            "11"
                        ]
                    }
                },
                ...
            ],
            "max_score": 1.3337513,
            "total": {
                "relation": "eq",
                "value": 6
            }
        },
        "timed_out": false,
        "took": 68
        }

        결과를 보면, 입력한 쿼리인 “하이브리드 차량”으로 정확히 시작하는 문서들이 주로 검색 결과에 나타났습니다.

        이때 각 문서의 score 값은 큰 차이를 보이지 않아, 어떤 문서가 쿼리와 더 유사한지 명확하게 구분하기 어렵다는 점이 확인됩니다.

        즉, 키워드 검색 방식에서는 단어의 일치 여부에 따라 결과가 결정되기 때문에, 의미적 유사성이나 문서의 다양성 측면에서는 한계가 있을 수 있습니다.


      Semantic Search Test

      시맨틱 검색은 딥러닝 기반의 텍스트 임베딩 모델을 활용해 쿼리와 문서를 벡터로 변환한 뒤, 이 벡터 간의 의미적 유사성을 기준으로 검색 결과를 산출합니다.

      사용자가 자연어로 입력한 질문이나 설명이 있을 때, OpenSearch의 신경망(Neural) 검색 모듈이 해당 텍스트를 임베딩 모델(예: Sentence Transformer)로 변환하여 고차원 벡터로 만듭니다.

      이 벡터는 단어의 표면적 일치가 아니라 의미적 맥락을 반영하므로, 쿼리와 문서가 표현은 다르지만 의미가 유사할 경우에도 높은 관련성 점수를 줄 수 있습니다.

      OpenSearch는 k-NN(최근접 이웃) 검색을 통해 쿼리 벡터와 가장 가까운(의미적으로 유사한) 문서 벡터를 찾아 결과를 반환합니다.

      덕분에 동의어, 유사어, 다양한 표현, 오타 등에도 강인하며, 자연어 질의 응답, 유사 문서 추천, 검색 정확도 향상 등에서 탁월한 성능을 보입니다.

      1. 데이터 프로세서

        저는 모델 "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2" 기반으로 문장 임베딩을 진행했습니다.

        https://huggingface.co/models 에서 보다 다양한 모델을 제공하고 있으니 다른 모델도 확인해 보시기 바랍니다.

        class DataProcessor:
            def __init__(self, model_name="sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2"):
                self.model = SentenceTransformer(model_name)
                self.dimensional = 384  # 모델의 출력 벡터 차원
        
            def load_and_process_data(self, file_path):
                df = pd.read_csv(file_path, sep="\t")
        
                df["pages"] = df["pages"].apply(lambda x: x.split(",") if isinstance(x, str) else [])
                df["images"] = df["images"].apply(lambda x: x.split(",") if isinstance(x, str) else [])
                df = df.dropna()
        
                # 텍스트 데이터를 임베딩 벡터로 변환
                df['embedding'] = df['context'].apply(lambda x: self.embedding_data(text=x))
        
                print(f"Processed {len(df)} rows of data.")
                return df
        
            def embedding_data(self, text):
                return self.model.encode([text])[0]

        Keyword Search와 마찬가지로 TSV파일을 읽어 결측지 제거하는 로직은 동일하지만 이후 LLM 모델을 기반으로 임베딩된 값을 data frame에 추가해 줍니다.

      2. 인덱서

        class Indexer:
            ...
            def create_index(self, index_name, dimensional):
                """
                KNN 벡터 검색을 위한 OpenSearch 인덱스 생성
                """
                index_body = {
                    "settings": {
                        "index": {
                            "knn": True
                        }
                    },
                    "mappings": {
                        "properties": {
                            "embedding": {
                                "type": "knn_vector",
                                "dimension": dimensional,
                                "method": {
                                    "name": "hnsw",
                                    "space_type": "l2",
                                    "engine": "nmslib"
                                }
                            },
                            "context": {"type": "text"},
                            "pages": {"type": "keyword"},
                            "images": {"type": "keyword"}
                        }
                    }
                }
        
                response = self.client.indices.create(index=index_name, body=index_body, ignore=400)
                print(f"Index creation response for '{index_name}':", response)
        
            def index_data(self, index_name, data):
                for i, row in data.iterrows():
                    doc = {
                        "chunk_id": row["chunk_id"],
                        "context": row["context"],
                        "pages": row["pages"],
                        "images": row["images"],
                        "embedding": row["embedding"]
                    }
                    response = self.client.index(index=index_name, body=doc, id=str(i), refresh=True)
                    print(f"Indexed document {i}: {response}")
        
            def search_query(self, index_name, embedding_data, top_k):
                query_body = {
                    "query": {
                        "knn": {
                            "embedding": {
                                "vector": embedding_data,
                                "k": top_k
                            }
                        }
                    },
                    "_source": False,
                    "fields": ["chunk_id", "context", "pages", "images"]
                }
        
                response = self.client.search(body=query_body, index=index_name)
        
                print(f"Index search response for '{index_name}':", json.dumps(response, ensure_ascii=False, indent=4, sort_keys=True))

        인덱서의 기본적인 함수 구조는 Keyword Search와 동일합니다. 하지만, 인덱스를 만드는 로직에서 다른 부분이 있습니다.

        인덱스의 성능을 좌우하는 중요한 변수가 3개가 존재하는데 이것에 대해 알아보도록 하겠습니다.

        image

        • dimension

          기본 최대 10,000 차원 설정이 가능하며 NMSLIB, Faiss, Lucene 엔진 설정시 최대 16,000 차원까지 지원합니다.

          Data Processor에서 사용한 모델로 임베딩된 벡터의 dimension 값을 사용하게 됩니다.

        • space_type

          • l2 (유클리드 거리): ∑(Xᵢ - Yᵢ)² → 일반적인 유사도 계산에 적합

          • l1 (맨해튼 거리): ∑|Xᵢ - Yᵢ| → 희소 데이터/차원 차이 강조 시

          • cosinesimil (코사인 유사도): 1 - (X·Y)/(||X||·||Y||) → 텍스트/NLP 작업

          • innerproduct (내적): -∑Xᵢ·Yᵢ → 추천 시스템

          • linf (최대 거리): max(|Xᵢ - Yᵢ|) → 이상치 탐지

          • hammingbit (해밍 거리): 이진 벡터 전용

            일반적으로 텍스트 유사도 검색에는 코사인 유사도를 사용하며 추천 시스템에는 내적 유사도를 사용하게 됩니다.

            그렇지만 저는 일반적인 실험을 진행하기 위해 본문에서 l2를 사용했습니다.

          # 텍스트 유사도 검색 → 코사인 유사도
          index_body["mappings"]["properties"]["embedding"]["method"]["space_type"] = "cosinesimil"
          
          # 추천 시스템 → 내적 유사도
          index_body["mappings"]["properties"]["embedding"]["method"]["space_type"] = "innerproduct"
          
          # 이미지 검색 → L2 거리 (기본값)
          index_body["mappings"]["properties"]["embedding"]["method"]["space_type"] = "l2"
        • engine

          벡터 검색 엔진은 고차원 벡터 공간에서 유사한 데이터를 빠르게 찾아내는 데 특화된 소프트웨어입니다.

          대표적으로 NMSLIB, Faiss, Lucene 기반 엔진이 널리 사용되고 있으며, 각각의 특징과 활용 사례는 아래와 같습니다.

          • NMSLIB (Non-Metric Space Library)

            범용 k-NN(최근접 이웃) 검색에 강점을 가진 라이브러리입니다. 다양한 거리 함수와 검색 방법을 지원하며, 비(非)메트릭 공간에서도 효율적으로 동작하도록 설계되었습니다.

            특히 HNSW(Hierarchical Navigable Small World)와 같은 최신 근접 이웃 탐색 알고리즘을 포함하고 있어, 대규모 데이터셋에서도 빠르고 정확한 검색이 가능합니다.

            NMSLIB는 텍스트, 이미지, 추천 시스템 등 다양한 분야에서 활용되며, 실시간 검색보다는 오프라인이나 배치성 대량 유사도 계산에 적합합니다.

            또한 C++와 Python 등 여러 언어에서 사용할 수 있어 확장성도 뛰어납니다.

          • Faiss (Facebook AI Similarity Search)

            Facebook AI Research에서 개발한 대규모 벡터 데이터셋에 특화된 라이브러리로, CPU뿐만 아니라 GPU 가속을 지원하는 것이 큰 장점입니다.

            수백만에서 수십억 개에 이르는 방대한 벡터 데이터를 효율적으로 검색할 수 있으며, IVF(Inverted File Index), PQ(Product Quantization), HNSW 등 다양한 인덱싱 및 압축 기법을 제공합니다.

            GPU를 활용하면 실시간 검색도 가능해져, 이미지 검색이나 대규모 추천 시스템, 자연어 임베딩 검색 등에서 주로 사용됩니다.

            특히 대량의 벡터 데이터를 빠르게 처리해야 하는 환경에서 탁월한 성능을 보여줍니다.

          • Lucene 기반 벡터 검색

            원래 텍스트 검색 엔진으로 출발했지만, 최근에는 HNSW 등 벡터 검색 알고리즘도 지원하면서 텍스트와 벡터를 결합한 하이브리드 검색이 가능해졌습니다.

            Lucene 기반의 OpenSearch나 Elasticsearch 같은 솔루션에서는 키워드 기반의 텍스트 검색과 의미 기반의 벡터 검색을 동시에 수행할 수 있어, 자연어 검색이나 복합적인 검색 서비스에 적합합니다.

            기존의 텍스트 필터링, 정렬, 집계 기능과 벡터 검색을 결합할 수 있다는 점이 큰 장점이며, 별도의 벡터 전용 데이터베이스 없이도 통합 검색 환경을 구축할 수 있습니다.

        이렇게 3가지의 속성을 Search가 진행될 데이터와 검색 요건에 부합하도록 설정했을때 효율적으로 원하는 결과값을 얻을 수 있겠습니다.

      3. 실행 결과

        메인 함수의 구조는 Keyword Search와 동일하며 쿼리를 search query 함수에 전달할 때 임베딩해서 전달하는 로직만 추가 됩니다.

        def main():
            ...
            # data 조회
            # 검색 문장을 embedding
            search_query = "하이브리드 차량 정보 알려줘"
            search_query_embedding = data_processor.embedding_data(text=search_query)
        
            # 검색 쿼리 실행
            os_handler.search_query(index_name=index_name, embedding_data=search_query_embedding, top_k=top_k)

        search query 실행 결과는 다음과 같습니다.

        {
          "_shards": {
            "failed": 0,
            "skipped": 0,
            "successful": 1,
            "total": 1
          },
          "hits": {
            "hits": [
              {
                "_id": "1",
                "_index": "rkm-test",
                "_score": 0.11565096,
                "fields": {
                  "chunk_id": [
                    9966
                  ],
                  "context": [
                    "#####\\n하이브리드 차량: 작동\\n본 하이브리드 차량은 부드러운, 스포티 등의 주행 스타일, 교통 상황 및 주행 모드에 따라 엔진 또는 전기 모터를 선택합니..."
                  ],
                  "images": [
                    "http://9966"
                  ],
                  "pages": [
                    "12"
                  ]
                }
              },
              {
                "_id": "3",
                "_index": "rkm-test",
                "_score": 0.097650155,
                "fields": {
                  "chunk_id": [
                    9968
                  ],
                  "context": [
                    "#####\\n스마트키 사용 방법 및 주의 사항\\n■ 스마트키의 기능\\n1 차량 잠금 및 위치 확인 버튼\\n2 테일게이트 조작 버튼\\n3 차량 잠금해제 버튼..."
                  ],
                  "pages": [
                    "14"
                  ]
                }
              },
              ...
            ],
            "max_score": 0.11565096,
            "total": {
              "relation": "eq",
              "value": 3
            }
          },
          "timed_out": false,
          "took": 6
        }

        Keyword Search와는 달리, 문서가 반드시 “하이브리드 차량”으로 시작하지 않더라도 의미 유사도를 기반으로 다양한 내용의 문서들이 검색 결과에 포함되었습니다.

        또한, 각 문서의 score 값에서도 의미 있는 차이가 나타났습니다.

        이처럼 단순히 특정 단어의 포함 여부만을 기준으로 검색하는 방식보다, 문서의 의미를 기반으로 검색할 때 훨씬 더 풍부하고 다양한 결과를 얻을 수 있음을 확인할 수 있었습니다.


      Hybrid Search Test

      하이브리드 검색은 키워드 검색과 시맨틱 검색의 장점을 결합한 방식입니다.

      OpenSearch는 먼저 쿼리와 문서에 대해 각각 BM25(키워드 기반) 점수와 벡터 유사도(시맨틱 기반) 점수를 산출합니다.

      이후, 두 점수를 정규화하여(예: min-max, L2-Euclidean 등) 다양한 방식으로 결합합니다.

      결합 방법에는 산술 평균, 기하 평균, 조화 평균 등이 사용될 수 있으며, 이를 통해 최종적으로 랭킹된 문서 리스트를 만듭니다.

      이 과정은 OpenSearch의 검색 파이프라인에서 자동으로 처리됩니다.

      하이브리드 검색은 키워드의 정확한 일치와 의미적 유사성 모두를 반영하므로, 단순 키워드 일치만으로는 찾기 힘든 결과도 놓치지 않으면서,

      동시에 의미가 완전히 다른 문서가 상위에 노출되는 문제도 줄일 수 있습니다.

      실제로, 복잡한 자연어 질의나 다양한 표현이 혼재된 데이터셋에서 검색 품질을 극대화하는 데 매우 효과적입니다

      1. 데이터 프로세서

        class DataProcessor:
            ...
            def merge_document(self, semantic_results, keyword_results, top_k, semantic_weight=0.5):
                keyword_weight = 1 - semantic_weight
        
                merged_dict = {}
        
                # Semantic Results 처리
                for doc in semantic_results:
                    chunk_id = doc["meta_data"]["chunk_id"]
                    merged_dict[chunk_id] = doc
                    merged_dict[chunk_id]["meta_data"]["semantic_score"] = merged_dict[chunk_id]["meta_data"]["_score"]
                    merged_dict[chunk_id]["meta_data"]["_score"] = merged_dict[chunk_id]["meta_data"]["_score"] * semantic_weight # semantic 가중치 반영
        
                # Keyword Results 처리
                for doc in keyword_results:
                    chunk_id = doc["meta_data"]["chunk_id"]
                    if chunk_id not in merged_dict:
                        merged_dict[chunk_id] = doc
                        merged_dict[chunk_id]["meta_data"]["keyword_score"] = merged_dict[chunk_id]["meta_data"]["_score"]
                        merged_dict[chunk_id]["meta_data"]["_score"] = merged_dict[chunk_id]["meta_data"]["_score"] * keyword_weight
                    else:
                        merged_dict[chunk_id]["meta_data"]["keyword_score"] = doc["meta_data"]["_score"]
                        merged_dict[chunk_id]["meta_data"]["_score"] = merged_dict[chunk_id]["meta_data"]["_score"] + doc["meta_data"]["_score"] * keyword_weight
        
                merged_list = list(merged_dict.values())
                sorted_list = sorted(merged_list, key=lambda x: x["meta_data"]["_score"], reverse=True)
        
                return sorted_list[:top_k]

        데이터 프로세서는 Semantic Search의 것과 유사하지만 여기에 merge_document 함수가 추가됩니다.

        이 함수는 두 Search 결과를 chunk_id(데이터 id)기반으로 병합합니다.

        한 가지 유의할 사항은 Keyword Search의 score 값과 Semantic Search의 score값 기준이 다르기 때문에 단순 두 값을 비교하기에는 무리가 있습니다.

        더 유사한 문서를 추출하기 위해서는 Semantic Search score값에 가중치를 찾는 것이 중요합니다.

      2. 인덱서

        class OpenSearchHandler:
            def __init__(self, host, port, username, password):
                # 다른 Search와 동일
        
            def create_index(self, index_name, dimensional):
                index_body = {
                    "settings": {
                        "index": {
                            "knn": True
                        }
                    },
                    "mappings": {
                        "properties": {
                            "embedding": {
                                "type": "knn_vector",
                                "dimension": dimensional,
                                "method": {
                                    "name": "hnsw",
                                    "space_type": "cosinesimil", 
                                    "engine": "lucene"
                                }
                            },
                            "chunk_id": {"type": "integer"},
                            "context": {"type": "text"},
                            "pages": {"type": "keyword"},
                            "images": {"type": "keyword"}
                        }
                    }
                }
        
                response = self.client.indices.create(index=index_name, body=index_body, ignore=400)
                print(f"Index creation response for '{index_name}':", response)
        
            def index_data(self, index_name, data):
                for i, row in data.iterrows():
                    doc = {
                        "chunk_id": row["chunk_id"],
                        "context": row["context"],
                        "pages": row["pages"],
                        "images": row["images"],
                        "embedding": row["embedding"]
                    }
                    response = self.client.index(index=index_name, body=doc, id=str(i), refresh=True)
                    print(f"Indexed document {i}: {response}")
        
            def format_as_documents(self, search_response):
                documents = []
                for result in search_response["hits"]["hits"]:
                    source = result["_source"]
                    documents.append({
                        "page_content": source["context"],
                        "meta_data": {
                            "_score": result["_score"],
                            "chunk_id": source.get("chunk_id"),
                            "pages": source.get("pages"),
                            "images": source.get("images")
                        }
                    })
                return documents
        
            async def as_document_keyword_search(self, index_name, keyword_query, top_k):
                query_body = {
                    "query": {
                        "match": {  # match 쿼리를 사용하여 키워드 기반 검색 수행.
                            "context": keyword_query
                        }
                    },
                    "_source": ["chunk_id", "context", "pages", "images"],
                    "size": top_k
                }
        
                response = self.client.search(body=query_body, index=index_name)
        
                print(f"Keyword search response for '{index_name}':", json.dumps(response, ensure_ascii=False, indent=4, sort_keys=True))
        
                return self.format_as_documents(response)
        
        
            async def as_document_semantic_search(self, index_name, embedding_data, top_k):
                """
                검색 쿼리 실행
                """
                query_body = {
                    "query": {
                        "knn": {
                            "embedding": {
                                "vector": embedding_data,
                                "k": top_k
                            }
                        }
                    },
                    "_source": ["chunk_id", "context", "pages", "images"]
                }
        
                response = self.client.search(body=query_body, index=index_name)
        
                print(f"Semantic search response for '{index_name}':", json.dumps(response, ensure_ascii=False, indent=4, sort_keys=True))
        
                return self.format_as_documents(response)
        • create_index(self, index_name, dimensional)

          Keyword Search에서 활용해야 하는 필드와 Semantic Search에서 사용하는 필드를 모두 포함한 인덱스를 생성합니다.

          여기에 추가로 데이터의 id를 의미하는 chunk_id가 포함되며, 해당 값을 기반으로 추후 결과를 병합할 수 있게 됩니다.

          한 가지 더 변경사항은 space_type 값입니다.

          기존에는 l2를 사용했었지만 병합과정에서 비중계산을 하기에 점수대가 너무 낮아 cosinesimil을 사용했습니다.

        • index_data(self, index_name, data)

          두 가지 search를 진행하기 위해 양쪽 search에서 사용하는 모든 필드를 인덱스합니다.

        • format_as_documents(self, search_response)

          두 가지 search의 결과를 통일시켜주기 위한 함수입니다.

        • as_document_keyword_search(self, index_name, keyword_query, top_k)

          Keyword Search를 진행합니다.

        • as_document_semantic_search(self, index_name, embedding_data, top_k)

          Semantic Search를 진행합니다.

      3. 실행 결과

        async def main():
            # 설정값 및 클래스 객체화 로직은 동일
            ...
        
            # Hybrid Search 실행
            search_query = "하이브리드 차량 정보 알려줘"
        
            # 키워드 쿼리와 임베딩 쿼리 생성
            search_query_embedding = data_processor.embedding_data(text=search_query)
        
            # 비동기 검색 실행
            semantic_results_task = os_handler.as_document_semantic_search(index_name, search_query_embedding, top_k * 2)
            keyword_results_task = os_handler.as_document_keyword_search(index_name, search_query, top_k * 2)
        
            semantic_results, keyword_results = await asyncio.gather(semantic_results_task, keyword_results_task)
        
            print(f"semantic doc : {semantic_results}")
            print(f"keyword doc : {keyword_results}")
        
            # semantic_weight 파라미터에 0~1 까지 semantic 결과 비중을 지정. 디펄트는 0.5 (keyword_weight 는 1-semantic_weight 값)
            merge_document = data_processor.merge_document(semantic_results, keyword_results, top_k, 0.8)
        
            print(f"merged document :", json.dumps(merge_document, ensure_ascii=False, indent=4, sort_keys=True))
        [
            {
                "meta_data": {
                    "_score": 0.83621658,
                    "chunk_id": 9966,
                    "images": [
                        "http://9966"
                    ],
                    "keyword_score": 1.0657785,
                    "pages": [
                        "12"
                    ],
                    "semantic_score": 0.7788261
                },
                "page_content": "#####\\n하이브리드 차량: 작동\\n본 하이브리드 차량은 부드러운, 스포티 등의 주행 스타일, 교통 상황 및 주행 모드에 따라 엔진 또는 전기 모터를 선택합니다. ■ 에너지 회생 제동 강도 표시등 1\\n주행 중 변속 레버가..."
            },
            {
                "meta_data": {
                    "_score": 0.81920292,
                    "chunk_id": 9965,
                    "images": [
                        "http://9965.1",
                        "http://9965.2"
                    ],
                    "keyword_score": 1.2193202,
                    "pages": [
                        "10",
                        "11"
                    ],
                    "semantic_score": 0.7191736
                },
                "page_content": "하이브리드 차량: 소개\\n1 엔진\\n2 12볼트 배터리\\n3 오렌지색 전기 배선\\n4 구동 배터리\\n5 전기 모터\\n하이브리드 차량은 전기 모터를 이용하여 가속, 시동 등 엔진의 성능을 향상시킵니다. 본 차량은 더 많은 가속 토크를..."
            },
            {
                "meta_data": {
                    "_score": 0.8156074799999999,
                    "chunk_id": 9967,
                    "images": [],
                    "keyword_score": 1.2280822,
                    "pages": [
                        "13"
                    ],
                    "semantic_score": 0.7124888
                },
                "page_content": "#####\\n하이브리드 차량: 주의 사항\\n경고\\n\\n\\n\\n\\n\\n       \\n다음 사항을 준수하십시오. 이 지침을 준수하지 않으면 화재, 화상, 감전 또는 심각한 부상으로 사망에 이를 위험이 있습니다. 차량에 충격 또는 사고가 난 경우\\n차량 하부에 표지판, 보도 혹은 도로 시설물과 접촉 등의 충격 또는 사고가..."
            }
        ]

      Hybrid Search 성능을 개선하기 위해 세 가지 개선점을 도출했습니다.

      1. 검색 파라미터 최적화가 필요합니다.

        다양한 임베딩 모델(예: all-mpnet-base-v2, all-MiniLM-L6-v2)을 실험하여 정확도와 처리 속도의 균형을 찾아야 합니다.

        하이브리드 검색의 가중치(semantic과 keyword 비율)를 동적으로 조정하는 것도 효과적입니다.

        예를 들어 “how to”가 포함된 자연어 질문에는 semantic 비중을 높이고, 단순 키워드 검색 시에는 keyword 비중을 높일 수 있습니다.

        A/B 테스트를 통해 클릭률(CTR)을 추적하면서 최적의 조합을 찾는 과정이 필수적입니다.

      2. 벡터 데이터 구조를 개편해야 합니다.

        기존에는 사용설명서 본문 전체를 벡터로 변환했지만, 예상 질문 리스트를 별도로 생성해 벡터로 저장하는 방식으로 변경합니다.

        예를 들어 T5 모델로 본문에서 자동 질문을 생성하거나, 고객센터 문의 데이터를 기반으로 수동 라벨링한 질문을 활용할 수 있습니다.

        이렇게 하면 검색 시 사용자의 실제 질문과 예상 질문 간 유사도 계산이 더 정확해집니다.

      3. 점수 정규화 체계를 구축해야 합니다.

        semantic 점수(01)와 keyword 점수(010)처럼 스케일이 다른 점수를 공정하게 비교하려면 Min-Max 정규화나 Z-score 정규화를 적용해야 합니다.

        예를 들어 semantic 점수는 최소-최대 범위로, keyword 점수는 평균과 표준편차를 이용해 표준화한 뒤 7:3 비율로 가중 평균합니다.

        쿼리 길이에 따라 가중치를 자동 조절하는 로직도 추가할 수 있습니다.

      지금까지 에이닷 오토의 RAG 서비스 개발 과정에서 OpenSearch를 활용해 키워드 검색의 정밀도, 시맨틱 검색의 맥락 해석 능력, 하이브리드 검색의 통합적 장점을 실제 데이터에 적용하며 얻은 정보를 공유드렸습니다.

      단순 검색 엔진의 한계를 넘어, 의미와 구조화된 데이터가 결합된 검색 체계가 LLM의 답변 품질을 어떻게 혁신하는지 확인할 수 있었던 값진 경험이었습니다.

      이 글이 검색 시스템 고도화를 위한 기술적 참고자료로, 데이터와 사용자 경험의 새로운 접점을 모색하는 분들에게 작은 영감으로 작용하기를 기대합니다. 감사합니다.

      댓글 0

      DEVOTEE를 활성화 시키면
      지금 작성한 댓글에 AI가 댓글을 달아줍니다.

      코난신 님의 최신 블로그

      더보기

      DEVOTEE 추천 블로그

      동영상 기고하기