LARGE-SCALE DOCUMENT RAG · DRIVE SYNC PIPELINE

CCC Chatbot

수십 년간 공유 드라이브에 쌓인 문서 27,500건이 있는데, 정작 필요한 자료를 찾을 방법이 없었습니다. 이 프로젝트의 핵심은 챗봇 UI가 아니라 그 많은 파일을 어떻게 다 집어넣고, 원본이 바뀔 때마다 어떻게 따라가느냐였습니다. 드라이브를 그대로 원본으로 두고, 변경된 파일만 골라 다시 색인하는 동기화 파이프라인을 만들었습니다. Decades of work had left 27,500 documents on a shared drive with no practical way to find anything in them. The hard part of this project wasn't the chat UI — it was getting that many files in, and keeping up as the originals change. The drive stays the source of truth, and a sync pipeline re-indexes only what actually changed.

Role · 단독 개발·운영Sole engineer & operator Stack · FastAPI · Gemini File Search · Google Drive API · PostgreSQL Status · 사내 운영 중 (비공개 시스템)In production internally (private system)
Indexed
~27.5K docs
주제별 corpus 4개across 4 corpora
Re-index
변경분만Delta only
전체 재색인 없음never a full rebuild
App overhead
<0.4s
나머지 96%는 모델 추론the other 96% is inference
API surface
79 endpoints
21 tables
INGESTION PIPELINE

2만 건이 넘는 파일을 집어넣는 과정Getting twenty-thousand-plus files in

한 번에 다 올리는 게 아니라, 드라이브 변경을 받아 파일 단위로 흘려보냅니다. 어느 단계에서 실패해도 그 파일만 다시 처리됩니다.Nothing is bulk-loaded. Drive changes arrive and files flow through one at a time, so a failure anywhere only ever costs that one file.

01 · WATCH
변경 감지Detect
Drive Changes API가 webhook으로 변경을 알림. 채널이 만료되기 전에 자동 갱신The Drive Changes API pushes a webhook; the channel renews itself before it expires
02 · DEDUPE
중복 차단Deduplicate
파일별 lock + 동시성 제한. modifiedTime이 같으면 건너뜀A per-file lock and a concurrency cap; identical modifiedTime is skipped
03 · PARSE
포맷별 추출Extract
PDF·이미지·구형 .hwp 등 사내에 실제로 쌓인 포맷을 처리Handles what actually accumulated: PDFs, images, legacy .hwp
04 · INDEX
색인 대기Index
managed RAG에 업로드하고 완료까지 폴링. 실패는 3회 재시도Uploads to managed RAG and polls until done; failures retry three times
05 · RECONCILE
경로 정규화Reconcile
유니코드 정규화 후 DB에 반영. 인용 링크가 원본을 정확히 가리키게 함Normalizes Unicode before the DB write so citation links hit the right original

대량 인제스천에서 실제로 발목을 잡은 건 검색이 아니라 파일명이었습니다. 같은 파일 이름이 소스마다 다른 유니코드 형태로 존재합니다 — 카탈로그는 NFC, macOS와 Drive API 응답은 NFD. 운영 데이터의 78%가 비NFC였고, 정규화 없이 조인하면 1,767건 중 9건만 맞았습니다. 게다가 문서 식별자가 파일명이 아니라 상대경로(역사기록/2010/…/문서.pdf)이고 이름이 겹치는 경우가 수백 건이라, 인용 링크가 정확히 걸리려면 경계에서 NFC 정규화와 경로 기반 구분을 함께 적용해야 했습니다. What actually blocked mass ingestion wasn't retrieval — it was filenames. The same name exists in different Unicode forms depending on the source: NFC in the catalog, NFD from macOS and the Drive API. 78% of production rows were non-NFC, and joining without normalization matched 9 of 1,767. On top of that the document identifier is a relative path (history/2010/…/doc.pdf) rather than a filename, with hundreds of collisions — so accurate citation links required both NFC normalization at the boundary and path-based disambiguation.

SYNC ARCHITECTURE

Google Drive를 원본으로 두는 구조Keeping Google Drive as the source of truth

