RAG 기능

VaisDB는 RAG(Retrieval-Augmented Generation) 파이프라인을 데이터베이스 레벨에서 기본 지원합니다. 외부 라이브러리 없이 시맨틱 청킹, 임베딩 관리, 컨텍스트 보존, 에이전트 메모리를 처리합니다.


RAG 개요

기존 RAG 파이프라인은 여러 외부 컴포넌트가 필요합니다.

기존 RAG:
  문서 → [청킹 라이브러리] → [임베딩 API] → [벡터 DB]
                                            + [관계형 DB]
                                            + [검색 엔진]
                          ↓
                        LLM

VaisDB RAG:
  문서 → [VaisDB]  →  LLM
         ├ 시맨틱 청킹 (내장)
         ├ 임베딩 관리 (내장)
         ├ 벡터 + 그래프 + SQL + 전문 검색 (내장)
         └ 컨텍스트 보존 (내장)

시맨틱 청킹

VaisDB는 의미 경계를 기준으로 문서를 자동 분할합니다. 단순 길이 기반 청킹이 아닌, 문장과 절의 의미 구조를 분석하여 청크 경계를 결정합니다.

SEMANTIC_CHUNK 함수

SEMANTIC_CHUNK(content [, options])
옵션설명기본값
max_tokens청크당 최대 토큰 수512
overlap_tokens청크 간 오버랩 토큰 수64
preserve_sentences문장 경계 유지 여부true
language언어 코드auto

예제

-- 문서를 시맨틱 청크로 분할하여 삽입
INSERT INTO document_chunks (document_id, chunk_index, content, embedding)
SELECT
    $1 AS document_id,
    ROW_NUMBER() OVER () - 1 AS chunk_index,
    chunk.text AS content,
    EMBED(chunk.text) AS embedding
FROM SEMANTIC_CHUNK(
    '긴 문서 내용...',
    max_tokens=512,
    overlap_tokens=64,
    preserve_sentences=true
) AS chunk;

Vais에서 사용:

U vaisdb::{Database};

