메인 콘텐츠로 건너뛰기

Sonamu에 벡터 검색이 필요한 이유

Sonamu로 웹 앱을 만들다 보면 이런 기능을 구현하게 됩니다:
  • 지식 베이스: “비슷한 문서 찾기”
  • 커머스: “이 상품과 유사한 제품”
  • 콘텐츠: “관련 글 추천”
  • 고객 지원: “비슷한 질문 찾기”
전통적인 키워드 검색(LIKE '%keyword%')은 한계가 있습니다:
  • ❌ “TypeScript 프레임워크” 검색 시 “Node.js API 라이브러리” 못 찾음
  • ❌ 오타에 취약 (“타입스크립트” vs “타입스크립트”)
  • ❌ 동의어 처리 안 됨 (“프레임워크” vs “라이브러리”)
의미 기반 검색이 필요합니다. 이를 위해 벡터 검색을 사용합니다.

왜 pgvector인가?

벡터 검색을 구현하려면 벡터를 저장하고 검색할 데이터베이스가 필요합니다.

선택지

방식장점단점Sonamu 추천
pgvectorPostgreSQL 그대로 사용, 추가 인프라 불필요, 기존 데이터와 JOIN 가능전문 벡터 DB보다 성능 낮음⭐⭐⭐⭐⭐
Pinecone벡터 검색 최적화, 관리형 서비스추가 비용, 별도 동기화 필요⭐⭐
Elasticsearch강력한 검색 기능무거움, 설정 복잡⭐⭐⭐
Weaviate/Milvus전문 벡터 DB별도 인프라, 학습 곡선⭐⭐

Sonamu 프로젝트에서 pgvector를 추천하는 이유

1. 이미 PostgreSQL을 쓰고 있습니다
// sonamu.config.ts
export default defineConfig({
  database: {
    client: "pg",
    connection: { /* ... */ }
  }
});
Sonamu는 PostgreSQL + Knex 기반입니다. 벡터 검색을 위해 별도 데이터베이스를 추가할 필요가 없습니다. 2. 기존 데이터와 함께 쓸 수 있습니다
-- 기존 데이터와 JOIN
SELECT 
  d.id, d.title, d.category,
  1 - (d.embedding <=> ?) AS similarity
FROM documents d
JOIN categories c ON d.category_id = c.id
WHERE c.active = true
ORDER BY similarity DESC;
벡터 검색과 일반 SQL을 섞어서 쓸 수 있습니다. 별도 DB면 데이터 동기화가 필요합니다. 3. 인프라가 단순합니다
  • Pinecone: 별도 API, 비용, 동기화
  • pgvector: 확장만 설치, 추가 비용 없음
4. Sonamu Model과 통합이 자연스럽습니다
class DocumentModelClass extends BaseModel {
  @api({ httpMethod: 'POST' })
  async search(query: string) {
    // Puri로 벡터 검색 SQL 작성
    const results = await this.getPuri().raw(`...`);
    return results.rows;
  }
}

pgvector란?

