본문 바로가기
WIKI 기술 지식 베이스

AI & 임베딩

원문 보기 위키 갱신

AI & 임베딩 (AI & Embeddings)

벡터 유사도 검색(vector similarity search)이 Turso와 libSQL Server에 네이티브 기능으로 내장되어 있어요. 확장 프로그램 없이도 벡터 검색을 바로 쓸 수 있으니, 임베딩 기반 AI 애플리케이션을 만들 때 딱이에요.

출처: 문서

본문

Turso와 libSQL은 확장 프로그램 없이 벡터 검색 기능을 제공해요.

동작 방식

  • 벡터 컬럼(예: FLOAT32)을 하나 이상 가진 테이블을 만들어요
  • 벡터 값을 바이너리 형식으로 제공하거나, 적절한 변환 함수(예: vector32(...))로 텍스트 표현을 바이너리로 변환해요
  • 전용 벡터 함수(예: vector_distance_cos)로 테이블 안의 벡터들 사이 또는 쿼리 자체의 벡터와의 유사도를 계산해요
  • 최근접 이웃(nearest neighbors) 쿼리를 빠르게 하기 위해 특수한 벡터 인덱스를 만들어요(CREATE INDEX 문에서 libsql_vector_idx(column) 표현식을 사용해 벡터 인덱스를 만들어요)
  • 특수한 vector_top_k(idx_name, q_vector, k) 테이블 값 함수로 인덱스를 쿼리해요

벡터 (Vectors)

타입

LibSQL은 벡터 컬럼에 네이티브 SQLite BLOB 스토리지 클래스를 사용해요. SQLite 어피니티 규칙에 맞추기 위해 모든 타입 이름에 두 가지 대안이 있어요. 하나는 타이핑하기 쉬운 이름이고, 다른 하나는 어피니티 규칙과 일치하는 _BLOB 접미사를 가진 이름이에요.

라이브러리 작성자는 결과를 더 일반적이고 보편적으로 만들기 위해 _BLOB 접미사가 붙은 타입 이름을 쓰는 것이 좋아요. 일반 애플리케이션에서는 개발자가 어느 쪽이든 고를 수 있어요. 타입 이름은 SQLite와 외부 확장에 대한 힌트 역할만 하거든요.

LibSQL은 새로운 스토리지 클래스를 도입하지 않기 때문에, 벡터에 관한 모든 메타데이터도 BLOB 자체 안에 인코딩돼요. 행마다 몇 바이트의 비용이 들지만, 기능 설계가 크게 단순해져요.

아래 표는 LibSQL이 현재 지원하는 여섯 가지 벡터 타입을 나열한 거예요. 더 정밀하고 저장 공간을 많이 쓰는 타입부터 더 컴팩트하지만 덜 정밀한 타입 순으로 나열했어요(벡터의 차원 수 $D$를 사용해 벡터 하나의 저장 요구량을 계산해요).

타입 이름 저장 공간 (바이트) 설명
FLOAT64 | F64_BLOB $8D + 1$ 64비트 부동 소수점 수를 위한 IEEE 754 배정밀 형식 구현
FLOAT32 | F32_BLOB $4D$ 32비트 부동 소수점 수를 위한 IEEE 754 단정밀 형식 구현
FLOAT16 | F16_BLOB $2D + 1$ 16비트 부동 소수점 수를 위한 IEEE 754-2008 반정밀 형식 구현
FLOATB16 | FB16_BLOB $2D + 1$ 16비트 부동 소수점 수를 위한 bfloat16 형식 구현
FLOAT8 | F8_BLOB $D + 14$ LibSQL 고유 구현. 각 벡터 구성 요소를 단일 u8 바이트 b로 압축하고, 간단한 변환 $\texttt{shift} + \texttt{alpha} \cdot b$로 값을 재구성해요
FLOAT1BIT | F1BIT_BLOB $\lceil \frac{D}{8} \rceil + 3$ LibSQL 고유 구현. 각 벡터 구성 요소를 1비트로 압축하고 여러 구성 요소를 하나의 머신 워드에 패킹해 매우 컴팩트한 표현을 달성해요

