RagService 오프라인 B2B 배포
목적과 구성
RagService의 온프레미스 스택은 PostgreSQL 16, pgvector 0.7.0, pg_bigm, mecab-ko textsearch_ko, 로컬 BGE-M3 임베딩 API, Ollama 또는 vLLM, 그리고 독립 관리자 포탈 API를 하나의 Compose 계약으로 배포한다. 운영 환경은 외부 인터넷을 호출하지 않으며, 모델·extension source·base image는 연결된 staging 환경에서 검증·반입한다.
| 서비스 | 역할 | 외부 네트워크 필요 여부 |
|---|---|---|
postgres | 1024차원 vector, pg_bigm, mecab-ko FTS, FORCE RLS | 실행 시 불필요 |
embedding-api | read-only BGE-M3 HuggingFace cache를 통한 OpenAI 호환 /v1/embeddings | 실행 시 불필요 |
ollama | 로컬 Llama3/Solar 계열 chat API | 실행 시 불필요 |
vllm | 선택적 OpenAI 호환 GPU chat API | 실행 시 불필요 |
ragcenter-api | Directory token 검증, SET LOCAL RLS, 검색/chat API, 관리자 UI | 실행 시 불필요 |
연결된 staging 환경에서의 반입 준비
infra/postgres/vendor/는 PostgreSQL 확장 source bundle을 보관하는 staging 전용 경로이다. scripts/prepare_offline_postgres_bundle.sh를 연결된 승인 환경에서 실행해 pg_bigm, mecab-ko runtime/dictionary, textsearch_ko, 고정 pgvector/pgvector:0.7.0-pg16 image tarball을 확보한다. 출력된 SHA-256을 내부 artifact manifest에 기록한 다음, 승인된 반입 절차로 폐쇄망 서버에 전달한다.
bash scripts/prepare_offline_postgres_bundle.sh
# isolated server
sudo docker load -i infra/postgres/vendor/pgvector-0.7.0-pg16.tarBGE-M3 및 LLM은 staging host에서 각 runtime cache에 다운로드하고, .env.offline의 HF_MODEL_CACHE_DIR, OLLAMA_MODEL_CACHE_DIR로 지정한 디렉터리를 폐쇄망 서버에 read-only 또는 managed volume으로 반입한다. model cache 경로가 비어 있으면 embedding container가 시작에 실패하도록 설계되어 온라인 fallback을 만들지 않는다.
기동
cp .env.offline.example .env.offline
# deployment secret manager 또는 root-only permission으로 비밀값 입력
sudo docker compose --env-file .env.offline -f docker-compose.offline.yml build postgres ragcenter-api
sudo docker compose --env-file .env.offline -f docker-compose.offline.yml --profile embedding --profile ollama up -d
# vLLM을 사용할 때만
sudo docker compose --env-file .env.offline -f docker-compose.offline.yml --profile vllm up -dGPU host는 NVIDIA driver와 NVIDIA Container Toolkit을 먼저 설치해야 한다. Compose의 GPU profile은 gpus: all을 사용하며, GPU가 없는 개발 host에서는 embedding-api, ollama, vllm profile을 실행하지 않고 기존 CPU inference endpoint를 PRIVATE_EMBEDDING_URL, PRIVATE_LLM_URL로 지정한다.
실측 검증(CPU host, 게이트웨이 조합)
오프라인 번들 없이도 위 조합을 손으로 재현해 전 구간을 확인할 수 있다(실제로 이 순서로 검증했다).
# 1) 검색 스택 이미지 — vendor/ tarball 4종으로 한 번만 빌드한다.
docker build -f infra/postgres/Dockerfile -t ragcenter-postgres:16-pgvector0.7-pgbigm1.2 infra/postgres
# 2) 기동 — 확장·RLS·검색 함수는 init SQL 이 만든다(빈 data dir 최초 1회).
docker run -d --name ragcenter-pg -e POSTGRES_DB=ragcenter -e POSTGRES_USER=<admin> \
-e POSTGRES_PASSWORD=<pw> -e RAG_APP_PASSWORD=<pw> \
-v <pgdata>:/var/lib/postgresql/data -v $PWD/infra/postgres/init:/docker-entrypoint-initdb.d:ro \
-p 55432:5432 ragcenter-postgres:16-pgvector0.7-pgbigm1.2
docker run -d --name embedding-api -e EMBEDDING_DEVICE=cpu -e EMBEDDING_MODEL_PATH=/models/bge-m3 \
-v <hf cache>/bge-m3:/models/bge-m3:ro -p 8081:8081 <embedding image> # compose 의 embedding-api
# 3) 임베딩 endpoint 는 admin settings 가 소유한다(기본값은 compose DNS) — 기동 직후 한 번 PUT 한다.
curl -X PUT -H "Authorization: Bearer <admin JWT>" -H 'content-type: application/json' \
-d '{"embedding":{"provider":"openai","endpoint":"http://embedding-api:8081","model":"bge-m3","dimensions":1024}}' \
http://<ragcenter-host>:8080/api/v1/admin/settings
# 4) 수집 → 검색 → 게이트웨이 어댑터 순서로 확인한다.
curl -X POST -H "Authorization: Bearer <user JWT>" -H 'content-type: application/json' \
-d '{"portalId":"pc","sourceDocumentId":"<uuid>","sourceVersion":"v1","tenantId":"<uuid>",
"organizationIds":["<org uuid>"],"securityLevel":"INTERNAL","classification":"사내 규정",
"title":"...","contentLocator":"http://<file-host>/doc.html","checksum":"<sha256 hex>",
"contentType":"text/html","registrant":{"id":"u1","departmentCode":"HR"},"sizeBytes":1234}' \
http://<ragcenter-host>:8080/api/v1/documents
curl -X POST -H "Authorization: Bearer <user JWT>" -H 'content-type: application/json' \
-d '{"question":"...","limit":3}' http://<ragcenter-host>:8080/api/v1/search
curl -X POST -H "Authorization: Bearer $RAG_EXPLORE_GATEWAY_API_KEY" \
-H "x-explore-user-token: <user token>" -H 'content-type: application/json' \
-d '{"question":"..."}' http://<ragcenter-host>:8080/api/explore/v1/ask점검 기준: 수집은 {"indexed":true}, 검색은 vector_score·hybrid_score 가 있는 contexts, /api/explore/v1/ask 는 게이트웨이 매핑이 없으면 근거 없음 폴백이 정상이고 매핑·토큰이 준비되면 실제 답변 + citations 를 돌려준다. contentLocator 는 HTTP(S) URL 이며 RAG_SERVICE_CONTENT_ALLOWED_HOSTS 로 호스트를 고정할 수 있고, checksum 은 본문 소문자 hex SHA-256 이어야 한다(불일치 시 checksum_mismatch). 조직 행(tb_organization)은 PortalConsole 디렉터리 동기화가 선행해야 한다(아래 RLS 절 참조).
모델 접근 경로 — ModelGateway 경유(권장)
모델·키·쿼터·비용·감사는 ModelGateway 단일 창구가 정본이다. MODEL_GATEWAY_URL과 MODEL_GATEWAY_SERVICE_TOKEN이 설정되면 답변 생성은 로컬 vLLM 대신 게이트웨이를 경유하고, 목적(x-purpose: rag.answer)만 보낸다(모델은 게이트웨이가 purposeModels로 고른다). 두 값이 없으면 기존처럼 local_llm(PRIVATE_LLM_URL)로 폴백하므로 설정 없이도 동작한다.
RagService --(x-purpose: rag.answer, Bearer 서비스 토큰)--> ModelGateway --> 모델 공급자RAG_PORTAL_ID로 귀속 헤더x-portal-id를 바꿀 수 있다(기본rag-center).PRIVATE_LLM_TIMEOUT_SECONDS는 두 경로 모두의 요청 타임아웃이다.
ExploreGateway 어댑터(검색·답변)
ExploreGateway(§14)는 상단 검색의 검색·답변 오케스트레이터다. 이 서비스는 두 어댑터를 노출하고, 둘 다 게이트웨이가 보낸 사용자 위임 토큰으로 RLS를 직접 강제한다.
| 경로 | 카탈로그 능력 | 용도 |
|---|---|---|
POST /api/explore/v1/search | search(kind=document) | 문서 결과를 상단 검색에 합류 |
POST /api/explore/v1/ask | answer | 근거 기반 답변(provider) |
RAG_EXPLORE_GATEWAY_API_KEY는 게이트웨이가 보내는 서비스 토큰과 같아야 한다(카탈로그rag-center등록값).PORTAL_CONSOLE_JWT_AUDIENCE는 게이트웨이가 재위임한 토큰도 받도록rag-center,explore-gateway로 둔다.
동적 RLS 보안 모델
tb_ai_document_registry는 원본 문서 lifecycle을 보유하고, tb_doc_chunk_vector.document_id는 ON DELETE CASCADE로 registry를 참조한다. 원본 삭제 이벤트가 registry purge로 변환되면 모든 child vector가 단일 DB transaction에서 제거된다. 소스 수정·재색인 시에는 staging COPY에 없는 이전 chunk_index를 먼저 삭제해 stale vector가 남지 않게 한다.
애플리케이션은 PortalConsole가 서명한 bearer token의 tenant_id, organization_id, clearance_level, directory_admin/rag:admin claim으로만 RlsPrincipal을 만들고, 다음 값을 요청 transaction 안에서 set_config(..., true)로 설정한다.
my.app_tenant_id
my.app_user_org_id
my.app_user_clearance_level
my.app_is_admintrue는 PostgreSQL transaction-local 값임을 의미한다. commit/rollback 뒤 pool connection에 값이 남지 않으므로 다른 사용자의 principal이 재사용되지 않는다. 브라우저가 이 값을 header, body, form field로 보내는 API는 제공하지 않는다.
public_organization_id(공용 버킷)까지 포함해 5개 값이 설정되며, directory_admin(또는 rag:admin) principal은 app_current_is_admin()으로 조직 조건이 우회된다. 따라서 RLS 격리 검증·회귀 테스트는 반드시 비관리자 principal로 수행한다(관리자 토큰만 쓰는 검증은 격리를 증명하지 못한다).
조직 행 선조건 — PortalConsole 디렉터리 동기화
tb_ai_document_registry.organization_id와 tb_rag_audit_logs.organization_id는 tb_organization을 참조한다. 즉 수집·검색 전에 사용자·문서가 속한 조직 행이 이미 있어야 한다(소유자는 PortalConsole 디렉터리 동기화). 행이 없으면 수집은 tb_ai_document_registry_organization_id_fkey 위반으로 거절되고, 검색은 감사 insert의 FK 위반이 RAG_SERVICE_AUDIT_REQUIRED에 걸려 503 rag_audit_unavailable로 fail closed 된다. 전사 공용 버킷 행은 ragcenter/identity.py의 public_organization_id() 값으로 함께 만들어 둔다.
한국어 하이브리드 검색
textsearch_ko가 제공하는 korean configuration으로 search_tsv generated column과 GIN index를 만들고, pg_bigm GIN index로 CJK substring matching을 보조한다. fn_hybrid_search_rrf는 RLS가 먼저 허용한 tb_doc_chunk_vector와 tb_ai_document_registry 행만 후보로 삼고, vector cosine·한국어 FTS·bigram 후보 순위를 Reciprocal Rank Fusion(RRF)으로 결합한다. 관리자가 조정하는 rrf_k는 순위 차이에 대한 민감도를 제어하며, LLM에는 최종 상위 3~5개 안전 컨텍스트만 전달한다.
관리자 포탈
http://<ragcenter-host>:8080/은 RagService 독립 관리자 화면이다. PortalConsole 중앙 관리자 또는 위임된 rag:admin token이 있어야 overview·설정 API를 호출할 수 있다. 화면은 다음 값을 관리한다.
| 범주 | 설정 |
|---|---|
| Ingestion | chunk size/overlap, worker 수, batch size, retry/backoff |
| Embedding | provider, endpoint, model, 1024 dimension 정책 |
| Retrieval | RRF k, korean FTS config, 안전 컨텍스트 수 |
| Local LLM | Ollama/vLLM provider, endpoint, model, temperature, context 상한 |
| Lifecycle | RAG 발행 승인, 보존 기간, 원본 삭제 cascade, FileStore 승인 필수 여부 |
화면은 secret 원문, database password, Directory signing key, 사용자 RLS principal을 저장하거나 표시하지 않는다. 운영에서는 PortalConsole OIDC/session gateway가 Authorization header를 주입하며, UI의 bearer token 입력은 로컬 개발 검증용으로만 사용한다.
References
[1] pg_bigm releases — PostgreSQL 16 support