Hybrid Search - 키워드 검색과 벡터 검색을 함께 쓰기
지금까지는 벡터 유사도 검색만으로 RAG의 검색 단계를 구성해왔습니다. 그런데 실무에 적용하다 보면 벡터 검색만으로는 잘 안 잡히는 케이스들이 계속 나옵니다. 이번 글에서는 이 빈틈을 메우는 Hybrid Search(하이브리드 검색)를 다룹니다.
1. 벡터 검색이 놓치는 경우
벡터 검색은 "의미"를 기준으로 비교하기 때문에, 정확한 단어나 코드, 고유명사, 제품명처럼 글자 그대로 일치해야 하는 검색에는 약한 편입니다.
질문: "SHD-2024-0091 계약서 내용 알려줘"
벡터 검색: "계약서", "내용" 같은 일반 단어의 의미로 유사 문서를 찾음
→ 정작 SHD-2024-0091이라는 정확한 식별자를 포함한 문서는
의미적으로 두드러지지 않아 상위에 안 뜰 수 있음
사람 이름, 사내 코드, 제품 모델명, 법 조항 번호처럼 "의미"보다 "정확히 일치하는가"가 중요한 질문에서는 벡터 검색만으로 충분하지 않습니다.
2. 키워드 검색(BM25)이 강한 지점
이런 상황에서는 오히려 전통적인 키워드 검색 알고리즘인 BM25가 더 정확하게 문서를 찾아냅니다. BM25는 질문에 포함된 단어가 문서에 얼마나 자주, 얼마나 특징적으로 등장하는지를 기준으로 점수를 매깁니다.
BM25 검색: "SHD-2024-0091" 문자열이 정확히 포함된 문서를 최우선으로 매칭
→ 의미 유사도와 무관하게 정확한 키워드 일치를 우선시
반대로 BM25는 "재택근무 몇 번 가능해?"처럼 표현이 다양하게 바뀔 수 있는 자연어 질문에는 벡터 검색보다 약합니다. 즉 둘은 서로의 약점을 보완하는 관계입니다.
| 구분 | 벡터 검색 | BM25 (키워드 검색) |
|---|---|---|
| 강점 | 의미가 비슷한 표현 찾기 | 정확한 단어·식별자 매칭 |
| 약점 | 정확한 키워드 일치에 약함 | 표현이 달라지면 매칭 실패 |
| 적합한 질문 | "휴가 정책 알려줘" | "SHD-2024-0091 문서 찾아줘" |
3. Hybrid Search의 기본 구조
Hybrid Search는 두 검색을 각각 수행한 뒤, 결과를 하나의 순위로 합치는 방식입니다.
질문
├─ 벡터 검색 → 결과 A (의미 기준 순위)
└─ BM25 검색 → 결과 B (키워드 기준 순위)
↓
두 결과를 점수 기준으로 합산 (RRF 등)
↓
최종 순위 → LLM에 전달
4. 결과를 합치는 방법 - RRF (Reciprocal Rank Fusion)
두 검색 결과를 합칠 때 가장 널리 쓰이는 방식이 RRF입니다. 각 결과에서 문서가 몇 등을 했는지(순위)만 가지고 점수를 매기기 때문에, 벡터 유사도 점수와 BM25 점수처럼 척도가 서로 다른 값을 억지로 정규화할 필요가 없습니다.
RRF Score(문서) = 1 / (k + 벡터검색순위) + 1 / (k + BM25검색순위)
예시 (k=60)
문서 A: 벡터검색 1위, BM25검색 5위
→ 1/(60+1) + 1/(60+5) = 0.0164 + 0.0154 = 0.0318
문서 B: 벡터검색 8위, BM25검색 1위
→ 1/(60+8) + 1/(60+1) = 0.0147 + 0.0164 = 0.0311
두 검색 중 한쪽에서만 상위권이어도 어느 정도 점수를 받고, 양쪽 모두에서 상위권인 문서는 더 높은 점수를 받는 구조입니다.
5. PostgreSQL에서 Hybrid Search 구현하기
pgvector를 쓰고 있다면, PostgreSQL의 내장 전문 검색(Full Text Search) 기능을 BM25 대용으로 함께 사용할 수 있습니다.
ALTER TABLE documents ADD COLUMN content_tsv tsvector
GENERATED ALWAYS AS (to_tsvector('simple', content)) STORED;
CREATE INDEX idx_documents_tsv ON documents USING GIN (content_tsv);
WITH vector_search AS (
SELECT id, content,
ROW_NUMBER() OVER (ORDER BY embedding <=> :query_vector) AS rank
FROM documents
ORDER BY embedding <=> :query_vector
LIMIT 20
),
keyword_search AS (
SELECT id, content,
ROW_NUMBER() OVER (ORDER BY ts_rank(content_tsv, plainto_tsquery('simple', :query_text)) DESC) AS rank
FROM documents
WHERE content_tsv @@ plainto_tsquery('simple', :query_text)
LIMIT 20
)
SELECT COALESCE(v.id, k.id) AS id,
COALESCE(v.content, k.content) AS content,
COALESCE(1.0 / (60 + v.rank), 0) + COALESCE(1.0 / (60 + k.rank), 0) AS rrf_score
FROM vector_search v
FULL OUTER JOIN keyword_search k ON v.id = k.id
ORDER BY rrf_score DESC
LIMIT 5;
벡터 검색 결과와 키워드 검색 결과를 각각 서브쿼리로 구하고, FULL OUTER JOIN으로 합친 뒤 RRF 점수로 재정렬하는 구조입니다. 한쪽에만 걸린 문서도 살아남고, 양쪽에 걸린 문서는 자연스럽게 상위로 올라옵니다.
6. Spring Boot에 적용하기
@Repository
@RequiredArgsConstructor
public class HybridSearchRepository {
private final JdbcTemplate jdbcTemplate;
public List<SearchResult> hybridSearch(float[] queryVector, String queryText, int topK) {
String sql = """
WITH vector_search AS (
SELECT id, content, ROW_NUMBER() OVER (ORDER BY embedding <=> ?::vector) AS rank
FROM documents ORDER BY embedding <=> ?::vector LIMIT 20
),
keyword_search AS (
SELECT id, content,
ROW_NUMBER() OVER (ORDER BY ts_rank(content_tsv, plainto_tsquery('simple', ?)) DESC) AS rank
FROM documents
WHERE content_tsv @@ plainto_tsquery('simple', ?)
LIMIT 20
)
SELECT COALESCE(v.id, k.id) AS id, COALESCE(v.content, k.content) AS content,
COALESCE(1.0 / (60 + v.rank), 0) + COALESCE(1.0 / (60 + k.rank), 0) AS score
FROM vector_search v FULL OUTER JOIN keyword_search k ON v.id = k.id
ORDER BY score DESC LIMIT ?
""";
return jdbcTemplate.query(sql,
new Object[]{new PGvector(queryVector), new PGvector(queryVector), queryText, queryText, topK},
(rs, rowNum) -> new SearchResult(rs.getLong("id"), rs.getString("content"), rs.getDouble("score")));
}
}
이렇게 만든 hybridSearch 결과를 이전 글에서 다룬 Reranker에 그대로 넘기면, "넓게 두 방식으로 후보를 모으고 → 정밀하게 재정렬한다"는 검색 파이프라인이 완성됩니다.
7. 언제 Hybrid Search가 특히 필요한가
모든 서비스에 무조건 필요한 건 아닙니다. 다음 특징이 있는 도메인일수록 효과가 큽니다.
- 사용자가 정확한 코드·번호·이름을 입력해서 찾는 경우가 많음 (법률, 계약, 티켓 번호 등)
- 전문 용어나 제품명이 자주 등장하는 도메인 (의료, 제조, 특정 산업 용어)
- 자연어 질문과 정확한 키워드 질문이 섞여서 들어오는 일반 사내 챗봇
반대로 검색 대상이 짧고 일반적인 자연어 문서 위주라면, 벡터 검색만으로도 충분한 경우가 많아 굳이 복잡도를 늘릴 필요는 없습니다.
8. 정리
- 벡터 검색은 의미 기반 매칭에 강하지만, 정확한 코드나 고유명사 같은 키워드 일치에는 약합니다.
- BM25 같은 키워드 검색과 벡터 검색을 함께 사용하고 RRF로 결과를 합치면 두 방식의 약점을 서로 보완할 수 있습니다.
- PostgreSQL의 전문 검색 기능을 pgvector와 함께 쓰면, 별도 검색 엔진 없이도 하나의 DB 안에서 Hybrid Search를 구현할 수 있습니다.
다음 글에서는 지금까지 다룬 단일 질문-답변 구조를 넘어서, 이전 대화 맥락을 기억하고 이어가는 멀티턴 대화형 RAG 챗봇을 구현해보겠습니다.
'RAG를 활용한 LLM 서비스 구축' 카테고리의 다른 글
| STEP 18 - 권한 기반 RAG 접근 제어 (0) | 2026.07.14 |
|---|---|
| STEP 17 - 대화형 RAG 챗봇 구현하기 (0) | 2026.07.14 |
| STEP 15 - 임베딩 모델 교체 시 벡터 데이터 마이그레이션하기 (0) | 2026.07.13 |
| STEP 14 - RAG 챗봇 트래픽 대응하기 (0) | 2026.07.10 |
| STEP 13 - Spring Boot에 RAG 연동하기 (0) | 2026.07.10 |