TurboVec란 무엇인가?

TurboVec는 Rust로 작성되고 Python 바인딩을 제공하는 고성능 벡터 인덱스 라이브러리로, Google Research의 TurboQuant 양자화 알고리즘을 기반으로 구축되었습니다. 핵심 목표는 RAG(검색 증강 생성) 시스템의 두 가지 주요 문제인 메모리 사용량과 검색 속도를 해결하는 것입니다.

왜 TurboVec가 필요한가?

전통적인 벡터 검색 시나리오에서 1,000만 개의 문서 코퍼스를 다룬다고 가정해 봅시다. float32 형식으로 벡터를 저장하면 약 31GB의 RAM이 필요합니다. 반면 TurboVec는 데이터 무관 양자화(data-oblivious quantization) 기술을 통해 동일한 데이터셋을 단 4GB로 압축합니다. 메모리 사용량이 87% 줄어들면서 검색 속도는 FAISS를 능가합니다.

TurboVec vs FAISS

특성 FAISS TurboVec
메모리 사용량 높음(float32 또는 PQ 양자화) 매우 낮음(TurboQuant 양자화)
훈련 필요 여부 ✅ 필요 ❌ 불필요, 플러그 앤 플레이
온라인 벡터 추가 ⚠️ 인덱스 재구축 필요 ✅ 실시간 추가, 재구축 불필요
필터링 검색 후처리 필요 ✅ 커널 레벨 지원, 성능 손실 없음
SIMD 최적화 있음 수작업 NEON(ARM) + AVX-512BW(x86)
순수 로컬 실행 ✅ ✅
Python 바인딩 ✅ ✅
Rust 네이티브 지원 ❌ ✅

핵심 기술: TurboQuant 알고리즘

TurboQuant는 Google Research가 2025년에 제안한 새로운 양자화 알고리즘으로, 주요 특징은 다음과 같습니다:

  • 데이터 무관(Data-Oblivious): 특정 데이터셋에 대한 코드북(codebook) 훈련이 필요하지 않아 전통적인 PQ(Product Quantization)의 훈련 오버헤드를 피합니다
  • 샤논 하한 근접: 왜곡도 측면에서 이론적 최적값에 근접합니다
  • 유연한 비트 폭: 2비트, 4비트, 8비트 등 다양한 양자화 정밀도를 지원하여 재현율과 메모리 사용량의 균형을 맞춥니다

벤치마크 테스트에서 TurboVec는 ARM 아키텍처에서 FAISS IndexPQFastScan보다 12-20% 빠르며, x86 아키텍처에서는 비슷하거나 약간 우수합니다.


TurboVec 설치

TurboVec는 Python과 Rust 두 가지 언어 인터페이스를 제공합니다.

Python 설치

BASH
pip install turbovec

LangChain, LlamaIndex 등 프레임워크와 통합하려면 추가 종속성을 설치하세요:

BASH
# LangChain 통합
pip install turbovec[langchain]

# LlamaIndex 통합
pip install turbovec[llama-index]

# Haystack 통합
pip install turbovec[haystack]

# Agno 통합
pip install turbovec[agno]

Rust 설치

Cargo.toml에 종속성 추가:

TOML
[dependencies]
turbovec = "0.1"

빠른 시작: Python 기본 사용법

인덱스 생성 및 벡터 추가

PYTHON
import numpy as np
from turbovec import TurboQuantIndex

# 인덱스 생성: 차원 1536(OpenAI 임베딩 기본 차원), 4비트 양자화
index = TurboQuantIndex(dim=1536, bit_width=4)

# 샘플 벡터 생성(실제 사용 시 임베딩 벡터로 대체)
vectors = np.random.rand(10000, 1536).astype(np.float32)

# 인덱스에 벡터 추가
index.add(vectors)

# 더 많은 벡터를 계속 추가할 수 있으며, 인덱스 재구축이 필요 없습니다
more_vectors = np.random.rand(5000, 1536).astype(np.float32)
index.add(more_vectors)

print(f"인덱스에 총 {len(index)}개의 벡터가 있습니다")

검색 실행

PYTHON
# 쿼리 벡터 생성
query = np.random.rand(1536).astype(np.float32)

