---
tags: [research, rag, robeing, pgvector, jsonb, postgres, schema]
type: research
status: closed
closed_date: 2026-03-22
closed_reason: 설계 목적 달성, 200개 대상 구현 완료, Unresolved는 3차 계획 또는 후속 문서로 이관
research_target: Markdown 메타·청크·임베딩을 PostgreSQL 중심으로 적재하는 스키마 방향 정리
---

# PGVector·JSONB RAG 스키마 설계 리서치

## 상태

- proposed

**작성일**: 2026-03-20  
**목적**: Markdown 중간표현, 메타데이터, 청크, 임베딩을 PostgreSQL 중심으로 관리하는 스키마 방향을 정리한다.

---

## 1. 결론

- 현재 로빙 구조에서는 별도 벡터 DB보다 PostgreSQL + PGVector + JSONB가 더 실용적이다.
- 문서 원본 레벨과 청크 레벨을 분리해 관리해야 한다.
- 사람이 읽는 Markdown과 검색이 읽는 청크를 분리하면 재처리와 품질 개선이 쉬워진다.

## 2. 권장 테이블 분리

| 테이블 | 역할 |
|--------|------|
| `document_files` | 원본 파일 및 Markdown 파생본 단위 관리 |
| `document_chunks` | 임베딩·검색용 청크 관리 |
| `document_relations` | 명시적 문서 연결 관리 |
| `document_jobs` | OCR/임베딩/동기화 작업 상태 관리 |

## 3. `document_files` 권장 필드

- `id`
- `source_path`
- `md_path`
- `title`
- `file_type`
- `status`
- `author`
- `created_at`
- `modified_at`
- `indexed_at`
- `metadata_jsonb`
- `content_hash`

## 4. `document_chunks` 권장 필드

- `id`
- `document_file_id`
- `chunk_index`
- `chunk_type`
- `content`
- `content_tsv`
- `embedding`
- `token_count`
- `metadata_jsonb`
- `created_at`

## 5. 추천 SQL 초안

```sql
create table if not exists document_files (
    id bigserial primary key,
    source_path text not null unique,
    md_path text not null,
    title text not null,
    file_type text not null,
    status text not null default 'active',
    author text,
    created_at timestamptz,
    modified_at timestamptz,
    indexed_at timestamptz default now(),
    content_hash text,
    metadata_jsonb jsonb not null default '{}'::jsonb
);

create table if not exists document_chunks (
    id bigserial primary key,
    document_file_id bigint not null references document_files(id) on delete cascade,
    chunk_index integer not null,
    chunk_type text not null default 'text',
    content text not null,
    content_tsv tsvector,
    token_count integer,
    metadata_jsonb jsonb not null default '{}'::jsonb,
    embedding vector(768),
    created_at timestamptz not null default now(),
    unique (document_file_id, chunk_index)
);

create index if not exists idx_document_files_metadata_jsonb
    on document_files using gin (metadata_jsonb);

create index if not exists idx_document_chunks_metadata_jsonb
    on document_chunks using gin (metadata_jsonb);

create index if not exists idx_document_chunks_tsv
    on document_chunks using gin (content_tsv);

create index if not exists idx_document_chunks_embedding_hnsw
    on document_chunks using hnsw (embedding vector_cosine_ops);
```

## 6. 내가 추천하는 검색 방식

- 1차: SQL 필터로 범위를 줄인다.
- 2차: `tsvector` 키워드 검색과 PGVector 의미 검색을 함께 돌린다.
- 3차: 결과를 합성해 상위 청크를 고른다.
- 4차: 필요시 `document_files.metadata_jsonb`와 `related` 관계를 따라 확장한다.

## 7. JSONB를 넣는 이유

- front matter 전체를 유연하게 저장할 수 있다.
- 파일 타입별 메타 차이를 허용하기 쉽다.
- SQL 필터와 운영 상태 갱신이 쉽다.
- 향후 메타 항목이 늘어나도 마이그레이션 부담이 줄어든다.

## 8. 내가 반대한 구조

- 문서 전체를 한 테이블 한 컬럼에만 몰아넣는 방식
- 메타데이터를 JSONB 없이 text 배열로만 관리하는 방식
- 청크 테이블 없이 문서 전체에만 벡터를 매다는 방식

## 9. 운영 관점 추천

- `content_hash`로 재처리 필요 여부를 판정한다.
- `status`와 `metadata_jsonb` 안의 `sync_status`, `ocr_status`, `embedding_status`를 함께 관리한다.
- soft delete를 우선 사용해 감사 추적과 복구 가능성을 남긴다.

## 현재 상태 보정 (2026-03-22)

- 설계 별칭 → 운영 실체: document_files = team_document, document_chunks = team_document_chunk, document_relations = Apache AGE 그래프.
- team_document: 1,174건 / team_document_chunk: 3,474건.
- tsvector + GIN 인덱스: 구현 완료 (team_document_chunk.tsv, 트리거 자동 생성).
- 하이브리드 검색 (vector + keyword + graph RRF): 구현 완료.
- 임베딩: Gemini Embedding 2, 768d.
- Apache AGE: companyx_docs, document_graph 그래프 2개 운용 중.

## Unresolved

- 설계 별칭과 운영 실체를 한 번에 읽을 수 있는 정합화 표가 아직 없다.
- `document_jobs` 수준의 작업 상태 테이블을 둘지, 기존 테이블 메타와 로그로 버틸지 확정되지 않았다.

## 10. 관련 문서

- [다형식 문서 RAG 자동수집·정규화 전략 리서치](./260320_다형식문서_RAG_자동수집_정규화_전략_리서치.md)
- [Front Matter 메타데이터 설계 리서치](./260320_FrontMatter_메타데이터_설계_리서치.md)
- [PostgreSQL 그래프 확장 설계 리서치](./260320_PostgreSQL_그래프확장_설계_리서치.md)
