AI

Supabase 사용해보기 (3) | Supabase Vector DB 구성하기

pepega 2026. 1. 31. 21:30

저번 포스팅에선 Supabase를 회원가입한 뒤 키를 발급 받았고 간단한 테이블을 생성하여 CRUD를 했습니다.

이번 포스팅에선 RAG 관련한 간단한 테스트 진행을 위해 새 프로젝트를 추가하고 벡터 DB를 생성하는 포스팅을 해보겠습니다.

이번 시리즈에서는 공공 약품 데이터를 활용해 자연어로 질문하면 관련 약품 정보를 찾아주는 RAG(Retrieval-Augmented Generation) 시스템을 구축합니다.

예를 들어 "임신 중에 먹으면 안 되는 약은?", "아스피린 부작용이 뭐야?" 처럼 자연어로 질문하면 관련 약품 정보를 검색하고 Claude가 답변을 생성해주는 시스템입니다.

전체 스택

  • Supabase (pgvector) — 벡터 DB 및 API
  • Voyage AI — 임베딩 모델 (Anthropic 공식 권장)
  • Claude — 자연어 답변 생성 (다른 AI를 사용해도 됩니다. 여기선 Claude를 사용해요)
  • Supabase Edge Functions — 데이터 적재 및 검색 API (개인 백엔드 서비스로 대체 가능해요)

3편에서는 Supabase 테이블 설계와 검색 함수 생성까지 다룹니다.

전에 만들었던 프로젝트는 냅두고 새 프로젝트를 생성해서 진행합니다~~


1. 프로젝트 구성하기

Supabase dashboard에 들어가서 New project를 클릭합니다.

Create a new project에서 정보들을 입력해둡니다.


2. Vector DB로 RAG 시스템 만들어보기

Supabase는 PostgreSQL 기반이기 때문에 pgvector 확장(extension)을 통해 벡터 데이터로 유사성 검색을 할 수 있다고 하네요.

이는 RAG(Retrieval Augmented Generation) 시스템이나 추천 시스템 등 다양한 AI 서비스에 사용된다고 합니다. 심지어 RDB로 관리도 용이한 느낌이에요.

  1. Supabase dashboard에서 Database → Extensions를 클릭합니다.
  2. 검색창에 vector를 검색한 뒤 vector extensions를 enabled로 변경합니다.
  3. 이 글에서 테이블 구성은 모두 Supabase의 SQL Editor에서 수행합니다.

3. 테이블 구성

설계 원칙: 항목별 청킹(Chunking)

RAG 시스템에서 검색 품질을 높이는 핵심은 청킹 전략입니다.

약품 하나의 정보를 통째로 하나의 벡터로 만들면 "부작용이 뭐야?"라는 질문에 효능, 복용법, 보관법까지 섞인 벡터가 검색됩니다. 정확도가 떨어질 수밖에 없어요.

대신 약품 정보를 항목별(효능, 복용법, 주의사항, 상호작용, 부작용, 보관법)로 분리해서 각각 임베딩하면, "부작용" 관련 질문에는 부작용 청크만 정확히 히트합니다.

약품 1개 → 7개 청크 (효능 / 복용법 / 경고 / 주의사항 / 상호작용 / 부작용 / 보관법)

medicines 테이블 (원본 데이터)

CREATE TABLE medicines (
  id           bigint primary key generated always as identity,
  item_seq     text unique not null,   -- 약품 고유번호
  item_name    text not null,          -- 약품명
  entp_name    text,                   -- 제조사
  open_de      text,                   -- 공개일
  update_de    text,                   -- 수정일
  item_image   text,                   -- 이미지 URL
  bizrno       text,                   -- 사업자번호
  raw_json     jsonb,                  -- 원본 JSON 전체 보존
  created_at   timestamptz default now()
);

원본 JSON을 raw_json 컬럼에 통째로 보존해두면 나중에 필드가 추가되거나 스키마가 변경될 때 재처리하기 편합니다.

medicine_chunks 테이블 (청크 + 임베딩)

CREATE TABLE medicine_chunks (
  id           bigint primary key generated always as identity,
  medicine_id  bigint references medicines(id) on delete cascade,
  item_seq     text not null,
  item_name    text not null,
  chunk_type   text not null,  -- efcy / useMethod / atpnWarn / atpn / intrc / se / deposit
  chunk_label  text not null,  -- 효능 / 복용법 / 경고 / 주의사항 / 상호작용 / 부작용 / 보관법
  content      text not null,  -- 실제 청크 텍스트
  embedding    vector(1024),   -- Voyage AI voyage-3 기준 (1024차원)
  created_at   timestamptz default now()
);

임베딩 차원 수: OpenAI의 text-embedding-3-small은 1536차원이지만, Voyage AI의 voyage-3는 1024차원입니다. 모델에 맞게 설정해야 합니다.

인덱스 생성

-- 벡터 유사도 검색 인덱스 (ivfflat)
CREATE INDEX ON medicine_chunks
  USING ivfflat (embedding vector_cosine_ops)
  WITH (lists = 100);

-- 일반 필터링용 인덱스
CREATE INDEX ON medicine_chunks (item_seq);
CREATE INDEX ON medicine_chunks (chunk_type);

ivfflat은 대용량 데이터에 적합한 근사 최근접 이웃(ANN) 인덱스입니다. lists 값은 데이터 수의 제곱근 정도로 설정하는 게 일반적입니다.


