이 사이트의 "나에게 질문하기"(메인 사이트)와 "뜻으로 찾기"(이 도서관)는 RAG (검색 증강 생성)를 서버 없이 방문자 브라우저 안에서 돌린다. 찾기는 EmbeddingGemma 2(2026-10 공개)가, 문장으로 답하기(실험)는 Gemma 4 E2B가 맡는다. 구현하며 정한 것과 부딪힌 문제를 순서대로 적는다.
전체 흐름
[미리 · 내 컴퓨터에서, 글을 고칠 때만]
md 문서 → 청크로 자르기 → 문서 임베딩 → 색인 JSON 커밋
[그때그때 · 방문자 브라우저]
질문 → 질문 임베딩 → 색인과 내적 → 상위 문단
→ (PC에서 원할 때) 문단 3개 + 질문을 생성 모델에 → 답 + 근거찾기
- 청크: 마크다운 제목(
#)을 경계로 나누고, 섹션 안 문단을 약 800자까지 잇는다. 코드 블록 안의# 주석줄은 제목으로 보지 않는다. 임베딩할 때는 문서 제목과 섹션 제목을 앞에 붙인다. "## 한계" 같은 섹션은 제목이 없으면 무엇의 한계인지 알 수 없다 - 색인은 빌드 타임에: 문서 임베딩은 로컬 Node 스크립트로 미리 만들어 정적 JSON으로 커밋한다. 배포 빌드는 모델을 돌리지 않는다. 청크 원문의 해시가 같으면 이전 벡터를 다시 쓰므로 고친 문단만 새로 임베딩한다. 이 사이트에서는 노트 473개가 청크 1,460개, 색인 1.3MB가 됐다
- 작게 저장: 768차원 벡터를 MRL(Matryoshka Representation Learning)로 256차원까지 자르고 다시 정규화했다. 그다음 숫자를 int8로 줄였다 → 텍스트 임베딩, 모델 양자화. 청크 하나를 JSON 한 줄로 쓰므로 노트 하나를 고쳐도 diff는 그 줄들에만 생긴다
- 벡터 DB 없이: 1,460개 × 256차원이면 곱셈이 약 37만 번이라 배열을 모두 도는 이중 for문으로 충분하다. 수만 개를 넘으면 근사 최근접 탐색(Approximate Nearest Neighbor, ANN)을 검토한다
- 점수는 순위로만: 관련 없는 글도 0.5~0.6이 나와서 고정 기준을 두지 않았다. 그래서 관련 없는 질문에도 결과는 늘 나온다. 관련 없는 결과를 걸러 내는 일은 생성 모델이나 리랭커(reranker)에 맡긴다
답하기 (실험)
- 지시문 + 질문 + 참고 문단 3개를 한 메시지로 이어 붙인다. 2B급 모델이라 지시 문구에 따라 답이 크게 달라졌다
- "자료에 없으면 지어내지 말라"를 강하게 쓰면 답이 있는데도 "자료에 없다"고 했다
- 역할("당신은 ○○ 도우미")을 주면 답 앞에 자기소개를 붙였다
- 참고 글을 앞에 두면 관련 없는 질문에도 참고 글 이야기를 했다
- 질문을 먼저 두고 지시를 짧게 바꾸자 세 문제가 모두 줄었다
- 그래도 환각은 남는다. 회고 글 속 다른 사람들의 의견 차를 주어를 바꿔 본인이 겪은 일처럼 쓴 답이 나왔다. 그래서 생성한 답에는 "틀릴 수 있다"는 안내를 붙인다. 근거 문단도 접은 상태(
<details>)로 늘 함께 둔다
브라우저에서 돌리며 배운 것
- 런타임: Hugging Face의 ONNX 모델을 transformers.js로 실행한다. 앱 의존성에 넣으면 서버용 런타임(약 290MB)까지 배포 설치에 섞인다. 그래서 브라우저에서 CDN으로 동적 import 한다 → 코드 스플리팅과 동적 임포트
- WebGPU 필수: 이 사이트에서 쓴 양자화 모델은 wasm 백엔드에 없는 연산을 써서 CPU로는 돌지 않았다. WebGPU는 HTTPS나 localhost 같은 보안 컨텍스트에서만 열린다
- 필요한 부품만: 텍스트만 쓰므로 이미지·오디오 인코더를 빼고 받는다. 그러자 임베딩 모델 내려받기가 약 510MB에서 207MB로 줄었다
- 기기별로 나누기: 휴대폰에서 생성 모델(약 3.1GB)을 받다가 탭이 닫혔다. 그래서 생성 기능은 마우스 포인터가 있고 메모리가 넉넉한 기기에서만 연다
- 버전 고정: 문서 벡터와 질문 벡터는 같은 모델에서 나와야 한다. 그래서 모델 저장소의 커밋까지 고정했다
- 캐시는 출처별: 받은 모델은 출처(origin)마다 따로 저장된다. 처음에는 메인 사이트와 도서관이 서브도메인으로 나뉘어 같은 모델을 각각 받았다. 그래서 도서관을 메인 사이트의
/library아래로 프록시해 같은 출처로 묶었다
로딩 UX
- 모달을 처음 열 때 대화 컴포넌트를 붙인다. 모델은 버튼을 눌러야 받는다(지연 로딩)
- 상태는
off → loading(진행률) → ready | error. 받는 중이거나 실패해도 기존 제목 찾기는 그대로 동작한다 - 생성 결과는 토큰이 나오는 대로 화면에 붙인다. 이 사이트를 PC WebGPU로 돌렸을 때 첫 글자까지 1~3초, 전체 답까지 4~7초가 걸렸다
- 한 번 켠 기기는 다음 방문 때 자동으로 켜고, 모델은 브라우저 캐시에서 다시 쓴다
다음에 볼 것
다음 과제는 네 가지다. 검색 품질을 숫자로 재는 평가 셋, 고유명사에 약한 임베딩을 보완하는 하이브리드 검색(BM25), 관련 없는 결과를 걸러 내는 리랭커, 휴대폰을 위한 서버 생성이다. 개념은 RAG (검색 증강 생성)에 정리했다.
출처: EmbeddingGemma 2 발표 Google · embeddinggemma-2-ONNX 모델 카드 · gemma-4-E2B-it-ONNX 모델 카드