대부분의 애플리케이션에서는 FLOAT32 타입이 좋은 출발점이에요. 하지만 테이블에 벡터를 담은 행이 많다면 더 컴팩트한 옵션을 탐색해 보고 싶을 수도 있어요.

FLOAT16과 FLOATB16은 같은 저장 공간을 쓰지만, 속도와 정확도 사이에서 서로 다른 절충안을 제공해요. 일반적으로 bfloat16에 대한 연산이 더 빠르지만 정밀도가 낮아져요.

함수

벡터를 다루기 위해 LibSQL은 벡터 도메인에서 동작하는 여러 함수를 제공해요. 각 함수는 위에서 설명한 여섯 타입에 맞는 바이너리 형식의 벡터나, 숫자 하나짜리 JSON 배열 형태의 텍스트 형식 벡터를 이해해요.

현재 LibSQL이 지원하는 함수는 다음과 같아요:

함수 이름 설명
vector64 | vector32 | vector16 | vectorb16 | vector8 | vector1bit 유효한 벡터를 받아 대응하는 대상 타입으로 변환하는 변환 함수
vector vector32 변환 함수의 별칭
vector_extract 유효한 벡터를 받아 텍스트 표현을 반환하는 추출 함수
vector_distance_cos 같은 타입이고 같은 차원의 벡터에 대해 동작하는 코사인 거리(1 - 코사인 유사도) 함수
vector_distance_l2 같은 타입이고 같은 차원의 벡터에 대해 동작하는 유클리드 거리 함수

벡터 사용하기

1. 테이블 만들기

F32_BLOB 데이터 타입으로 벡터를 저장할 컬럼을 선언하는 것부터 시작해요:

CREATE TABLE movies (
  title     TEXT,
  year      INT,
  embedding F32_BLOB(4) -- 4차원 f32 벡터
);

괄호 안의 숫자 (4)는 벡터의 차원 수를 지정해요. 즉 이 컬럼의 각 벡터는 정확히 4개의 구성 요소를 가져요.

2. 임베딩 생성 및 삽입

데이터의 임베딩을 생성하면(LLM을 통해) 테이블에 삽입할 수 있어요:

INSERT INTO movies (title, year, embedding)
VALUES
  ('Napoleon', 2023, vector32('[0.800, 0.579, 0.481, 0.229]')),
  ('Black Hawk Down', 2001, vector32('[0.406, 0.027, 0.378, 0.056]')),
  ('Gladiator', 2000, vector32('[0.698, 0.140, 0.073, 0.125]')),
  ('Blade Runner', 1982, vector32('[0.379, 0.637, 0.011, 0.647]'));

LangChain, Hugging Face, OpenAI 같은 널리 쓰이는 도구로 임베딩을 생성할 수 있어요.

3. 벡터 유사도 검색 수행하기

이제 벡터와 표준 SQLite 데이터를 결합한 쿼리를 작성할 수 있어요:

SELECT title,
       vector_extract(embedding),
       vector_distance_cos(embedding, vector32('[0.064, 0.777, 0.661, 0.687]')) AS distance
FROM movies
ORDER BY distance ASC;

거리 결과 이해하기

vector_distance_cos 함수는 다음과 같이 정의되는 코사인 거리를 계산해요:

코사인 거리는 0부터 2까지의 범위를 가지고:

  • 0에 가까운 거리는 벡터가 거의 동일하거나 정확히 일치한다는 뜻이에요
  • 1에 가까운 거리는 벡터가 직교(수직)라는 뜻이에요
  • 2에 가까운 거리는 벡터가 서로 반대 방향을 가리킨다는 뜻이에요

0에 아주 가까운 작은 음수(예: -10^-14)는 부동 소수점 연산 정밀도 때문에 가끔 나타날 수 있어요. 이런 값은 사실상 0으로 해석하면 돼요. 벡터 사이가 정확히 또는 거의 정확히 일치한다는 의미거든요.