F ingest_document(db: &Database, doc_id: i64, title: str, content: str) {
    tx := db.begin()?;

    # 원본 문서 저장
    tx.execute("
        INSERT INTO documents (id, title, full_content)
        VALUES ($1, $2, $3)
    ", [doc_id, title, content])?;

    # 시맨틱 청킹 + 임베딩 자동 생성
    tx.execute("
        INSERT INTO document_chunks
            (document_id, chunk_index, content, embedding)
        SELECT
            $1,
            ROW_NUMBER() OVER () - 1,
            chunk.text,
            EMBED(chunk.text)
        FROM SEMANTIC_CHUNK($2,
            max_tokens=512,
            overlap_tokens=64
        ) AS chunk
    ", [doc_id, content])?;

    tx.commit()?;
    println("문서 '{title}' 청킹 완료");
}

청크 관계 그래프

시맨틱 청킹 후 청크 간 순서 관계와 의미 유사도 관계를 그래프로 자동 구성합니다.

-- 청크 테이블
CREATE TABLE document_chunks (
    id          INTEGER PRIMARY KEY AUTOINCREMENT,
    document_id INTEGER REFERENCES documents(id),
    chunk_index INTEGER NOT NULL,
    content     TEXT NOT NULL,
    embedding   VECTOR(1536),
    token_count INTEGER
);

-- 청크 관계 엣지 테이블
CREATE TABLE chunk_edges (
    src_chunk_id INTEGER REFERENCES document_chunks(id),
    dst_chunk_id INTEGER REFERENCES document_chunks(id),
    edge_type    TEXT NOT NULL,  -- 'next', 'prev', 'semantic_similar'
    weight       FLOAT DEFAULT 1.0
);

청크 관계 자동 구성:

U vaisdb::{Database};

F build_chunk_graph(db: &Database, document_id: i64) {
    # 1. 순서 관계 (next/prev) 생성
    db.execute("
        INSERT INTO chunk_edges (src_chunk_id, dst_chunk_id, edge_type, weight)
        SELECT
            c1.id, c2.id, 'next', 1.0
        FROM document_chunks c1
        JOIN document_chunks c2
            ON c1.document_id = c2.document_id
           AND c2.chunk_index = c1.chunk_index + 1
        WHERE c1.document_id = $1
    ", [document_id])?;

    # 2. 의미 유사 관계 (유사도 0.8 이상인 청크 간 연결)
    db.execute("
        INSERT INTO chunk_edges (src_chunk_id, dst_chunk_id, edge_type, weight)
        SELECT c1.id, v.chunk_id, 'semantic_similar', v.similarity
        FROM document_chunks c1
          VECTOR_SEARCH(
              (SELECT embedding FROM document_chunks WHERE id = c1.id),
              c1.embedding, top_k=5
          ) v
          JOIN document_chunks c2 ON v.chunk_id = c2.id
        WHERE c1.document_id = $1
          AND v.similarity > 0.8
          AND c1.id != v.chunk_id
    ", [document_id])?;
}

임베딩 관리

EMBED 함수

EMBED() 함수는 텍스트에서 벡터 임베딩을 자동으로 생성합니다.

-- 텍스트를 즉시 임베딩으로 변환
SELECT EMBED('검색할 텍스트');

-- 삽입 시 자동 임베딩
INSERT INTO documents (title, content, embedding)
VALUES ('제목', '내용', EMBED('내용'));

-- 쿼리 벡터 생성
SELECT d.title, v.similarity
FROM documents d
  VECTOR_SEARCH(d.embedding, EMBED('검색 쿼리'), top_k=10) v;

임베딩 모델 설정

-- 기본 임베딩 모델 설정
SET embedding.model = 'text-embedding-3-small';
SET embedding.dimensions = 1536;
SET embedding.api_key = 'sk-...';

-- 또는 로컬 모델 사용
SET embedding.model = 'local:nomic-embed-text-v1.5';
SET embedding.local_path = '/models/nomic-embed-text-v1.5.gguf';

Vais 설정:

U vaisdb::{Database, EmbeddingConfig};

F main() {
    config := EmbeddingConfig {
        model: "text-embedding-3-small",
        dimensions: 1536,
        api_key: std::env::get("OPENAI_API_KEY"),
        batch_size: 100,      # 배치 임베딩 크기
        cache_enabled: true,  # 동일 텍스트 임베딩 캐싱
    };

    db := Database::open_with_config("knowledge.vaisdb", config)?;
}

임베딩 갱신

문서 내용이 변경되면 임베딩을 재계산해야 합니다.

-- 특정 문서의 임베딩 갱신
UPDATE documents
SET embedding = EMBED(content)
WHERE id = 42;

-- 임베딩이 없는 문서 일괄 갱신
UPDATE documents
SET embedding = EMBED(content)
WHERE embedding IS NULL;

RAG_SEARCH는 VaisDB의 최상위 RAG 검색 함수입니다. 벡터 검색, 그래프 컨텍스트 탐색, 전문 검색을 결합하여 LLM에 전달할 최적의 컨텍스트를 구성합니다.

기본 문법

RAG_SEARCH(query, options)
옵션설명기본값
top_k반환할 청크 수10
expand_context그래프로 주변 청크 포함true
context_depth컨텍스트 확장 깊이1
rerank결과 재랭킹 여부true
min_score최소 관련성 점수0.5

예제

-- 기본 RAG 검색
SELECT
    chunk_id,
    document_title,
    content,
    relevance_score,
    context_type      -- 'primary' | 'context'
FROM RAG_SEARCH(
    '트랜스포머의 어텐션 메커니즘은 어떻게 작동하나요?',
    top_k=5,
    expand_context=true,
    context_depth=1
);

Vais에서 RAG 파이프라인 구성:

U vaisdb::{Database, RagResult};

S RagContext {
    chunks:  Vec<str>,
    sources: Vec<str>,
    scores:  Vec<f32>,
}

F build_rag_context(db: &Database, question: str) -> Result<RagContext, str> {
    results := db.query("
        SELECT
            dc.content,
            d.title AS source,
            r.relevance_score
        FROM RAG_SEARCH($1,
            top_k=8,
            expand_context=true,
            context_depth=1,
            rerank=true
        ) r
        JOIN document_chunks dc ON r.chunk_id = dc.id
        JOIN documents d        ON dc.document_id = d.id
        ORDER BY r.relevance_score DESC
    ", [question])?;

    ctx := RagContext {
        chunks:  results.map(|r| r.content),
        sources: results.map(|r| r.source),
        scores:  results.map(|r| r.relevance_score),
    };

    R Ok(ctx);
}

F answer_question(db: &Database, llm: &LlmClient, question: str) -> str {
    ctx := build_rag_context(db, question)?;

    # LLM에 컨텍스트와 함께 질문 전달
    prompt := build_prompt(question, &ctx);
    llm.complete(prompt)?
}

컨텍스트 확장

RAG 검색에서 직접 매칭된 청크만 반환하면 문맥이 끊길 수 있습니다. VaisDB는 그래프를 활용하여 관련 청크를 자동으로 포함합니다.

컨텍스트 윈도우 확장

-- 매칭된 청크와 전/후 청크를 함께 반환
WITH matched_chunks AS (
    SELECT dc.id, dc.document_id, dc.chunk_index, v.similarity
    FROM document_chunks dc
      VECTOR_SEARCH(dc.embedding, EMBED($1), top_k=5) v
    WHERE v.similarity > 0.65
),
context_chunks AS (
    -- 매칭 청크 포함
    SELECT mc.id, mc.similarity, 'primary' AS chunk_type
    FROM matched_chunks mc
    UNION
    -- 앞뒤 청크 포함 (컨텍스트 확장)
    SELECT dc.id, mc.similarity * 0.8, 'context' AS chunk_type
    FROM document_chunks dc
    JOIN matched_chunks mc
        ON dc.document_id = mc.document_id
       AND ABS(dc.chunk_index - mc.chunk_index) = 1
)
SELECT dc.content, cc.similarity, cc.chunk_type
FROM context_chunks cc
JOIN document_chunks dc ON cc.id = dc.id
ORDER BY dc.document_id, dc.chunk_index;

그래프 기반 컨텍스트 탐색

-- 의미적으로 연결된 청크까지 포함
WITH primary_chunks AS (
    SELECT dc.id, v.similarity
    FROM document_chunks dc
      VECTOR_SEARCH(dc.embedding, EMBED($1), top_k=5) v
),
extended_chunks AS (
    SELECT pc.id, pc.similarity, 'primary' AS type
    FROM primary_chunks pc
    UNION
    -- 그래프로 연결된 관련 청크
    SELECT g.node_id AS id, pc.similarity * g.relevance, 'graph_context' AS type
    FROM primary_chunks pc
      GRAPH_TRAVERSE(pc.id, direction='any', depth=1,
                     edge_type='semantic_similar') g
)
SELECT dc.content, ec.similarity, ec.type
FROM extended_chunks ec
JOIN document_chunks dc ON ec.id = dc.id
ORDER BY ec.similarity DESC;

에이전트 메모리

VaisDB는 AI 에이전트의 장단기 메모리를 관리하는 전용 기능을 제공합니다.

메모리 스키마

-- 에이전트 메모리 테이블
CREATE TABLE agent_memory (
    id           INTEGER PRIMARY KEY AUTOINCREMENT,
    agent_id     TEXT NOT NULL,
    session_id   TEXT,
    memory_type  TEXT NOT NULL,  -- 'episodic' | 'semantic' | 'procedural'
    content      TEXT NOT NULL,
    embedding    VECTOR(1536),
    importance   FLOAT DEFAULT 1.0,
    access_count INTEGER DEFAULT 0,
    created_at   TIMESTAMP DEFAULT NOW(),
    last_accessed TIMESTAMP DEFAULT NOW(),
    expires_at   TIMESTAMP
);

CREATE INDEX idx_memory_agent  ON agent_memory(agent_id);
CREATE INDEX idx_memory_type   ON agent_memory(memory_type);
CREATE VECTOR INDEX idx_memory_embedding ON agent_memory(embedding)
    USING hnsw WITH (m=16, ef_construction=200);

메모리 저장 및 검색

U vaisdb::{Database};

S AgentMemory {
    agent_id:    str,
    session_id:  str,
    memory_type: str,
    content:     str,
    importance:  f32,
}

F store_memory(db: &Database, memory: &AgentMemory) {
    db.execute("
        INSERT INTO agent_memory
            (agent_id, session_id, memory_type, content, embedding, importance)
        VALUES ($1, $2, $3, $4, EMBED($4), $5)
    ", [
        memory.agent_id,
        memory.session_id,
        memory.memory_type,
        memory.content,
        memory.importance,
    ])?;
}

F recall_memories(
    db:       &Database,
    agent_id: str,
    context:  str,
    limit:    i64
) -> Result<Vec<str>, str> {
    # 시맨틱 유사도 + 중요도 + 최근성 결합
    results := db.query("
        SELECT m.content, v.similarity, m.importance, m.last_accessed
        FROM agent_memory m
          VECTOR_SEARCH(m.embedding, EMBED($2), top_k=$3) v
        WHERE m.agent_id = $1
          AND (m.expires_at IS NULL OR m.expires_at > NOW())
        ORDER BY
            (v.similarity * 0.5 + m.importance * 0.3 +
             RECENCY_SCORE(m.last_accessed) * 0.2) DESC
        LIMIT $3
    ", [agent_id, context, limit])?;

    # 접근 카운트 업데이트
    db.execute("
        UPDATE agent_memory
        SET access_count = access_count + 1,
            last_accessed = NOW()
        WHERE agent_id = $1
          AND id IN (
              SELECT id FROM agent_memory
                VECTOR_SEARCH(embedding, EMBED($2), top_k=$3) v
              WHERE agent_id = $1
          )
    ", [agent_id, context, limit])?;

    R Ok(results.map(|r| r.content));
}

메모리 응집 (Consolidation)

오래된 에피소드 메모리를 시맨틱 메모리로 요약합니다.

U vaisdb::{Database};

F consolidate_episodic_memories(
    db:       &Database,
    agent_id: str,
    llm:      &LlmClient
) {
    # 최근 7일 에피소드 메모리 조회
    episodes := db.query("
        SELECT content, created_at
        FROM agent_memory
        WHERE agent_id = $1
          AND memory_type = 'episodic'
          AND created_at < NOW() - INTERVAL '7 days'
          AND access_count < 3
        ORDER BY created_at
    ", [agent_id])?;

    I episodes.len() == 0 { R; }

    # LLM으로 요약 생성
    episode_texts := episodes.map(|e| e.content).join("\n");
    summary := llm.summarize(episode_texts)?;

    tx := db.begin()?;

    # 시맨틱 메모리로 저장
    tx.execute("
        INSERT INTO agent_memory
            (agent_id, memory_type, content, embedding, importance)
        VALUES ($1, 'semantic', $2, EMBED($2), 0.8)
    ", [agent_id, summary])?;

    # 응집된 에피소드 메모리 만료 처리
    tx.execute("
        UPDATE agent_memory
        SET expires_at = NOW()
        WHERE agent_id = $1
          AND memory_type = 'episodic'
          AND created_at < NOW() - INTERVAL '7 days'
          AND access_count < 3
    ", [agent_id])?;

    tx.commit()?;
}

완전한 RAG 파이프라인 예제

아래는 문서 수집부터 질의응답까지의 전체 RAG 파이프라인입니다.

U vaisdb::{Database};
U std::env;

# --- 1단계: 문서 수집 및 인덱싱 ---

F index_document(db: &Database, title: str, content: str) {
    tx := db.begin()?;

    # 원본 문서 저장
    doc_id := tx.execute_returning_id("
        INSERT INTO documents (title, full_content, embedding)
        VALUES ($1, $2, EMBED($2))
        RETURNING id
    ", [title, content])?;

    # 시맨틱 청킹 + 임베딩 자동 생성
    tx.execute("
        INSERT INTO document_chunks
            (document_id, chunk_index, content, embedding, token_count)
        SELECT $1, ROW_NUMBER() OVER () - 1, chunk.text, EMBED(chunk.text), chunk.tokens
        FROM SEMANTIC_CHUNK($2, max_tokens=512, overlap_tokens=64) AS chunk
    ", [doc_id, content])?;

    # 청크 간 관계 그래프 구성
    tx.execute("
        INSERT INTO chunk_edges (src_chunk_id, dst_chunk_id, edge_type, weight)
        SELECT c1.id, c2.id, 'next', 1.0
        FROM document_chunks c1
        JOIN document_chunks c2
            ON c1.document_id = c2.document_id
           AND c2.chunk_index = c1.chunk_index + 1
        WHERE c1.document_id = $1
    ", [doc_id])?;

    tx.commit()?;
    println("인덱싱 완료: {title}");
}

# --- 2단계: RAG 검색 ---

S RagResult {
    content: str,
    source:  str,
    score:   f32,
}

F rag_retrieve(db: &Database, question: str) -> Vec<RagResult> {
    db.query("
        SELECT
            dc.content,
            d.title  AS source,
            r.relevance_score AS score
        FROM RAG_SEARCH($1,
            top_k=8,
            expand_context=true,
            context_depth=1,
            min_score=0.5
        ) r
        JOIN document_chunks dc ON r.chunk_id = dc.id
        JOIN documents d        ON dc.document_id = d.id
        ORDER BY r.relevance_score DESC
    ", [question])?.map(|row| RagResult {
        content: row.content,
        source:  row.source,
        score:   row.score,
    })
}

# --- 3단계: 프롬프트 구성 + LLM 호출 ---

F answer(db: &Database, question: str) -> str {
    chunks := rag_retrieve(db, question);

    context := chunks.map(|c| "출처: {c.source}\n{c.content}").join("\n\n---\n\n");

    prompt := "다음 문서들을 참고하여 질문에 답하세요.\n\n{context}\n\n질문: {question}\n답변:";

    # LLM 호출 (예: vais-server의 LLM 클라이언트)
    llm_call(prompt)
}

F main() {
    db := Database::open("knowledge.vaisdb")?;

    # 문서 인덱싱
    index_document(&db, "VaisDB 소개",    "VaisDB는 RAG-native 하이브리드 데이터베이스...");
    index_document(&db, "HNSW 알고리즘", "HNSW는 계층적 탐색 가능 소세계 그래프...");
    index_document(&db, "BM25 랭킹",     "BM25는 확률 기반 전문 검색 랭킹 모델...");

    # 질의응답
    answer := answer(&db, "VaisDB에서 벡터 검색은 어떻게 동작하나요?");
    println("답변:\n{answer}");
}

운영 팁

청크 품질 모니터링

-- 청크 크기 분포 확인
SELECT
    AVG(token_count) AS avg_tokens,
    MIN(token_count) AS min_tokens,
    MAX(token_count) AS max_tokens,
    COUNT(*)         AS total_chunks
FROM document_chunks;

-- 임베딩 없는 청크 확인
SELECT COUNT(*) AS missing_embeddings
FROM document_chunks
WHERE embedding IS NULL;

VACUUM으로 스토리지 최적화

-- 삭제된 청크 공간 회수
VACUUM document_chunks;

-- 벡터 인덱스 재구성
REINDEX VECTOR INDEX idx_chunks_embedding;

다음 단계

  • 쿼리 가이드 — SQL, VECTOR_SEARCH, GRAPH_TRAVERSE, FULLTEXT_MATCH 상세
  • VaisDB 개요 — 아키텍처와 핵심 기능으로 돌아가기