SQLite-Vec

SQLite-Vec

sqlite-vec는 SQLite에서 바로 벡터 검색(Vector Search)을 할 수 있게 해주는 오픈소스 SQLite 확장이에요. 임베딩(embedding) 벡터를 vec0라는 가상 테이블에 저장하고, KNN(최근접 이웃) 검색을 순수 SQL 쿼리로 수행할 수 있어요. 별도의 전용 벡터 데이터베이스 서버 없이, 일반 SQLite 파일 하나에서 의미 기반 검색(semantic search), RAG, 추천 시스템 등을 구현하고 싶을 때 아주 유용해요.

노트북, 서버, 모바일 기기, WASM을 통한 브라우저, 라즈베리파이 등 어디서든 실행할 수 있고, Python·Ruby·Node.js/Deno/Bun·Go·Rust 등 여러 언어용 바인딩을 제공해요. CREATE·INSERT·SELECT 문만으로 동작해서 별도 설정이나 서버가 필요 없는 순수 SQL 방식이에요.

출처: 문서

본문

설치

여러 언어에서 패키지 매니저로 쉽게 설치할 수 있어요.

pip install sqlite-vec       # Python
npm install sqlite-vec       # Node.js
bun install sqlite-vec       # Bun
deno add npm:sqlite-vec      # Deno
gem install sqlite-vec       # Ruby
cargo add sqlite-vec         # Rust
go get -u github.com/asg017/sqlite-vec-go-bindings/cgo        # Go (CGO)
go get -u github.com/asg017/sqlite-vec-go-bindings/ncruces    # Go (WASM ncruces)
datasette install datasette-sqlite-vec                                 # Datasette
sqlite-utils install sqlite-utils-sqlite-vec                           # sqlite-utils

미리 컴파일된 로드 가능한 확장(loadable extension)은 sqlite-vec의 Github Releases에서 다운로드할 수 있어요. install.sh 스크립트를 쓰면 플랫폼에 맞는 확장을 자동으로 받아줘요.

# yolo
curl -L 'https://github.com/asg017/sqlite-vec/releases/latest/download/install.sh' | sh
# ok lets play it safe
curl -o install.sh -L https://github.com/asg017/sqlite-vec/releases/latest/download/install.sh
# inspect your scripts
cat install.sh
# TODO Test if execute permissions?
./install.sh

sqlite-vec는 단일 sqlite-vec.csqlite-vec.h 파일로 구성되어 있어서, 플랫폼별로 쉽게 컴파일하거나 더 큰 애플리케이션에 정적으로 링크할 수도 있어요.

vec0 가상 테이블 기본 사용법

vec0 가상 테이블에 벡터 열을 선언해서 사용해요. 열 타입으로 float[N]처럼 차원 수를 명시해요.

-- store 768-dimensional vectors in a vec0 virtual table
create virtual table vec_movies using vec0(
  synopsis_embedding float[768]
);

벡터는 JSON 또는 컴팩트한 BLOB 형태로 삽입할 수 있어요.

-- insert vectors into the table, as JSON or compact BLOBs
insert into vec_movies(rowid, synopsis_embedding)
  select
    rowid,
    embed(synopsis) as synopsis_embedding
  from movies;

KNN 검색

MATCH 절과 k 파라미터를 이용해 KNN 검색을 해요. 기본 거리 함수는 L2 유클리드 거리예요.

-- KNN search!
select
  rowid,
  distance
from vec_movies
where synopsis_embedding match embed('scary futuristic movies')
order by distance
limit 20;

vec0 테이블에서 KNN 쿼리를 하는 예시를 볼게요.

create virtual table vec_documents using vec0(
  document_id integer primary key,
  contents_embedding float[768]
);

insert into vec_documents(document_id, contents_embedding)
 select id, embed(contents)
 from documents;
select
 document_id,
 distance
from vec_documents
where contents_embedding match :query
 and k = 10;

SQLite 3.41 이상 버전에서는 k = 10 대신 LIMIT을 쓸 수도 있어요.

-- This example ONLY works in SQLite versions 3.41+
-- Otherwise, use the `k = 10` method described above!
select
 document_id,
 distance
from vec_documents
where contents_embedding match :query
limit 10; -- LIMIT only works on SQLite versions 3.41+

KNN 결과를 원본 테이블과 JOIN해서 다시 연결하고 싶다면 CTE를 사용할 수 있어요.

with knn_matches as (
 select
  document_id,
  distance
  from vec_documents
  where contents_embedding match :query
  and k = 10
)
select
 documents.id,
 documents.contents,
 knn_matches.distance
from knn_matches
left join documents on documents.id = knn_matches.document_id

벡터 열 정의에 distance_metric=cosine을 지정해서 코사인 거리로 바꿀 수도 있어요.