# 가장 유사한 10개 벡터 검색
scores, indices = index.search(query, k=10)

print("유사도 점수:", scores)
print("벡터 인덱스:", indices)

영구 저장 및 로드

PYTHON
# 인덱스를 디스크에 저장
index.write("my_index.tq")

# 디스크에서 인덱스 로드
loaded_index = TurboQuantIndex.load("my_index.tq")

# 로드 확인
scores, indices = loaded_index.search(query, k=10)

고급 기능 1: 외부 ID 매핑 사용

실제 애플리케이션에서는 벡터 인덱스를 데이터베이스의 문서 ID와 연결해야 하는 경우가 많습니다. TurboVec는 이러한 요구사항을 지원하기 위해 IdMapIndex를 제공합니다.

ID가 있는 벡터 추가

PYTHON
import numpy as np
from turbovec import IdMapIndex

# 외부 ID를 지원하는 인덱스 생성
index = IdMapIndex(dim=1536, bit_width=4)

# 3개의 벡터가 있고, 해당 외부 ID가 1001, 1002, 1003이라고 가정
vectors = np.random.rand(3, 1536).astype(np.float32)
external_ids = np.array([1001, 1002, 1003], dtype=np.uint64)

# 벡터와 해당 외부 ID 추가
index.add_with_ids(vectors, external_ids)

# 검색 시 내부 인덱스가 아닌 외부 ID를 반환
query = np.random.rand(1536).astype(np.float32)
scores, ids = index.search(query, k=10)

print("반환된 외부 ID:", ids)  # [1001, 1003, 1002, ...]

벡터 삭제

IdMapIndex는 외부 ID를 통해 벡터를 직접 삭제할 수 있으며, 시간 복잡도는 O(1)입니다:

PYTHON
# ID가 1002인 벡터 삭제
index.remove(1002)

# 다시 검색하면 1002가 결과에 나타나지 않음
scores, ids = index.search(query, k=10)
print("삭제 후 ID:", ids)  # 더 이상 1002 포함되지 않음

ID가 있는 인덱스 영구 저장

PYTHON
# 저장
index.write("my_index.tvim")

# 로드
loaded_index = IdMapIndex.load("my_index.tvim")

이는 TurboVec의 핵심 하이라이트 중 하나입니다. 전통적인 벡터 데이터베이스에서 특정 테넌트나 시간 범위의 결과로 제한하려면 일반적으로 많은 후보 결과를 먼저 검색한 후 애플리케이션 레벨에서 필터링해야 합니다. 이는 재현율 저하와 성능 낭비를 초래합니다.

TurboVec는 커널 레벨에서 필터링을 지원합니다. allowlist 매개변수를 통해 허용된 ID 목록을 전달하면 SIMD 커널이 계산 과정에서 허용되지 않은 슬롯을 직접 건너뜁니다.

시나리오: 멀티테넌트 RAG 시스템

각 테넌트가 자신의 문서에만 접근할 수 있는 멀티테넌트 RAG 시스템이 있다고 가정해 봅시다:

PYTHON
import numpy as np
from turbovec import IdMapIndex

# 인덱스 생성
idx = IdMapIndex(dim=1536, bit_width=4)

# 10,000개의 벡터가 있고, 각 벡터가 문서 ID에 대응한다고 가정
vectors = np.random.rand(10000, 1536).astype(np.float32)
doc_ids = np.arange(1, 10001, dtype=np.uint64)
idx.add_with_ids(vectors, doc_ids)

# 데이터베이스 쿼리 시뮬레이션: 테넌트 A의 문서 ID 목록 가져오기
# 실제 시나리오에서는 PostgreSQL / MySQL에서 쿼리한 결과입니다
tenant_a_docs = np.array([1, 5, 10, 15, 20, 25, 30, 35, 40, 45], dtype=np.uint64)

# 테넌트 A의 문서 범위 내에서 검색
query = np.random.rand(1536).astype(np.float32)
scores, ids = idx.search(query, k=5, allowlist=tenant_a_docs)

print("테넌트 A의 가장 관련성 높은 문서:", ids)
# 출력에는 tenant_a_docs의 ID만 포함됩니다

성능 이점

