2장 — 벡터 저장소와 임베딩, 검색 전략

1장에서 RAG의 R과 A가 개발자의 몫이라고 했습니다. 2장은 그 R을 어떻게 잘할 것인가에 대한 장입니다.

아무리 강력한 언어 모델이라도 자체적으로 학습하지 않은 최신 정보나 사내 전용 자료에는 접근할 수 없습니다. 따라서 모델이 답변을 생성할 때 필요한 데이터를 어떻게, 얼마나 정확하게 가져올 수 있는지가 애플리케이션의 품질과 직결됩니다. 이 과정의 주인공이 벡터 저장소와 임베딩입니다.

벡터와 임베딩

RAG 파이프라인 구성의 핵심은 벡터 저장소(vector store) 입니다. 사용자의 질문과 LLM에 전달할 데이터가 서로 관련이 있는지 판단하려면 벡터(vector) 가 필요합니다.

벡터는 숫자가 일렬로 나열된 데이터 구조입니다. 벡터는 단어나 문장 간의 유사도를 파악해서 차원화합니다. 즉 단어나 문장을 특정 차원의 점으로 표현하는 것이고, 이 변환에 쓰이는 것이 임베딩(embedding) 입니다.

임베딩 모델은 무엇을 학습하나

임베딩 모델은 단어가 문맥에서 함께 등장하는 경향(동시 출현 패턴)을 학습합니다. 같은 맥락에서 자주 등장하는 단어들이 벡터 공간에서 가깝게 놓이도록 학습되는 것입니다.

책의 예시가 명쾌합니다. ‘왕은 왕자의 아버지다’와 ‘여왕은 왕자의 어머니다’라는 두 문장에서 ‘왕은’과 ‘여왕은’은 모두 ‘왕자의’ 앞에 옵니다. 두 단어가 ‘붙어 있어서’가 아니라 문맥적 사용 패턴이 유사하기 때문에 임베딩 공간에서 가까이 놓입니다.

구글의 임베딩 프로젝터(Embedding Projector) 에서 Word2Vec 10K 데이터로 ‘queen’을 검색하면 가장 가까운 다섯 단어가 이렇게 나옵니다.

단어 거리
elizabeth 0.316
anne 0.445
king 0.489
mary 0.500
princess 0.532

모두 ‘queen’이 쓰이는 문장에서 자주 등장하는 단어들입니다.

벡터 저장소에는 메타데이터도 함께

벡터 저장소는 임베딩 모델로 변환한 벡터를 저장하는 데이터베이스입니다. 그런데 벡터만 저장해서는 안 되고 메타데이터를 함께 저장해야 합니다. 문서의 이름이나 페이지 번호 같은 데이터입니다.

이유는 환각(hallucination) 때문입니다. 환각은 LLM이 데이터에 기반하지 않고 사실과 다른 정보를 생성하는 현상입니다. LLM 서비스를 운영한다면 어떤 문서를 참고했는지 출처를 함께 보여주는 것이 중요합니다. 벡터 저장소에 메타데이터를 같이 저장해 두면 “어떤 문서의 몇 페이지에서 가져왔다”를 답변과 함께 보여줄 수 있고, 이는 답변의 신뢰도를 보증하고 서비스 품질을 높이는 데 기여합니다.

왜 청킹이 필요한가

RAG 파이프라인에서는 벡터 저장소를 대상으로 유사도 검색(similarity search) 을 해 질문과 가장 비슷한 문서를 가져옵니다. 이때 문서 전체를 넘기면 속도가 느리고 토큰 수 초과로 답변이 생성되지 않습니다.

책의 실습 데이터인 종합부동산세법은 16만 자, 토큰으로는 약 35만 토큰입니다. GPT-5, Claude Haiku 4.5, Gemini 3 Pro처럼 컨텍스트 윈도가 커진 모델이라면 — Gemini 3 Pro는 100만 토큰을 처리하니 — 35만 토큰을 한 번에 넘겨도 답변은 나옵니다. 그런데도 그렇게 하면 안 되는 이유가 셋입니다.

문제 내용
비용 토큰 사용량에 따라 과금되는 LLM API 특성상 비용이 크게 발생한다
지연 답변 생성에 많은 시간이 걸려 사용자 만족도가 떨어진다
환각 컨텍스트를 많이 소비할수록 환각 가능성이 높아진다(관련 논문)

그래서 청킹(chunking) 으로 긴 문서를 의미 있는 작은 단위(단락, 섹션)나 고정 길이로 나눠 저장합니다. 문맥이 끊기지 않도록 앞뒤 내용을 일부 겹치게(overlap) 자르기도 합니다.

