하이브리드 RAG 검색 시스템, 설계하면서 부딪힌 4가지 갈림길

2026. 7. 13. 08:03·강의&프로젝트

RAG(Retrieval-Augmented Generation) 시스템을 실제로 구축하다 보면, 튜토리얼에서는 절대 알려주지 않는 결정의 순간들이 찾아옵니다. "벡터DB에 다 넣으면 되는 거 아냐?"라고 생각하며 시작했다가, 문서를 어디에 어떻게 저장할지부터 사용자의 질의를 어떻게 해석할지까지, 매 단계가 선택의 연속이었습니다.

이 글은 여러 종류의 전문 문서를 다루는 검색 시스템을 설계하면서 실제로 부딪혔던 네 개의 갈림길과, 각 지점에서 내린 판단의 근거를 정리한 기록입니다. 완성된 정답을 나열하기보다, "왜 그렇게 결정했는가"의 사고 과정을 담으려 했습니다.

시작점: 왜 저장소를 세 개로 나누는가

가장 먼저 정리해야 할 것은 데이터가 어디에 사는가입니다. 처음에는 벡터DB 하나면 충분해 보였지만, 실제로는 세 개의 저장소로 나누는 것이 정석이었습니다.

구조는 이렇습니다. 문서를 잘게 나눈 청크(chunk)를 만들면, 먼저 원본저장소에 통째로 넣습니다. 여기가 단일 진실 공급원(Single Source of Truth)입니다. 표시용 전문, 임베딩용 텍스트, 각주, 메타데이터까지 모든 것이 chunk_id를 열쇠로 이곳에 보관됩니다.

그다음 원본저장소가 파생 데이터를 두 갈래로 흘려보냅니다. 한쪽은 임베딩용 텍스트를 벡터로 변환해 벡터DB로, 다른 한쪽은 검색용 텍스트와 필터 메타데이터를 검색엔진으로 보냅니다. 세 저장소는 모두 같은 chunk_id를 공유합니다.

여기서 핵심 원칙 하나. 벡터DB든 검색엔진이든, 이들이 답하는 것은 오직 "어느 청크가 관련 있는가"뿐입니다. 조사자에게 최종적으로 보여줄 완전한 내용은 항상 원본저장소에서 chunk_id로 꺼냅니다. 이렇게 해야 표시용 텍스트가 세 곳에 중복 저장되지 않고, 한 곳만 고치면 모든 것이 일관되게 유지됩니다.

검색 시점의 흐름은 이렇습니다. 질의가 들어오면 벡터DB와 검색엔진 양쪽에 동시에 던지고, 각각 "관련 있는 상위 N개의 chunk_id"를 점수와 함께 돌려받습니다. 이 두 결과를 합쳐서 최종 목록을 만든 뒤, 그 chunk_id들로 원본저장소에서 전문을 꺼냅니다.

첫 번째 갈림길: 두 검색 결과를 어떻게 합칠 것인가

벡터 검색과 키워드 검색을 함께 쓰는 것을 하이브리드 검색이라고 부릅니다. 그런데 여기에 함정이 있습니다. 벡터DB는 "의미가 비슷한" 청크를, 검색엔진은 "단어가 일치하는" 청크를 각각 다른 점수 체계로 돌려줍니다. 이 두 결과를 그냥 더할 수 없습니다. 점수의 스케일이 다르기 때문입니다.

실무에서 가장 널리 쓰이는 해법은 **RRF(Reciprocal Rank Fusion, 상호 순위 융합)**입니다. 발상이 우아합니다. 점수의 절대값을 버리고, 각 결과에서의 순위만 가지고 합치는 것입니다. "벡터 검색에서 2등, 키워드 검색에서 5등인 청크"에 순위 기반 점수를 부여해 최종 순서를 정합니다.

이 방식의 장점은 서로 다른 점수 체계를 섞을 때 안전하다는 것입니다. 예를 들어 "쿠팡"이라는 단어가 정확히 들어간 문서(키워드 검색이 강한 영역)와, "납품업체 쥐어짜기"처럼 단어는 다르지만 의미가 같은 문서(벡터 검색이 강한 영역)를 균형 있게 섞어줍니다.