create virtual table vec_documents using vec0(
 document_id integer primary key,
 contents_embedding float[768] distance_metric=cosine
);

-- insert vectors into vec_documents...

-- this MATCH will now use cosine distance instead of the default L2 distance
select
 document_id,
 distance
from vec_documents
where contents_embedding match :query
 and k = 10;

vec0 테이블의 비벡터 열 3가지

vec0 가상 테이블에는 벡터가 아닌 열을 저장하는 방법이 3가지 있어요. 각각 장단점이 달라요.

create virtual table vec_chunks using vec0(
 document_id integer partition key,
 contents_embedding float[768],

 -- partition key column, denoted by 'partition key'
 user_id integer partition key,

 -- metadata column, appears as normal column definition
 label text,

 -- auxiliary column, denoted by '+'
 +contents text
);
열 종류 저장 가능한 데이터 장점 단점
Metadata 열 벡터와 함께 boolean·integer·float·text 저장 KNN 쿼리의 WHERE 절에 포함 가능 전체 스캔이 느리고, 긴 문자열(12자 초과)에 비효율적
Auxiliary 열 내부 별도 테이블에 모든 데이터 저장 외부 JOIN 불필요 KNN 쿼리의 WHERE 절에 사용 불가
Partition Key 주어진 키로 벡터 인덱스를 내부 샤딩 선택적 쿼리를 훨씬 빠르게 과도한 샤딩 시 KNN이 느려질 수 있음. partition key 값당 수백 개 이상의 벡터 권장

Metadata 열

Metadata 열은 vec0 테이블 정의에 포함할 수 있는 일반 열이에요. 선언된 벡터 열과 함께 인덱싱되며, KNN 쿼리 중에 추가 WHERE 조건을 걸 수 있어요.

create virtual table vec_movies using vec0(
 movie_id integer primary key,
 synopsis_embedding float[1024],
 genre text,
 num_reviews int,
 mean_rating float,
 contains_violence boolean
);

이 테이블에서 메타데이터 조건을 함께 적용한 KNN 쿼리 예시예요.

select *
from vec_movies
where synopsis_embedding match '[...]'
 and k = 5
 and genre = 'scifi'
 and num_reviews between 100 and 500
 and mean_rating > 3.5
 and contains_violence = false;

여기서 앞의 두 조건(synopsis_embedding matchk = 5)이 KNN 쿼리임을 나타내고, 나머지는 sqlite-vec가 KNN 계산 중에 적용하는 메타데이터 제약 조건이에요. 즉 최대 5개 행만 반환되며, 모두 메타데이터 값 조건을 만족해요.

Metadata 열은 벡터 생성자에서 일반 열 정의처럼 이름과 타입을 적어 선언해요. 지원하는 타입은 다음과 같으며 모두 엄격하게 타입이 지정돼요.

  • TEXT — 텍스트와 문자열
  • INTEGER — 8바이트 정수
  • FLOAT — 8바이트 부동소수점 숫자
  • BOOLEAN — 1비트 0 또는 1

타입 이름은 대소문자를 구분하지 않아요.

일반 테이블과 스칼라 함수로 수동 KNN

vec0 가상 테이블 없이도 sqlite-vec로 KNN 검색을 할 수 있어요. 일반 테이블의 일반 열에 벡터를 저장하고, vec_distance_L2(), vec_distance_L1(), vec_distance_cosine() 같은 스칼라 함수와 ORDER BY로 브루트포스 KNN을 수행하는 방식이에요.

create table documents(
 id integer primary key,
 contents text,
 -- a 4-dimensional floating-point vector
 contents_embedding blob
);

insert into documents values
 (1, 'alex', vec_f32('[1.1, 1.1, 1.1, 1.1]')),
 (2, 'brian', vec_f32('[2.2, 2.2, 2.2, 2.2]')),
 (3, 'craig', vec_f32('[3.3, 3.3, 3.3, 3.3]'));
select
 id,
 contents,
 vec_distance_L2(contents_embedding, '[2.2, 2.2, 2.2, 2.2]') as distance
from documents
order by distance;

/*
┌────┬──────────┬──────────────────┐
│ id │ contents │ distance         │
├────┼──────────┼──────────────────┤
│ 2  │ 'brian'  │ 0.0              │
│ 3  │ 'craig'  │ 2.19999980926514 │
│ 1  │ 'alex'   │ 2.20000004768372 │
└────┴──────────┴──────────────────┘
*/

vec0 가상 테이블 방식은 더 빠르고 컴팩트하지만 유연성이 떨어지고 원본 테이블로의 JOIN이 필요해요. 이 "수동" 방식은 더 유연하지만 성능은 떨어져요.

더 알아보기 (Learn more)