Skip to main content
Orux AIOruxAI

Orux AI / 모델 노트

Embedding API 이전: 벡터 재구축과 롤백

벡터 공간을 섞지 않고 text-embedding-3-small에서 large로 이전하세요. 호환성 검사 여섯 가지를 실행하고 재구축 비용, 저장 공간, 전환 방법을 비교합니다.

Orux AI게시 19 분 읽기
Embedding API 이전: 벡터 재구축과 롤백 — 벡터 공간을 섞지 않고 text-embedding-3-small에서 large로 이전하세요.
목차

임베딩 모델을 바꾸면 검색 인덱스도 새로 만들어야 합니다

text-embedding-3-small에서 text-embedding-3-large로 이전할 때는 원본 문서 텍스트로 별도 인덱스를 만들고, 검색 질의도 그 인덱스에 맞는 임베딩 설정으로 처리하세요. 기존 벡터에 0을 추가해 길이만 맞추거나, 벡터 길이가 같다는 이유로 두 모델이 같은 좌표계를 쓴다고 판단하면 안 됩니다. 새 문서 처리 경로와 질의 처리 경로가 자체 검색 평가를 통과할 때까지 기존 인덱스를 유지하세요.

제품 도움말 검색 서비스를 이전하는 문서 담당 팀이라면 전체 코퍼스 재임베딩 비용, 두 인덱스를 임시로 보관할 저장 공간, 되돌릴 수 있는 설정 전환을 계획해야 합니다. 아래에서 실행할 수 있는 Python 예제는 차원 수만 검사해서는 부족한 이유를 보여 주고, 잘못된 테스트 입력 여섯 가지를 거부하며, 구체적인 재구축 예산을 계산합니다. 모든 처리는 오프라인으로 실행됩니다. 예제의 2차원 벡터는 직접 정한 기하학적 값이며, 어느 OpenAI 모델의 출력도 아닙니다.

기존 공간과 회전한 공간에서는 각각 A를 검색하지만, 두 공간을 섞으면 B를 검색하는 수작업 벡터 예제.
기존 공간과 회전한 공간에서는 각각 A를 검색하지만, 두 공간을 섞으면 B를 검색하는 수작업 벡터 예제.

직접 정한 벡터를 각 공간 안에서 일관되게 사용하면 올바른 결과가 유지됩니다. 질의와 인덱스의 공간을 섞으면 벡터 길이가 같아도 B가 반환됩니다. 실제로 로컬에서 실행한 예제를 바탕으로 만든 도식입니다.

모델 차원 수와 현재 Orux에서 확인되는 범위

OpenAI 임베딩 가이드는 기본 벡터 길이를 text-embedding-3-small은 1,536, text-embedding-3-large는 3,072로 명시합니다. 3세대 임베딩을 줄이는 네이티브 dimensions 매개변수도 설명합니다. 지원되는 모델의 출력을 줄이는 것은 설정의 선택이며, 그 모델을 다른 모델로 바꾸는 작업은 아닙니다.

Orux에는 현재 두 모델의 정확한 별칭이 모두 등록되어 있습니다. text-embedding-3-small 모델 페이지와 text-embedding-3-large 모델 페이지는 텍스트 입력과 float 형식의 벡터를 제공합니다. 저장된 공개 예제에서도 각각 1,536차원과 3,072차원을 보고합니다. 이 예제는 보고된 출력 형태를 뒷받침할 뿐, 모델 간의 일반적인 정확도 순위를 보여 주지는 않습니다.

2026년 11월 3일에 확인한 입력 폼에는 차원 수를 지정하는 항목이 없습니다. 따라서 OpenAI 네이티브 문서에 해당 기능이 나온다는 이유만으로 Orux에서도 축소된 인덱스를 사용할 수 있다고 계획하지 마세요. 그 옵션에 의존하기 전에 실제 연동 규격을 확인해야 합니다. 이 글은 유료 임베딩 요청을 보내지 않았으며, Orux를 통해 차원 설정이 전달되는지도 입증하지 않았습니다. 현재 연동 방식은 Orux API 문서를 확인하세요.