SELECT vector_distance_cos('[1000]', '[1000]');
-- 출력: -2.0479999918166e-09

벡터 제한 사항

  • 1비트 FLOAT1BIT 벡터에는 유클리드 거리가 지원되지 않아요
  • LibSQL은 최대 65536 차원의 벡터까지만 다룰 수 있어요

인덱싱

최근접 이웃(NN) 쿼리는 다양한 AI 기반 애플리케이션에서 널리 쓰여요(RAG는 NN 쿼리로 관련 정보를 뽑아 내고, 추천 엔진은 임베딩 유사도에 기반해 항목을 추천할 수 있어요).

LibSQL은 벡터 컬럼을 가진 테이블에서 근사 최근접 이웃(approximate nearest neighbors) 쿼리를 빠르게 하기 위해 DiskANN 알고리즘을 구현했어요.

DiskANN 알고리즘은 검색 정확도를 속도와 바꾸는 방식이라, 행이 많은 테이블에서 LibSQL 쿼리가 약간 최적이 아닌 이웃을 반환할 수 있어요.

벡터 인덱스

LibSQL은 고정된 거리 함수(기본값은 코사인 유사도)에 대해 최근접 이웃 쿼리 속도를 높여 주는 사용자 지정 인덱스 타입을 도입했어요.

문법 측면에서 벡터 인덱스는 일반적인 애플리케이션 정의 B-Tree 인덱스와 달리, 벡터 컬럼을 다음처럼 libsql_vector_idx 마커 함수로 감싸야 해요:

CREATE INDEX movies_idx ON movies (libsql_vector_idx(embedding));

벡터 인덱스는 위에서 설명한 벡터 타입 중 하나인 컬럼에서만 동작해요.

벡터 인덱스는 LibSQL 코어에 완전히 통합되어 있어서, 일반 인덱스의 모든 연산과 대부분의 기능을 물려받아요:

  • 기존 데이터가 있는 테이블에 만든 인덱스는 이 데이터로 자동으로 채워져요
  • 베이스 테이블의 모든 업데이트는 인덱스에 자동으로 반영돼요
  • REINDEX movies_idx 명령으로 인덱스를 처음부터 다시 만들 수 있어요
  • DROP INDEX movies_idx 명령으로 인덱스를 삭제할 수 있어요
  • 사용자 지정 필터링 규칙으로 부분(partial) 벡터 인덱스를 만들 수 있어요:
CREATE INDEX movies_idx ON movies (libsql_vector_idx(embedding))
WHERE year >= 2000;

쿼리

현재로서는 벡터 인덱스를 반드시 특수한 vector_top_k(idx_name, q_vector, k) 테이블 값 함수로 명시적으로 쿼리해야 해요. 이 함수는 인덱스 이름, 쿼리 벡터, 반환할 이웃 수를 받아요. k개의 근사 최근접 이웃을 검색해 해당 행의 ROWID를 반환하고, 베이스 인덱스가 ROWID를 갖지 않는 경우 PRIMARY KEY를 반환해요.

테이블 값 함수가 동작하려면 쿼리 벡터는 반드시 같은 벡터 타입과 차원 수를 가져야 해요.

설정

LibSQL 벡터 인덱스는 선택적으로 설정을 받을 수 있어요. 설정은 libsql_vector_idx 함수의 가변 매개변수로 key=value 형식의 문자열로 지정해요:

CREATE INDEX movies_idx
ON movies(libsql_vector_idx(embedding, 'metric=l2', 'compress_neighbors=float8'));

현재 LibSQL이 지원하는 설정은 다음과 같아요:

