PostgreSQL 확장 기능

pg_vector

Drizzle 스키마 내에서 확장 기능을 생성하기 위한 특별한 코드는 없습니다. 벡터 타입, 인덱스, 쿼리를 사용하고 있다면, pg_vector 확장 기능이 설치된 PostgreSQL 데이터베이스를 사용하고 있다고 가정합니다.

pg_vector는 PostgreSQL을 위한 오픈소스 벡터 유사성 검색 도구입니다.

여러분의 데이터와 함께 벡터를 저장할 수 있습니다. 지원 기능은 다음과 같습니다:

컬럼 타입

vector

벡터 데이터를 다른 데이터와 함께 저장할 수 있습니다.

더 자세한 정보는 공식 pg_vector 문서를 참고하세요. 문서 보기

const table = pgTable('table', {
    embedding: vector({ dimensions: 3 })
})
CREATE TABLE IF NOT EXISTS "table" (
	"embedding" vector(3)
);

인덱스

Drizzle에서 제공하는 인덱스 API를 사용하면 PostGIS를 위한 모든 인덱스를 작성할 수 있습니다.

예제

// CREATE INDEX custom_idx ON table USING GIST (geom);

const table = pgTable('table', {
    geo: geometry({ type: 'point' }),
}, (table) => ({
    gist: index('custom_idx').using('gist', table.geo)
}))

L2 거리, 내적, 코사인 거리

// L2 거리 인덱스 생성
// CREATE INDEX ON items USING hnsw (embedding vector_l2_ops);
// 내적 인덱스 생성
// CREATE INDEX ON items USING hnsw (embedding vector_ip_ops);
// 코사인 거리 인덱스 생성
// CREATE INDEX ON items USING hnsw (embedding vector_cosine_ops);

const table = pgTable('items', {
    embedding: vector({ dimensions: 3 })
}, (table) => ({
    l2: index('l2_index').using('hnsw', table.embedding.op('vector_l2_ops')),
    ip: index('ip_index').using('hnsw', table.embedding.op('vector_ip_ops')),
    cosine: index('cosine_index').using('hnsw', table.embedding.op('vector_cosine_ops'))
}))

위 코드는 PostgreSQL에서 items 테이블에 대한 다양한 거리 측정 방식의 인덱스를 생성하는 예제입니다. 각 인덱스는 hnsw 알고리즘을 사용하며, vector_l2_ops, vector_ip_ops, vector_cosine_ops 연산자를 통해 각각 L2 거리, 내적, 코사인 거리를 계산합니다.

L1 거리, 해밍 거리, 자카드 거리 - pg_vector 0.7.0 버전에서 추가됨

// CREATE INDEX ON items USING hnsw (embedding vector_l1_ops);
// CREATE INDEX ON items USING hnsw (embedding bit_hamming_ops);
// CREATE INDEX ON items USING hnsw (embedding bit_jaccard_ops);

const table = pgTable('table', {
    embedding: vector({ dimensions: 3 })
}, (table) => ({
    l1: index('l1_index').using('hnsw', table.embedding.op('vector_l1_ops')),
    hamming: index('hamming_index').using('hnsw', table.embedding.op('bit_hamming_ops')),
    bit: index('bit_jaccard_index').using('hnsw', table.embedding.op('bit_jaccard_ops'))
}))

pg_vector 0.7.0 버전에서는 L1 거리, 해밍 거리, 자카드 거리와 같은 새로운 거리 측정 방법이 추가되었습니다. 이를 통해 벡터 데이터를 더욱 효율적으로 인덱싱하고 검색할 수 있습니다. 위의 코드 예제는 이러한 거리 측정 방법을 사용하여 인덱스를 생성하는 방법을 보여줍니다. 각각의 인덱스는 hnsw 알고리즘을 사용하며, vector_l1_ops, bit_hamming_ops, bit_jaccard_ops 연산자를 통해 거리를 계산합니다.

헬퍼 함수

쿼리를 작성할 때 벡터를 위한 미리 정의된 함수를 사용하거나 SQL 템플릿 연산자를 이용해 커스텀 함수를 만들 수 있습니다.

다음과 같은 헬퍼 함수를 사용할 수도 있습니다:

