
질문을 세 단어로 치면 검색 결과가 하나도 안 나오는데, 이유가 뭘까요? 벡터 검색기는 질문 문장 전체의 의미를 임베딩해서 찾는데, 단어 몇 개만 던지면 비교할 문맥이 부족해 관련 문서와의 유사도 점수가 낮게 나옵니다. 이럴 때 쓰는 방법이 쿼리 재작성이고, 그래도 리콜이 안 오르면 가상의 답변 문서를 먼저 만들어 검색하는 HyDE로 넘어갑니다. 두 방법 다 검색 품질을 끌어올리려는 목적은 같지만 작동 방식과 지연 비용이 달라서, 아무 파이프라인에나 똑같이 적용하면 오히려 응답 속도만 나빠질 수 있습니다.
짧은 질문에서 벡터 검색이 자꾸 실패하는 이유가 뭘까요?
임베딩 기반 검색은 질문 문장을 고차원 벡터로 바꾼 뒤, 문서 벡터와의 코사인 유사도로 순위를 매기는 구조입니다. “쿼리 재작성 왜?”처럼 서술어와 목적어가 생략된 문장을 넣으면, 모델이 참고할 문맥이 적어서 벡터가 특정 주제 쪽으로 뾰족하게 쏠리지 않고 문서 공간 전체에서 애매한 위치에 놓입니다. 그 결과 상위권에 진짜 관련 문서 대신 범용적인 문서가 섞여 들어옵니다.
이 현상은 BM25 같은 키워드 매칭 검색에서는 상대적으로 덜 나타납니다. 키워드 검색은 형태소가 문서에 그대로 들어 있는지만 확인하기 때문에, 짧아도 핵심 단어만 맞으면 매칭이 됩니다. 반대로 말하면 쿼리 재작성이나 HyDE는 임베딩 기반 dense retrieval을 쓰는 RAG 파이프라인에서 효과가 두드러지는 처방이고, 키워드 검색만 쓰는 시스템에는 크게 의미가 없습니다.
쿼리 재작성이 하는 일 — 문장을 완전하게 바꾸는 것
쿼리 재작성은 검색 전에 LLM 한 번을 거쳐, 짧거나 대명사가 많은 질문을 검색엔진이 이해하기 좋은 완전한 문장으로 바꾸는 단계입니다. 이 구조를 학계에서는 Rewrite-Retrieve-Read(RRR)라고 부르는데, 질문을 다시 쓰고(Rewrite) → 검색하고(Retrieve) → 답을 생성(Read)하는 순서를 뜻합니다. RAG(검색 증강 생성)의 개념 자체가 궁금하다면 위키백과의 Retrieval-augmented generation 항목에서 전체 구조를 확인할 수 있습니다.
실제 코드는 이렇게 짧게 시작할 수 있습니다. OpenAI Python SDK 1.x 기준이며, 재작성 전용으로는 응답 속도가 빠른 소형 모델을 쓰는 편이 비용 면에서 유리합니다.

from openai import OpenAI
client = OpenAI()
def rewrite_query(question: str) -> str:
resp = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": "사용자의 짧은 질문을 검색엔진에 적합한 완전한 문장으로 바꿔라."},
{"role": "user", "content": question}
],
temperature=0
)
return resp.choices[0].message.content
print(rewrite_query("쿼리 재작성 왜?"))
# 출력 예: "정보 검색 시스템에서 쿼리 재작성을 사용하는 이유와 그 효과는 무엇인가?"
한 번 더 확장하면 하나의 질문을 여러 표현으로 바꿔 병렬로 검색한 뒤 결과를 합치는 방식도 있습니다. 표현 차이 때문에 리콜이 낮은 경우에는 단일 재작성보다 이쪽이 잘 먹히지만, 검색 호출 자체가 여러 번 늘어나므로 벡터 DB 쿼리 비용도 함께 늘어난다는 점은 감안해야 합니다.
HyDE는 질문이 아니라 가상의 답을 임베딩합니다
HyDE(Hypothetical Document Embeddings)는 접근 방식이 반대입니다. 질문 문장을 다듬는 대신, LLM에게 “이 질문에 답하는 문서를 써봐”라고 시켜서 가상의 답변 문서를 먼저 만들고, 그 문서를 임베딩해서 검색에 씁니다. 질문-문서 쌍보다 문서-문서 쌍의 유사도가 임베딩 공간에서 더 잘 잡히는 경우가 많다는 점을 이용한 방식으로, 원 논문은 Gao 등의 “Precise Zero-Shot Dense Retrieval without Relevance Labels”에서 처음 제안했습니다.
def generate_hypothetical_doc(question: str) -> str:
resp = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": f"다음 질문에 답하는 문서를 서술형으로 작성하라: {question}"}],
temperature=0.3
)
return resp.choices[0].message.content
hypo_doc = generate_hypothetical_doc("쿼리 재작성 왜?")
query_vector = embeddings.embed_query(hypo_doc) # 질문이 아니라 가상 문서를 임베딩
여기서 트레이드오프가 하나 생깁니다. 가상 문서는 어디까지나 LLM이 지어낸 텍스트라서, 실제 자료에 없는 수치나 고유명사를 그럴듯하게 채워 넣는 경우가 있습니다. 이런 세부 사실이 검색 벡터에 반영되면 오히려 엉뚱한 문서를 끌어오는 역효과가 나므로, 정확한 수치나 날짜를 찾는 질의에는 HyDE보다 단순 재작성이나 키워드 검색 병행이 더 안전합니다.
쿼리 재작성과 HyDE, 비용과 품질을 함께 보면
두 방법 모두 검색 전에 LLM 호출이 하나 더 붙는다는 공통점이 있지만, 무엇을 임베딩하느냐와 실패했을 때 나타나는 증상이 다릅니다.

