본문 바로가기

DEVELOP_NOTE/ML

Vector DB란 무엇일까? 그리고 어떤 Vector DB를 사용해야할까? (1)

오늘은 VectorDB의 종류와 장단점에 대해서 살펴보려 한다.

먼저 VectorDB는 다른 DB들과 어떻게 다른지 간단하게 정리하고 vector DB에 대해 자세히 알아보도록 하자.

 


 

1. DataBase의 종류

DataBase는 크게 '관계형 데이터베이스', '비관계형 데이터베이스', 그리고 최근에 주목받고 있는 '벡터 데이터베이스'로 분류할 수 있다.

 

 

1) 관계형 데이터베이스(RDBMS)


관계형 DB는 데이터를 테이블로 구성하여 관리하는 데이터베이스를 의미한다.

SQL(Structured Query Language)을 사용하여 데이터를 쿼리하고 조작하며, 강력한 데이터 무결성을 가지며,

복잡한 쿼리 처리 능력, 트랜잭션 관리 등이 가능한 DB형태를 가진다.
대표적으로는 MySQL, PostgreSQL, Oracle, Microsoft SQL Server 등이있고,
주로 금융 시스템, 고객 관리 시스템, 온라인 상점 등 데이터 관계가 중요한 애플리케이션과 같은 서비스에 사용된다.

 

2) 비관계형 데이터베이스(NoSQL)


비관계형 DB는 스키마가 없거나 유연한 스키마를 가지며, 대량의 분산된 데이터를 저장하고 관리하는 데 최적화된 DB이다.

키-값 저장소, 문서 저장소, 와이드 컬럼 저장소, 그래프 DB 등 다양한 유형이 있으며,

확장성과 유연한 데이터 모델, 빠른 쓰기/읽기 성능을 특징으로 가진다.
대표적으로 MongoDB, Cassandra, Redis, Neo4j 등이 있으며, 주로 소셜 네트워크, 실시간 애널리틱스, 

빅데이터 처리 등에 사용된다.

예를들어 아래와 같은 형태로 데이터가 저장되는 DB로 이해하면 된다.

 

ex) MongoDB

# 아래는 MongoDB에서 Json형태로 저장된 데이터형태의 예시이다.
{
  "_id": "12345",
  "name": "우리흥",
  "email": "sony@naver.com",
  "age": 32,
  "interests": ["music", "football", "coding"]
}

 

ex) Redis

# 아래는 Redis에서 key:value형태로 저장된 데이터의 예시이다. 다만, 키밸류형태 외에도 str, list, set 등 다양한 형태로 저장될 수 있다.

Key: "user:1000"
Value: "Sony"

 

ex) Cassandra

# 아래는 Cassandra와 같은 컬럼 패밀리 데이터베이스에서 사용되는 '열'기반의 구조 저장 방식이다. 
# 각 컬럼은 컬럼이름, 값, timestamp로 구성된다

{
  "userId": "1001",
  "columns": {
    "name": "Sony",
    "email": "sony@naver.com",
    "age": 25
  }
}

 

ex) Neo4j

# Neo4j와 같은 그래프 데이터베이스는 노드(엔티티), 관계(엔티티간의 연결), 속성(노드와 관계에 대한 정보)으로 데이터를 저장한다.

(Node:Player {name: "Sony", age: 30})-[RELATIONSHIP:FRIEND_OF]->(Node:Person {name: "Minjae Kim", age: 25})

 

 

3) 벡터 데이터베이스(VectorDB)


오늘 알아볼 Vector DB는 AI와 머신러닝 모델에서 생성된 벡터를 효과적으로 관리할 수 있도록 설계된 모델이다.

높은 차원의 데이터에서도 빠른 검색 속도, 유사 벡터 검색 기능을 지원하며,
대표적으로는 Milvus, Faiss (Facebook AI Similarity Search), Chroma, Qdrant, Pinecone과 같은 vectorDB가 있다.
주로, 이미지 검색, 추천 시스템, 자연어 처리 등 AI 기반의 애플리케이션에 사용되는 DB이다.

 