직원들이 이미 드라이브에서 일하고 있는데 별도 업로드 절차를 만들면 아무도 쓰지 않습니다. 드라이브가 원본이고 색인은 그 그림자입니다. 동기화 워커는 단일 워커 컨테이너로 분리해 같은 작업이 두 번 돌지 않게 했습니다.Staff already work in the drive; inventing a separate upload step would mean nobody uses it. The drive is the original and the index is its shadow. The sync worker runs in its own single-worker container so no job ever fires twice.

Source of truth
Google Shared Drive staff edit here, nowhere else
↓ Changes.watch webhook
Sync worker
Single worker no duplicate firingper-file lock + semaphoreretry ×34 scheduled jobs full sweep · channel renew
↓ changed files only
Index
Gemini File Search managed chunking + embedding
↑ query · ↓ grounding
Query path
Permission scope group → corpusTool routing docs vs webNL→SQL read-only, cappedCitation resolve path → Drive link
App & data
FastAPI multi-workerPostgreSQL paths · groups · audit log
WHAT IT DOES

주요 기능Key capabilities

Ask, don't browse

폴더를 헤매지 않고 물어봅니다Ask instead of digging through folders

"2000년대 초 여름수련회 자료 어디 있어?"처럼 물으면 해당 문서를 찾아 답하고 원본 드라이브 링크를 붙입니다. 인용은 검색된 문서 전체가 아니라 실제로 답의 근거가 된 문서에서만 뽑기 때문에, 링크를 열면 정말 그 내용이 있습니다.Ask "where are the materials from the early-2000s summer retreats?" and it finds them, attaching a link to the original drive file. Citations come only from the documents that actually grounded the answer — not everything retrieved — so opening the link really does show what was quoted.

grounded citationsDrive links
Documents and the web

사내 자료와 최신 정보를 알아서 구분합니다It tells internal material from current information apart

사내 규정을 묻는지, 오늘의 외부 정보를 묻는지에 따라 찾아야 할 곳이 다릅니다. 질문을 받으면 사내 문서를 볼지, 웹을 볼지, 둘 다 볼지, 아니면 검색 없이 바로 답할지를 모델이 스스로 고릅니다. 규칙을 손으로 짜 넣지 않았습니다.A question about internal policy and a question about today's news need different sources. For each question the model itself picks whether to read internal documents, search the web, do both, or answer without searching — with no hand-written rules.

model-chosen routingdocs · web · both
Permissions & analytics

권한대로만 검색되고, 통계는 말로 물어봅니다Retrieval respects permissions; analytics answer in words

그룹별로 볼 수 있는 자료가 다릅니다. 답을 만든 뒤 걸러내는 게 아니라 질문을 받는 시점에 검색 범위 자체를 좁혀, 권한 없는 문서는 애초에 검색되지 않습니다. 관리자는 "이번 달 접속자 몇 명?"처럼 물어 통계를 차트로 받는데, 이때 만들어지는 SQL은 조회만 허용되고 결과 건수도 강제로 제한됩니다.Different groups see different material. Rather than filtering after an answer is written, the search scope narrows the moment a question arrives, so unpermitted documents are never retrieved. Administrators can ask "how many visitors this month?" and get a chart — with the generated SQL restricted to reads and its result count capped.

scope before retrievalread-only NL→SQLSSO
Three clients, one backend

웹앱·위젯·홈페이지가 같은 API를 씁니다The app, the widget, and the homepage share one API

사내 전용 웹앱, 다른 사이트에 script 한 줄로 붙는 임베드 위젯, 공개 홈페이지 세 곳이 하나의 백엔드를 공유합니다. 계약을 문서로 고정해 두어 소비자 쪽 프론트엔드가 따로 배포되어도 깨지지 않습니다.An internal web app, an embeddable widget that drops into another site with one script tag, and the public homepage all share a single backend — with the contract pinned in a document so consumer front-ends can deploy on their own schedule without breaking.

embeddable widgetpinned API contract
TECH STACK

기술 스택Tech stack

Gemini File SearchFunction callingGoogle Search grounding Google Drive Changes APIwebhook syncdelta re-index FastAPIPydantic v2SQLAlchemyAlembicPostgreSQL 15Gunicorn React 18Material UIVite Docker ComposeNginxGCPGitHub ActionsJWT + SSO