· drizzle / postgres / pgvector

Hướng dẫn cài đặt Drizzle ORM với Postgres và pgvector

Cài đặt Drizzle ORM với Postgres 16 và pgvector. Bao gồm bước CREATE EXTENSION mà hầu hết hướng dẫn bỏ qua và mẫu query cosine distance để tận dụng HNSW index.

Bởi

1.828 từ · 10 phút đọc

Drizzle 0.45.2 với postgres.js trên Postgres 16 là bộ công cụ nên dùng. Có hai điểm khiến hầu hết mọi người mắc kẹt trước khi thấy được một query chạy đúng: extension vector không tự cài đặt, và mẫu query cosine distance trông có vẻ đúng nhưng lại âm thầm bỏ qua HNSW index. Bài viết này xử lý cả hai, kèm các lệnh cụ thể cần chạy.

Bài viết này dành cho ai

Các developer TypeScript đang thêm tính năng tìm kiếm embedding vào database Postgres. Bạn cần có dự án Node.js 18+ đang hoạt động với TypeScript. Nếu bạn còn đang cân nhắc ORM nào phù hợp, Drizzle vs KyselyPrisma vs Drizzle so sánh các lựa chọn — bài viết này giả định bạn đã chọn Drizzle và đang kết nối với Postgres.

Những gì chúng tôi đã thử nghiệm

  • drizzle-orm 0.45.2
  • drizzle-kit 0.31.10
  • postgres (postgres.js) 3.x
  • pgvector 0.8.2
  • PostgreSQL 16
  • Node.js 22

Bước 1: Yêu cầu trước khi bắt đầu

Bạn cần:

  • Node.js 18+
  • PostgreSQL 16 (hỗ trợ từ 13+, khuyến nghị dùng 16)
  • Extension pgvector có sẵn trên instance của bạn

Cách nhanh nhất để có Postgres 16 với pgvector ở local:

docker run -d \
  --name pgvector-dev \
  -e POSTGRES_PASSWORD=postgres \
  -p 5432:5432 \
  pgvector/pgvector:pg16

Lệnh này dùng Docker image chính thức của pgvector. Extension đã được biên dịch sẵn — bạn vẫn cần bật nó theo từng database (Bước 4), nhưng nó đã có sẵn.

Lỗi thường gặp: Nếu bạn đang dùng managed provider (RDS, Cloud SQL, AlloyDB), hãy xác nhận pgvector có trên tier của bạn trước khi bắt đầu. AWS RDS Postgres hỗ trợ pgvector trên tất cả instance types chạy PostgreSQL 15.3+ (nguồn); lưu ý db.t3.micro chỉ có 1 GB RAM, điều này hạn chế hiệu năng HNSW indexing khi có tải. pgvector là extension được hỗ trợ trên Google Cloud SQL — chạy CREATE EXTENSION IF NOT EXISTS vector; qua psql hoặc Cloud Shell để bật (nguồn). Kiểm tra điều này sớm giúp bạn tránh một lần migration thất bại về sau.

Bước 2: Cài đặt packages

npm install [email protected] postgres
npm install -D [email protected] tsx

postgres ở đây là postgres.js — driver được hỗ trợ tốt, có TypeScript types đầy đủ và tích hợp gọn với connection pooling của Drizzle. Drizzle cũng hỗ trợ node-postgres (xem Drizzle docs), nhưng postgres.js nhẹ hơn và là driver được dùng xuyên suốt bài viết này.

Lỗi thường gặp: Cài drizzle-orm không ghim version có thể cho bạn phiên bản cũ hơn 0.31.0. Phiên bản đó thêm hỗ trợ column pgvector. Các phiên bản cũ hơn không nhận diện kiểu column vector(), và bạn sẽ gặp lỗi type khi định nghĩa schema. Luôn ghim version khi dùng pgvector.

Bước 3: Cấu hình Drizzle

Tạo file drizzle.config.ts ở thư mục gốc dự án:

import { defineConfig } from 'drizzle-kit';

export default defineConfig({
  schema: './src/schema.ts',
  out: './drizzle',
  dialect: 'postgresql',
  dbCredentials: {
    url: process.env.DATABASE_URL!,
  },
});

Thêm database URL vào .env:

DATABASE_URL=postgres://postgres:postgres@localhost:5432/mydb

Lỗi thường gặp: Nếu bạn đặt dialect: 'mysql' hoặc dialect: 'sqlite', các kiểu column pgvector sẽ không khả dụng — chúng chỉ dành cho Postgres. TypeScript vẫn compile được, nhưng SQL sinh ra sẽ sai.