4. 검색 함수 생성

RAG 시스템에서는 세 가지 검색 방식을 구현합니다.

방식 특징 적합한 쿼리

벡터 검색 의미 기반 유사도 검색 "혈전 억제 약 부작용은?"
FTS (Full-text Search) 키워드 기반 검색 "아스피린 보관법"
Hybrid Search 두 방식 결합 (권장) 모든 쿼리

FTS 컬럼 추가

ALTER TABLE medicine_chunks
  ADD COLUMN IF NOT EXISTS fts tsvector
    GENERATED ALWAYS AS (to_tsvector('simple', content)) STORED;

CREATE INDEX ON medicine_chunks USING gin(fts);

벡터 검색 함수

CREATE OR REPLACE FUNCTION search_medicines(
  query_embedding  vector(1024),  -- ⚠️ 차원 명시 필수! 없으면 similarity가 항상 0으로 반환됨
  match_threshold  float    DEFAULT 0.35,  -- 관련/무관 질문 분포를 측정 후 그 사이 값으로 설정
  match_count      int      DEFAULT 5,
  filter_type      text     DEFAULT null
)
RETURNS TABLE (
  id           bigint,
  item_seq     text,
  item_name    text,
  chunk_type   text,
  chunk_label  text,
  content      text,
  similarity   float
)
LANGUAGE sql STABLE
AS $$
  SELECT
    mc.id, mc.item_seq, mc.item_name,
    mc.chunk_type, mc.chunk_label, mc.content,
    1 - (mc.embedding <=> query_embedding) AS similarity
  FROM medicine_chunks mc
  WHERE
    1 - (mc.embedding <=> query_embedding) > match_threshold
    AND (filter_type IS NULL OR mc.chunk_type = filter_type)
  ORDER BY mc.embedding <=> query_embedding
  LIMIT match_count;
$$;

⚠️ 주의: query_embedding vector처럼 차원을 명시하지 않으면 similarity가 항상 0.0000으로 반환되는 버그가 발생합니다. 반드시 저장된 임베딩 차원과 동일하게 vector(1024)로 명시해야 합니다.

Hybrid Search 함수 (권장)

벡터 검색과 FTS 결과를 RRF(Reciprocal Rank Fusion) 방식으로 결합합니다. 각 방식의 순위를 점수로 변환해서 합산하는 방식으로, 단순 점수 합산보다 훨씬 안정적인 결과를 냅니다.

CREATE OR REPLACE FUNCTION search_medicines_hybrid(
  query_text       text,
  query_embedding  vector(1024),  -- ⚠️ 차원 명시 필수!
  match_count      int   DEFAULT 5,
  rrf_k            int   DEFAULT 60
)
RETURNS TABLE (
  id          bigint,
  item_seq    text,
  item_name   text,
  chunk_type  text,
  chunk_label text,
  content     text,
  score       float
)
LANGUAGE sql STABLE
AS $$
  WITH vector_search AS (
    SELECT
      id,
      row_number() OVER (ORDER BY embedding <=> query_embedding) AS rank
    FROM medicine_chunks
    LIMIT match_count * 2
  ),
  fts_search AS (
    SELECT
      id,
      row_number() OVER (ORDER BY ts_rank(fts, plainto_tsquery('simple', query_text)) DESC) AS rank
    FROM medicine_chunks
    WHERE fts @@ plainto_tsquery('simple', query_text)
    LIMIT match_count * 2
  ),
  combined AS (
    SELECT
      COALESCE(v.id, f.id) AS id,
      COALESCE(1.0 / (rrf_k + v.rank), 0) +
      COALESCE(1.0 / (rrf_k + f.rank), 0) AS score
    FROM vector_search v
    FULL OUTER JOIN fts_search f ON v.id = f.id
  )
  SELECT
    mc.id, mc.item_seq, mc.item_name,
    mc.chunk_type, mc.chunk_label, mc.content,
    c.score
  FROM combined c
  JOIN medicine_chunks mc ON mc.id = c.id
  ORDER BY c.score DESC
  LIMIT match_count;
$$;

5. Studio에서 확인하기

모든 SQL 실행 후 Supabase Studio Table Editor에서 테이블이 정상 생성됐는지 확인합니다.

SQL Editor에서 아래 쿼리로 FTS 검색도 바로 테스트해볼 수 있습니다. (벡터 검색은 임베딩 데이터가 있어야 하므로 데이터 적재 후 테스트합니다.)

-- 테이블 확인
SELECT * FROM medicines;
SELECT id, item_name, chunk_type, chunk_label, content
FROM medicine_chunks;

6. 마무리

이번 편에서는 RAG 시스템의 기반이 되는 데이터베이스를 구성했습니다.

핵심 요약

  • 약품 정보를 항목별로 청킹해서 검색 정확도를 높임
  • Voyage AI voyage-3 모델 기준 vector(1024) 차원으로 설정 — 차원 명시를 빠뜨리면 similarity가 0으로 반환되므로 반드시 명시할 것
  • 벡터 검색 + FTS를 결합한 Hybrid Search 함수로 다양한 쿼리 대응
  • match_threshold 기본값은 0.35로 설정 — 임베딩 모델에 따라 관련/무관 질문의 similarity 분포를 측정한 후 조정 필요

다음 편에서는 Supabase Edge Functions를 활용해 실제 약품 데이터를 적재하는 과정을 다루겠습니다.