참고로 이 병합을 직접 구현하기 부담스럽다면, 오픈서치(OpenSearch)나 엘라스틱서치(Elasticsearch)는 벡터 검색 기능을 내장하고 있어서 검색엔진 하나로 하이브리드를 처리할 수도 있습니다. 문서가 수천에서 수만 건 수준이라면 별도 벡터DB 없이 검색엔진 하나로 통합하는 편이 운영 부담이 훨씬 적습니다. Qdrant 같은 전용 벡터DB는 벡터 검색 성능이 극한으로 필요한 대규모 환경에서 빛을 발합니다.

두 번째 갈림길: 검색엔진에 무엇을 넣고 무엇을 뺄 것인가

검색엔진에 넣을 데이터를 선별하는 기준은 의외로 단순합니다. "검색·필터·결과 표시 판단에 쓰이는가"면 넣고, "최종적으로 보여주기만 하는 무거운 데이터"면 뺍니다.

넣어야 할 것들을 하나씩 보면 이렇습니다.

id는 열쇠라서 반드시 넣습니다. 검색 결과가 원문으로 연결되는 유일한 통로입니다.

본문 텍스트는 넣되, 마커가 없는 버전을 씁니다. [^12] 같은 각주 마커는 키워드 색인에 노이즈가 되기 때문에, 표시용 텍스트가 아니라 마커 없는 순수 본문을 색인합니다.

법령 참조나 판례 번호는 반드시 넣습니다. 이것이 하이브리드 검색의 결정적 이점입니다. "제2026-016호" 같은 짧은 고유 식별자는 벡터 검색이 특히 약합니다. 의미적 유사도로는 이런 정확한 번호를 잡아내기 어렵기 때문입니다. 반면 키워드 검색은 이걸 정확히 잡아냅니다. 각주 안에 묻혀 있던 의결 번호를 밖으로 꺼내 별도 필드로 색인하면, 정확한 매칭이 필요한 검색에서 큰 힘을 발휘합니다.

핵심어 필드와 제목, 섹션 경로도 넣습니다. 특히 제목은 본문보다 검색 가중치를 높게 주는 것이 보통입니다. 섹션 경로에 핵심어가 들어 있으면 그 자체로 좋은 검색 신호가 됩니다.

필터·정렬용 메타데이터(문서 종류, 작성 팀, 발행일 등)도 넣습니다. "우리 팀 자료만", "최근 자료순" 같은 조건을 검색 단계에서 걸러주려면 이 필드들이 색인돼 있어야 합니다.

반대로 빼야 할 것들이 있습니다. 표시용 전문은 검색엔진에서 뺍니다. 검색용 텍스트와 내용이 같고 마커만 더 있으니, 중복 저장하면 색인만 무거워집니다. 필요할 때 원본저장소에서 꺼내면 됩니다. 각주 원문 전체도 뺍니다. 각주에서 검색에 정말 필요한 건 법령·판례 번호인데, 그건 이미 별도 필드로 뽑아냈기 때문입니다. 벡터값 역시 벡터DB의 몫이라 뺍니다.

세 번째 갈림길: 메타데이터가 키워드 검색을 오염시킨다

여기서 아주 실전적인 우려가 생깁니다. 작성 팀 이름 같은 메타데이터를 검색엔진에 넣으면, 예를 들어 사용자가 "경제"라고만 검색했을 때 "경제산업조사실"이 들어간 모든 청크가 우수수 걸려서 결과가 오염되지 않을까?

결론부터 말하면, 이건 "대세에 지장 없는 수준"으로 넘길 문제가 아니라 인덱스 설계로 명시적으로 해결해야 하는 문제입니다. 검색엔진이 알아서 구분해주지 않습니다. 설계자가 지정해줘야 합니다.

다행히 오픈서치와 엘라스틱서치는 이걸 정확히 통제할 수 있는 두 가지 장치를 제공합니다.

첫째, 필드 타입 구분. 작성 팀 필드를 keyword 타입으로 지정하면, 이 필드는 값 전체가 정확히 일치할 때만 매칭됩니다. "경제산업조사실 금융공정거래팀" 전체가 통째로 일치해야 하고, 사용자가 본문 검색으로 "경제"라고 쳤을 때는 걸리지 않습니다. 반대로 본문 텍스트는 text 타입으로 둬서 형태소 분석을 거쳐 부분 검색이 되게 합니다. 타입만 제대로 나눠도 우려의 대부분이 사라집니다.