import { l2Distance, l1Distance, innerProduct, 
          cosineDistance, hammingDistance, jaccardDistance } from 'drizzle-orm'

l2Distance(table.column, [3, 1, 2]) // table.column  '[3, 1, 2]'
l1Distance(table.column, [3, 1, 2]) // table.column  '[3, 1, 2]'

innerProduct(table.column, [3, 1, 2]) // table.column  '[3, 1, 2]'
cosineDistance(table.column, [3, 1, 2]) // table.column  '[3, 1, 2]'

hammingDistance(table.column, '101') // table.column  '101'
jaccardDistance(table.column, '101') // table.column  '101'

만약 pg_vector에서 사용할 수 있는 다른 함수가 있다면, 기존에 있는 함수를 참고하여 구현을 복제할 수 있습니다. 아래는 그 방법입니다:

export function l2Distance(
  column: SQLWrapper | AnyColumn,
  value: number[] | string[] | TypedQueryBuilder | string,
): SQL {
  if (is(value, TypedQueryBuilder) || typeof value === 'string') {
    return sql`${column}  ${value}`;
  }
  return sql`${column}  ${JSON.stringify(value)}`;
}

원하는 이름으로 함수를 만들고 연산자를 변경하면 됩니다. 이 예제는 숫자 배열, 문자열 배열, 문자열, 심지어 쿼리까지 허용합니다. 원하는 타입을 자유롭게 만들거나, 기여하고 PR을 제출해도 좋습니다.

예제

pg_vector 문서에서 몇 가지 쿼리 예제를 가져와 Drizzle로 변환해 보겠습니다.

import { l2Distance } from 'drizzle-orm';

// SELECT * FROM items ORDER BY embedding  '[3,1,2]' LIMIT 5;
db.select().from(items).orderBy(l2Distance(items.embedding, [3,1,2]))

// SELECT embedding  '[3,1,2]' AS distance FROM items;
db.select({ distance: l2Distance(items.embedding, [3,1,2]) })

// SELECT * FROM items ORDER BY embedding  (SELECT embedding FROM items WHERE id = 1) LIMIT 5;
const subquery = db.select({ embedding: items.embedding }).from(items).where(eq(items.id, 1));
db.select().from(items).orderBy(l2Distance(items.embedding, subquery)).limit(5)

// SELECT (embedding  '[3,1,2]') * -1 AS inner_product FROM items;
db.select({ innerProduct: sql`(${maxInnerProduct(items.embedding, [3,1,2])}) * -1` }).from(items)

// 그리고 더 많은 예제들이 있습니다!

postgis

Drizzle 스키마 내부에서 확장 기능을 생성하는 특별한 코드는 없습니다. PostGIS 타입, 인덱스, 쿼리를 사용한다면, postgis 확장 기능이 설치된 PostgreSQL 데이터베이스를 사용하고 있다고 가정합니다.

PostGIS 웹사이트에서 언급한 대로:

PostGIS는 PostgreSQL 관계형 데이터베이스의 기능을 확장하여 지리 공간 데이터를 저장, 인덱싱, 쿼리하는 기능을 추가합니다.

PostGIS 확장 기능과 함께 introspect 또는 push 커맨드를 사용하면서 PostGIS 테이블을 포함하고 싶지 않다면, extensionsFilters를 사용하여 모든 PostGIS 테이블을 무시할 수 있습니다.

컬럼 타입: Geometry

geometry

여러분의 데이터와 함께 지리 데이터를 저장할 수 있습니다.

더 자세한 정보는 공식 PostGIS 문서를 참고하세요: PostGIS 공식 문서

const items = pgTable('items', {
  geo: geometry('geo', { type: 'point' }),
  geoObj: geometry('geo_obj', { type: 'point', mode: 'xy' }),
  geoSrid: geometry('geo_options', { type: 'point', mode: 'xy', srid: 4000 }),
});

mode

geometry 타입은 데이터베이스에서 매핑할 때 두 가지 모드를 제공합니다: tuplexy.

type

현재 릴리스에서는 point라는 미리 정의된 타입을 제공합니다. 이는 PostgreSQL PostGIS 확장에서의 geometry(Point) 타입에 해당합니다. 다른 타입을 사용하고 싶다면 여기에 원하는 문자열을 지정할 수 있습니다.