사실 그냥 정의만 들어서는 Vector DB의 개념과 사용형태에 대해 이해하 잘 되지않을 수 있는데,

아래 저장되는 형태와 검색과정 예시를 보면 어느정도 감이 잡힐 것이다.

아래 예시는 가장 대표적인 VectorDB중 하나인 'Milvus'를 기준으로 데이터를 저장하고 검색하는 과정을 정리하였다.

from milvus import Milvus, DataType

# 가장 먼저 Milvus 클라이언트를 초기화한다.
# Milvus 서버에 연결
milvus_client = Milvus(host='localhost', port='19530')

######################################################################

# 뒤에 다루겠지만, 컬렉션이라는 개념에 대해(RDBMS의 테이블과 유사한 개념)정의하고 생성한다.
collection_name = "example_collection"
fields = [
    {"name": "ID", "type": DataType.INT64, "is_primary": True, "auto_id": True},
    {"name": "vector", "type": DataType.FLOAT_VECTOR, "params": {"dim": 128}},
    {"name": "timestamp", "type": DataType.INT64},
    {"name": "description", "type": DataType.VARCHAR, "params": {"max_length": 1024}}
]

# 컬렉션 생성
milvus_client.create_collection(collection_name, fields=fields)

######################################################################

# 데이터는 아래와 같이 추가된다
# insert data define
vectors = [[0.1, 0.2, ..., 0.128]]  # 128차원 벡터 예시
timestamps = [1629264352]
descriptions = ["Example vector"]

# 데이터 insert
insert_data = {
    "vector": vectors,
    "timestamp": timestamps,
    "description": descriptions
}
milvus_client.insert(collection_name, insert_data)

######################################################################

# 인덱스 생성(인덱스 개념도 뒤에 설명하겠지만, 쿼리에 대한 색인이 가능하게 하는 기준 개념으로 이해하자)
index_param = {
    "index_type": "IVF_FLAT",
    "metric_type": "L2",
    "params": {"nlist": 4096}
}
milvus_client.create_index(collection_name, "vector", index_param)

# 검색 실행
search_param = {
    "params": {"nprobe": 10},
    "metric_type": "L2",
    "topk": 10
}
query_vectors = [[0.1, 0.2, ..., 0.128]]
results = milvus_client.search(collection_name, "vector", query_vectors, search_param)

 

Vector DB는 DB에 따라 지원하는 쿼리 검색방법, 특징이 약간씩 다른데, 구체적인 내용은 아래에서 천천히

확인해보도록 하자.

 


 

자 이제, 위의 각 데이터베이스의 특징을 비교 정리해보면, 

 

'관계형 데이터베이스'는 정형화된 테이블 구조를 사용하며, 테이블과 테이블간의 관계를 가지며, 높은 무결성을 바탕으로 SQL을 사용하여 데이터를 조회하고 처리할 수 있는 DB를 의미한다.

 

'비관계형 데이터베이스'는 관계형 데이터베이스와 달리 테이블간의 관계를 정의하지 않고,

키-값 저장소, 문서 저장소, 컬럼 패밀리 저장소, 그래프 데이터베이스 등과 같은 다양한 '데이터 모델'을 사용하여 데이터를 저장하며,

각 DB고유의 쿼리나 API를 통해 데이터를 핸들링할 수 있는 DB를 의미한다.

 

여기서 위의 '다양한 데이터 모델'이라는 개념이 모호한데, 아래 몇가지 예시들을 통해 이해할 수 있을것이다.


1) MQL

문서 지향 DB인 MongoDB는 JSON 유사 문서를 사용하며, MQL(MongoDB Query Language)을 사용하여 데이터를

조회하고 조작한다.

 

2) CQL
또한, 컬럼 패밀리 데이터 모델을 사용하는 Cassandra는 SQL과 유사한 구문을 가진 CQL(Cassandra Query Language)

