법률 AI 검색 실험기 (6) — 하이브리드 검색과 쿼리 분해 실험기
벡터 검색만으로 단답형 질문은 거의 100% recall을 달성했다. 그런데 "부당해고 구제절차와 관련 판례"처럼 정답이 여러 개인 복수정답 질문에서는 벡터 검색이 한계를 드러냈다. 정답 조문 중 일부만 Top-10에 들어오고, 나머지는 빠지는 문제가 반복됐다. 자연스럽게 다음 질문이 떠올랐다. 벡터만으로 안 되면 키워드를 섞으면 어떨까?
업계의 하이브리드 검색 흐름
하이브리드 검색은 RAG 파이프라인에서 이미 표준에 가까운 접근법이 됐다. Dense retrieval(벡터 검색)은 의미적 유사성을 잘 잡지만, 법령 번호나 조문명 같은 정확한 식별자를 놓치는 경우가 있다. 반대로 BM25 같은 sparse retrieval은 키워드 매칭에 강하지만 의미적 확장이 안 된다. 2025~2026년 시점에서 Pinecone, Weaviate, Qdrant 등 주요 벡터 데이터베이스는 모두 하이브리드 검색을 지원하고 있고, 실무에서도 둘을 병합하는 것이 단독 사용보다 일관적으로 좋은 성능을 보인다는 결과가 축적되어 있다.
다만 나의 경우는 일반적인 BM25 + 벡터 조합이 아니었다. 법률 도메인의 특성상, 문서 간 인용 관계(citation graph)를 활용하는 그래프 검색을 키워드 검색 대신 사용했다.
document_refs: 인용 그래프를 검색에 활용하기
647,309개의 인용 관계
법률 문서는 서로를 인용한다. 판례는 근거 조문을 인용하고, 해석례는 관련 판례를 참조한다. 이 인용 관계를 document_refs라는 엣지 컬렉션으로 구축했다. 총 647,309건의 관계가 담겼다.
| 관계 유형 | 건수 |
| 국세판례 -> 법조문 | 178,289 |
| 해석례 -> 법조문 | 177,060 |
| 판례 -> 법조문 | 128,696 |
| 판례 -> 판례 | 105,238 |
| 국세판례 -> 판례 | 52,676 |
| 해석례 -> 판례 | 5,350 |
그래프 검색의 원리: Co-citation
그래프 검색의 핵심 아이디어는 co-citation이다. 사용자 질문에서 특정 조문(예: 근로기준법 제27조)이 언급되면, 그 조문을 인용한 모든 문서를 먼저 찾고, 그 문서들이 함께 인용한 다른 조문을 집계한다. "이 조문과 자주 함께 인용되는 조문"을 찾는 셈이다.
기존에는 판례 컬렉션 하나에서 _parsedLawRefs 필드를 $unwind해서 공출현을 찾았는데, document_refs 도입 후에는 판례, 해석례, 국세판례 전체를 아우르는 2단계 조회로 바뀌었다. 검색 범위가 훨씬 넓어진 것이다.
하이브리드 파이프라인과 RRF 병합
파이프라인 구조는 다음과 같다.
사용자 질문 -> 프리라이터(Gemini Flash Lite) -> 벡터검색 + 그래프검색(병렬) -> RRF 병합 -> Top-10
벡터 검색과 그래프 검색을 병렬로 실행한 뒤, RRF(Reciprocal Rank Fusion)로 두 순위 목록을 하나로 합친다.
점수(문서) = 1/(k + 벡터순위) + 1/(k + 그래프순위) (k=60)
양쪽 모두에 등장한 문서가 높은 점수를 받는 구조다. 업계에서도 RRF는 하이브리드 검색의 표준 병합 전략으로 널리 쓰이고 있다.
프리라이터 설계: 세 가지 접근법
벡터 검색과 그래프 검색은 입력 형식이 다르다. 벡터는 자연어 쿼리가, 그래프는 법령명과 키워드가 필요하다. 이 차이를 프리라이터(prerewriter)로 해결하려 했고, 세 가지 방식을 실험했다.
| 방식 | 하이브리드 | 벡터만 | 차이 |
| A. 통합 프롬프트 1개 | 17/31 (55%) | 15/31 (48%) | +2 |
| B. 합친 프롬프트 (벡터+그래프 원칙 통합) | 15/31 (48%) | 12/31 (39%) | +3 |
| C. 분리 (벡터용 + 그래프용 각각) | 16/31 (52%) | 14/31 (45%) | +2 |
방식 B가 그래프 기여도(+3)는 가장 높았지만, 벡터 성능 자체가 12/31로 크게 떨어졌다. 벡터 검색용 searchQuery 표현을 그래프 쪽 요구사항에 맞추다 보니 벡터 품질이 훼손된 것이다.
가장 중요한 발견은 벡터 검색이 searchQuery 표현에 극도로 민감하다는 점이었다. 프롬프트를 조금만 바꿔도 성능이 12~15 사이에서 크게 흔들렸다. 반면 그래프 검색은 일관적으로 +1~3의 안정적인 기여를 보여줬다.
전체 68개 질문 평가 결과
프리라이터 분리 방식(C)으로 전체 68개 질문을 평가했다.
하이브리드: 74/88 (84%) vs 벡터만: 71/88 (81%) -> +3
Full Recall@10: 59/68 vs 58/68
| 그룹 | 질문 수 | 결과 | 비고 |
| 단답 기본 (Q1~Q20) | 20 | 전부 정답 | 벡터만으로 충분 |
| 시나리오 (Q101~Q120) | 20 | 전부 정답 | 벡터만으로 충분 |
| 세법 (Q201~Q217) | 17 | 전부 정답 | 벡터만으로 충분 |
| 복수정답 (Q301~Q311) | 11 | MISS 발생 | 그래프 +3 개선 |
단답형, 시나리오형, 세법 질문은 벡터 검색만으로 100%였다. 문제는 오직 복수정답 케이스에서만 발생했다. 예를 들어 Q5(부당해고)는 벡터만으로는 정답 3개 중 1개만 찾았는데, 그래프 검색 덕분에 3/3 ALL을 달성했다. Q9(양도소득)에서는 그래프가 소득세법 104조를 매번 찾아줬다.
하지만 RRF 병합의 부작용도 있었다. Q8, Q10 같은 케이스에서는 그래프의 엉뚱한 결과가 벡터가 정확히 찾은 문서를 밀어내는 현상이 관찰됐다.
쿼리 분해 실험: 기대와 현실
복수정답 문제를 더 근본적으로 풀기 위해 쿼리 분해(Query Decomposition)를 시도했다. 아이디어는 단순하다. 복수 주제 질문을 서브질문으로 쪼개고, 각각 독립적으로 검색한 뒤 결과를 병합하는 것이다.
"법인세 얼마? 접대비 비용처리?"
-> 서브1: "법인세 과세표준과 세율" -> Top-5 검색
-> 서브2: "접대비 비용처리 한도" -> Top-5 검색
-> RRF 병합 -> 최종 Top-10
치팅 실험 vs 클린 실험
먼저 few-shot 예시에 평가 질문과 유사한 예시를 넣어 "치팅 실험"을 했다.
치팅: 하이브리드 20/31 (65%) vs 벡터만 19/31 (61%)
Q11(소득세 계산)이 1/4에서 4/4로 극적으로 개선됐다. 하지만 이건 오버피팅이다. 평가 질문과 무관한 few-shot으로 바꾼 "클린 실험" 결과는 달랐다.
클린: 하이브리드 15/31 (48%) vs 벡터만 16/31 (52%)
기존 단일 쿼리 방식(17/31)보다 오히려 성능이 하락했다.
왜 실패했나
실패 원인은 세 가지였다. 첫째, LLM이 분해를 안 하는 경우가 있었다. Q11은 4개 주제를 담고 있는데 LLM이 단일 질문으로 판단해버렸다. 둘째, 분해된 쿼리의 표현이 벡터 검색에 맞지 않았다. "담장 파손"이라는 구체적 표현이 "불법행위"라는 추상적 법률 용어로 바뀌면서 검색 품질이 떨어졌다. 셋째, few-shot 예시 의존도가 너무 높아서 일반화가 불가능했다.
근본적인 문제는 명확했다. 프롬프트 튜닝은 테스트셋 오버피팅일 뿐, 실제 서비스에서의 일반화를 보장할 수 없다.
실험에서 배운 것
이 실험들에서 얻은 교훈을 정리하면 다음과 같다.
벡터 검색은 단일 정답 질문에 이미 충분하다. 57개 단답형/시나리오형 질문에서 100% recall을 달성했다. 문제 영역을 정확히 식별하는 것이 중요하다.
그래프 검색은 안정적이지만 제한적이다. 복수정답 케이스에서 일관적으로 +1~3의 기여를 했지만, 그 이상의 극적인 개선은 없었다.
RRF 병합은 양날의 검이다. 두 검색 결과를 합치는 과정에서 오히려 정답이 밀려나는 부작용이 있었다. 가중치 조정이나 다른 병합 전략이 필요하다.
쿼리 분해는 (적어도 이 시점에서는) 효과가 없었다. few-shot 의존도가 높고, LLM의 분해 판단이 불안정하며, 분해된 쿼리가 벡터 검색에 최적화되지 않았다.
프리라이터 표현이 벡터 성능을 좌우한다. 이것이 가장 실용적인 발견이었다. 검색 아키텍처를 복잡하게 만드는 것보다, 벡터 검색에 들어가는 쿼리 표현 자체를 개선하는 것이 더 효과적일 수 있다.
이 실험 이후 방향은 두 가지로 좁혀졌다. 하나는 검색 후 리랭킹(post-retrieval reranking)으로 Top-30 후보를 뽑은 뒤 LLM으로 정밀 순위를 매기는 것, 다른 하나는 프리라이터의 쿼리 표현 품질 자체를 높이는 것이었다.

