← 블로그 목록
임베딩벡터 검색pgvectorRAG검색

임베딩으로 검색 만들기: 키워드 검색과 무엇이 다른가

의미를 좌표로 바꾸는 임베딩 검색의 원리와, 고유명사·숫자·부정문에서 실패하는 지점, 청킹과 하이브리드 검색까지 정리했습니다.

Vibeollio 팀-

검색이 안 되는 게 아니라 다르게 안 되는 것

검색 기능을 붙였는데 "환불 어떻게 해요"로 찾으면 "반품 정책" 문서가 안 나옵니다. 사용자는 검색이 고장 났다고 느끼지만, 키워드 검색 입장에서는 정상 동작입니다. 두 문장에 겹치는 단어가 없으니까요.

임베딩 검색은 이 지점을 다르게 접근합니다. 단어가 아니라 의미를 좌표로 바꿔서 가까운 것을 찾습니다.

임베딩이 하는 일

임베딩 모델은 문장을 숫자 배열로 바꿉니다. 수백에서 수천 개의 숫자로 이루어진 벡터입니다.

"환불 어떻게 해요"  → [0.021, -0.115, 0.334, ...]
"반품 정책 안내"    → [0.019, -0.108, 0.341, ...]
"서버 재시작 방법"  → [-0.442, 0.203, -0.077, ...]

핵심은 의미가 비슷한 문장이 가까운 좌표를 갖도록 학습됐다는 점입니다. 그래서 겹치는 단어가 하나도 없어도 앞의 두 문장은 서로 가깝고, 세 번째는 멀리 떨어집니다.

가까운 정도는 보통 코사인 유사도로 잽니다. 두 벡터가 이루는 각도를 보는 방식이라, 문장 길이의 영향을 덜 받습니다.

검색은 이렇게 동작합니다. 문서를 미리 전부 벡터로 만들어두고, 질문이 들어오면 질문도 벡터로 바꿔서, 가장 가까운 문서를 꺼냅니다.

키워드 검색과 각각 못하는 것

둘은 우열 관계가 아니라 실패하는 지점이 다릅니다.

키워드 검색이 놓치는 것: 같은 뜻 다른 단어(환불/반품), 질문형 표현, 오타.

임베딩 검색이 놓치는 것이 오히려 실무에서 더 아픕니다.

  • 정확한 고유명사: 제품 코드 A-2291을 찾는데 A-2290 문서가 나옵니다. 벡터 공간에서 둘은 거의 같은 위치입니다.
  • 숫자와 날짜: "2024년 매출"과 "2025년 매출"을 잘 구분하지 못합니다.
  • 부정문: "환불 가능"과 "환불 불가"가 가깝게 나옵니다. 의미는 정반대인데 문장 구조가 비슷하기 때문입니다.
  • 완전 일치 요구: 사용자가 정확한 제목을 입력했는데 그 문서가 1위로 안 나오는 경우가 있습니다.

그래서 실무에서는 둘을 같이 씁니다. 키워드 검색과 벡터 검색을 각각 돌려 결과를 합치는 방식입니다. 합칠 때는 두 점수를 그대로 더할 수 없으니(척도가 다릅니다) 순위 기반으로 섞는 방법이 흔히 쓰입니다.

문서를 그대로 넣으면 안 되는 이유

긴 문서 하나를 통째로 벡터 하나로 만들면 검색 품질이 떨어집니다. 여러 주제가 한 벡터에 평균되어 뭉개지기 때문입니다.

그래서 문서를 조각(청크)으로 나눠 각각 임베딩합니다. 나누는 방식이 검색 품질을 크게 좌우합니다.

  • 너무 잘게 나누면: 조각만 봐서는 무슨 얘기인지 모릅니다. "이 값을 3으로 설정하세요" 같은 조각이 나옵니다.
  • 너무 크게 나누면: 관련 없는 내용이 섞여 정확도가 떨어집니다.
  • 문맥 경계를 무시하면: 문장이나 표가 중간에서 잘립니다.