을 사용하여 데이터에 접근한다.

 

3) Cypher

그래프 데이터베이스인 Neo4j는 그래프 쿼리 언어인 Cypher를 사용하여 노드, 관계, 패턴을 쿼리할 수 있다.

 

4) 간단한 명령어 세트 사용
키-값 저장소인 Redis는 SQL 혹은 CQL과 같은 전통적인 쿼리 언어 대신, 간단한 명령어 세트를 사용하여 데이터를 조작할 수 있다.


 

자, 이제 각 DB가 어떻게 구분되고 사용되어지는지와, VectorDB의 활용성에 대해 대략적으로 이해가 되었을것이다.

이제부터, 오늘 알아볼 Vector Database에 대해서 구체적으로 살펴보도록 하자.

 


2. Vector Database ?

vector db의 가장 큰 특징을 뽑는다면,

1) 이미지, 오디오, 텍스트 등 다양한 비정형 정보를 고차원의 'Vector'형태로 저장한다.
2) ANN(Approximate Nearest Neighbor)과 같은 유사도 기반의 검색방식을 택한다.
3) 빠른 검색 속도

정도로 요약할 수 있다.

 

"이미지, 오디오, 텍스트 등 다양한 비정형 정보를 'Vector'형태로 저장한다 ?"

VectorDatabase는 AI에 적용하기 용이한 특징으로, 최근 주목 받고있는데,

이는 딥러닝 모델이 생성하는 대량의 고차원 벡터 데이터를 효율적으로 저장하고 검색할 수 있기 때문이다.

 

먼저 vectorDB에 데이터를 input하기 위해서는 

이미지, 오디오, 텍스트 등의 데이터는 임베딩이 가능한 딥러닝 모델을 통해 vectorization을 진행할 수 있다.

 

모든 딥러닝 모델은 벡터를 통해서 계산되어지고, 특징을 추출할 수 있는데,

Vector DB는 이렇게 생성된 벡터값을 저장, 관리, 사용하는데 강점이 있는 데이터베이스로

특히, 최근 LLM과 유사도 기반의 검색방식을 통합한 RAG(Retrieval-Augmented Generation)방식이 유행함에 따라,

vector를 저장하고, 검색할 수 있는 vector DB가 많이 사용되고 있다.

 

 

"ANN(Approximate Nearest Neighbor)과 같은 유사도 기반의 검색방식을 택한다"

사실 위 내용에서 ANN 보다도 '유사도 기반의 검색방식'을 택한다라는 부분을 기억하자.

Vector DB는 이름그대로 vector를 저장하는 데이터베이스로, 전체 데이터 중 주어진 쿼리와 가장 '유사한' document를 추출해내는것을

목적으로하는 데이터베이스이다.

테이블 구조를 가지고 key를 기반으로 데이터를 얻는 관계형데이터베이스에 반해, Vector DB는 전체 데이터들과 쿼리간의 유사도를 계산하고, 가장 유사도가 높은 document를 추출해야한다는 목적성과 특성때문에

'정확하고 빠른' document 추출이 Vector DB 구축에 가장 핵심요소라고 할 수 있다.

이 때문에, 뒤에 설명할 Vector DB들은 각각 여러가지의 검색 방식을 제공하고 있는데, 대표적인 Vector DB 검색 방식들을 정리해보도록 하자.

 

 


 

1) 브루트 포스 검색 (Brute-Force Search) == 'Flat Indexing'

브루트 포스 검색 방식은 모든 데이터포인트를 순회하며, 주어진 쿼리 포인트와의 거리를 일일이 계산한다.

가장 기본적이고 단순하면서도 확실한 형태의 검색방법인데, 당연히 데이터셋의 크기가 커질수록

계산량이 기하급수적으로 증가하여 비효율적이다.

 

2) Approximate Nearest Neighbor(ANN)