| 구분 | 임베딩 대상 | 추가 LLM 호출 | 잘 맞는 상황 | 취약한 상황 |
|---|---|---|---|---|
| 쿼리 재작성 | 다듬어진 질문 문장 | 1회 | 질문이 짧거나 대명사·생략이 많을 때 | 원래 질문 의도를 LLM이 잘못 해석할 때 |
| 다중 쿼리 확장 | 여러 표현의 질문 묶음 | 1회(다수 생성) | 동의어·표현 차이로 리콜이 낮을 때 | 검색 호출·벡터 DB 비용이 배로 늘 때 |
| HyDE | LLM이 생성한 가상 답변 문서 | 1회(문서 생성) | 질문과 문서 어휘 격차(lexical gap)가 클 때 | 정확한 수치·고유명사를 찾는 질의일 때 |
표에서 보듯 셋 다 정답을 보장하는 만능 해법이 아니라, 실패 패턴이 서로 다른 보정 수단입니다. 원본 질문 임베딩만으로 충분히 잘 검색되는 파이프라인이라면 굳이 이 단계를 추가할 필요가 없고, 지연 시간과 토큰 비용만 늘어나는 셈이 됩니다.
이 기준으로 방법을 골라보세요
질문 로그를 먼저 살펴보고 판단하는 편이 정확합니다. 사용자가 “가격?”, “설치 안됨” 같은 단문을 자주 입력한다면 쿼리 재작성부터 붙이는 게 순서에 맞고, 질문은 충분히 길지만 문서가 격식체 기술 문서라서 표현 격차가 크다면 HyDE 쪽이 더 효과적입니다. 두 문제가 같이 있다면 재작성으로 문장을 완성한 뒤 그 결과를 HyDE 입력으로 넘기는 순차 조합도 가능하지만, LLM 호출이 두 번 겹치는 만큼 응답 지연이 그대로 누적된다는 점은 미리 계산해 둬야 합니다.
쿼리를 재작성했는데 왜 응답이 더 느려지나요?
원래 파이프라인은 질문 임베딩 → 검색 → 답변 생성 세 단계였는데, 재작성을 추가하면 그 앞에 LLM 호출이 하나 더 붙습니다. HyDE까지 쓰면 문서 생성과 임베딩 호출이 추가로 붙어서 총 지연 시간이 원래보다 LLM 응답 시간만큼 늘어납니다. 반복적으로 들어오는 질문이라면 재작성 결과를 캐싱하거나, 재작성 전용 모델을 지연이 짧은 소형 모델로 분리하는 방식으로 체감 속도를 줄일 수 있습니다.
짧은 질문 앞에서는 이 순서로 시도하면 됩니다
정리하면 질문이 짧고 생략이 많아서 검색이 안 되는 경우에는 쿼리 재작성을 먼저 붙이고, 질문과 문서 사이 어휘 격차가 원인으로 보이면 HyDE를 시도하는 순서가 합리적입니다. 두 방법 다 LLM 호출이 추가되는 만큼 지연·비용이 늘어난다는 트레이드오프가 있으니, 적용 전후로 검색 리콜과 응답 시간을 함께 로그로 남겨서 실제로 개선됐는지 비교해 보는 단계가 다음 할 일이 됩니다. 실서비스에 바로 적용하기보다는, 실패한 질문 로그 20~30개를 모아 재작성/HyDE 적용 전후 상위 문서를 비교하는 작은 실험부터 시작하는 편을 권합니다.
