키워드 검색은 왜 같은 뜻을 놓치는가
단어를 비교하면 표현이 다를 때 실패하고, 단어만 겹치면 엉뚱한 결과가 나온다.
생성 모델은 모르는 내용을 지어내는 환각이 있고, 매번 관련 문서를 넣으면 컨텍스트 윈도우를 넘긴다. 그래서 질문과 관련된 부분만 검색해 넣는 RAG가 필요하다. 기존 SQL LIKE나 색인 검색은 단어 자체를 비교하므로, 문서가 '결제 취소 절차'인데 사용자가 '환불 규정'으로 물으면 걸리지 않는다.
'랩탑 구매 가이드'와 '노트북'처럼 같은 대상도 단어가 다르면 놓친다. 반대로 '자바 강의 자료'와 '자바 섬 여행 후기'처럼 단어만 겹치고 의미가 다른 결과가 올라온다. 한국어는 같은 의미를 여러 표현으로 말할 수 있어 이 한계가 더 크다. 그래서 문자열이 아니라 의미를 비교해야 한다.
- 어휘 불일치
- 같은 의미를 다른 단어로 쓰거나, 같은 단어를 다른 의미로 쓸 때 검색이 실패하는 문제다.
- 키워드 검색
- 문자열 일치를 기준으로 찾는 방식이다. 단어가 다르면 의미가 같아도 결과에 없다.
- 의미 검색
- 단어가 아니라 뜻의 가까움을 기준으로 찾는 방식이다. 임베딩이 이를 가능하게 한다.
키워드 검색
- 단어가 정확히 같아야 걸린다
- '환불'로 '결제 취소'를 못 찾는다
- 단어만 겹치면 오답이 섞인다
의미 검색
- 표현이 달라도 뜻이 가까우면 걸린다
- 벡터 거리로 관련도를 잰다
- 동의어·구어체에 강하다
한국어처럼 표현이 다양한 언어일수록 차이가 커진다.
임베딩은 텍스트를 어떤 숫자로 바꾸는가
의미가 비슷하면 가까운 위치, 무관하면 먼 위치에 놓이는 실수 벡터다.
임베딩은 텍스트를 의미를 보존한 실수 벡터로 변환하는 것이다. '환불 규정이 궁금하다'를 넣으면 여러 실수값으로 된 배열이 나온다. 각 원소는 -1과 1 사이의 실수다. 모델마다 출력 차원 수는 다르지만, 한 모델은 입력이 단어든 문단이든 항상 같은 크기를 출력한다. 그래야 서로 비교할 수 있다. 생성 모델은 다음 토큰을 예측해 문장을 만들지만, 임베딩 모델은 입력 전체를 벡터 하나로 바꾼다. 생성을 하지 않으므로 출력 토큰 과금이 없다.
문서는 길고 서술적이며 질문은 짧고 구어체다. 그래서 저장할 때와 검색할 때 임베딩 타입을 다르게 둔다.
- 임베딩 (Embedding)
- 텍스트를 의미를 보존한 실수 벡터로 바꾸는 것이다. 의미가 비슷한 쌍은 가까운 벡터가 된다.
- 차원 (Dimension)
- 벡터를 이루는 실수의 개수다. 한 모델은 입력 길이와 무관하게 항상 같은 차원을 출력한다.
- passage
- 문서를 등록할 때 쓰는 임베딩 타입이다. 길고 서술적인 문서에 맞춘다.
- query
- 검색할 때 질문을 임베딩하는 타입이다. 짧고 구어체인 질문에 맞춘다.
| 구분 | passage | query |
|---|---|---|
| 대상 | 문서 | 사용자 질문 |
| 시점 | 문서 등록 시 | 검색 시 |
| 목적 | DB에 저장할 벡터 생성 | 저장된 벡터와 비교할 벡터 생성 |
유사도는 크게, 거리는 작게
코사인 유사도는 -1에서 1이고, pgvector의 <=>는 1에서 유사도를 뺀 거리다.
코사인 유사도는 두 벡터가 얼마나 같은 방향을 보는지 잰다. 벡터의 크기보다 방향을 본다. 1에 가까우면 유사하고 0 근처면 무관하다. pgvector의 <=>는 코사인 거리를 반환하는데, 값이 1 - 코사인 유사도라서 거리는 작을수록 유사하다.
Top-K 검색은 요청한 K개를 채우려 한다. 무관한 질문에도 그럴듯한 결과가 나온다. threshold 또는 min_similarity로 최소 유사도를 정해야 한다.
- 코사인 유사도
- -1에서 1 사이 값이다. 1에 가까울수록 의미가 유사하고 0 근처면 무관하다.
- 코사인 거리
<=> - pgvector의 연산자로 1 - 코사인 유사도를 반환한다. 작을수록 유사하다.
- Top-K
- 유사도 순으로 K개를 반환하는 방식이다. 관련도와 무관하게 개수를 채우려 한다.
- threshold (min_similarity)
- 결과로 쓸 최소 유사도다. 이보다 낮은 문서를 걸러 무관한 답을 막는다.
| 구분 | 값이 클 때 | 정렬 |
|---|---|---|
| 코사인 유사도 | 더 유사 | 내림차순 |
코사인 거리 (<=>) | 덜 유사 | 오름차순 |
벡터는 어디에 어떤 조건으로 저장하는가
차원과 모델이 같아야 비교된다. 모델을 바꾸면 기존 벡터를 전부 다시 만들어야 한다.
매번 임베딩 API를 호출할 수는 없다. 약관처럼 잘 바뀌지 않는 문서는 한 번 임베딩해 DB에 저장하고 이후에는 조회한다. pgvector는 PostgreSQL에 벡터 타입과 거리 함수·연산자, 벡터 인덱스를 추가하는 확장이다. 테이블에 vector(n) 컬럼을 만들고 n차원 벡터를 저장한다.
차원이 다르면 벡터 위치 자체를 비교할 수 없다. 같은 차원이어도 다른 모델의 벡터 공간은 호환되지 않는다. 모델을 교체하면 기존 벡터를 전부 다시 만들어야 하는데 이것이 재인덱싱이다. 또한 벡터만 저장하면 원문을 찾기 어려우므로 원문 텍스트도 함께 저장한다.
- pgvector
- PostgreSQL에 벡터 타입, 거리 함수·연산자, 벡터 인덱스를 추가하는 확장 플러그인이다.
- vector(n)
- n차원 임베딩 벡터를 담는 컬럼 타입이다. 모델의 출력 차원과 n이 맞아야 한다.
- 재인덱싱 (Re-indexing)
- 임베딩 모델을 바꿀 때 기존 문서 벡터를 전부 다시 만드는 작업이다.
- 모델 선택
- 한국어 문서에는 한국어 성능이 좋은 모델을 써야 한다. 아니면 벡터가 엉뚱한 위치에 생긴다.
검색은 두 단계로 돌아간다
문서를 passage로 저장하고, 질문을 query로 검색한다.
Supabase JS 쿼리 빌더에서는 <=> 연산자를 직접 쓰기 어렵다. 그래서 유사도 검색 로직을 DB 함수로 만들고 클라이언트가 RPC로 호출한다.
함수 이름은 match_documents이고 match_count, min_similarity 등을 인자로 받는다. RLS를 걸면 로그인한 사용자만 검색할 수 있다. 이번 주에 한 것은 어떤 문서를 가져올지 정하는 검색 단계이며, 이 결과를 생성 모델의 컨텍스트에 넣는 보강·생성 단계가 뒤에 이어진다.
- RPC
- DB에 정의한 함수를 클라이언트에서 호출하는 방식이다. pgvector 연산자를 함수 안에 감싼다.
- match_documents
- 유사도 검색을 감싼 Supabase RPC 함수다. match_count와 min_similarity를 인자로 받는다.
- RLS (Row Level Security)
- 행 단위 접근을 제한하는 PostgreSQL 기능이다. 로그인한 사용자만 검색하게 유지한다.
1~3단계는 문서 등록, 4~5단계가 검색이다. 실제 검색은 RPC(match_documents)로 호출한다.
시험 직전 체크
EXAM POINTS- 키워드 검색은 어휘 불일치로 같은 의미를 놓치고, 단어만 겹치는 오답도 낸다.
- 임베딩은 입력 길이·내용과 무관하게 항상 같은 차원의 벡터를 출력한다.
- 문서는 passage, 질문은 query로 비대칭 임베딩한다.
- 코사인 유사도는 -1~1이고, pgvector
<=>는 1 - 유사도라 작을수록 유사하다. - Top-K는 무관한 결과도 K개 채우므로 threshold(min_similarity)가 필요하다.
- 차원이나 모델이 다르면 비교할 수 없고, 모델 교체 시 재인덱싱해야 한다.
헷갈리는 것만
COMMON TRAPS반대다. 거리는 1 - 유사도이므로 유사도가 높을수록 거리가 작다.
안 된다. 같은 차원이어도 모델마다 벡터 공간이 달라 호환되지 않는다.
아니다. 무관한 문서까지 섞인다. 임계값으로 걸러야 한다.
아니다. 원문을 함께 저장해야 결과를 읽고 컨텍스트로 쓸 수 있다.
KEYWORDS
- 임베딩
- 벡터 검색
- 어휘 불일치
- 코사인 유사도
- 코사인 거리
- pgvector
- Top-K
- 임계값
- 재인덱싱
- passage/query
연결해서 기억하기
- 3주차의 컨텍스트 윈도우 문제가 여기서는 필요한 문서만 골라 넣는 검색으로 이어진다.
- 1주차의 환각·토큰 비용 문제가 RAG의 출발점이다.
- 이번 주는 검색까지이고, 이 결과를 프롬프트에 넣는 보강과 생성이 다음 단계다.