임베딩 차원은 왜 모델 옵션이 아니라 데이터베이스 스키마일까
임베딩 모델을 바꿀 때, 우리는 정말 API 호출만 바꾸고 있는 걸까?
임베딩 차원은 왜 모델 옵션이 아니라 데이터베이스 스키마일까
- 카테고리: ai_dev
- 예상 읽기 시간: 10분
- 오늘의 질문: 임베딩 모델을 바꿀 때, 우리는 정말 API 호출만 바꾸고 있는 걸까?
- 핵심 출처:
- Vector embeddings | OpenAI API Docs - 게시일 미표기, 확인일 2026-07-15
- Embeddings | Gemini API - 게시일 미표기, 확인일 2026-07-15
- Vector columns | Supabase Docs - 게시일 미표기, 확인일 2026-07-15
- pgvector README - 게시일 미표기, 확인일 2026-07-15
- Introduction to Matryoshka Embedding Models | Hugging Face - 2024-02-23, 확인일 2026-07-15
1. 왜 지금 봐야 하나
RAG를 붙인 팀이 가장 쉽게 하는 말이 있다. “나중에 더 좋은 임베딩 모델로 바꾸면 검색 품질이 좋아지겠지.” 틀린 말은 아니다. 하지만 운영 중인 서비스에서 임베딩 모델 교체는 단순한 API 옵션 변경이 아니다. 벡터 길이, 거리 함수, 인덱스, 저장 비용, 재색인 작업, 검색 평가셋이 함께 움직이는 데이터베이스 스키마 변경에 가깝다.
이번에 확인한 공식 문서들은 같은 방향을 가리킨다. OpenAI 임베딩 문서는 text-embedding-3-small의 기본 벡터 길이가 1536, text-embedding-3-large의 기본 벡터 길이가 3072이며 dimensions 파라미터로 크기를 줄일 수 있다고 설명한다. Google Gemini 임베딩 문서는 gemini-embedding-001과 gemini-embedding-2가 Matryoshka Representation Learning(MRL)을 사용하며 기본 3072차원 출력에서 768, 1536, 3072 같은 차원을 권장한다고 설명한다. Supabase vector columns 문서는 vector(384)처럼 컬럼의 차원을 명시하고, 이 숫자를 임베딩 모델이 만드는 차원과 맞추라고 안내한다. pgvector는 HNSW 같은 approximate index가 속도와 recall을 맞바꾸며, 거리 함수별 operator class를 따로 둔다.
즉 “새 모델이 더 좋다”는 판단만으로는 부족하다. 저장소가 몇 차원 벡터를 받는지, 기존 인덱스를 재사용할 수 있는지, query와 document가 같은 방식으로 임베딩되는지, 차원 축소가 검색 recall을 얼마나 떨어뜨리는지 확인해야 한다. 오늘 글은 임베딩을 모델 기능이 아니라 운영 스키마로 보는 기준을 정리한다.
2. 핵심 개념
임베딩은 텍스트를 숫자 배열로 바꾸는 기능이지만, 제품 안에서는 네 가지 계약으로 굳어진다.
첫째, 차원 계약이다. 벡터 DB나 Postgres pgvector 컬럼은 “대략 벡터”를 저장하지 않는다. vector(384), vector(768), vector(1536)처럼 길이가 맞아야 한다. OpenAI나 Gemini처럼 같은 모델군에서도 출력 차원을 줄일 수 있는 옵션이 생기면, 개발자는 “작게 저장해도 되는가”를 결정해야 한다. 차원이 작아지면 저장 공간, 메모리, 네트워크 전송, 인덱스 크기는 줄어들 수 있다. 대신 특정 도메인에서 구분력이 떨어질 수 있다.
둘째, 거리 함수 계약이다. OpenAI 문서는 OpenAI 임베딩이 길이 1로 정규화되어 있어 cosine similarity를 dot product로 더 빠르게 계산할 수 있고, cosine similarity와 Euclidean distance가 같은 ranking을 만들 수 있다고 설명한다. 하지만 모든 모델과 전처리가 항상 그런 것은 아니다. pgvector는 L2, inner product, cosine, L1 등 여러 거리 연산자를 지원하고, HNSW 인덱스도 거리 함수별 operator class를 따로 만든다. 모델을 바꿨는데 정규화 가정은 그대로 두면 점수 분포와 threshold가 조용히 달라진다.
셋째, 태스크 계약이다. Gemini 문서는 gemini-embedding-001에서 task_type을 지정할 수 있고, gemini-embedding-2에서는 task를 프롬프트 instruction에 포함하라고 설명한다. 검색에서는 query와 document가 같은 “의미 공간”에 들어가야 한다. 문서만 retrieval_document 방식으로 만들고 query는 일반 similarity 방식으로 만들면, 벡터 길이가 맞아도 랭킹 품질은 흔들릴 수 있다.
넷째, 재색인 계약이다. 임베딩 모델, 차원, 거리 함수, chunking 규칙 중 하나가 바뀌면 기존 벡터와 새 벡터를 한 인덱스에 섞어도 되는지 판단해야 한다. 대부분의 경우 섞으면 안 된다. 같은 문장이라도 모델이 다르면 좌표계가 다르고, 같은 모델이라도 차원이 다르면 저장 스키마가 다르며, chunking이 바뀌면 document id와 citation 경계가 바뀐다.
3. 최신 이슈와 연결
최근 임베딩 문서에서 눈에 띄는 변화는 “큰 벡터 하나를 무조건 쓰자”가 아니라 차원을 선택할 수 있는 모델이 일반화되고 있다는 점이다. OpenAI는 v3 임베딩에서 dimensions 파라미터를 제공하고, Gemini는 MRL 기반으로 기본 3072차원에서 더 작은 출력 차원을 선택할 수 있다고 설명한다. Hugging Face의 Matryoshka 글도 고차원 임베딩의 앞부분 prefix가 더 작은 유용한 표현이 되도록 학습하는 아이디어를 설명한다.
이 변화는 좋은 소식이다. 작은 팀은 3072차원 전체를 저장하지 않고도 768차원으로 비용과 지연을 줄이는 실험을 해볼 수 있다. 하지만 동시에 위험도 생긴다. 차원이 모델 옵션이 되면, 같은 코드베이스 안에 768차원 문서, 1536차원 문서, 3072차원 문서가 섞일 수 있다. 데이터베이스는 이를 타입이나 컬럼으로 막아주지 않으면 조용히 운영 사고가 된다.
또 하나의 연결점은 approximate search다. pgvector는 기본 exact search가 완전한 recall을 제공하지만, HNSW나 IVFFlat 같은 approximate index는 속도를 얻는 대신 일부 recall을 포기할 수 있다고 설명한다. 차원을 줄이고 approximate index를 쓰면 비용과 지연은 좋아질 수 있다. 그러나 두 최적화를 동시에 넣으면 어떤 변화가 품질 저하를 만들었는지 분리하기 어렵다. 임베딩 운영에서는 “모델 교체”, “차원 축소”, “인덱스 변경”, “chunking 변경”을 한 번에 배포하지 않는 것이 중요하다.
4. 개발자 관점 해석
개발자에게 임베딩 모델 선택은 성능표를 고르는 일이 아니라 마이그레이션 계획을 세우는 일이다. 아래 세 가지 질문으로 보면 판단이 쉬워진다.
첫째, 이 벡터는 어떤 버전의 스키마인가? 문서 테이블에 embedding_model, embedding_dimensions, embedding_task, chunker_version, distance_metric을 남겨야 한다. 나중에 “왜 이 문서는 검색이 안 되지?”를 디버깅할 때 벡터 값 자체만 보면 알 수 없다. 어떤 모델과 어떤 차원, 어떤 chunking으로 만들어졌는지 메타데이터가 있어야 한다.
둘째, 새 임베딩은 기존 검색 결과와 어떻게 비교할 것인가? “좋아 보인다”는 몇 개의 수동 검색으로는 부족하다. 대표 query 30~100개, 기대 문서, 허용 가능한 대체 문서, 실패하면 안 되는 query를 작게라도 만들어야 한다. 차원 축소가 recall을 조금 떨어뜨려도 비용을 크게 줄이면 받아들일 수 있는 서비스가 있고, 법무·의료·보안 문서 검색처럼 recall 손실이 큰 문제가 되는 서비스도 있다.
셋째, 롤백은 가능한가? 임베딩 교체는 원본 텍스트만 있으면 다시 만들 수 있지만, 전체 재색인은 시간과 비용이 든다. 운영에서는 documents_v1, documents_v2처럼 병렬 인덱스를 만들고, 읽기 트래픽 일부만 새 인덱스로 보내 평가한 뒤 전환하는 방식이 안전하다. 새 벡터가 나쁘면 API 옵션을 되돌리는 것만으로는 충분하지 않다. 검색 서버, 인덱스, threshold, reranker 입력까지 같이 되돌려야 한다.
실패 모드도 명확하다. 가장 흔한 실패는 차원 mismatch로 저장 단계에서 바로 터지는 경우다. 이건 차라리 좋은 실패다. 더 나쁜 실패는 저장은 되지만 의미 공간이 섞이는 경우다. 예를 들어 query는 새 모델, documents는 예전 모델로 임베딩하면 검색 결과가 이상해진다. 또 cosine threshold를 예전 점수 분포에 맞춰둔 채 모델을 바꾸면, 너무 많은 문서가 통과하거나 아무 문서도 통과하지 않을 수 있다.
5. 내 프로젝트에 적용할 체크포인트
- 임베딩 컬럼명에 버전을 드러낸다. 예:
embedding_v1 vector(1536),embedding_v2 vector(768). - 문서 row에
embedding_model,embedding_dimensions,embedding_created_at,chunker_version,source_updated_at을 저장한다. - query 임베딩과 document 임베딩이 같은 모델·차원·task 계약을 쓰는지 테스트한다.
- 모델 교체, 차원 축소, chunking 변경, HNSW 파라미터 변경을 한 배포에 묶지 않는다.
- cosine, dot product, L2 중 어떤 거리 함수를 쓰는지 코드와 인덱스 이름에 남긴다.
- 검색 threshold는 모델 교체 후 재사용하지 말고 점수 분포를 다시 본다.
- 재색인 작업은 idempotent하게 만든다. 같은 문서를 여러 번 처리해도 같은 embedding version row를 덮어쓸 수 있어야 한다.
- 배포 전 “검색되면 안 되는 문서”도 eval에 넣는다. recall만 보면 관련 없는 문서가 많이 섞이는 문제를 놓친다.
6. 오늘 10분 액션
오늘은 코드를 크게 바꾸지 말고, 내 프로젝트의 임베딩 계약 표를 하나 만든다.
1. 현재 임베딩 모델 이름:
2. 출력 차원:
3. 저장 컬럼 타입 또는 벡터 DB index dimension:
4. query/document task 또는 프롬프트 규칙:
5. 거리 함수:
6. approximate index 사용 여부:
7. chunking 버전:
8. 검색 threshold:
9. 재색인에 걸리는 예상 시간:
10. 롤백 방법:
이 표에서 비어 있는 칸이 많다면, 임베딩은 아직 “기능”으로만 붙어 있고 “운영 계약”은 없는 상태다. 다음 작업은 새 모델을 붙이는 것이 아니라, embedding version 메타데이터와 작은 retrieval eval set을 먼저 만드는 것이다.
7. 더 볼 자료
- OpenAI 임베딩 문서: v3 임베딩의 기본 차원,
dimensions파라미터, 정규화와 cosine/dot product 설명을 확인한다. - Gemini 임베딩 문서: MRL,
output_dimensionality, task instruction 차이를 확인한다. - Supabase vector columns: pgvector 컬럼 차원을 모델 출력과 맞추는 방법을 확인한다.
- pgvector README: exact search, HNSW/IVFFlat, 거리 함수별 인덱스 계약을 확인한다.
- Hugging Face Matryoshka 글: 왜 “앞부분만 잘라 쓰는 임베딩”이 가능한지 배경을 이해한다.
중복 회피 메모
로컬 content/generated와 Supabase 최근 ai_dev 글을 확인했다. 최근 글은 멀티모달 컨텍스트 예산, LLM 서비스 티어, 프롬프트 인젝션 신뢰 경계, Programmatic Tool Calling, RAG citation/grounding, 에이전트 trace, token counting, RAG recall/rerank를 다뤘다. 이번 글은 RAG 검색 품질 자체나 토큰 예산 반복이 아니라, 임베딩 모델의 출력 차원·거리 함수·task 설정·재색인을 데이터베이스 스키마와 운영 마이그레이션으로 관리하는 관점에 집중한다.
핵심 출처
로그인하면 이 글을 북마크하고, 나만 보는 한 줄 메모를 남길 수 있어요.
댓글 0
최신순 ▾혹시 이 글을 읽는 동료 개발자가 있다면, GitHub으로 로그인하고 한 줄 흔적을 남겨줘요. (스팸 방지용 로그인이에요)