필터링은 SIMD 커널 내부에서 발생하며, 32 벡터 블록 granularity의 단락 메커니즘을 사용합니다:

  • 블록에 허용된 슬롯이 없으면 전체 블록의 LUT 조회 및 스코어링 계산을 건너뜁니다
  • 블록에 일부 허용된 슬롯이 있으면 허용된 슬롯만 스코어링합니다
  • 선택성이 강한 필터링 조건(허용된 ID가 전체에서 차지하는 비율이 매우 작은 경우) 대부분의 SIMD 계산 오버헤드를 피할 수 있습니다

출력 결과 길이는 min(k, len(allowlist))이며, 허용 목록이 k보다 작을 때 정확히 len(allowlist)개의 결과를 반환하고 관련 없는 결과로 채우지 않습니다.


주요 RAG 프레임워크와의 통합

TurboVec는 LangChain, LlamaIndex, Haystack 및 Agno와의 원활한 통합을 제공하며, import 문만 교체하면 됩니다.

LangChain 통합

PYTHON
from langchain_community.vectorstores import TurboVec
from langchain_openai import OpenAIEmbeddings

# 임베딩 초기화
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")

# TurboVec 벡터 스토어 생성
vector_store = TurboVec.from_documents(
    documents=documents,  # Document 리스트
    embedding=embeddings,
    bit_width=4  # 4비트 양자화
)

# 유사성 검색
results = vector_store.similarity_search("질문", k=5)

# 영구 저장
vector_store.save_local("turbovec_index")

# 로드
loaded_store = TurboVec.load_local("turbovec_index", embeddings)

LlamaIndex 통합

PYTHON
from llama_index.vector_stores.turbovec import TurboVecVectorStore
from llama_index.core import VectorStoreIndex, SimpleDirectoryReader

# 문서 로드
documents = SimpleDirectoryReader("./data").load_data()

# TurboVec 벡터 스토어 생성
vector_store = TurboVecVectorStore(dim=1536, bit_width=4)

# 인덱스 생성
index = VectorStoreIndex.from_documents(
    documents,
    vector_store=vector_store
)

# 쿼리 엔진
query_engine = index.as_query_engine()
response = query_engine.query("질문")
print(response)

Haystack 통합

PYTHON
from haystack_integrations.document_stores.turbovec import TurboVecDocumentStore
from haystack.components.embedders import SentenceTransformersDocumentEmbedder
from haystack import Pipeline

# 문서 스토어 생성
document_store = TurboVecDocumentStore(dim=768, bit_width=4)

# 임베더
embedder = SentenceTransformersDocumentEmbedder(model="sentence-transformers/all-MiniLM-L6-v2")

# 파이프라인 구축
pipeline = Pipeline()
pipeline.add_component("embedder", embedder)
# ... 다른 컴포넌트 계속 추가

Rust 네이티브 사용법

고성능 백엔드 서비스를 Rust로 구축 중이라면 TurboVec의 네이티브 Rust API를 직접 사용할 수 있습니다.

기본 사용법

RUST
use turbovec::TurboQuantIndex;
use ndarray::Array2;

fn main() {
    // 인덱스 생성: 1536차원, 4비트 양자화
    let mut index = TurboQuantIndex::new(1536, 4);

    // 벡터 데이터 준비(여기서는 랜덤 숫자 예시)
    let vectors = Array2::<f32>::random((10000, 1536), &mut rand::thread_rng());

    // 벡터 추가
    index.add(&vectors);

    // 쿼리 벡터 준비
    let query = Array1::<f32>::random(1536, &mut rand::thread_rng());

    // 가장 유사한 10개 벡터 검색
    let results = index.search(&query, 10);

    println!("Top 10 결과: {:?}", results);

    // 영구 저장
    index.write("index.tv").unwrap();

    // 로드
    let loaded = TurboQuantIndex::load("index.tv").unwrap();
}

외부 ID가 있는 인덱스

RUST
use turbovec::IdMapIndex;

fn main() {
    let mut index = IdMapIndex::new(1536, 4);

    let vectors = Array2::<f32>::random((100, 1536), &mut rand::thread_rng());
    let ids = vec![1001u64, 1002, 1003, /* ... */];

    index.add_with_ids(&vectors, &ids);

    let query = Array1::<f32>::random(1536, &mut rand::thread_rng());
    let (scores, returned_ids) = index.search(&query, 10);

    println!("반환된 외부 ID: {:?}", returned_ids);

    // 삭제
    index.remove(1002);

    // 영구 저장
    index.write("index.tvim").unwrap();
    let loaded = IdMapIndex::load("index.tvim").unwrap();
}