pgvector는 PostgreSQL에서 벡터(임베딩) 데이터를 저장하고 검색할 수 있게 해주는 확장입니다. 주요 기능:
  • vector(N) 데이터 타입 (N차원 벡터)
  • 유사도 연산자 (<=>, <->, <#>)
  • 인덱스 (IVFFlat, HNSW)

필수 패키지 설치

pnpm add pgvector voyageai
패키지:
  • pgvector: PostgreSQL pgvector 타입 지원 (Knex와 함께 사용)
  • voyageai: Voyage AI 임베딩 (한국어 추천)
  • @ai-sdk/openai: OpenAI 임베딩 (선택)

PostgreSQL 확장 설치

환경별 설치 방법

# PostgreSQL 개발 패키지
sudo apt-get install postgresql-server-dev-14

# pgvector 빌드 및 설치
git clone --branch v0.5.1 https://github.com/pgvector/pgvector.git
cd pgvector
make
sudo make install

확장 활성화

PostgreSQL에 접속하여 확장을 활성화합니다:
-- pgvector 확장 활성화
CREATE EXTENSION IF NOT EXISTS vector;

-- 설치 확인
SELECT * FROM pg_extension WHERE extname = 'vector';

-- 버전 확인
SELECT vector_version();  -- 0.5.1 이상 권장

Sonamu 프로젝트에 적용하기

1. 환경변수 설정

# PostgreSQL (이미 있을 것)
DATABASE_URL=postgresql://user:password@localhost:5432/mydb

# 임베딩 API (나중에 사용)
VOYAGE_API_KEY=pa-...
# 또는
OPENAI_API_KEY=sk-...

2. Sonamu Config 확인

// sonamu.config.ts
import { defineConfig } from "sonamu";

export default defineConfig({
  database: {
    name: "myapp",
    defaultOptions: {
      client: "pg",
      connection: {
        host: "localhost",
        port: 5432,
        user: "postgres",
        password: "postgres",
        database: "myapp",
      },
    },
  },
});
Sonamu는 이미 PostgreSQL을 쓰고 있습니다. 추가 설정 불필요합니다.

3. Knex Migration으로 테이블 생성

Sonamu의 Migration을 사용하여 벡터 테이블을 만듭니다:
// migrations/20240101000000_add_vector_search.ts
import type { Knex } from "knex";

export async function up(knex: Knex): Promise<void> {
  // 1. pgvector 확장 활성화
  await knex.raw('CREATE EXTENSION IF NOT EXISTS vector');
  
  // 2. embedding 컬럼 추가
  await knex.schema.table('documents', (table) => {
    // Voyage AI는 1024차원
    table.specificType('embedding', 'vector(1024)');
  });
  
  // 3. 인덱스는 나중에 (데이터가 쌓인 후)
  // await knex.raw(`
  //   CREATE INDEX ON documents 
  //   USING hnsw (embedding vector_cosine_ops)
  // `);
}

export async function down(knex: Knex): Promise<void> {
  await knex.schema.table('documents', (table) => {
    table.dropColumn('embedding');
  });
}
실행:
pnpm sonamu migrate:latest

4. 처음부터 벡터 테이블 만들기

새 테이블을 만든다면:
// migrations/20240101000001_create_knowledge_base.ts
import type { Knex } from "knex";

export async function up(knex: Knex): Promise<void> {
  await knex.raw('CREATE EXTENSION IF NOT EXISTS vector');
  
  await knex.schema.createTable('knowledge_base', (table) => {
    table.increments('id').primary();
    table.text('title').notNullable();
    table.text('content').notNullable();
    table.string('category', 50);
    
    // 벡터 컬럼
    table.specificType('embedding', 'vector(1024)');
    
    table.timestamps(true, true);
    
    // 일반 인덱스
    table.index('category');
  });
}

export async function down(knex: Knex): Promise<void> {
  await knex.schema.dropTableIfExists('knowledge_base');
}

벡터 차원 이해하기

임베딩 모델마다 벡터 차원이 다릅니다:
import { Embedding } from "sonamu/vector";

// Voyage AI: 1024차원
const voyageDim = Embedding.getDimensions('voyage');
console.log(voyageDim);  // 1024

// OpenAI: 1536차원
const openaiDim = Embedding.getDimensions('openai');
console.log(openaiDim);  // 1536
테이블 생성 시 차원 수를 맞춰야 합니다:
-- Voyage AI 사용 시
CREATE TABLE docs (
  embedding vector(1024)
);

-- OpenAI 사용 시
CREATE TABLE docs (
  embedding vector(1536)
);

인덱스 - 나중에 만들기

중요: 인덱스는 데이터가 충분히 쌓인 후에 만듭니다.

왜 나중에?

// ❌ 나쁜 순서
await knex.raw('CREATE INDEX ...');  // 먼저 인덱스
await DocumentModel.saveOne({ embedding });  // 나중에 데이터

// ✅ 좋은 순서
await DocumentModel.saveOne({ embedding });  // 먼저 데이터 (100개 이상)
await knex.raw('CREATE INDEX ...');  // 나중에 인덱스
데이터가 없으면 인덱스가 최적화되지 않습니다.

HNSW 인덱스 (권장)

데이터가 100개 이상 쌓인 후:
// migrations/20240101000002_add_vector_index.ts
export async function up(knex: Knex): Promise<void> {
  await knex.raw(`
    CREATE INDEX idx_docs_embedding 
    ON documents 
    USING hnsw (embedding vector_cosine_ops)
    WITH (m = 16, ef_construction = 64)
  `);
}
파라미터:
  • m = 16: 연결 수 (기본값, 대부분 OK)
  • ef_construction = 64: 구축 시 탐색 크기

IVFFlat 인덱스 (빠른 구축)

HNSW가 너무 느리면:
CREATE INDEX idx_docs_embedding 
ON documents 
USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);

실전 시나리오

시나리오: 지식 베이스 구축

Sonamu로 사내 지식 베이스를 만들고 있습니다. 1단계: 테이블 설계
CREATE TABLE knowledge_base (
  id SERIAL PRIMARY KEY,
  title TEXT NOT NULL,
  content TEXT NOT NULL,
  category VARCHAR(50),
  embedding vector(1024),  -- Voyage AI
  created_at TIMESTAMP DEFAULT NOW()
);
2단계: 데이터 입력 (나중에)
// 임베딩 생성 후 저장 (embeddings.mdx 참고)
await KnowledgeBaseModel.saveOne({
  title: "Sonamu 시작하기",
  content: "...",
  category: "documentation",
  embedding: [...],  // 1024개 숫자 배열
});
3단계: 인덱스 생성 (데이터 100개 이상 쌓인 후)
CREATE INDEX ON knowledge_base 
USING hnsw (embedding vector_cosine_ops);
4단계: 검색 API (나중에)
class KnowledgeBaseModelClass extends BaseModel {
  @api({ httpMethod: 'POST' })
  async search(query: string) {
    // vector-search.mdx 참고
    const results = await this.getPuri().raw(`...`);
    return results.rows;
  }
}

주의사항

pgvector 설정 시 주의사항:
  1. 차원 수 일치: 테이블과 임베딩 모델의 차원이 같아야 함
    -- Voyage AI (1024)
    CREATE TABLE docs (embedding vector(1024));
    
  2. 인덱스는 나중에: 데이터 100개 이상 후 생성
    // 1. 데이터 먼저
    await saveDocuments();
    
    // 2. 인덱스 나중에
    await createIndex();
    
  3. NULL 허용: 모든 문서에 즉시 임베딩 못 만들 수 있음
    -- NULL 허용 (나중에 업데이트 가능)
    embedding vector(1024)
    
  4. Migration으로 관리: 직접 SQL보다 Migration 사용
    pnpm sonamu migrate:latest
    
  5. 확장 버전: 0.5.1 이상 권장
    SELECT vector_version();
    

다음 단계

pgvector 설치가 완료되었습니다. 이제 임베딩을 생성하고 검색을 구현할 차례입니다.