수십 년간 공유 드라이브에 쌓인 문서 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.
한 번에 다 올리는 게 아니라, 드라이브 변경을 받아 파일 단위로 흘려보냅니다. 어느 단계에서 실패해도 그 파일만 다시 처리됩니다.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.
대량 인제스천에서 실제로 발목을 잡은 건 검색이 아니라 파일명이었습니다. 같은 파일 이름이 소스마다 다른 유니코드 형태로 존재합니다 — 카탈로그는 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.
직원들이 이미 드라이브에서 일하고 있는데 별도 업로드 절차를 만들면 아무도 쓰지 않습니다. 드라이브가 원본이고 색인은 그 그림자입니다. 동기화 워커는 단일 워커 컨테이너로 분리해 같은 작업이 두 번 돌지 않게 했습니다.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.
"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.
사내 규정을 묻는지, 오늘의 외부 정보를 묻는지에 따라 찾아야 할 곳이 다릅니다. 질문을 받으면 사내 문서를 볼지, 웹을 볼지, 둘 다 볼지, 아니면 검색 없이 바로 답할지를 모델이 스스로 고릅니다. 규칙을 손으로 짜 넣지 않았습니다.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.
그룹별로 볼 수 있는 자료가 다릅니다. 답을 만든 뒤 걸러내는 게 아니라 질문을 받는 시점에 검색 범위 자체를 좁혀, 권한 없는 문서는 애초에 검색되지 않습니다. 관리자는 "이번 달 접속자 몇 명?"처럼 물어 통계를 차트로 받는데, 이때 만들어지는 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.
사내 전용 웹앱, 다른 사이트에 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.