
기존에 리뷰 검색은 Elasicsearch의 Nori 형태소 분석기를 사용하고 있었다.
Nori는 텍스트를 형태소로 쪼개서 매칭한다.
위 리뷰가 저장되어있다고 가정해보자.
"복지 좋아요?" 라고 질문하면 DB에 일치하는 형태소가 존재하지 않으므로 매칭이 안 될 것이다.
의미는 비슷한데 단어가 다르면 못 찾는다. 이걸 해결하는 게 임베딩이다. 텍스트를 768개의 숫자로 이루어진 벡터로 변환하고, 의미가 비슷한 문장일수록 벡터의 방향이 비슷하고 가까울수록 코사인 유사도가 1에 가깝다.
질문과 저장된 리뷰 벡터들을 각각 비교하면 이런 결과가 나온다.
임베딩 과정이 없었다면 '복지'라는 형태소가 없는 리뷰들은 결과에서 제외됐을 것이다.
RAG는 Retrieval-Augmented Generation의 약자이고 직역하면 '검색으로 보강한 생성'이다. LLM에게 그냥 "이 회사 복지 어때요?"라고 물으면 학습 데이터 기반의 일반론만 답한다. 실제 리뷰 내용을 알 리 없다.
그래서 질문과 관련된 리뷰를 먼저 검색하고, 그 내용을 LLM에게 함께 전달한다.
세 단계로 정리하면: 검색(Retrieval) → 주입(Prompt) → 생성(Generation)
별도의 VectorDB를 도입하지 않았다. Elasticsearch 8.x 부터 dense_vector 타입을 지원해서 기존 ES를 VectorDB로 그대로 재활용할 수 있다.
인덱스 구조는 일반 ES 인덱스와 거의 같고 embedding 필드 하나만 다르다고한다.
dims(768)은 임베딩 모델의 출력 크기이다(배열의 길이). 모델을 바꾸면 이 값도 바꿔야 한다. 나머지 필드는 일반 ES 매핑과 동일하다.
인덱싱 코드도 단순하다. 임베딩 모델에 텍스트를 넣으면 float[] 배열이 나온다. 이걸 ES 문서에 저장하는 것뿐이다.
title과 content를 합치는 이유는 제목 맥락까지 벡터에 반영하기 위함이다.
전체 벡터를 모두 비교하면 정확하지만 느리다. 리뷰가 100만 건이면 100만 번 코사인 유사도를 계산해야 한다.
ES는 HNSW 알고리즘으로 이를 해결한다. 벡터들을 미리 그래프 구조로 연결해두고, 검색 시 그래프를 탐색하며 가까운 노드로 이동하는 방식이다. 전체를 보지 않고도 유사한 벡터 근방에 빠르게 도달한다.
numCandidates=50은 이 그래프 탐색에서 살펴볼 후보 개수다. 후보 50개를 추린 뒤 그 안에서 코사인 유사도가 가장 높은 5개(k=5)를 최종 반환한다. 후보를 넉넉하게 잡는 이유는 HNSW가 근사 알고리즘이라 전수 비교 대비 정확히 상위 5개를 보장하지 않기 때문이다. 후보를 충분히 확보해야 실제 상위 5개가 그 안에 포함될 가능성이 높아진다.
numCandidates 가 클수록 정확도는 높아지고 속도는 느려지는 트레이드오프다.
filter는 companyId 기준으로 해당 회사 리뷰만 검색 대상으로 좁힌다. 다른 회사 리뷰가 유사도가 높아도 결과에서 제외된다.
앞서 설명한 임베딩, 인덱싱, kNN 검색을 모두 포함한 ask() 메서드이다.
이번 포스팅의 중점은 RAG의 개념을 학습하고 VectorDB를 간단히 사용해보는것에 있다. 리뷰 검색 흐름은 올바르게 동작하지만, 검색 정확도가 충분한지 판단하기는 어렵다.
numCandidates=50, k=5로 설정한 것도 레퍼런스를 따른 것이지 직접 측정해서 튜닝한 값이 아니다. 한국어 리뷰에 영어 중심 임베딩 모델인 nomic-embed-text 을 쓰는 것도 마찬가지다. 모델 선택과 정확도 튜닝은 AI 개발자의 영역이라고 생각한다.
백엔드 입장에서 보완이 필요하다고 느끼는 부분은 따로 있다.
첫 번째 보완이 필요한 점은 다음 포스팅에서 바로 다뤄 볼 예정이다.