Bước 4: Bật extension pgvector

Đây là bước hầu hết các hướng dẫn bỏ qua. Cả drizzle-kit push lẫn drizzle-kit migrate đều không chạy CREATE EXTENSION cho bạn. Bạn phải làm điều này trước bất kỳ migration nào tạo vector column.

Tạo file drizzle/0000_enable_vector.sql:

CREATE EXTENSION IF NOT EXISTS vector;

Chạy nó trên database:

psql $DATABASE_URL -f drizzle/0000_enable_vector.sql

Nếu bạn muốn giữ mọi thứ trong code, có thể chạy qua Drizzle client trước lần migration đầu tiên:

import postgres from 'postgres';

const sql = postgres(process.env.DATABASE_URL!);
await sql`CREATE EXTENSION IF NOT EXISTS vector`;
await sql.end();

Lỗi thường gặp: Bỏ qua bước này, migration đầu tiên của bạn sẽ báo lỗi type "vector" does not exist. Migration dừng lại, và trạng thái migration của Drizzle trở nên không nhất quán — nó ghi nhận migration là thất bại, nhưng một số SQL trước đó trong cùng file có thể đã được áp dụng một phần. Cách khắc phục: chạy CREATE EXTENSION IF NOT EXISTS vector; trực tiếp trên database, xóa bảng bị tạo một phần nếu có, rồi chạy lại migration.

Bước 5: Định nghĩa schema

Tạo file src/schema.ts:

import { pgTable, serial, text, index } from 'drizzle-orm/pg-core';
import { vector } from 'drizzle-orm/pg-core';

export const documents = pgTable(
  'documents',
  {
    id: serial('id').primaryKey(),
    content: text('content').notNull(),
    embedding: vector('embedding', { dimensions: 1536 }),
  },
  (table) => [
    index('documents_embedding_idx')
      .using('hnsw', table.embedding.op('vector_cosine_ops')),
  ]
);

1536 dimensions tương ứng với model text-embedding-3-small của OpenAI. Thay con số này cho phù hợp với model bạn đang dùng — ví dụ embed-english-v3 của Cohere có output 1024 dimensions.

HNSW index với vector_cosine_ops báo cho pgvector biết hàm tính khoảng cách nào query của bạn sẽ dùng. Bạn khai báo điều này lúc tạo index — query planner không thể tự suy ra ở runtime.

Lỗi thường gặp: Nếu bạn đang thêm vector column vào bảng đã có qua ALTER TABLE, hãy dùng drizzle-orm 0.31.0+. Các phiên bản cũ hơn sinh ra câu lệnh ALTER TABLE có dấu ngoặc kép quanh kiểu dữ liệu ("vector(1536)") khiến Postgres báo lỗi type "vector(1536)" does not exist. Cách khắc phục là nâng cấp drizzle-orm. Với bảng mới, định nghĩa vector column ngay trong CREATE TABLE ban đầu để tránh đường ALTER TABLE hoàn toàn.

Bước 6: Tạo và chạy migrations

Tạo migration:

npx drizzle-kit generate

Lệnh này ghi một file .sql vào ./drizzle. Mở file đó và kiểm tra nó có chứa CREATE TABLE với vector column và CREATE INDEX cho HNSW index trước khi chạy.

Chạy migration:

npx drizzle-kit migrate

Để chạy migration theo chương trình lúc app khởi động, tạo script src/migrate.ts:

import { drizzle } from 'drizzle-orm/postgres-js';
import { migrate } from 'drizzle-orm/postgres-js/migrator';
import postgres from 'postgres';

const migrationClient = postgres(process.env.DATABASE_URL!, { max: 1 });
await migrate(drizzle(migrationClient), { migrationsFolder: './drizzle' });
await migrationClient.end();

Chạy với lệnh: npx tsx src/migrate.ts

Lỗi thường gặp: drizzle-kit push bỏ qua các file migration và áp dụng thay đổi schema trực tiếp. Cách này ổn để lặp nhanh ở local nhưng không phù hợp cho production — không có audit trail, không có rollback, và không thể replay migration trên database mới. Nếu bạn đã dùng push trong quá trình phát triển rồi chuyển sang migrate ở production, bảng trạng thái migration sẽ không khớp với schema đã được áp dụng. Hãy bắt đầu với generate + migrate ngay từ đầu, kể cả ở môi trường dev.

Bước 7: Thiết lập database client

Tạo file src/db.ts:

import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
import * as schema from './schema';

const client = postgres(process.env.DATABASE_URL!);
export const db = drizzle(client, { schema });

