벡터 데이터베이스는 어떻게 동작하는가: 임베딩 저장, 조회, 매칭의 원리
벡터 데이터베이스가 임베딩을 어떻게 저장하고, 어떤 방식으로 근사 최근접 탐색을 수행하며, 원본 문서와 어떻게 매칭되는지 공학적으로 정리합니다.
벡터 데이터베이스는 어떻게 동작하는가: 임베딩 저장, 조회, 매칭의 원리
벡터 데이터베이스를 이해하려면 "의미 검색이 된다"는 말보다 한 단계 더 들어가야 한다.
실제로 궁금한 건 이거다.
- 임베딩은 어디에 저장되는가
- 검색할 때는 무엇을 기준으로 찾는가
- 원본 문서와 벡터는 어떻게 연결되는가
- 왜 정확히가 아니라 "가까운 것"을 찾는가
이 글은 그 원론적 동작을 공학적으로 정리한다.
한 줄 결론
- 벡터 DB는 원본 데이터 + 벡터 + 메타데이터를 함께 저장한다.
- 검색은 보통 근사 최근접 탐색(ANN) 으로 수행한다.
- 결과는 벡터 자체가 아니라, 벡터에 연결된 원본 문서 ID를 통해 다시 가져온다.
1) 먼저 저장 구조부터 보자
벡터 DB에 문서를 넣는다고 해서 원문 전체를 벡터로 바꿔 저장하는 건 아니다. 보통은 다음과 같은 구조로 저장한다.
- id: 문서나 청크를 식별하는 키
- vector: 임베딩 값
- payload / metadata: 출처, 언어, 타입, 작성일, 권한 같은 부가 정보
- original text or reference: 원문 전체 또는 원문을 찾기 위한 참조
예를 들면 이런 느낌이다.
{
"id": "doc_123_chunk_4",
"vector": [0.12, -0.44, 0.88, ...],
"metadata": {
"docId": "doc_123",
"source": "handbook",
"lang": "ko",
"chunkIndex": 4
},
"text": "이 조각의 원문 내용..."
}
핵심은 벡터만 단독으로 있는 게 아니라, 반드시 식별자와 메타데이터가 같이 붙는다는 점이다.
2) 임베딩은 어떻게 저장되나
임베딩은 텍스트나 이미지를 모델이 숫자 배열로 바꾼 결과다.
이 숫자 배열은 보통 수백 차원의 실수 벡터다.
이 벡터는 문서 내용의 의미를 압축한 표현이라서, 비슷한 의미의 문서들은 벡터 공간에서도 가까운 위치에 놓이게 된다.
저장할 때 중요한 것
- 어떤 임베딩 모델로 만들었는지
- 차원 수가 얼마인지
- 어떤 데이터 단위로 쪼갰는지
- 메타데이터를 어떻게 붙였는지
이게 달라지면 같은 시스템이어도 검색 품질이 확 달라진다.
3) 검색할 때는 어떻게 찾나
검색 시에는 사용자의 질문도 똑같이 임베딩으로 바꾼다.
예:
- 질문 "RAG에서 벡터 DB는 왜 필요해?"를 입력한다.
- 이 문장을 같은 임베딩 모델로 벡터화한다.
- DB 안에 저장된 벡터들과 유사도를 계산한다.
- 가장 가까운 벡터들을 찾는다.
- 그 벡터에 연결된 원본 텍스트나 문서 ID를 반환한다.
즉, 검색은 문자열 매칭이 아니라 벡터 공간에서의 거리 계산이다.
4) 어떤 원리로 "가까움"을 계산하나
벡터 DB는 벡터 사이의 거리를 계산해 유사도를 판단한다.
대표적으로는 다음 기준을 많이 쓴다.
- cosine similarity: 방향이 얼마나 비슷한가
- dot product: 내적 기반 점수
- Euclidean distance: 좌표상 얼마나 가까운가
실무에서는 보통 cosine similarity나 내적을 많이 본다.
중요한 건 수식 자체보다 벡터가 가깝다 = 의미가 비슷하다는 모델의 학습 특성을 활용한다는 점이다.
5) 왜 정확한 검색이 아니라 ANN인가
벡터 공간은 차원이 높고 데이터도 많아서, 모든 벡터를 전부 비교하면 너무 느리다.
그래서 대부분의 벡터 DB는 근사 최근접 탐색(Approximate Nearest Neighbor, ANN) 을 쓴다.
ANN은 말 그대로:
- 완벽히 전수 비교하지 않고
- 매우 비슷한 후보를 빠르게 찾는 방식
대표적인 인덱싱 아이디어는 이런 것들이다.
- 그래프 기반 탐색
- 클러스터 분할
- 양자화(quantization)
- 계층적 탐색
정확도를 조금 양보하는 대신 속도를 얻는 구조다.
6) 원본 값과 벡터는 어떻게 매칭되나
이 부분이 핵심이다.
벡터 DB는 보통 벡터 자체를 답으로 주지 않는다. 대신 벡터에 연결된 id를 반환한다.
예를 들면:
- 벡터 검색 결과:
doc_123_chunk_4 - 애플리케이션은 이 id로 원본 문서나 청크를 다시 조회
- 최종적으로 사용자에게는 원문 텍스트나 요약을 보여줌
즉, 매칭 방식은 다음과 같다.
- 저장 단계: 원문을 청크로 나누고 각 청크에 id 부여
- 임베딩 단계: 청크마다 벡터 생성
- 색인 단계: 벡터와 id를 묶어서 저장
- 검색 단계: 벡터 유사도 계산 후 id 반환
- 재조회 단계: id로 원문을 다시 가져옴
이 구조 덕분에 벡터는 검색용, 원문은 표현용으로 분리된다.
7) 왜 원문과 벡터를 따로 관리하나
벡터는 검색에 좋지만 원본 텍스트처럼 읽기 쉬운 형태는 아니다.
반대로 원문은 사람이 이해하기 좋지만 유사도 계산에는 적합하지 않다.
그래서 둘을 분리한다.
- 벡터: 찾기 위한 표현
- 원문: 보여주기 위한 표현
이 분리가 있어야 RAG도 깔끔하게 돌아간다.
8) 실무에서 주의할 점
청크 크기
너무 크면 검색이 둔해지고, 너무 작으면 맥락이 깨진다.
메타데이터 필터
언어, 권한, 문서 종류, 최신성 필터가 없으면 엉뚱한 결과가 섞인다.
재랭킹
처음 찾은 결과가 최종 정답은 아니다. 다시 정렬해야 하는 경우가 많다.
최신성
임베딩 모델이나 인덱스가 바뀌면 재색인이 필요하다.
마무리
벡터 DB는 마법 상자가 아니다.
하지만 원리를 보면 꽤 단순하다.
- 문서를 쪼갠다.
- 임베딩을 만든다.
- 벡터와 id를 저장한다.
- 질의도 임베딩한다.
- ANN으로 가까운 벡터를 찾는다.
- id로 원문을 다시 가져온다.
이 흐름만 이해하면 벡터 DB를 공학적으로 다룰 수 있다.
내 추천은 이거다.
- 검색 시스템을 만들 때는 벡터와 원문을 분리해서 설계하고
- 메타데이터 필터와 재랭킹을 처음부터 넣고
- ANN 인덱스의 속도/정확도 트레이드오프를 의식해라.