모델 별칭, 반환된 차원 수, 텍스트 전처리 버전, 거리 측정 방식을 함께 기록하세요. 예를 들어 title + body로 구성한 도움말 문서의 임베딩은 body만 임베딩할 때와 전처리 버전을 다르게 지정해야 합니다. 문서 리비전 해시도 청크 식별자와 함께 보관하면 나중에 재구축할 때 오래된 텍스트를 찾을 수 있습니다. 차원 수 검사는 명백한 형태 오류를 발견합니다. 형태만으로 드러나지 않는 설정 오류는 나머지 규격 필드로 확인합니다.

차원 수 검사로 놓치는 불일치를 보여 주는 2차원 예제

간단한 기존 공간에 문서 벡터 A=(1, 0), B=(0, 1)이 있다고 가정해 봅시다. 기존 질의 (1, 0)으로 검색하면 A가 반환됩니다. 이제 공간 전체를 90도 회전합니다. A는 (0, 1), B는 (-1, 0)이 되고, 개념적으로 같은 질의도 (0, 1)이 됩니다. 어느 공간에서든 그 공간의 벡터만 일관되게 사용하면 계속 A를 검색합니다.

하지만 회전한 질의를 기존 문서 벡터와 함께 사용하면 B가 1위가 됩니다. 모든 벡터는 여전히 좌표 두 개를 갖습니다. 계산은 정상적으로 실행되지만 검색의 의미는 달라집니다. 이는 차원 수가 같으면 호환된다는 가정의 수학적 반례입니다. 어느 모델이 학습한 동작을 시뮬레이션한 것도 아니고, 실제 이전 실패율을 추정한 것도 아닙니다.

기존 벡터에 0을 채우면 길이는 바뀌지만 새 모델이 학습한 표현으로 매핑되지는 않습니다. 벡터를 잘라도 서로 다른 모델 간 호환성이 생기지 않습니다. 지원되는 한 모델 안에서 차원을 줄이더라도 문서와 질의에 일관된 차원 정책을 적용하고, 그 결과의 검색 동작을 평가해야 합니다. 네이티브 차원 축소 기능이 있다고 해서 새 표현과 기존 표현을 무작정 섞어도 되는 것은 아닙니다.

예제의 검사는 코사인 유사도를 계산하기 전에 전체 설정을 비교합니다. 다만 메타데이터가 정확해야만 유효합니다. 새 질의를 고의로 기존 설정의 질의라고 표시하면 검사를 우회합니다. 아래의 unsafe_mixed_top 결과는 이 한계를 의도적으로 보여 줍니다. 운영 환경의 출처 정보는 호출자의 검증되지 않은 주장 대신 인코더 설정과 저장 경로에서 확보해야 합니다.

재구축 비용은 벡터 개수가 아닌 코퍼스를 기준으로 계산하세요

아래 가격은 2026년 11월 3일에 확인했습니다. Orux 요금은 두 공개 모델 레코드를 기준으로 합니다. OpenAI 직접 이용의 표준 입력 요금은 small 모델 가격과 large 모델 가격을 기준으로 합니다. 서로 별개의 구매 선택지이며, 계정 조건이나 운영상의 동작이 같다는 뜻은 아닙니다.

현재 설정된 환산 기준은 1 Credit = USD 0.005입니다. 아래 USD 금액은 산술적으로 환산한 값이며, Orux의 과금 단위는 Credits입니다. 모든 요금은 입력 토큰 100만 개당 금액으로, 문서 100만 개당 금액이 아닙니다. 표에는 별도의 저장 공간, 인덱스 호스팅, 개발 작업 비용을 포함하지 않았으며, Batch 할인이나 무료 재구축도 가정하지 않았습니다.

모델Orux 입력 요금 / 토큰 100만 개OpenAI 직접 이용 표준 요금 / 토큰 100만 개Orux 토큰 500만 개 재구축 계획액
text-embedding-3-small1.53336 Credits (USD 0.0076668)4 Credits 상당 (USD 0.02)7.6668 Credits (USD 0.038334)
text-embedding-3-large9.96672 Credits (USD 0.0498336)26 Credits 상당 (USD 0.13)49.8336 Credits (USD 0.249168)

좌우로 밀어 전체 표를 확인하세요.

입력 토큰이 평균 500개인 청크 10,000개로 계획용 코퍼스를 잡으면 총량은 토큰 500만 개입니다. 이 코퍼스 전체를 large로 옮기려면 표의 두 행 사이 차액만이 아니라 large로 재구축하는 전체 비용을 예산에 반영해야 합니다. 과거에 small에 지불한 비용으로 large 벡터를 이미 구매한 것은 아닙니다. 이 시점의 Orux 요금에서 large는 small의 약 6.5배이지만, 이 비율은 자신의 문서 검색이 얼마나 좋아지는지 알려 주지 않습니다.