Lỗi thường gặp: Nếu bạn đang chạy sau AWS RDS Proxy, hãy tắt prepared statements. postgres.js bật chúng theo mặc định; RDS Proxy sẽ ghim một backend connection cho mỗi client connection khi prepared statements được dùng, loại bỏ lợi ích của pooling (AWS docs). Tắt bằng cách:

const client = postgres(process.env.DATABASE_URL!, { prepare: false });

Bước 8: Chèn embeddings

import { db } from './db';
import { documents } from './schema';

// Gọi model embedding của bạn — OpenAI, Cohere, Ollama, v.v.
const embedding: number[] = await getEmbedding('Hello, world');

await db.insert(documents).values({
  content: 'Hello, world',
  embedding,
});

Column embedding nhận một mảng JavaScript thuần túy gồm các số thực. Không cần serialize đặc biệt — Drizzle và postgres.js tự xử lý việc chuyển đổi sang wire format của pgvector.

Lỗi thường gặp: Chèn mảng có số chiều sai sẽ báo lỗi expected 1536 dimensions, not N. Số dimensions trong định nghĩa schema phải khớp chính xác với kích thước output của model. Kiểm tra tài liệu model và cập nhật schema nếu cần.

Bước 9: Query theo độ tương đồng

Index có được dùng hay không phụ thuộc vào cách bạn viết mệnh đề orderBy.

Đúng — HNSW index được sử dụng:

import { asc } from 'drizzle-orm';
import { cosineDistance } from 'drizzle-orm';
import { db } from './db';
import { documents } from './schema';

const queryEmbedding = await getEmbedding('search query');

const results = await db
  .select()
  .from(documents)
  .orderBy(asc(cosineDistance(documents.embedding, queryEmbedding)))
  .limit(10);

Sai — HNSW index bị bỏ qua:

// Đừng làm thế này
import { desc, sql } from 'drizzle-orm';

const results = await db
  .select()
  .from(documents)
  .orderBy(desc(sql`1 - ${cosineDistance(documents.embedding, queryEmbedding)}`))
  .limit(10);

Mẫu 1 - cosineDistance(...) tính cosine similarity thay vì cosine distance. Index được xây dựng trên hàm distance — Postgres không thể dùng nó khi biểu thức bị đảo ngược. orderBy(asc(cosineDistance(...))) trả về kết quả giống hệt (distance nhỏ nhất trước = similarity cao nhất trước) và tận dụng được index.

Lỗi thường gặp: Không có index, Postgres thực hiện sequential scan. Với 10k hàng bạn sẽ không nhận ra. Với 500k hàng, query chạy mất vài giây thay vì vài millisecond. Xác nhận index có được dùng bằng EXPLAIN ANALYZE:

psql $DATABASE_URL -c "EXPLAIN ANALYZE SELECT * FROM documents ORDER BY embedding <=> '[0.1, 0.2, 0.3]'::vector LIMIT 10;"

Tìm Index Scan using documents_embedding_idx trong output. Nếu thấy Seq Scan, index không được kích hoạt — kiểm tra lại mẫu query và xác nhận HNSW index đã được tạo.

Bước tiếp theo

Với managed Postgres có pgvector được bật sẵn, SupabaseNeon đều hỗ trợ ngay từ đầu — không cần Docker, không cần setup extension:

  • Supabase bật pgvector trên tất cả các gói và có table editor hiển thị vector columns. Row Level Security hoạt động với vector queries mà không cần cấu hình thêm, điều này quan trọng nếu bạn đang xây dựng tính năng tìm kiếm tài liệu theo từng user.
  • Neon phù hợp với quy trình migration được mô tả ở đây — nó hỗ trợ branch-per-PR, mỗi pull request có một database branch với schema đã được migrate và không có shared state giữa các lần chạy test.
  • Railway là lựa chọn hợp lý nếu bạn thích Postgres ít-managed hơn và muốn tự host phiên bản pgvector của mình.

Nếu bạn đang chọn giữa Supabase và Neon, Neon vs Supabase so sánh giá, branching và connection pooling của hai dịch vụ.

Drizzle Studio cung cấp GUI trên trình duyệt để xem bảng và chạy query trên dev database mà không rời khỏi terminal:

npx drizzle-kit studio

Nhóm Drizzle cũng đang trong chuỗi release v1.0.0-beta — phần API được đề cập trong bài này ổn định qua các phiên bản đó, nhưng hãy theo dõi changelog trước khi nâng cấp nếu bạn có các SQL fragment tùy chỉnh.