RAG와 임베딩 · 글 11편
RAG와 임베딩이 글에서 다루는 것키워드 검색은 같은 말을, 의미 검색은 같은 뜻을 찾습니다. 두 방식의 차이, 임베딩 없이 검색 품질을 높이는 방법, 둘을 합치는 하이브리드 검색과 재순위를 정리합니다.
검색은 모델에게 줄 근거를 고르는 일
문서가 많아지면 전부를 모델에 넣을 수 없습니다. 그래서 질문과 관련된 조각을 먼저 골라야 합니다. 이 고르는 일이 검색입니다.
검색은 크게 둘로 나뉩니다. 키워드 검색은 같은 말을 잘 찾고, 의미 검색(시맨틱 검색)은 같은 뜻을 잘 찾습니다.
| 구분 | 키워드 검색(BM25) | 의미 검색(벡터) |
|---|---|---|
| 기준 | 단어 | 뜻 |
| 고유명사, 기관명 | 강함 | 경우에 따라 약함 |
| 조문 번호, 문서 번호 | 강함 | 약할 수 있음 |
| 표현이 다른 비슷한 문장 | 약함 | 강함 |
| 임베딩 모델 | 필요 없음 | 필요 |
| 구현 | 비교적 단순 | 더 복잡 |
| 결과 설명 | 어떤 단어로 찾았는지 보임 | 점수만 보임 |
둘 중 하나를 골라야 하는 것은 아닙니다. 함께 쓸 수 있습니다.
키워드 검색의 네 단계
키워드 검색도 수준이 여럿입니다.
| 방식 | 쉽게 말하면 | 잘하는 것 |
|---|---|---|
| 문자열 포함 검사 | 그 글자가 들어 있는지 확인 | 정확한 단어 찾기 |
| Fuse.js | 글자가 얼마나 비슷한지 비교 | 오타, 부분 일치 |
| FTS(전문 검색) | 단어가 어느 문서에 있는지 미리 색인 | 빠른 본문 검색 |
| BM25 | 검색어가 중요한 문서에 점수를 줌 | 결과 순위 |
BM25는 단어가 있는지만 보지 않습니다. 세 가지를 함께 따집니다.
- 검색어가 그 조각에 얼마나 자주 나오는가
- 그 단어가 전체 문서에서 얼마나 드문가
- 조각이 지나치게 길지는 않은가
모든 조각에 나오는 “기관”보다 몇 조각에만 나오는 “관내출장”에 높은 점수를 줍니다.
SQLite로 만들기
SQLite에는 FTS5라는 전문 검색 기능이 들어 있습니다. 파일 하나로 동작하고 서버가 필요 없습니다.
CREATE VIRTUAL TABLE documents USING fts5(title, content);
SELECT title, bm25(documents) AS score
FROM documents
WHERE documents MATCH '행정 자동화'
ORDER BY score;
별도의 벡터 데이터베이스 없이 로컬 문서 검색을 만들 수 있습니다. 문서명, 사업명, 법령명처럼 정확한 단어가 중요한 업무에서 실용적입니다.
파이썬으로 만들기
rank_bm25 라이브러리를 쓰면 몇 줄로 끝납니다.
from rank_bm25 import BM25Okapi
import re
def tok(s): # 간단한 한국어 토큰화
return re.findall(r"[가-힣]+|[a-zA-Z0-9]+", s)
corpus = [tok(c["text"]) for c in chunks]
bm = BM25Okapi(corpus)
def search(q, k=5):
scores = bm.get_scores(tok(q))
top = scores.argsort()[-k:][::-1]
return [chunks[i] for i in top]
한국어에서 생기는 문제
키워드 검색은 글을 단어로 나누는 방식(토큰화)에 민감합니다. 한국어는 조사가 붙고 띄어쓰기가 일정하지 않습니다.
| 검색어 | 문서의 표현 | 결과 |
|---|---|---|
| 인공지능 | 인공 지능 | 토큰화 방식에 따라 못 찾음 |
| 행정 자동화 | 행정업무 자동화 | 띄어쓰기 기준으로 나누면 “행정”이 안 맞음 |
| 출장비 | 출장비는, 출장비를 | 조사를 떼지 않으면 못 찾음 |
| 자동차 | 차량 | 기본 상태에서는 못 찾음 |
대응 방법입니다.
- 형태소 분석기(kiwi, okt 등)로 조사를 떼고 단어를 나눕니다.
- 글자를 2~3자씩 겹쳐 자르는 N-gram 방식으로 색인합니다. 띄어쓰기와 복합어에 덜 민감해집니다.
- 기관에서 쓰는 약어, 별칭, 업무 용어를 사전으로 만들어 둡니다.
검색이 조사나 복합어 때문에 실패하면 어떤 전처리를 넣었는지 기록합니다. 나중에 같은 문제를 다시 만났을 때 원인을 빨리 찾습니다.
임베딩 없이 뜻을 따라가는 방법
“연차”로 물었는데 문서에는 “휴가”라고 적혀 있는 문제는 임베딩 없이도 상당 부분 풀 수 있습니다. 질문을 그대로 한 번 검색하지 않고, 검색어를 넓힌 뒤 검색합니다. 이를 질의 확장이라고 합니다.
- 질문에서 핵심어를 뽑습니다.
- 동의어와 관련어로 넓힙니다.
- FTS와 BM25로 여러 후보를 찾습니다.
- 상위 조각만 모델에 넣습니다.
2번을 언어 모델에게 맡길 수 있습니다.
질문: 예산 낭비 사업을 찾아줘
검색용 구조:
{
"keywords": ["예산 낭비", "예산 과다", "불용액",
"집행률 저조", "성과 미달", "중복 사업"],
"date_from": "2024-01-01"
}
여기서 모델은 답을 만들지 않고 검색어를 만듭니다. 실제로 문서를 고르는 일은 SQL과 BM25가 합니다. 어떤 단어로 검색했는지 눈으로 확인할 수 있어서, 결과가 이상할 때 원인을 찾기 쉽습니다.
| 방법 | 개념 | 특징 |
|---|---|---|
| 질의 확장 | 질문을 관련 키워드로 넓힘 | 벡터 데이터베이스 없이 가능 |
| 동의어 사전 | 자동차=차량처럼 연결 | 사람이 통제하기 쉬움 |
| 임베딩 | 문장을 숫자 벡터로 변환 | 의미 유사도에 강함 |
하이브리드 검색
키워드 검색과 의미 검색을 함께 돌리고 결과를 합치는 방식입니다. 질문의 종류에 따라 어느 쪽이 맞는지가 다르기 때문입니다.
| 질문 | 잘 맞는 검색 |
|---|---|
| “AI-2026-00451 문서를 찾아줘” | 키워드 |
| “성과가 좋지 않았던 사업을 찾아줘” | 의미 |
| “최근 3년 복지 분야 AI 사업만” | 날짜는 SQL 조건, 나머지는 키워드와 의미 |
결과를 합치는 방법
두 검색의 점수는 범위가 다릅니다. BM25 점수와 코사인 유사도를 그대로 더하면 한쪽이 전체를 덮습니다. 세 가지 방법이 있습니다.
- 합치고 중복 제거. 키워드 상위 5개와 의미 상위 5개를 합친 뒤 같은 조각을 하나로 줄입니다. 처음에는 이것으로 충분합니다.
- 순위로 합치기. 점수 대신 순위를 씁니다. RRF(Reciprocal Rank Fusion)가 대표적입니다. 두 검색에서 모두 상위에 오른 조각이 위로 올라갑니다.
- 점수를 맞춘 뒤 가중치 주기. 두 점수를 같은 범위로 정규화하고 비율을 정해 더합니다. 조문 번호를 묻는 질문에는 키워드 쪽에, 뜻을 묻는 질문에는 의미 쪽에 가중치를 더 줍니다.
어느 방법이든 결과마다 두 검색의 점수와 최종 점수를 함께 남깁니다. 왜 그 조각이 올라왔는지 설명할 수 있어야 고칠 수 있습니다. 한쪽 점수가 전체를 압도하면 정규화와 후보 수를 먼저 확인합니다.
재순위(리랭킹)
재순위는 검색이 가져온 후보를 다시 정렬하는 단계입니다. 1차 검색으로 후보를 넉넉히(예: 20개) 뽑고, 질문과 각 후보를 더 꼼꼼히 비교해 상위 몇 개만 남깁니다.
질문 → 키워드 검색 + 의미 검색 → 후보 합치기 → 재순위 → 상위 3~5개 → 모델
재순위 전용 모델(리랭커)을 쓰는 방법이 있고, 대화 모델에게 “이 조각이 질문에 답하는 데 쓸모 있는가”를 판단하게 하는 방법도 있습니다. 둘 다 시간이 더 걸립니다. 강의의 도구 선택표에서도 재순위는 마지막 단계에 놓입니다. 검색 결과에 정답 조각이 들어 있는데 순위가 낮아서 잘리는 문제가 확인될 때 넣습니다.
권장 순서
문자열 검색 → FTS → BM25 → 질의 확장 → 의미 검색 → 하이브리드 → 재순위 순서로 올라갑니다. 각 단계에서 실제로 놓치는 질문이 생길 때 다음 기술을 더합니다.
| 내가 원하는 것 | 먼저 써 볼 것 |
|---|---|
| 정확한 단어, 사업명, 기관명 찾기 | FTS, BM25 |
| 오타나 비슷한 문자열 찾기 | Fuse.js, N-gram |
| 뜻은 같은데 표현이 다른 문서 찾기 | 질의 확장, 임베딩 |
| 정확한 단어와 뜻 둘 다 중요 | 하이브리드 |
| 벡터 데이터베이스 없이 시작 | SQLite FTS5와 BM25, 질의 확장 |
연습으로는 문서 20~100건을 SQLite에 넣고 FTS5와 BM25 검색을 먼저 만듭니다. 그다음 동의어를 넓혀 결과가 얼마나 달라지는지 봅니다. 마지막으로 임베딩 검색을 더해 같은 질문으로 세 결과를 나란히 비교합니다.