콘텐츠로 이동

임베딩

ES에 저장된 문서 본문을 의미 벡터로 변환하여 kNN 검색이 가능하게 만드는 단계. 인덱서가 적재 시 한 번(문서 임베딩), 백엔드가 사용자 질문 처리 시 한 번(query 임베딩) — 양쪽이 동일 모델(text-embedding-3-large, 3072 dims)을 써야 cosine 유사도가 의미를 갖는다. Python 측에는 SQLite 기반 로컬 캐시가 있어 재적재 시 OpenAI 호출 비용을 회피한다.

사용자 여정

인덱싱 시 문서 임베딩 (Python OpenAIEmbedder)

sequenceDiagram
    autonumber
    participant IDX as XxxIndexer
    participant EMB as OpenAIEmbedder
    participant CACHE as SQLite (.db)
    participant TIK as tiktoken
    participant API as OpenAI Embeddings API

    IDX->>EMB: get_embeddings_batch([text1, text2, ...])
    loop 각 텍스트
        EMB->>EMB: SHA256(strip(text)) → text_hash
        EMB->>CACHE: SELECT vector FROM cache WHERE key=text_hash
        alt hit
            CACHE-->>EMB: 기존 벡터 반환 (3072 floats)
        else miss
            EMB->>EMB: miss_indices.append(i)
        end
    end
    alt miss_texts 있음
        EMB->>TIK: encode(text)로 토큰 수 확인
        Note over EMB: 8000 토큰 초과 시 자동 절단
        EMB->>API: POST /embeddings { input: miss_texts, model: text-embedding-3-large }
        API-->>EMB: 3072-dim vectors (배치)
        EMB->>CACHE: put_many([(hash, vector), ...])
    end
    EMB-->>IDX: List[List[float]] (입력 순서대로)

검색 시 query 임베딩 (Backend Kotlin EmbeddingUtil)

sequenceDiagram
    autonumber
    participant SVC as StreamingRagProcessor
    participant HYDE as HydeService
    participant EU as EmbeddingUtil
    participant LLM as OpenAI

    SVC->>HYDE: generateHypotheticalAnswer(question)
    HYDE-->>SVC: 가상 답변 텍스트
    SVC->>EU: embed(가상 답변)
    EU->>LLM: openai.embeddings.create
model: ModelId("text-embedding-3-large")
input: [hypothetical] LLM-->>EU: 3072-dim List EU-->>SVC: List Note over SVC: ES kNN query에 query_vector로 전달

백엔드 구현

계층 클래스 / 파일 역할
Embedder (Python, 문서) OpenAIEmbedder (packages/data-pipeline/.../embeddings/openai_embedder.py) 인덱서가 사용. tiktoken truncation + SHA256 캐시 키 + tenacity 재시도
Cache (Python) EmbeddingCache (embeddings/cache.py) SQLite WAL 모드, 바이너리 BLOB 저장. 이전 JSON 캐시(9.2GB 폭증, 손상 문제)를 대체
Alt embedder (Python) GeminiEmbedder (embeddings/gemini_embedder.py) gemini-embedding-001 (3072 dims). Vertex AI 인증 필요. 현재 인덱서/CLI 통합 없음 — 실험용
Embedder (Backend, query) EmbeddingUtil (apps/backend/.../application/service/EmbeddingUtil.kt) Spring @Component. openai.embeddings(EmbeddingRequest(model=ModelId("text-embedding-3-large"), input=[text]))
Backfill script packages/data-pipeline/scripts/backfill_vectors.py 기존 ES 문서에 누락된 content_vector 채우기 (text-embedding-3-large 기본)

도메인 규칙

규칙 위치 값 / 설명
운영 모델 indexer CLI 기본값 + EmbeddingUtil text-embedding-3-large (양쪽 동일) — 3072 dims
차원 indexer 매핑 + ES 인덱스 3072dense_vector.dims. 모든 인덱스 동일
유사도 함수 indexer 매핑 cosineEmbeddingUtil이 정규화하지 않으므로 cosine 사용
코드 기본값 vs 실제 운영 OpenAIEmbedder.__init__ 클래스 기본값은 text-embedding-3-small이지만 CLI(cli/index.py:183)가 항상 text-embedding-3-large로 override. 운영에서 small은 사용 안 함
입력 토큰 한도 MAX_TOKENS (module-level in openai_embedder.py:18) 8000 (모델 한도 8191에서 마진). 초과 시 tiktoken으로 자동 절단 + warning 로그
캐시 키 OpenAIEmbedder._get_hash() SHA256(text.strip()) — 공백만 정규화. 대소문자/유니코드 변환 없음
캐시 저장소 EmbeddingCache.db_path 기본 data/embedding_cache.db (SQLite). WAL 모드로 crash-safe 쓰기
캐시 마이그레이션 EmbeddingCache 레거시 JSON 캐시 자동 감지/마이그레이션
재시도 정책 OpenAIEmbedder._fetch_embedding tenacity — exponential backoff (min=1s, max=20s), max 3회
빈 문자열 get_embedding("") [] 반환 (API 호출 없음). bulk 시 동일 위치에 빈 벡터 보존
배치 처리 get_embeddings_batch 캐시 hit/miss 분리 → miss만 1회 API 호출로 묶어 처리
백엔드 트레이싱 라벨 LangfuseTracingService.embeddingModel 코드 기본값 문자열은 "text-embedding-3-small"로 남아 있으나 실제 호출은 EmbeddingUtil이 large 사용 — 트레이싱 메타데이터 불일치 (개선 여지)