둘째, 검색 대상 필드의 명시적 지정. 실제 검색 질의를 짤 때 "어느 필드를 검색할지"를 직접 고릅니다. 사용자가 자연어로 검색하면 본문·제목·법령참조·핵심어 이 네 필드만 검색 대상으로 지정하고, 작성 팀이나 문서 종류, 발행일은 검색에서 제외합니다. 그러면 "경제산업조사실"이라는 글자가 데이터에 존재해도 본문 키워드 검색에는 절대 끼어들지 않습니다.

정리하면 이런 그림입니다. 본문·제목·법령참조·핵심어는 검색에 참여시키되 서로 다른 가중치를 주고(제목을 가장 높게), 작성 팀·문서 종류·발행일·페이지는 필터와 정렬 전용으로 뺍니다. 이 구분을 인덱스 매핑에서 설계자가 명시적으로 갈라주는 것이 답입니다.

한 가지 회색지대가 있습니다. 섹션 경로는 애매합니다. 섹션 제목에 핵심어가 들어 있어서 검색에 도움이 될 때도 있지만, 반대로 노이즈가 될 수도 있습니다. 검색에 참여시키되 가중치를 아주 낮게 주거나, 아니면 필터 전용으로 빼는 선택지가 있고, 실제 검색 패턴을 보고 조정할 영역입니다.

또 하나 미묘한 지점. 본문 텍스트 안에 팀명이나 사람 이름이 우연히 등장할 수 있습니다. 하지만 이건 실제 본문에 나온 것이므로 검색에 걸려도 자연스럽습니다. 문제가 되는 건 오직 메타데이터로만 존재하는 값이 검색에 새는 것이고, 그건 위의 필드 분리로 막힙니다. 이 둘을 구분하는 것이 중요합니다.

네 번째 갈림길: 사용자 질의를 누가 쪼개는가

여기서 가장 까다로운 문제에 도달합니다. 필드를 검색용과 필터용으로 나눠뒀다고 끝이 아닙니다. 사용자는 이렇게 질의합니다.

"경제산업조사실에서 발간한 쿠팡 플랫폼 보고서 관련 최신 자료 찾아줘"

이 한 문장 안에 성격이 다른 세 종류의 정보가 섞여 있습니다. "경제산업조사실에서 발간한"은 필터 조건이고, "쿠팡 플랫폼 보고서 관련"은 검색어이며, "최신 자료"는 정렬 조건입니다.

사용자 질의에서 검색어, 필터, 정렬 정보 구분하기

 

 

 

앞서 필드를 검색용과 필터용으로 나눴으니, 이 질의도 똑같이 쪼개서 각 부분을 맞는 곳으로 보내야 합니다. 그런데 검색엔진은 이 쪼개기를 알아서 해주지 않습니다. 문장 전체를 그냥 검색어로 던지면, "경제산업조사실"이 필터로 작동하지 못하고 붕 떠버립니다.

그래서 질의와 인덱스 사이에 질의 분해(query understanding) 단계가 반드시 필요합니다. 방법은 두 가지입니다.

방법 1 — LLM에게 쪼개게 한다. 요즘의 표준입니다. 사용자 질의를 검색엔진에 던지기 전에, LLM에게 먼저 이렇게 지시합니다. "다음 질의를 검색어, 필터, 정렬로 분해해서 JSON으로 만들어라. 작성 팀, 문서 종류, 발행일은 필터 필드다." 그러면 LLM이 자연어를 구조화된 형태로 변환합니다. 검색어는 "쿠팡 플랫폼 거래질서", 필터는 작성 팀 지정, 정렬은 발행일 내림차순. 이 구조화된 결과로 검색엔진 질의를 조립하면, "그 팀 자료 중에서, 쿠팡 플랫폼 관련 내용을, 최신순으로"라는 사용자 의도가 정확히 실행됩니다. 이 방식을 질의 재작성(query rewriting)이라고도 부르며, RAG 검색 품질을 끌어올리는 가장 효과적인 층 중 하나입니다.

방법 2 — 규칙과 사전으로 쪼갠다. 전통적인 방식입니다. 미리 팀 이름 목록을 사전으로 갖고 있다가, 질의에 그 단어가 나오면 필터로 빼냅니다. LLM보다 빠르고 비용이 없지만, 표현 변형("경제산업 쪽에서 낸")에 약하고 사전을 계속 관리해야 하는 부담이 있습니다.