사용자가 질문하면 시스템은 의미적으로 가장 가까운 ‘최적의 청크’ 몇 개만 벡터 저장소에서 선별해 가져오고, 이 청크들이 프롬프트 안에 ‘참고 자료(context)’ 로 삽입되어 LLM에 전달됩니다. 결과적으로 LLM은 전체 문서가 아니라 질문 해결에 꼭 필요한 핵심 정보만 참고하므로 훨씬 정확하고 효율적인 답변을 만듭니다.

한국어 임베딩: OpenAI vs 업스테이지

이 장에서 가장 실무적인 대목입니다. 책은 영단어 “king”과 우리말 “왕”을 벡터화해 코사인 유사도로 비교합니다.

king_embedding_response = openai_client.embeddings.create(
    input="king",
    model="text-embedding-3-large"
)
king_vector = np.array(king_embedding_response.data[0].embedding)

코사인 유사도는 원점으로부터 두 벡터가 이루는 각도를 측정해 유사도를 구합니다. 값이 클수록 유사도가 높고, 통상 0.7 이상이면 유사하다고 봅니다.

def cosine_similarity(vec1, vec2):
    dot_product = np.dot(vec1, vec2)
    norm_vec1 = np.linalg.norm(vec1)
    norm_vec2 = np.linalg.norm(vec2)

    if norm_vec1 == 0 or norm_vec2 == 0:
        return 0.0

    return dot_product / (norm_vec1 * norm_vec2)

결과가 흥미롭습니다.

임베딩 모델 “king” ↔ “왕” 유사도 판정
OpenAI text-embedding-3-large 0.548 0.7 미만 — 크게 관련 없음
업스테이지 solar-embedding-1-large-query 0.852 유사한 단어로 판단

OpenAI 공식 문서는 text-embedding-3-small이 다양한 언어를 지원한다고 하지만, 저자의 경험상 한국어 임베딩에는 적합하지 않습니다. 저자의 팀에서 LLM 평가자를 구현하며 비교한 결과, 업스테이지 임베딩이 OpenAI 대비 retrieval 성능이 30% 정도 뛰어났다고 합니다. 그래서 우리말 데이터라면 가급적 업스테이지 임베딩을 추천합니다.

참고로 업스테이지 클라이언트는 별도 패키지 없이 OpenAI SDK의 base_url만 바꿔 씁니다. OpenAI를 제외한 다른 공급자들이 흔히 쓰는 방식입니다.

upstage_client = OpenAI(
    api_key=os.getenv("UPSTAGE_API_KEY"),
    base_url="https://api.upstage.ai/v1/solar"
)

검색 방식 세 가지

벡터 저장소에서 질문과 관련 있는 데이터를 가져올 때는 보통 키워드 검색, 벡터(의미) 검색, 하이브리드 검색을 씁니다. 각각 고유한 장단점이 있어 사용 사례에 따라 골라야 합니다.

키워드 검색

사용자가 입력한 단어가 문서에 포함되어 있는지 확인하는 가장 전통적인 방식입니다. 기초적으로는 SQL의 %LIKE% 쿼리 같은 문자열 매칭이지만, 실제 검색 시스템에서는 형태소 분석이나 인덱싱 같은 전처리를 포함합니다.

구현 방식은 정확한 문자열 매칭 / 부분 문자열 매칭 / 정규 표현식 매칭 세 가지로 나뉩니다. 작은 데이터셋에서 빠르고 직관적인 결과를 주지만, 단어의 존재 여부만 판단하므로 동의어나 변형 표현을 인식하지 못합니다.

이 한계를 보완하려고 정보 검색 분야에서 발전한 대표적인 기법이 tf-idf와 BM25입니다.

tf-idf는 특정 단어가 한 문서에 얼마나 자주 등장하는지(tf)와 전체 문서 집합에서 얼마나 드문 단어인지(idf)를 조합해 가중치를 부여합니다. 모든 문서에 흔한 단어는 중요도를 낮추고, 특정 문서에만 자주 나오는 단어는 높은 점수를 얻습니다.

BM25(best matching 25) 는 이를 발전시킨 확률적 검색 모델입니다. tf-idf는 단어 빈도가 늘수록 점수가 계속 높아지지만, BM25는 특정 단어가 과도하게 반복될 경우 점수 증가폭을 제한(saturation)하고 문서 길이에 따른 가중치를 보정해 검색 품질을 높입니다.

일래스틱서치, 아파치 루씬, 아파치 솔라 같은 검색엔진은 기본적으로 BM25로 관련성 점수를 계산합니다.

구현할 때 고려할 것들도 함께 정리되어 있습니다.