API 엔드포인트

해당 없음 — 임베딩은 내부 컴포넌트. 사용자 향 API는 /conversations/stream이 RAG 흐름 안에서 query 임베딩을 트리거 (리포트 생성 참조).

데이터 모델

erDiagram
    EMBEDDING_INPUT {
        string text "문서 본문 또는 사용자 질문 (또는 HyDE 가상 답변)"
    }
    SQLITE_CACHE_ROW {
        string key PK "SHA256(text.strip())"
        blob vector "3072 floats (binary BLOB, struct pack)"
    }
    ES_DOC_FIELD {
        string _id "문서 ID"
        string content "원본 본문"
        dense_vector content_vector "3072 dims, cosine, indexed for kNN"
    }
    OPENAI_REQUEST {
        string model "text-embedding-3-large"
        string input "텍스트 (또는 배열)"
    }
    EMBEDDING_INPUT ||--|| OPENAI_REQUEST : "miss인 경우만 호출"
    EMBEDDING_INPUT ||--|| SQLITE_CACHE_ROW : "hit/miss key 조회"
    SQLITE_CACHE_ROW ||--|| ES_DOC_FIELD : "벡터 그대로 ES content_vector에 저장"

설정

항목 위치 비고
OpenAI API key (Python) env OPENAI_API_KEY 또는 CLI --api-key 인덱서 --embed 사용 시 필수
OpenAI API key (Backend) yaml openai.token (env override: OPENAI_TOKEN) EmbeddingUtil 및 chat completion 모두 동일 키
모델 (Python CLI) semugpt-index bulk --model 기본 text-embedding-3-large — 변경 시 indexer 매핑 dims도 함께 변경 필요
모델 (Backend) EmbeddingUtil.kt:21 하드코딩 ModelId("text-embedding-3-large") — config 외부화 안 됨
캐시 위치 OpenAIEmbedder(cache_path=...) 기본 data/embedding_cache.json (실제 저장은 .db SQLite)
Gemini 대체 (실험) GeminiEmbedder.MODEL_NAME gemini-embedding-001, 3072 dims. 인덱서 CLI 미통합 — 사용 시 코드 수정 필요
토큰 한도 MAX_TOKENS=8000 (module-level in openai_embedder.py:18) 하드코딩 (모델 8191 한도 - 마진)

알려진 이슈 / 개선 예정

  • 모델 정합성 self-discipline에 의존 — Python 인덱서가 text-embedding-3-large로 적재하는데 backend EmbeddingUtil이 다른 모델로 query embed하면 검색이 silent하게 무의미해진다. 양쪽 모두 코드 hardcode + CLI 기본값으로만 일치 유지 중 — config 단일 출처 부재가 위험.
  • Backend 모델 하드코딩EmbeddingUtil.ktModelId("text-embedding-3-large")가 application.yml로 외부화되어 있지 않음. 모델 교체 = 코드 변경 + 재배포.
  • Langfuse 트레이싱 라벨 mismatchLangfuseTracingService.embeddingModel 기본값이 "text-embedding-3-small"로 남아있어 트레이스 메타데이터가 실제 호출 모델과 불일치. 검색 품질 분석 시 혼동 가능.
  • 임베딩 텍스트 절단 — 8000 토큰 초과 문서는 뒷부분이 임베딩에 반영되지 않음. 긴 판례/예규는 chunk 분할이 권장되지만 미구현 (1 doc = 1 embedding).
  • 캐시 키가 strip만 적용 — 공백 외 정규화(대소문자, NFKC 등) 없음. 동일 의미의 텍스트라도 미세 차이로 캐시 miss → 중복 호출 가능.
  • OpenAIEmbedder 기본 모델 = small — 클래스 기본값이 text-embedding-3-small이라 CLI 외 경로에서 인스턴스 생성 시 잘못된 모델 사용 위험. 운영에서는 CLI가 항상 large로 명시.
  • 재시도 후 영구 실패 처리 없음tenacity 3회 재시도 후 실패 시 예외 전파 → indexer 전체 batch 실패. dead-letter / 부분 skip 옵션 없음.
  • 임베딩 비용 모니터링 부재--embed 사용 시 비용 알람/카운터 없음. 캐시 hit rate 통계도 미출력 — 대량 재적재 시 청구 surprise 가능.

관련 문서

  • 인덱서 (Indexers) — 임베딩을 호출하는 측 (적재 시점)
  • Elasticsearchcontent_vector 필드 매핑 / cosine 유사도
  • 리포트 생성 — query 임베딩이 어떻게 RAG 검색에 사용되는지 (HyDE, RRF)
  • packages/data-pipeline/CLAUDE.md (리포 안 직접 참조) — Phase 2 ES 매핑 템플릿 (3072 dims, cosine)