이 합계는 가정한 토큰 수와 전체 합산 계산을 사용한 추정치입니다. 실제 청크 길이, 질의 트래픽, 재시도, 과금 단위에 따라 지출이 달라질 수 있습니다. 실제 실행을 승인하기 전에 적합한 토크나이저로 코퍼스의 토큰 수를 측정하세요. 실행 코드는 요금 계산에 정수 micro-Credits, 표시에 Decimal을 사용해 금액을 이진 부동소수점으로 계산하는 문제를 피합니다.

메모리는 따로 추산해야 합니다. float32로 벡터 10,000개를 저장할 때 1,536차원은 61,440,000바이트, 3,072차원은 122,880,000바이트가 필요하며, 각각 약 58.59 MiB와 117.19 MiB입니다. 이는 원시 좌표 배열만의 크기입니다. 메타데이터, 식별자, 검색 그래프 구조, 복제본, 직렬화에는 추가 공간이 듭니다. 되돌릴 수 있는 이전 과정에서 두 원시 배열을 함께 유지하려면 이러한 추가분을 제외해도 184,320,000바이트가 필요합니다.

호환성과 예산 데모 실행하기

아래 코드를 migration_demo.py로 저장하고 Python 3.9 이상에서 python3 migration_demo.py를 실행하세요. 표준 라이브러리만 사용하며 네트워크 요청을 하지 않습니다. 계산을 재현할 수 있도록 요금은 특정 시점의 값으로 고정했습니다. 이후의 이전을 계획할 때는 현재 요금을 확인하고 교체하세요.

Python
import json
import math
from dataclasses import asdict, dataclass
from decimal import Decimal

@dataclass(frozen=True)
class Contract:
    model: str
    dimensions: int
    preprocessing: str
    metric: str = 'cosine'

def validate(vector, contract):
    if len(vector) != contract.dimensions:
        raise ValueError('dimension mismatch')
    if any(isinstance(x, bool) or not isinstance(x, (int, float))
           or not math.isfinite(x) for x in vector):
        raise ValueError('non-finite or non-numeric vector')
    if math.hypot(*vector) == 0:
        raise ValueError('zero vector')

def search(query, query_contract, documents, index_contract):
    if query_contract != index_contract:
        raise ValueError('embedding contract mismatch')
    validate(query, query_contract)
    scores = []
    for doc_id, vector in documents.items():
        validate(vector, index_contract)
        score = sum(a*b for a, b in zip(query, vector)) / (
            math.hypot(*query) * math.hypot(*vector))
        scores.append((doc_id, score))
    return sorted(scores, key=lambda row: (-row[1], row[0]))

def budget(rate_micro_credits, total_tokens):
    micro = (rate_micro_credits * total_tokens + 999999) // 1000000
    credits = Decimal(micro) / 1000000
    return {'credits': str(credits), 'usd': str(credits * Decimal('0.005'))}

def main():
    # Handwritten two-dimensional geometry, NOT model outputs.
    old = Contract('toy-old', 2, 'title-body-v1')
    new = Contract('toy-new', 2, 'title-body-v1')
    old_docs = {'A': [1, 0], 'B': [0, 1]}
    new_docs = {'A': [0, 1], 'B': [-1, 0]}
    old_top = search([1, 0], old, old_docs, old)[0][0]
    new_top = search([0, 1], new, new_docs, new)[0][0]
    unsafe_top = search([0, 1], old, old_docs, old)[0][0]
    assert (old_top, new_top, unsafe_top) == ('A', 'A', 'B')
    failures = [
        lambda: search([0, 1], new, old_docs, old),
        lambda: validate([1], old),
        lambda: validate([0, 0], old),
        lambda: validate([float('nan'), 1], old),
        lambda: validate([True, 1], old),
        lambda: search([1, 0], Contract('toy-old', 2, 'title-body-v2'), old_docs, old),
    ]
    rejected = 0
    for test in failures:
        try:
            test()
        except ValueError:
            rejected += 1
        else:
            raise AssertionError('invalid fixture was accepted')
    chunks, tokens_per_chunk = 10000, 500
    tokens = chunks * tokens_per_chunk
    result = {
        'toy_results': {'old_top': old_top, 'new_top': new_top,
                        'unsafe_mixed_top': unsafe_top},
        'rejected_fixtures': rejected,
        'planning_tokens': tokens,
        'orux_small': budget(1533360, tokens),
        'orux_large': budget(9966720, tokens),
        'raw_float32_bytes': {'small_1536': chunks * 1536 * 4,
                             'large_3072': chunks * 3072 * 4},
        'new_contract': asdict(new),
    }
    print(json.dumps(result, indent=2))

