본문으로 건너뛰기
AIPida
채택됨고급업무 자동화

Notion+사내문서 RAG Slack 봇 — 매번 풀 재색인이 너무 느립니다 (증분 색인 패턴?)

Notion이랑 사내 문서를 임베딩해서 Slack에서 질문하면 답해주는 봇을 운영 중입니다. LlamaIndex + 벡터DB 조합이고요.

문제는 동기화 파이프라인입니다. 지금은 매일 새벽에 전체 문서를 다 끌어와서 통째로 재임베딩하는데, 문서가 늘면서 시간도 오래 걸리고 임베딩 비용도 계속 올라갑니다. 대부분 문서는 안 바뀌는데 매번 전부 다시 도는 게 낭비 같아요.

바뀐 문서만 골라서 증분 업데이트하고 싶은데, 깔끔하게 구현한 패턴이 있을까요? Notion API 기준으로 변경 감지를 어떻게 하는지도 궁금합니다.

답변 3

  • 채택된 답변에디터 검증

    증분 색인 핵심은 두 가지예요. (1) 변경 감지(2) 멱등한 upsert/삭제. 저도 같은 거 운영하는데 (2)에서 호되게 당했어서 좀 길게 씁니다.

    변경 감지는 Notion이 페이지마다 last_edited_time을 주니까 그걸로 합니다. 마지막 동기화 시각을 데이터스토어에 저장해두고, 데이터소스 쿼리나 Search에서 last_edited_time 그 이후 것만 필터하면 바뀐 페이지만 떨어져요. 전체 안 끌어와도 됩니다.

    진짜 함정은 멱등한 처리 쪽이에요. 문서 하나가 여러 청크로 쪼개지니까, 갱신할 때 "이 문서에서 나온 기존 청크를 먼저 지우고 새로 넣어야" 합니다. 안 그러면 옛날 청크가 벡터DB에 그대로 남아서 검색에 섞여 나와요(stale chunk). 그래서 각 청크 메타데이터에 doc_id를 박아두고, 갱신 시 delete(where doc_id == X)insert 순서로 처리하세요. LlamaIndex면 문서마다 안정적인 doc id 부여하고 refresh_ref_docs나 docstore 기반 업서트 쓰면 이 delete-then-insert를 알아서 해줍니다. 직접 짜도 되는데 그럼 doc_id 메타 박는 거 잊지 마시고요.

    그리고 삭제 동기화 — 이거 다들 빼먹어요. Notion에서 페이지가 trash로 가거나 지워지면 벡터DB에서도 빠져야 하는데, last_edited_time 증분만 돌리면 "삭제"는 이벤트가 안 와서 안 잡힙니다. 주기적으로(주 1회 정도) 전체 doc_id 목록 비교해서 소스에 없는 건 벡터DB에서 청크째 제거하는 reconcile 패스를 따로 두세요.

    정리하면 매일 = last_edited_time 증분 upsert, 주 1회 = 전체 reconcile(삭제 동기화). 이렇게 두 단 두면 비용·속도·정합성 다 잡힙니다. 비용은 대부분 문서가 안 바뀌니까 거의 첫날만 풀로 나가고 그 뒤론 확 떨어질 거예요.

  • 깔끔하게 하려면 동기화 상태 담는 작은 테이블 하나 만드세요. 컬럼은 doc_id, source_last_edited, content_hash, indexed_at 정도면 충분합니다. 이게 있으면 "무엇을 색인했는지"가 단일 진실 소스가 돼서, 어느 문서가 왜 누락/중복됐는지 추적이 됩니다. 상태 테이블 없이 벡터DB만 보고 증분 돌리다 나중에 정합성 깨지면 원인 추적이 지옥이에요. 작은 SQLite 하나라도 두는 거 추천.

  • 에디터 검증

    stale chunk랑 삭제 동기화 지적 정확합니다. 저도 처음에 삭제를 빼먹어서 폐기된 정책 문서를 봇이 몇 주 동안 계속 근거로 답하는 사고 있었어요. 사용자 입장에선 봇이 그냥 거짓말하는 걸로 보입니다.

    하나 더 — 변경 감지 정밀도 올리려면 본문 해시를 같이 저장하세요. last_edited_time은 누가 페이지 열어서 공백 하나 고치거나, 심지어 속성(태그 같은 거)만 바꿔도 갱신됩니다. 그러면 본문은 그대로인데 재임베딩이 도는 거예요. 그래서 저는 last_edited_time으로 1차 후보 추리고 → 본문 정규화 후 해시 비교 → 해시 다를 때만 재임베딩, 이렇게 2단 게이트를 둡니다. 안 바뀐 문서 재임베딩이 거의 0으로 떨어져서 비용 체감이 확 달라요.