항목 고려 사항
대소문자 처리 보통 소문자로 통일하는 게 좋지만, 의학 용어나 화학 기호처럼 대소문자가 의미를 구분할 때는 원래 형태를 유지해야 한다
불용어 처리 ‘그리고’, ‘또는’ 같은 단어는 검색 노이즈가 되므로 도메인별 불용어 목록을 관리한다. 단 법률 문서나 계약서처럼 모든 단어가 중요한 경우에는 제거를 신중히 한다
형태소 분석과 어간 추출 한국어는 ‘찾다’, ‘찾아서’, ‘찾았다’처럼 활용형이 다양해 기본형으로 통일할 수 있다. 다만 과도한 정규화는 의미의 미묘한 차이를 놓칠 수 있다

RAG 파이프라인에서 키워드 검색은 정확한 키워드 매칭이 필요할 때 특히 유용합니다. 저자는 간단한 챗봇을 만들 때 사용자 질문에서 키워드를 추출하는 체인을 두고 키워드 검색을 하는데, 벡터 검색에 비해 계산 비용이 적다는 장점 때문입니다. 물론 의미적 유사성과 문맥은 파악하지 못합니다.

벡터 검색

키워드 검색이 단어의 정확한 일치를 확인하는 방식이라면, 벡터 검색은 텍스트의 의미를 수치화해 유사성을 비교하는 방식입니다. 임베딩 모델로 텍스트를 수백~수천 차원의 벡터 공간으로 변환합니다.

이렇게 바뀐 벡터는 의미적 특성을 담고 있어 키워드 매칭으로는 찾기 어려운 연관성을 잡아냅니다. ‘강아지가 아픕니다’와 ‘반려견의 건강이 좋지 않아요’는 키워드가 완전히 다르지만 벡터 공간에서는 매우 가까운 거리에 있습니다.

핵심 구성 요소는 두 가지입니다.

  • 임베딩 모델 선택: OpenAI·업스테이지·코히어(Cohere) 등의 API 모델, 허깅 페이스의 sentence-transformers, 직접 학습한 도메인 특화 모델
  • 유사도 측정 방법: 코사인 유사도, 유클리드 거리, 점곱

동작 방식은 이렇습니다. 문서 청크를 미리 벡터화해 저장소에 넣어 두고, 질문이 들어오면 같은 임베딩 모델로 벡터화한 뒤 코사인 유사도 같은 지표로 가장 유사한 청크를 검색합니다. 문맥과 의미적 유사성을 파악하는 장점이 있지만, 임베딩 모델의 연산 비용이 많이 들고, 정확한 키워드 매칭이 필요한 때에는 오히려 성능이 떨어질 수 있습니다.

하이브리드 검색

그래서 실제 RAG 파이프라인에서는 두 방식의 장점을 결합한 하이브리드 검색(hybrid search) 이 효과적입니다. 구현 방식은 두 가지입니다.

방식 동작 효과
순차적 접근 키워드 검색으로 1차 필터링한 뒤, 그 결과에만 벡터 유사도 검색을 적용 검색 공간을 줄이면서도 의미적 연관성을 고려
병렬 접근 두 검색을 동시에 수행한 뒤 각 점수에 가중치를 부여해 최종 점수를 계산 키워드 정확성과 의미 유사성을 균형 있게 반영

가중치는 도메인에 따라 조정합니다. 정확한 용어 매칭이 중요한 전문 분야에서는 키워드 검색에, 일상적인 질의응답에서는 벡터 검색에 가중치를 줍니다.

하이브리드 검색은 단순한 조합 이상의 의미가 있습니다. 검색의 재현율(recall) 을 개선하고, 적절한 가중치 조정 시 정확성(precision) 도 함께 올라갑니다. RAG 파이프라인에서 LLM이 참조할 문맥이 더 풍부하고 정확해지는 것입니다. 다만 구현 복잡성이 증가하고 컴퓨팅 리소스가 더 필요하므로, 시스템 요구사항과 리소스 제약을 함께 고려해 선택해야 합니다.

정리

  • 임베딩 모델은 동시 출현 패턴을 학습한다. 같은 맥락에서 쓰이는 단어가 벡터 공간에서 가까워진다.
  • 벡터 저장소에는 벡터와 메타데이터를 함께 저장한다. 출처를 보여줄 수 있어야 환각에 대응하고 답변의 신뢰도를 보증할 수 있다.
  • 컨텍스트 윈도가 커졌어도 문서 전체를 넘기면 안 된다. 비용·지연·환각 세 가지 때문이며, 그래서 청킹과 오버랩이 필요하다.
  • 한국어라면 임베딩 모델 선택이 곧 검색 품질이다. “king”과 “왕”의 유사도가 OpenAI는 0.548, 업스테이지는 0.852로 갈린다.
  • 키워드 검색은 싸고 정확하지만 의미를 모르고, 벡터 검색은 의미를 알지만 비싸고 정확 매칭에 약하다. 실무에서는 하이브리드로 둘을 보완한다.