RagService Chunking·로컬 임베딩·COPY 적재
목적과 보안 경계
이 파이프라인은 FileStore 또는 사용자 포탈이 승인해 발행한 원본 문서를 다음 순서로 처리한다.
- 프라이빗 문서 정제기가 Markdown 텍스트와 표를 생성한다.
SemanticChunker가 의미 경계 우선 방식으로 기본 500자 chunk / 100자 overlap JSONL을 생성한다.- 각 chunk는 로컬 임베딩 API(Ollama
bge-m3기본값)에 배치 전송되고, 정확히 1024차원인지 확인된다. - RLS principal별로 분리한 PostgreSQL transaction에서 temporary staging table로
COPY한다. - 같은 transaction에서
source_documents,doc_chunks_vector에 upsert한다.
RLS tenant·조직·보안등급은 browser/CLI가 임의로 승격할 수 있는 입력이 아니다. FileStore/PortalConsole가 서명·검증한 metadata에서만 만들고, database connection에는
set_config(..., true)로 transaction-local 적용한다.
Chunk JSON 패키지
각 JSONL record는 vector와 함께 저장할 최소 원본 메타데이터를 가진다.
| 필드 | 설명 |
|---|---|
chunk_id, document_id, chunk_index | 멱등 upsert와 출처 추적 식별자 |
chunk_text, char_start, char_end, token_count_approx | 의미 단위와 위치 정보 |
tenant_id, organization_id, security_level | PostgreSQL RLS 및 검색 경계 |
metadata.classification, metadata.title | 문서 분류와 제목 |
metadata.registrant | 등록자 ID·이름·소속 부서 코드 |
metadata.registeredAt, metadata.sourceVersion | 등록일과 원본 버전 |
metadata.assetId, metadata.portalId | FileStore/소유 포탈 추적 정보 |
1. JSONL chunk 생성
metadata.json은 FileStore나 포탈 backend가 생성한다. 일반 브라우저가 tenant, organization, security level을 제출하는 방식은 허용하지 않는다.
{
"source_document_id": "0a998a31-73d3-4901-9b05-318d6344d1d4",
"tenant_id": "18a532ad-c7bb-4b7d-9fb7-87fbe704f2e8",
"organization_id": "fa9e9751-11e5-4332-b678-52669f06803e",
"classification": "업무 지침",
"title": "보안 운영 표준",
"registrant_id": "directory-admin-1",
"registrant_name": "홍길동",
"registrant_department_code": "SEC-OPS",
"registered_at": "2026-08-25T12:00:00Z",
"security_level": "CONFIDENTIAL",
"source_version": "asset-v14",
"source_type": "ASSET_ATTACHMENT",
"asset_id": "9e244110-3ddb-4ca4-a3e5-1c2b7552bf1b",
"extra": {"portalId": "process-center"}
}cd RagService
python3 scripts/chunk_document.py cleaned.md \
--metadata metadata.json \
--output chunks.jsonl \
--chunk-size 500 \
--overlap 1002. 로컬 임베딩과 PostgreSQL COPY 적재
requirements.txt를 설치한 뒤, pgvector/RLS schema와 로컬 embedding server가 준비된 환경에서 실행한다.
sudo pip3 install -r requirements.txt
PRIVATE_EMBEDDING_URL=http://127.0.0.1:11434 \
PRIVATE_EMBEDDING_PROVIDER=ollama \
PRIVATE_EMBEDDING_MODEL=bge-m3 \
PRIVATE_EMBEDDING_DIMENSIONS=1024 \
B2B_RLS_DATABASE_URL='postgresql://processcenter:***@127.0.0.1:5432/processcenter' \
python3 -m ragcenter.indexing chunks.jsonl --batch-size 16Ollama에서는 /api/embed와 embeddings[]를, OpenAI 호환 로컬 endpoint에서는 /v1/embeddings와 data[].embedding을 사용한다. 응답 개수·numeric value·1024차원이 맞지 않으면 database transaction 전에 즉시 중단한다.
COPY와 RLS 안전성
대량 chunk를 SQL 문자열로 이어 붙이지 않는다. Python psycopg의 row-based COPY FROM STDIN으로 temporary rag_chunk_stage에 적재한 뒤, parameterized transaction으로 최종 upsert한다. 하나의 RLS transaction에는 하나의 (tenant_id, organization_id, security_level) principal만 포함되며, 여러 principal의 JSONL은 embedding 후 자동으로 그룹 분리한다.
이 방식은 RLS candidate leakage를 방지하고, retry 시 (document_id, chunk_index) unique key로 동일 문서 버전의 vector를 갱신한다. 원본 파일 다운로드 URL과 object-storage signed URL은 JSONL 또는 RAG event에 넣지 않으며, RagService는 FileStore의 상호 인증 content retrieval API만 사용해야 한다.
검증
# deterministic chunk/metadata/embedding contract tests
python3 -m unittest tests/test_chunking.py
# running PostgreSQL+pgvector and Ollama bge-m3 stack required
python3 -m unittest tests/test_local_copy_pipeline.py두 번째 통합 테스트는 실제로 chunk를 생성하고, Ollama에서 1024차원 embedding을 받고, RLS가 적용된 doc_chunks_vector에 COPY한 뒤 application DB role로 조회해 차원과 행 수를 확인한다.