if __name__ == '__main__':
    main()

로컬 실행 결과는 old_top: A, new_top: A, unsafe_mixed_top: B, rejected_fixtures: 6이었습니다. 거부한 사례는 다른 모델 설정, 잘못된 벡터 길이, 영벡터, 유한하지 않은 좌표, 불리언 좌표, 전처리 버전 변경입니다. small과 large의 계획액은 각각 7.6668 Credits와 49.8336 Credits로 표와 일치합니다.

이는 호환성 규칙과 비용 산술을 실행해 확인하는 데모이며, 데이터베이스 어댑터는 아닙니다. 임베딩을 가져오거나, 근사 최근접 이웃 인덱스를 만들거나, 모델의 검색 품질을 입증하거나, 운영 환경의 접근 제어를 구현하지 않습니다. 합성 좌표 값은 작기 때문에, 운영 환경의 수치 라이브러리에서는 허용하는 입력의 전체 수치 범위도 처리해야 합니다.

인코더와 인덱스를 두 쌍으로 나누어 함께 전환하고 롤백할 수 있는 구성.
인코더와 인덱스를 두 쌍으로 나누어 함께 전환하고 롤백할 수 있는 구성.

배포 계획에서는 각 질의 인코더를 대응하는 인덱스에 연결한 상태로 유지하고, 한 쌍으로 전환하거나 롤백합니다. 이 도식은 제안한 구조를 보여 주며, 실제 배포된 이전 환경을 나타내지는 않습니다.

질의 인코더와 인덱스를 하나의 설정으로 전환하세요

대상 규격을 갖춘 새 컬렉션이나 네임스페이스를 만듭니다. 원본 텍스트를 읽고 기록된 청크 분할 및 전처리 규칙을 적용한 뒤, 변하지 않는 문서·청크 식별자 아래에 대상 모델의 벡터를 저장하세요. 기존 컬렉션은 그대로 둡니다. 처리한 행 수라는 단일 카운터 대신 예상 청크 ID, 완료 ID, 원본 리비전을 추적하세요.

재구축 중 원본 문서가 바뀐다면 명확한 일관성 정책을 선택해야 합니다. 유지보수 시간대의 스냅샷을 사용하는 간단한 방식은 쓰기 충돌을 피하지만 업데이트 반영이 늦어집니다. 온라인 재구축에는 전환 전에 수정과 삭제를 새 인덱스에 반영할 수 있도록 재생 가능한 변경 로그나 그에 준하는 대조 절차가 필요합니다. 삭제된 문서 하나와 누락된 문서 하나가 수치상 상쇄될 수도 있으므로 벡터 개수가 같다는 것만으로는 충분하지 않습니다.

그다음 각 질의 인코더를 예정된 인덱스와 연결합니다. small 컬렉션을 계속 읽는 동안 애플리케이션만 large 질의를 생성하도록 바꾸지 마세요. 전처리, 차원 수, 거리 측정 방식을 포함한 이 쌍을 버전이 있는 하나의 애플리케이션 설정으로 취급하세요. 저장된 컬렉션 정의와 질의 설정이 일치하지 않으면 요청을 거부합니다. 여러 테넌트를 다루는 시스템이라면 두 컬렉션 모두에서 같은 테넌트 필터와 문서 권한을 유지해야 합니다.

롤백을 위해 기존 질의 설정과 컬렉션을 함께 보관하세요. 애플리케이션이 호환되는 기존 질의를 더는 생성하지 못한다면, 기존 벡터만 남겨서는 부족합니다. 전환 후에는 결과 품질, 문서 누락 보고, 지연 시간, 질의 비용을 관찰하세요. 새 컬렉션에 쓰기가 성공했다는 이유만으로 기존 구성을 폐기하지 말고, 합의한 평가 기간과 보관 기간이 지난 뒤 기존 쌍을 정리하세요.