실무적인 출발점은 제목·문단 같은 구조를 따라 나누고, 각 조각 앞에 문서 제목과 상위 섹션명을 붙여주는 것입니다. 조각 하나만 읽어도 무슨 맥락인지 알 수 있게 만드는 게 목적입니다.

저장과 조회

벡터는 전용 저장소에 넣을 수도 있고, 이미 쓰는 데이터베이스의 확장 기능을 쓸 수도 있습니다. PostgreSQL이라면 pgvector 확장으로 컬럼 하나를 추가하는 정도입니다.

CREATE EXTENSION IF NOT EXISTS vector;

ALTER TABLE chunks ADD COLUMN embedding vector(1536);  -- 차원은 모델에 맞춘다

-- 코사인 거리 기준 가장 가까운 5개
SELECT id, body FROM chunks ORDER BY embedding <=> $1 LIMIT 5;

데이터가 많아지면 인덱스가 필요합니다. 다만 벡터 인덱스는 일반 인덱스와 성격이 다릅니다. 정확한 답을 보장하지 않고 근사치를 빠르게 찾는 방식이라, 속도와 정확도 사이에 설정값이 있습니다. 데이터가 적을 때는 인덱스 없이 전부 비교하는 편이 더 정확하고 충분히 빠릅니다.

주의할 점 하나. 질문과 문서는 같은 모델로 임베딩해야 합니다. 모델을 바꾸면 벡터 공간 자체가 달라지므로, 저장된 임베딩을 전부 다시 만들어야 합니다. 모델 버전을 함께 기록해두세요.

비용과 지연

문서 임베딩은 한 번 만들어두면 재사용되지만, 질문은 매번 임베딩해야 합니다. 검색 한 번에 임베딩 호출 한 번이 붙는 구조입니다.

임베딩 모델은 생성 모델보다 훨씬 저렴하고 빠르지만, 검색 트래픽이 많다면 자주 들어오는 질문의 벡터를 캐싱할 가치가 있습니다.

얼마나 비슷해야 "찾은 것"인가요?

유사도에 고정된 합격선은 없습니다. 모델과 데이터에 따라 분포가 달라서, 다른 프로젝트의 임계값을 가져다 쓰면 맞지 않습니다. 실제 질문 몇십 개를 넣어보고 점수 분포를 본 뒤 정하는 수밖에 없습니다. 그리고 임계값을 두는 것 자체가 중요합니다. 없으면 관련 없는 문서라도 무조건 가장 가까운 것을 내놓습니다.

한국어도 잘 되나요?

다국어를 지원하는 임베딩 모델을 쓰면 한국어도 동작합니다. 다만 모델마다 한국어 처리 품질 차이가 있으므로, 실제 데이터로 몇 가지를 비교해보고 고르는 편이 좋습니다. 영어 전용 모델에 한국어를 넣으면 결과가 눈에 띄게 나빠집니다.

검색 품질이 좋아졌는지 어떻게 확인하나요?

질문과 "이 문서가 나와야 한다"는 정답 쌍을 몇십 개 만들어두세요. 그리고 상위 5개 안에 정답이 들어오는 비율을 재면 됩니다. 이 숫자가 있어야 청킹 방식이나 모델을 바꿨을 때 좋아졌는지 나빠졌는지 알 수 있습니다. 감으로 판단하면 바꿀 때마다 원점으로 돌아갑니다.

붙이기 전에 재보기

임베딩 검색은 붙이기는 쉽고 잘 만들기는 어렵습니다. 정답 쌍 스무 개만 만들어도 지금 방식이 쓸 만한지 바로 드러납니다.

검색이 들어간 프로젝트를 만들었다면 Vibeollio에 프로젝트 등록해서 공개해보세요. 실제 사용자가 넣는 질문은 개발 중에 상상한 것과 많이 다릅니다.