11. 벡터 데이터베이스

11. 벡터 데이터베이스

학습 목표

  • 벡터 데이터베이스 개념
  • Chroma, FAISS, Pinecone 사용
  • 인덱싱과 검색 최적화
  • 실전 활용 패턴

1. 벡터 데이터베이스 개요

왜 벡터 DB인가?

전통 DB:
    SELECT * FROM docs WHERE text LIKE '%machine learning%'
    → 키워드 매칭만 가능

벡터 DB:
    query_vector = embed("What is AI?")
    SELECT * FROM docs ORDER BY similarity(vector, query_vector)
    → 의미적 유사성 검색

주요 벡터 DB

이름 타입 특징
Chroma 로컬/임베디드 간단, 개발용
FAISS 라이브러리 빠름, 대규모
Pinecone 클라우드 관리형, 확장성
Weaviate 오픈소스 하이브리드 검색
Qdrant 오픈소스 필터링 강점
Milvus 오픈소스 대규모, 분산

2. Chroma

설치 및 기본 사용

pip install chromadb
import chromadb
from chromadb.utils import embedding_functions

# 클라이언트 생성
client = chromadb.Client()  # 메모리
# client = chromadb.PersistentClient(path="./chroma_db")  # 영구 저장

# 임베딩 함수
embedding_fn = embedding_functions.SentenceTransformerEmbeddingFunction(
    model_name="all-MiniLM-L6-v2"
)

# 컬렉션 생성
collection = client.create_collection(
    name="my_collection",
    embedding_function=embedding_fn
)

문서 추가

# 문서 추가
collection.add(
    documents=["Document 1 text", "Document 2 text", "Document 3 text"],
    metadatas=[{"source": "a"}, {"source": "b"}, {"source": "a"}],
    ids=["doc1", "doc2", "doc3"]
)

# 임베딩 직접 제공
collection.add(
    embeddings=[[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]],
    documents=["Doc 1", "Doc 2"],
    ids=["id1", "id2"]
)

검색

# 쿼리 검색
results = collection.query(
    query_texts=["What is machine learning?"],
    n_results=3
)

print(results['documents'])  # 문서 내용
print(results['distances'])  # 거리
print(results['metadatas'])  # 메타데이터

# 메타데이터 필터링
results = collection.query(
    query_texts=["query"],
    n_results=5,
    where={"source": "a"}  # source가 "a"인 것만
)

# 복합 필터
results = collection.query(
    query_texts=["query"],
    where={"$and": [{"source": "a"}, {"year": {"$gt": 2020}}]}
)

LangChain 연동

from langchain_community.vectorstores import Chroma
from langchain_openai import OpenAIEmbeddings

embeddings = OpenAIEmbeddings()

# 생성
vectorstore = Chroma.from_texts(
    texts=["text1", "text2", "text3"],
    embedding=embeddings,
    persist_directory="./chroma_db"
)

# 검색
docs = vectorstore.similarity_search("query", k=3)

# Retriever로 사용
retriever = vectorstore.as_retriever()

3. FAISS

설치 및 기본 사용

pip install faiss-cpu  # CPU 버전
# pip install faiss-gpu  # GPU 버전
import faiss
import numpy as np

# 인덱스 생성
dimension = 384
index = faiss.IndexFlatL2(dimension)  # L2 거리

# 벡터 추가
vectors = np.random.random((1000, dimension)).astype('float32')
index.add(vectors)

print(f"Total vectors: {index.ntotal}")

검색

# 검색
query = np.random.random((1, dimension)).astype('float32')
distances, indices = index.search(query, k=5)

print(f"Indices: {indices}")
print(f"Distances: {distances}")

인덱스 타입

# Flat (정확, 느림)
index = faiss.IndexFlatL2(dimension)

# IVF (근사, 빠름)
quantizer = faiss.IndexFlatL2(dimension)
index = faiss.IndexIVFFlat(quantizer, dimension, nlist=100)
index.train(vectors)  # 학습 필요
index.add(vectors)
index.nprobe = 10  # 검색할 클러스터 수

