OPEN HYPER STEP
← 목록으로 (master-project)
MASTER-PROJECT · 36 / 50
master-project
CHAPTER 36 / 50
읽기 약 2
FUNCTION

RAG 구현: 문서 임베딩 + 벡터 검색


핵심 개념

pgvector·embedding·chunking·retrieval·prompt 주입 — 본인 문서 기반 AI 답변.

본문

RAG 흐름

📋 코드 (7줄)
1. 사용자 문서 업로드 (PDF·텍스트)
2. Chunking (500-1000 토큰 단위 분할)
3. Embedding (각 chunk → 1536 차원 벡터)
4. pgvector에 저장
5. 사용자 질문 → embedding → 유사 chunk top-5 검색
6. 검색 결과 + 질문 → LLM prompt
7. 답변 + 출처 표시

pgvector 셋업 (Supabase)

SQL📋 코드 (31줄)
-- 마이그레이션
create extension if not exists vector;

create table documents (
  id uuid primary key default gen_random_uuid(),
  user_id uuid references users(id) on delete cascade,
  filename text not null,
  content text,
  created_at timestamptz default now()
);

create table chunks (
  id uuid primary key default gen_random_uuid(),
  document_id uuid references documents(id) on delete cascade,
  content text not null,
  embedding vector(1536),  -- OpenAI text-embedding-3-small
  metadata jsonb
);

-- HNSW 인덱스 (빠른 KNN 검색)
create index idx_chunks_embedding on chunks
using hnsw (embedding vector_cosine_ops);

-- RLS
alter table documents enable row level security;
alter table chunks enable row level security;

create policy "users own docs" on documents for all using (auth.uid() = user_id);
create policy "users own chunks" on chunks for all using (
  exists (select 1 from documents where documents.id = chunks.document_id and documents.user_id = auth.uid())
);

Chunking

TS📋 코드 (12줄)
// src/lib/rag/chunk.ts
export function chunkText(text: string, size = 800, overlap = 100): string[] {
  const chunks: string[] = []
  let start = 0
  while (start < text.length) {
    chunks.push(text.slice(start, start + size))
    start += size - overlap  // 100자 오버랩 (문맥 유지)
  }
  return chunks
}

// 더 좋은 방법: 문장 경계 기반 (langchain RecursiveCharacterTextSplitter)

Embedding 생성

TS📋 코드 (19줄)
// src/lib/rag/embed.ts
import { openai } from '@ai-sdk/openai'
import { embed, embedMany } from 'ai'

export async function embedText(text: string) {
  const { embedding } = await embed({
    model: openai.embedding('text-embedding-3-small'),
    value: text,
  })
  return embedding  // 1536-dim 벡터
}

export async function embedBatch(texts: string[]) {
  const { embeddings } = await embedMany({
    model: openai.embedding('text-embedding-3-small'),
    values: texts,
  })
  return embeddings
}

인덱싱 (문서 저장 시)

TS📋 코드 (27줄)
// 문서 업로드 시
async function indexDocument(userId: string, filename: string, content: string) {
  const supabase = createServiceClient()

  // 1. 문서 저장
  const { data: doc } = await supabase
    .from('documents')
    .insert({ user_id: userId, filename, content })
    .select()
    .single()

  // 2. 청킹
  const chunks = chunkText(content)

  // 3. 임베딩 (배치)
  const embeddings = await embedBatch(chunks)

  // 4. 저장
  await supabase.from('chunks').insert(
    chunks.map((c, i) => ({
      document_id: doc.id,
      content: c,
      embedding: embeddings[i],
      metadata: { chunk_index: i },
    }))
  )
}

검색 (Retrieval)

TS📋 코드 (29줄)
// SQL 함수 생성
create or replace function match_chunks(
  query_embedding vector(1536),
  match_user_id uuid,
  match_count int
)
returns table (id uuid, content text, similarity float)
language sql stable as $$
  select c.id, c.content, 1 - (c.embedding <=> query_embedding) as similarity
  from chunks c
  join documents d on c.document_id = d.id
  where d.user_id = match_user_id
  order by c.embedding <=> query_embedding
  limit match_count
$$;

// TypeScript에서 호출
async function searchChunks(userId: string, query: string, k = 5) {
  const supabase = await createClient()
  const queryEmbedding = await embedText(query)

  const { data } = await supabase.rpc('match_chunks', {
    query_embedding: queryEmbedding,
    match_user_id: userId,
    match_count: k,
  })

  return data
}

RAG 답변 생성

TS📋 코드 (25줄)
// /api/chat/rag
import { streamText } from 'ai'
import { anthropic } from '@ai-sdk/anthropic'

export async function POST(request: Request) {
  const { question, userId } = await request.json()

  // 1. 검색
  const chunks = await searchChunks(userId, question, 5)
  const context = chunks.map(c => c.content).join('\n\n---\n\n')

  // 2. LLM
  const result = streamText({
    model: anthropic('claude-sonnet-4-6'),
    system: `당신은 사용자 문서 기반 어시스턴트입니다.
다음 문서를 참고하여 답하세요:

${context}

문서에 없는 내용은 "문서에 없습니다"라고 답하세요.`,
    prompt: question,
  })

  return result.toDataStreamResponse()
}

출처 표시

TSX📋 코드 (18줄)
const result = streamText({
  ...,
  toolCalls: [{
    name: 'cite_source',
    parameters: z.object({
      chunk_ids: z.array(z.string()),
    }),
  }],
})

// UI
<div>
  {answer}
  <p>출처:</p>
  <ul>
    {chunks.map(c => <li key={c.id}>{c.metadata.filename} (chunk {c.metadata.chunk_index})</li>)}
  </ul>
</div>

다음 챕터

CH.37 "AI 비용 관리: 사용량 추적 + 제한".


AI 프롬프트
🤖 AI에게 잘 물어보는 법 — 모델·전략별 프롬프트
Claude

무료: Sonnet 4.6 / Pro $20/mo: Opus 4.6

내 마스터 프로젝트의 RAG 구현 부분을 분석해서
실전 적용 + 개선 우선순위 3가지를 알려줘.
ChatGPT

무료: GPT-5.5 / Plus $20/mo: GPT-5.5 Pro

RAG 구현 관련 모범 사례·안티패턴 5개를
비교 분석해서 실전 적용를 위한 추천 방안을 알려줘.
Gemini

무료: 2.5 Flash / Pro $19.99/mo: 3.1 Pro

내 프로젝트 전체에서 RAG 구현
최적화 가능 위치와 리스크를 보고해줘.
Grok

무료: Grok 4.1 / SuperGrok $30/mo

2026년 한국 1인 개발자 시장의
RAG 구현 트렌드와 차별화 포인트를 정리해줘.

⭐ 이것만 기억하세요
RAG 구현: 문서 임베딩 + 벡터 검색 이 3가지만 확실히 잡으세요
1.pgvector + HNSW = Supabase 무료 RAG
2.Chunking → Embedding → Search → Prompt 주입
3.다음 챕터에서 AI 비용 관리


공유하기
진행도 36 / 50