벡터 크기 대신 정답을 표시한 검색 질문으로 결정하세요

전체를 재구축하기 전에 작더라도 대표성 있는 평가 세트를 준비하세요. 제품 도움말 서비스라면 정확한 부품 번호, 모호한 문제 해결 질문, 다국어 요청, 접근 제한이 있는 문서를 포함합니다. 각 질문에 관련된 문서 ID를 기록하세요. 생성한 답변이 유창한지와 검색 결과의 관련성 판단은 분리해야 합니다.

각 후보 설정에 대해 동일한 문서 스냅샷에서 같은 수의 결과를 검색하세요. 상위 결과에 관련 문서가 포함되는지 비교하고 중요한 누락을 살펴봅니다. 짧은 본문 조각으로 만든 small 인덱스와 제목·본문을 보강한 텍스트로 만든 large 인덱스를 비교한 뒤 차이 전체를 모델 탓으로 돌리면 안 됩니다. 청크 분할, 전처리, 접근 필터는 서로 독립적인 실험 변수입니다.

이 글은 실측에 근거한 승자를 제시하지 않습니다. small 자체 평가가 요구사항을 충족한다면, 현재의 낮은 입력 비용과 저장 공간 비용을 고려해 small을 선택할 수 있습니다. large는 자신의 작업에서 측정한 개선이 재구축, 더 큰 벡터, 지속적인 질의 비용을 정당화할 때 검토하세요. 로컬 호환성 테스트의 성공은 보호 장치를 확인한 것이지, 더 비싼 모델을 구매해야 할 이유는 아닙니다.

FAQ

모델을 바꿀 때 모든 문서 벡터를 다시 생성해야 하나요?

네. 이 글에서 다루는 이전에서는 검색 가능한 코퍼스를 대상 모델의 공간으로 다시 생성해야 합니다. 원본 텍스트와 안정적인 청크 ID를 보관하고, 이후 질의도 같은 대상 설정으로 인코딩하세요. 모델 필드의 이름만 바꾼다고 기존 벡터에 호환성이 생기지 않습니다.

1,536차원 벡터에 0을 채워 3,072차원으로 만들어도 되나요?

배열 길이 조건은 맞출 수 있지만 모델 간 매핑이 생기는 것은 아닙니다. 이 이전에서 호환성을 확보하는 방법으로는 적절하지 않습니다. 간단한 기하학 예제처럼 형태가 맞더라도 공간을 섞으면 잘못된 문서가 반환될 수 있습니다.

Orux는 OpenAI 네이티브 dimensions 매개변수를 제공하나요?

확인한 두 Orux 별칭의 공개 폼에는 해당 항목이 없습니다. OpenAI 네이티브 문서만으로 Orux의 동작을 검증할 수는 없습니다. 축소된 출력에 의존하기 전에 실제 연동 규격을 확인하고, 일관된 설정으로 인덱스를 만들고 질의하세요.

text-embedding-3-large가 제 검색에 자동으로 더 좋은가요?

이 글은 특정 작업에서의 개선을 입증하지 않았습니다. 자체적으로 정답을 표시한 질문과 동일한 원본 스냅샷을 사용해 두 설정을 평가하세요. 더 큰 차원 수와 높은 가격은 관련 문서를 검색하는지 측정하는 일을 대신하지 못합니다.

Python 예제는 실제로 무엇을 테스트했나요?

직접 정한 벡터의 기하학, 설정 규격 불일치에 따른 거부, 잘못된 테스트 입력 여섯 가지, 특정 시점의 예산과 저장 공간 산술을 테스트했습니다. 임베딩 API를 호출하지 않았고 Orux Credits를 사용하지 않았습니다. 모델 출력이나 실행 중인 벡터 데이터베이스를 벤치마크하지도 않았습니다.

Orux AI에서 text-embedding-3-small 모델 페이지 사용해 보기모델 페이지에서 온라인으로 실행하고 가격과 API 예시를 확인하세요.모델 열기Orux AI에서 text-embedding-3-large 모델 페이지 사용해 보기모델 페이지에서 온라인으로 실행하고 가격과 API 예시를 확인하세요.모델 열기

하나의 API로 이 모델들을 사용하세요

채팅·이미지·비디오·오디오 모델을 지원하는 OpenAI 호환 API이며 Credits로 과금됩니다.