# HNSW (매우 빠름)
index = faiss.IndexHNSWFlat(dimension, 32)  # 32 = M parameter
index.add(vectors)

# PQ (메모리 효율)
index = faiss.IndexPQ(dimension, 8, 8)  # M=8, nbits=8
index.train(vectors)
index.add(vectors)

저장/로드

# 저장
faiss.write_index(index, "index.faiss")

# 로드
index = faiss.read_index("index.faiss")

LangChain 연동

from langchain_community.vectorstores import FAISS
from langchain_openai import OpenAIEmbeddings

embeddings = OpenAIEmbeddings()

# 생성
vectorstore = FAISS.from_texts(
    texts=["text1", "text2"],
    embedding=embeddings
)

# 저장/로드
vectorstore.save_local("faiss_index")
vectorstore = FAISS.load_local("faiss_index", embeddings)

4. Pinecone

설치 및 설정

pip install pinecone-client
from pinecone import Pinecone, ServerlessSpec

# 클라이언트 생성
pc = Pinecone(api_key="your-api-key")

# 인덱스 생성
pc.create_index(
    name="my-index",
    dimension=384,
    metric="cosine",
    spec=ServerlessSpec(
        cloud="aws",
        region="us-east-1"
    )
)

# 인덱스 연결
index = pc.Index("my-index")

문서 추가

# Upsert (추가/업데이트)
index.upsert(
    vectors=[
        {"id": "vec1", "values": [0.1, 0.2, ...], "metadata": {"source": "a"}},
        {"id": "vec2", "values": [0.3, 0.4, ...], "metadata": {"source": "b"}},
    ]
)

# 배치 upsert
from itertools import islice

def chunks(iterable, batch_size=100):
    it = iter(iterable)
    chunk = list(islice(it, batch_size))
    while chunk:
        yield chunk
        chunk = list(islice(it, batch_size))

for batch in chunks(vectors, batch_size=100):
    index.upsert(vectors=batch)

검색

# 쿼리
results = index.query(
    vector=[0.1, 0.2, ...],
    top_k=5,
    include_metadata=True
)

for match in results['matches']:
    print(f"ID: {match['id']}, Score: {match['score']}")
    print(f"Metadata: {match['metadata']}")

# 메타데이터 필터링
results = index.query(
    vector=[0.1, 0.2, ...],
    top_k=5,
    filter={"source": {"$eq": "a"}}
)

LangChain 연동

from langchain_pinecone import PineconeVectorStore
from langchain_openai import OpenAIEmbeddings

embeddings = OpenAIEmbeddings()

vectorstore = PineconeVectorStore.from_texts(
    texts=["text1", "text2"],
    embedding=embeddings,
    index_name="my-index"
)

# 검색
docs = vectorstore.similarity_search("query", k=3)

5. 인덱싱 전략

인덱스 타입 비교

타입 정확도 속도 메모리 사용 시점
Flat 100% 느림 높음 소규모 (<100K)
IVF 95%+ 빠름 중간 중규모
HNSW 98%+ 매우 빠름 높음 대규모, 실시간
PQ 90%+ 빠름 낮음 메모리 제한

하이브리드 인덱스

# IVF + PQ
quantizer = faiss.IndexFlatL2(dimension)
index = faiss.IndexIVFPQ(
    quantizer,
    dimension,
    nlist=100,   # 클러스터 수
    m=8,         # PQ 세그먼트 수
    nbits=8      # PQ 비트 수
)
index.train(vectors)
index.add(vectors)

6. 메타데이터 활용

필터링 패턴

# Chroma 필터 문법
results = collection.query(
    query_texts=["query"],
    where={
        "$and": [
            {"category": "tech"},
            {"year": {"$gte": 2023}},
            {"author": {"$in": ["Alice", "Bob"]}}
        ]
    }
)

# 지원 연산자
# $eq, $ne: 같음, 다름
# $gt, $gte, $lt, $lte: 비교
# $in, $nin: 포함, 미포함
# $and, $or: 논리 연산