성능 벤치마크

공식 제공 벤치마크 데이터에 따르면 TurboVec는 다양한 데이터셋과 비트 폭에서 다음과 같은 성능을 보입니다:

재현율 비교(TurboQuant vs FAISS IndexPQ)

테스트 조건: 100K 벡터, k=64

데이터셋 비트 폭 TurboVec R@1 FAISS R@1 우위
GloVe d=200 4비트 +0.3 pts 기준 TurboVec 더 높음
OpenAI d=1536 2비트 +0.4 pts 기준 TurboVec 더 높음
OpenAI d=1536 4비트 +1.2 pts 기준 TurboVec 더 높음
OpenAI d=3072 4비트 +3.4 pts 기준 TurboVec 현저히 높음

모든 테스트에서 k=4일 때 둘 다 재현율 1.0으로 수렴합니다.

속도 비교

  • ARM(NEON): TurboVec가 FAISS IndexPQFastScan보다 12-20% 빠름
  • x86(AVX-512BW): TurboVec가 FAISS와 비슷하거나 약간 우세

메모리 사용량

벡터 수 차원 float32 메모리 TurboVec(4비트) 메모리 절감
1,000만 1536 ~31 GB ~4 GB 87%
100만 1536 ~3.1 GB ~400 MB 87%
10만 1536 ~310 MB ~40 MB 87%

실전 사례: 로컬 RAG 시스템 구축

다음은 TurboVec를 사용하여 클라우드 서비스 없이 완전히 로컬인 RAG 시스템을 구축하는 완전한 예제입니다.

환경 준비

BASH
pip install turbovec sentence-transformers langchain-community langchain-openai

전체 코드

PYTHON
import numpy as np
from turbovec import IdMapIndex
from sentence_transformers import SentenceTransformer
from langchain_community.document_loaders import TextLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter

# 1. 임베딩 모델 로드(로컬 실행, API 키 불필요)
print("임베딩 모델 로드 중...")
model = SentenceTransformer('all-MiniLM-L6-v2')  # 384차원
dim = 384

# 2. 문서 로드 및 분할
print("문서 로드 중...")
loader = TextLoader('./data/my_documents.txt', encoding='utf-8')
documents = loader.load()

splitter = RecursiveCharacterTextSplitter(
    chunk_size=500,
    chunk_overlap=50
)
chunks = splitter.split_documents(documents)

print(f"{len(chunks)}개의 텍스트 청크로 분할됨")

# 3. 벡터 생성 및 인덱스 구축
print("벡터 생성 및 인덱스 구축 중...")
index = IdMapIndex(dim=dim, bit_width=4)

texts = [chunk.page_content for chunk in chunks]
metadata_list = [chunk.metadata for chunk in chunks]

# 배치로 벡터 생성
embeddings = model.encode(texts, show_progress_bar=True)

# 벡터와 메타데이터 추가(인덱스를 외부 ID로 사용)
external_ids = np.arange(len(texts), dtype=np.uint64)
index.add_with_ids(embeddings.astype(np.float32), external_ids)

# 인덱스 저장
index.write("rag_index.tvim")
print("인덱스 저장 완료")

# 4. 검색 함수
def search(query: str, k: int = 5):
    """가장 관련성 높은 텍스트 청크 검색"""
    # 인덱스 로드
    idx = IdMapIndex.load("rag_index.tvim")

    # 쿼리 벡터 생성
    query_embedding = model.encode([query])[0].astype(np.float32)

    # 검색
    scores, ids = idx.search(query_embedding, k=k)

    # 결과 반환
    results = []
    for score, doc_id in zip(scores, ids):
        doc_id = int(doc_id)
        results.append({
            'text': texts[doc_id],
            'score': float(score),
            'metadata': metadata_list[doc_id]
        })

    return results