전문 자료를 다루는 시스템이라면 대개 방법 1이 잘 맞습니다. 사용자는 딱딱한 검색어가 아니라 말하듯이 질의할 가능성이 높기 때문입니다. "작년에 우리 팀이 낸 규제 관련 자료 중에 특정 주제 다룬 거" 같은 자연스러운 문장을 규칙 기반으로 정확히 분해하는 데는 한계가 있습니다.

핵심은 이것입니다. 인덱스 쪽에서 필드를 나눈 것과, 질의 쪽에서 의도를 나누는 것은 한 쌍입니다. 한쪽만 하면 반쪽짜리입니다. 필드를 나눠뒀어도 질의를 나눠주지 않으면 필터가 작동하지 않고, 반대도 마찬가지입니다.

보너스 갈림길: 원문을 어디에 저장할 것인가 — Postgres냐 MongoDB냐

마지막으로 원본저장소 자체를 어떤 DB로 구현할지의 문제가 남습니다. 자연스럽게 이런 직관이 생깁니다. "문서마다 메타데이터 구조가 다르다. 뉴스에는 각주나 페이지가 없고 대신 언론사, 기자, URL 같은 필드가 붙는다. 그렇다면 스키마가 자유로운 MongoDB 같은 다큐먼트 DB가 압도적으로 유리하지 않을까?"

이 관찰은 정확합니다. 이종 데이터를 담고 소스마다 필드 구성이 다르다면, 스키마 유연성 측면에서 MongoDB가 유리한 건 사실입니다. RDB에서 이걸 하나의 테이블로 억지로 담으면 NULL 투성이 컬럼이 생기거나 소스마다 테이블을 나눠야 하는 번거로움이 생깁니다.

그런데 여기 반전이 있습니다. 많은 사람이 놓치는 지점인데, PostgreSQL에는 JSONB라는 타입이 있어서 한 컬럼 안에 JSON 문서를 통째로 넣고 그 내부 필드로 인덱싱과 필터까지 할 수 있습니다. 즉 "관계형이냐 문서형이냐"의 이분법이 예전만큼 선명하지 않습니다. Postgres는 사실상 관계형과 문서형의 하이브리드로 쓸 수 있습니다.

구체적으로는 이런 설계가 가능합니다. 항상 존재하는 공통 필드(chunk_id, doc_id, 문서 종류, 발행일, 표시용 텍스트)는 일반 컬럼으로 두고, 소스마다 다른 가변 필드는 metadata라는 JSONB 컬럼 하나에 통째로 담습니다. 이러면 JSONB로 스키마 유연성을 확보하면서 동시에 관계형의 강점(공통 필드로 조인·정렬·제약)도 가져갑니다.

그렇다면 무엇을 기준으로 골라야 할까요? 스키마 유연성만으로는 둘 다 되니, 다른 축으로 판단해야 합니다. 진짜 갈리는 지점은 이것입니다. 원본저장소의 역할이 "id로 꺼내오는 단순 조회"에 그치는가, 아니면 복잡한 관계·질의가 필요한가.

우리 아키텍처에서 원본저장소의 주 임무는 chunk_id로 원문을 꺼내오는 단순 key 조회입니다. 여기서는 Mongo든 Postgres든 성능 차이가 거의 없습니다. 그런데 전문 자료를 다루는 맥락에는 관계형이 빛나는 요구가 숨어 있는 경우가 많습니다. 문서 간 인용 네트워크("이 판례를 인용한 모든 보고서"), 문서-청크의 계층 구조, 그리고 "이 청크의 문서 id가 실제 존재하는가" 같은 정합성 보장이 그렇습니다. 외래키 제약과 관계형 질의는 Postgres의 영역입니다.

여기에 결정적 보너스가 하나 더 있습니다. PostgreSQL에는 pgvector 확장이 있어서 벡터 검색까지 Postgres 안에서 할 수 있고, 전문 검색 기능도 내장돼 있습니다. 규모가 크지 않다면 Postgres 하나로 원본저장소와 벡터DB, 키워드 검색을 모두 통합하는 것도 현실적인 선택지입니다. 저장소를 셋에서 하나로 줄이면 운영 부담이 확 낮아집니다.