ANN 알고리즘은 가장 널리 사용되는 검색방법으로 '완벽한' 최근접 이웃을 찾기보다는 '근사적으로'가장 가까운 이웃을 찾는 방법이다.

여기서 거리계산의 '근사적'이라는 표현은 완벽한 정확도를 포기하고, 대신 검색 속도를 크게 향상시키는

트레이드 오프를 찾는 방식을 의미한다.

 

예를들어, 데이터를 미리 클러스터링하여 검색공간을 줄인다거나, 여러 버킷으로 나눈다음, 주어진 쿼리 포인트가 속할 가능성이

가장 높은 몇 개의 버킷만 검색하는 방식이다.

 

대표적으로,

1) Faiss(Facebook AI Similarity Search)

2) Annoy(Approximate Nearest Neighbors Oh Yeah)

 

와 같은 검색방식들이 이에 해당하는데, 위 검색방식들은 실제로 Vector DB 사용 시, 매우 많이 활용되는 개념으로

좀 더 자세하게 짚고 넘어가도록 하자.

1) Faiss(Facebook AI Similarity Search)
Faiss는 고차원 벡터 공간에서의 유사성 검색과 클러스터링 작업을 위해 설계된 라이브러리로, GPU가속을 지원하며, 양자화(Quantization)와 인덱싱을 통해 메모리 사용량을 최적화를 지원하는 검색 방식이다.
유사도 검색의 여러방법 중 ANN기법을 사용하는 대표적인 검색시스템이지만, ANN외에도 양자화, 고급 인덱싱기능도 지원한다고
이해하면 될 것 같다.
다른 검색방식에 대비된 장점으로는 
   - 대규모 데이터셋에서 우수한 검색속도 및 정확도
   - GPU를 통한 검색 가속 지원
   - 다양한 인덱싱 방법을 제공
이 있으며, 단점으로는 오히려 다양한 인덱싱 옵션과 파라미터 설정이 다소 복잡하게 느껴질 수 있고, 일부 인덱싱방법은 메모리 사용량이 많을 수 있다는 점이 꼽힌다.
아래에 Faiss 라이브러리를 통해 유사 벡터를 검색하는 과정을 살펴보자.
import numpy as np
import faiss

dimension = 64  # 벡터 차원
n = 10000  # 데이터베이스 크기
np.random.seed(123)
db_vectors = np.random.random((n, dimension)).astype('float32')
query_vectors = np.random.random((1, dimension)).astype('float32')

# 인덱스 생성 및 벡터 추가
index = faiss.IndexFlatL2(dimension)  # L2 거리를 사용하는 flat 인덱스
index.add(db_vectors)

# 쿼리에 대한 가장 가까운 이웃 검색
k = 4  # 찾고자 하는 이웃의 수
D, I = index.search(query_vectors, k)  # D: 거리, I: 인덱스

print("Nearest neighbors: ", I)
print("Distances: ", D)


2) Annoy(Approximate Nearest Neighbors Oh Yeah)
Annoy는 메모리 사용량을 최적화하면서 대용량 데이터셋에서도 빠르게 검색할 수 있는 방법으로, 트리를 기반으로 한 인덱스 구조를 사용하여, 검색 시간과 메모리 사용량 사이의 균형을 맞춘다. 

장점으로는
   - 디스크에 인덱스를 저장하고, 메모리 매핑 방식을 사용하여, 메모리 사용량을 최적화할 수 있다는 점
   - 설정 방식이 간단하다는 점
   - 데이터를 실시간으로 추가하고, 인덱스를 업데이트할 수 있다는 점
정도가 꼽힌다.
다만, 단점으로
   - GPU가속을 지원하지 않아, Faiss에 비해 대규모 데이터셋 처리 시, 검색속도가 느릴 수 있다는 점
   - Annoy는 근사 검색 방식으로 동작하기때문에, Faiss에 비해 다소 정확도가 낮을 수 있다는 점이 꼽힌다.