# 5. 검색 테스트
if __name__ == "__main__":
    query = "TurboVec란 무엇인가?"
    results = search(query, k=3)

    print("\n=== 검색 결과 ===")
    for i, result in enumerate(results, 1):
        print(f"\n[{i}] 유사도: {result['score']:.4f}")
        print(f"내용: {result['text'][:200]}...")

필터링 기능 추가

문서에 카테고리 태그가 있는 경우 검색 시 필터링할 수 있습니다:

PYTHON
def search_with_filter(query: str, category: str, k: int = 5):
    """카테고리 필터링이 있는 검색"""
    idx = IdMapIndex.load("rag_index.tvim")

    # 지정된 카테고리에 속하는 문서 ID 찾기
    allowed_ids = np.array([
        i for i, meta in enumerate(metadata_list)
        if meta.get('category') == category
    ], dtype=np.uint64)

    if len(allowed_ids) == 0:
        return []

    # 쿼리 벡터 생성
    query_embedding = model.encode([query])[0].astype(np.float32)

    # 필터링된 검색
    scores, ids = idx.search(query_embedding, k=k, allowlist=allowed_ids)

    results = []
    for score, doc_id in zip(scores, ids):
        doc_id = int(doc_id)
        results.append({
            'text': texts[doc_id],
            'score': float(score),
            'metadata': metadata_list[doc_id]
        })

    return results

# "기술" 카테고리 문서만 검색
results = search_with_filter("설치 방법은?", category="기술", k=3)

자주 묻는 질문

Q1: TurboVec는 어떤 시나리오에 적합한가?

  • 메모리가 제한된 환경: 제한된 RAM에서 대규모 벡터 인덱스를 저장해야 할 때
  • 개인정보 민감 시나리오: 데이터를 로컬이나 VPC 밖으로 내보낼 수 없을 때
  • 동적으로 증가하는 코퍼스: 새 벡터를 자주 추가해야 하며 인덱스 재구축 비용을 감당할 수 없을 때
  • 멀티테넌트 RAG: 검색 시 세분화된 권한 필터링이 필요할 때

Q2: TurboVec는 어떤 시나리오에 적합하지 않은가?

  • 초대규모 분산 검색: 인덱스를 여러 머신에 분산해야 한다면 Milvus, Weaviate 등 분산 벡터 데이터베이스를 고려해야 함
  • 복잡한 메타데이터 필터링 필요: TurboVec는 현재 ID 기반 필터링만 지원하며, 복잡한 메타데이터 쿼리는 애플리케이션 레벨에서 처리해야 함

Q3: bit_width를 어떻게 선택해야 하는가?

  • 4비트: 권장 기본값, 재현율과 메모리 간 좋은 균형 달성
  • 2비트: 극단적 압축, 재현율 약간 하락, 메모리가 극도로 제한된 시나리오에 적합
  • 8비트: 최고 정밀도, 메모리 사용량은 4비트의 약 2배, 재현율은 float32에 근접

Q4: TurboVec와 FAISS를 함께 사용할 수 있는가?

가능합니다. FAISS로 coarse search를 수행한 후 TurboVec로 rerank를 하거나 그 반대로 사용할 수 있습니다. 두 라이브러리의 API 설계 사상이 다르므로 상호 보완적으로 사용할 수 있습니다.


요약

TurboVec는 주목할 만한 신규 벡터 검색 라이브러리로, TurboQuant 알고리즘을 통해 메모리 사용량, 검색 속도, 사용 편의성 세 가지 측면에서 좋은 균형을 달성했습니다:

  • ✅ 메모리 87% 감소: 1,000만 벡터를 31GB에서 4GB로 감소
  • ✅ FAISS 초월 속도: ARM에서 12-20% 빠름, x86에서 비슷함
  • ✅ 훈련 불필요: 플러그 앤 플레이, 온라인 벡터 추가 지원
  • ✅ 커널 레벨 필터링: 멀티테넌트 RAG의 이상적 선택
  • ✅ 생태계 통합: LangChain, LlamaIndex, Haystack 원활 연동

RAG 프로젝트가 메모리 병목 현상에 직면하거나 순수 로컬 배포가 필요하다면 TurboVec를 시도해 볼 가치가 있습니다.

프로젝트 주소: https://github.com/RyanCodrai/turbovec

논문: TurboQuant: Data-Oblivious Vector Quantization