설정 키 값 타입 설명
metric cosine | l2 인덱스를 만들 때 사용할 거리 함수.
기본값: cosine
max_neighbors 양의 정수 DiskANN 그래프의 각 노드에 저장할 이웃 수. 설정값이 낮을수록 인덱스 저장 공간이 줄어들지만 검색 정밀도가 떨어져요.
기본값: $3 \sqrt{D}$ ($D$는 벡터 컬럼의 차원 수)
compress_neighbors float1bit|float8|
float16|floatb16|
float32
DiskANN 그래프에서 각 노드의 이웃을 저장할 때 사용할 벡터 타입. 더 컴팩트한 벡터 타입을 쓸수록 인덱스 저장 공간이 줄어들지만 검색 정밀도가 떨어져요.
기본값: 압축 없음 (이웃은 베이스 테이블과 같은 타입)
alpha 1 이상의 양의 실수 DiskANN 알고리즘에서 만드는 일반 희소 이웃 그래프의 "밀도" 매개변수. 값이 낮을수록 DiskANN 그래프가 희소해져 쿼리 속도가 빨라지지만 검색 정밀도가 낮아져요.
기본값: 1.2
search_l 양의 정수 벡터 검색 동안 방문할 이웃 수를 제한하는 설정. 값이 낮을수록 검색 쿼리가 빨라지지만 검색 정밀도가 떨어져요.
기본값: 200
insert_l 양의 정수 벡터 삽입 동안 방문할 이웃 수를 제한하는 설정. 값이 낮을수록 삽입 쿼리가 빨라지지만 DiskANN 그래프 탐색 속성이 나빠져요.
기본값: 70

타입 T1 컬럼의 벡터 인덱스가 max_neighbors=M과 compress_neighbors=T2를 사용할 때, N개 행에 대해 대략 $\texttt{N} (Storage(\texttt{T1}) + \texttt{M} \cdot Storage(\texttt{T2}))$ 바이트의 저장 공간을 사용해요.

인덱스 사용하기

1. 테이블 만들기

F32_BLOB 데이터 타입으로 벡터를 저장할 컬럼을 선언하는 것부터 시작해요:

CREATE TABLE movies (
  title     TEXT,
  year      INT,
  embedding F32_BLOB(4) -- 4차원 f32 벡터
);

괄호 안의 숫자 (4)는 벡터의 차원 수를 지정해요. 즉 이 컬럼의 각 벡터는 정확히 4개의 구성 요소를 가져요.

2. 임베딩 생성 및 삽입

데이터의 임베딩을 생성하면(LLM을 통해) 테이블에 삽입할 수 있어요:

INSERT INTO movies (title, year, embedding)
VALUES
  ('Napoleon', 2023, vector32('[0.800, 0.579, 0.481, 0.229]')),
  ('Black Hawk Down', 2001, vector32('[0.406, 0.027, 0.378, 0.056]')),
  ('Gladiator', 2000, vector32('[0.698, 0.140, 0.073, 0.125]')),
  ('Blade Runner', 1982, vector32('[0.379, 0.637, 0.011, 0.647]'));

LangChain, Hugging Face, OpenAI 같은 널리 쓰이는 도구로 임베딩을 생성할 수 있어요.

3. 인덱스 만들기

libsql_vector_idx 함수로 인덱스를 만드세요:

CREATE INDEX movies_idx ON movies(libsql_vector_idx(embedding));

이렇게 하면 embedding 컬럼에 대한 벡터 유사도 검색에 최적화된 인덱스가 만들어져요.

libsql_vector_idx 마커 함수는 필수예요. libSQL이 ANN 인덱스와 일반 B-Tree 인덱스를 구분하는 데 사용하거든요.

4. 인덱싱된 테이블 쿼리하기

SELECT title, year
FROM vector_top_k('movies_idx', vector32('[0.064, 0.777, 0.661, 0.687]'), 3)
JOIN movies ON movies.rowid = id
WHERE year >= 2020;

이 쿼리는 vector_top_k 테이블 값 함수를 사용해 인덱스로 [0.064, 0.777, 0.661, 0.687]과 가장 유사한 상위 3개 벡터를 효율적으로 찾아요.

인덱스 제한 사항

  • 벡터 인덱스는 ROWID가 있는 테이블 또는 단일 PRIMARY KEY를 가진 테이블에서만 동작해요. ROWID 없는 복합 PRIMARY KEY는 지원되지 않아요

더 알아보기 (Learn more)