메타데이터 업데이트

# Chroma
collection.update(
    ids=["doc1"],
    metadatas=[{"source": "updated"}]
)

# Pinecone
index.update(
    id="vec1",
    set_metadata={"source": "updated"}
)

7. 실전 패턴

문서 관리 클래스

class VectorStore:
    def __init__(self, persist_dir="./db"):
        self.client = chromadb.PersistentClient(path=persist_dir)
        self.embedding_fn = embedding_functions.SentenceTransformerEmbeddingFunction()
        self.collection = self.client.get_or_create_collection(
            name="documents",
            embedding_function=self.embedding_fn
        )

    def add_documents(self, texts, metadatas=None, ids=None):
        if ids is None:
            ids = [str(uuid.uuid4()) for _ in texts]
        self.collection.add(
            documents=texts,
            metadatas=metadatas,
            ids=ids
        )
        return ids

    def search(self, query, k=5, where=None):
        results = self.collection.query(
            query_texts=[query],
            n_results=k,
            where=where
        )
        return results

    def delete(self, ids):
        self.collection.delete(ids=ids)

증분 업데이트

import hashlib

def get_doc_id(text):
    return hashlib.md5(text.encode()).hexdigest()

def upsert_documents(texts, collection):
    """중복 방지 업서트"""
    ids = [get_doc_id(t) for t in texts]

    # 기존 문서 확인
    existing = collection.get(ids=ids)
    existing_ids = set(existing['ids'])

    # 새 문서만 추가
    new_texts = []
    new_ids = []
    for text, doc_id in zip(texts, ids):
        if doc_id not in existing_ids:
            new_texts.append(text)
            new_ids.append(doc_id)

    if new_texts:
        collection.add(documents=new_texts, ids=new_ids)

    return len(new_texts)

배치 처리

def batch_add(collection, texts, batch_size=100):
    """대량 문서 배치 추가"""
    total = len(texts)
    for i in range(0, total, batch_size):
        batch = texts[i:i + batch_size]
        ids = [str(uuid.uuid4()) for _ in batch]
        collection.add(documents=batch, ids=ids)
        print(f"Added {min(i + batch_size, total)}/{total}")

8. 성능 최적화

임베딩 캐싱

import pickle
import os

class CachedEmbeddings:
    def __init__(self, model, cache_dir="./embed_cache"):
        self.model = model
        self.cache_dir = cache_dir
        os.makedirs(cache_dir, exist_ok=True)

    def embed(self, text):
        cache_key = hashlib.md5(text.encode()).hexdigest()
        cache_path = os.path.join(self.cache_dir, f"{cache_key}.pkl")

        if os.path.exists(cache_path):
            with open(cache_path, 'rb') as f:
                return pickle.load(f)

        embedding = self.model.encode(text)

        with open(cache_path, 'wb') as f:
            pickle.dump(embedding, f)

        return embedding

인덱스 최적화

# FAISS 검색 파라미터 튜닝
index.nprobe = 20  # 더 많은 클러스터 검색 (정확도 ↑, 속도 ↓)

# 병렬 검색
faiss.omp_set_num_threads(4)  # 스레드 수 설정

정리

선택 가이드

상황 추천
개발/프로토타입 Chroma
대규모 로컬 FAISS
프로덕션 관리형 Pinecone
오픈소스 셀프호스트 Qdrant, Milvus

핵심 코드

# Chroma
collection = client.create_collection("name")
collection.add(documents=texts, ids=ids)
results = collection.query(query_texts=["query"], n_results=5)

# FAISS
index = faiss.IndexFlatL2(dimension)
index.add(vectors)
distances, indices = index.search(query, k=5)

# LangChain
vectorstore = Chroma.from_texts(texts, embeddings)
docs = vectorstore.similarity_search("query", k=3)

다음 단계

12_Practical_Chatbot.md에서 대화형 AI 시스템을 구축합니다.

to navigate between lessons