아래는 Annoy 라이브러리를 통해서 유사 데이터 포인트를 추출하는 예시코드이다.
from annoy import AnnoyIndex
import random

dimension = 40
n_trees = 10
index = AnnoyIndex(dimension, 'euclidean')

# 데이터 추가
for i in range(1000):
    vector = [random.gauss(0, 1) for z in range(dimension)]
    index.add_item(i, vector)

index.build(n_trees)

# 쿼리 벡터
query_vector = [random.gauss(0, 1) for z in range(dimension)]

# 가장 가까운 이웃 검색
nearest_neighbors = index.get_nns_by_vector(query_vector, 5, include_distances=True)

print("Nearest neighbors: ", nearest_neighbors)​


 


3) 고급 인덱싱 기반 검색 

데이터를 구조화하여 검색속도를 향상시키는 방법이다. 즉, 데이터를 미리 인덱싱하여 검색 시간을 단축시키는 방법인데,

예를들어 위의 Faiss의 경우,  데이터를 군집화하여 검색범위를 좁히는 방식인 IVF(Inverted File Index)를 통해 검색범위를 좁혀 ANN(최근접 이웃 검색)을 구현하거나, 벡터를 양자화해서 메모리사용을 줄이는 PQ(Product Quantization)방식을 통해 ANN을 구현할 수 있는 것이다.

 

아래는 고급 인덱싱 기반 검색 방법들에 대한 설명이다.

1) IVF(Inverted File Index)
IVF방식은 데이터셋을 미리 정의된 수의 군집(centroid)으로 나누고, 각 벡터를 가장 가까운 centroid에 할당함으로써 검색범위를 제한하는 방식으로 ANN에 공헌한다. 검색 시, 쿼리벡터가 속하는 군집을 먼저 찾고, 해당 군집내에서 가장 유사한 벡터를 찾는 방식이라고 이해하면 된다.
빠른 검색속도를 구현했지만, 매 검색 시 적절한 군집수를 선택해야하고, 군집화에 시간이 소요될 수 있다는 점이 단점이다.

2) PQ(양자화 기반 검색, Product Quantization)
PQ방식은 고차원 벡터를 여러개의 하위 벡터로 분할하고, 각 하위 벡터를 양자화하여 저장공간을 줄이는 방식이다. 
조금 더 구체적으로 설명하면, 예를들어 2차원 벡터공간에서 4개의 코드워드로 구성된 코드북을 생성했다고 가정하자.
이때, 코드워드는 벡터공간내의 대표 포인트를 의미하는데, centroid와 비슷한 개념으로 이해했다.
그리고 코드북은 이러한 코드워드들의 집합이다.
결국 모든 포인트는 가장 가까운 코드워드로 대체되며 전체 데이터포인트의 크기는 줄어들게 된다.
이를 양자화로 표현하는것이고, 이로 인해 줄어든 공간에서의 검색속도 또한 범위가 줄어들기 때문에 빨라질 수 밖에 없다는
원리이다.

3) HNSW(Hierarchical Navigable Small World)
HNSW방식은 다계층 그래프 구조를 사용하여, 고차원 데이터에 대한 효윬적인 최근접 이웃 검색을 수행하는 방법이다.
HNSW방식은 속도도 빠르고, 다른 인덱싱 기법에 비해 정확하다는 강점을 가진다.

4) LSH(Locality-Sensitive Hashing)
데이터 포인트를 해시 테이블에 매핑하여, 쿼리 벡터와 유사한 데이터 포인트들을 빠르게 검색하는 방법이다. LSH는 유사한 객체가
'같은 버킷'에 할당될 확률을 높이는 해시 함수를 사용한다.

 


 

 

 

지금까지, Vector DB의 특성과 Vector DB를 검색하는 여러 방법론들에 대해서 알아보았다.

분량이 많아, 각 Vector DB들의 비교정보는 다음에 포스팅 하도록 하겠다.

 

Vector DB 별 비교 정리 바로가기>  https://familia-89.tistory.com/90

 

Reference