종합하면 이렇습니다. "스키마가 다르니 Mongo"라는 추론은 전제는 맞지만 결론이 성급했습니다. 핵심 반전은 Postgres도 JSONB로 스키마 유연성을 확보한다는 점이고, 그 위에서 관계형의 이점(인용 네트워크, 계층 구조, 정합성)과 통합 가능성(pgvector)까지 더하면 종합 우위는 Postgres 쪽으로 기웁니다.

물론 공정하게 말하면 MongoDB가 유리한 시나리오도 있습니다. 소스가 수십 종으로 계속 늘어나고 각각 구조가 극단적으로 제각각이며, 인용 관계 같은 관계형 질의는 거의 쓰지 않고 오로지 "다양한 형태의 문서를 유연하게 쌓고 id로 꺼내는" 것이 전부라면, Mongo의 개발 편의성이 더 좋을 수 있습니다.

정리하며

RAG 검색 시스템 설계는 결국 여러 개의 균형 잡기입니다. 세 개의 저장소로 나누되 진실은 한 곳에만 두고, 두 검색 결과는 순위 기반으로 안전하게 병합하고, 검색 필드와 필터 필드를 명시적으로 분리하고, 사용자 질의도 그에 맞춰 쪼개주고, 저장소는 유연성과 관계형 이점을 함께 저울질해 고릅니다.

무엇보다 기억할 것은, 검색엔진이 "알아서 해주는" 영역과 "설계자가 명시적으로 지정해야 하는" 영역의 경계입니다. 이 경계를 정확히 인식하는 것이, 튜토리얼 수준의 RAG와 실전에서 작동하는 RAG를 가르는 지점이었습니다.

'강의&프로젝트' 카테고리의 다른 글

따라가는 공부는 남지 않는다 — 실무 기획자가 AI를 통해 성장하는 방식  (2) 2026.08.05
하네스는 언젠가 짐이 된다 — 그런데 A4 한 장은 끝까지 남는다  (0) 2026.08.04
[데이터 기획 실무] RDB의 한계를 넘는 온톨로지(Ontology) 활용법과 아키텍처 설계  (0) 2026.05.01
DB는 알지만 온톨로지는 이해하는 것: 왜 우리에게 '시맨틱 레이어'가 필요할까? 평범한 직장인 검색전문가 되기(1탄)  (0) 2026.04.29
2026년, 최신 Claude를 제대로 쓰는 법: Anthropic 연구로 배우는 프롬프트·컨텍스트 설계  (0) 2026.03.06
'강의&프로젝트' 카테고리의 다른 글
  • 따라가는 공부는 남지 않는다 — 실무 기획자가 AI를 통해 성장하는 방식
  • 하네스는 언젠가 짐이 된다 — 그런데 A4 한 장은 끝까지 남는다
  • [데이터 기획 실무] RDB의 한계를 넘는 온톨로지(Ontology) 활용법과 아키텍처 설계
  • DB는 알지만 온톨로지는 이해하는 것: 왜 우리에게 '시맨틱 레이어'가 필요할까? 평범한 직장인 검색전문가 되기(1탄)
AI강선생
AI강선생
AI강선생의 블로그 입니다.
  • AI강선생
    나의 배움과 성장의 궤적
    AI강선생
  • 전체
    오늘
    어제
    • 분류 전체보기 (27)
      • 강의&프로젝트 (9)
      • 독서&지식 (9)
      • 생각과다짐 (9)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
    • 개인정보처리방침
  • 링크

  • 공지사항

    • 소개 (About)
    • 개인정보처리방침
  • 인기 글

  • 태그

    이지스퍼블리싱
    지식그래프
    Claude
    claude code
    유리링
    FastAPI
    랭체인
    Rag
    에이전트
    온톨로지
    AI agent
    길벗
    인프런
    챌린지
    하네스엔지니어링
    클로드코드
    PostgreSQL
    rustfs
    멀티에이전트
    혼공바이브코딩
    cursor
    java
    docker
    국회
    한빛미디어
    게임기획
    시멘틱검색
    spring
    AI에이전트
    오레일리
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
AI강선생
하이브리드 RAG 검색 시스템, 설계하면서 부딪힌 4가지 갈림길
상단으로

티스토리툴바