
글이 12편이 되었을 때 RAG 검색을 넣어 볼까 싶어 Claude에게 물었습니다. 답은 아직 이르다는 쪽이었습니다. MEGAFIXEL은 서버와 DB가 없는 정적 사이트라, 질문을 처리하려면 서버나 외부 API를 새로 운영해야 하고 호출 비용과 악용 방지까지 따라온다는 이유였습니다.
그런데 임베딩만 하면 되는 일이라면 서버가 꼭 필요할까요? 작은 모델을 방문자 브라우저에 올리면 운영할 서버도, 호출 비용도 없습니다. 그렇게 다시 물었더니 판단이 바뀌었습니다. 남는 문제는 모델 용량 하나였습니다.

그래서 받고 싶은 사람만 받는 선택 기능으로 만들었습니다. 상단 돋보기 오른쪽의 아이콘을 누르면 바로 써 볼 수 있습니다. 쓰는 법은 사용법 공지에 따로 적었습니다.
글자 검색부터
모델을 고르기 전에 있던 검색부터 봤습니다. 검색어 전체가 글자 그대로 있어야 찾았고, 결과는 관련도와 상관없이 최신순이었습니다. 몬티 홀로 찾으면 본문에 몬티홀로 적힌 글이 나오지 않았습니다.
이건 모델 없이 고칠 수 있습니다. 검색어를 낱말로 나눠 모두 들어간 글만 남기고, 세 글자 이상은 띄어쓰기를 무시하고, 제목에 있으면 앞에 두었습니다. 두 글자까지 붙여 찾으면 가나가 내가 나를에도 걸려서 기준을 세 글자로 잡았습니다. 코드는 49줄이었습니다.
모델 후보
조건은 세 가지였습니다. 한국어가 되고, 브라우저용 ONNX 파일이 있고, 상업적으로 써도 되는 라이선스일 것. 처음 떠올린 multilingual-e5-small은 양자화한 파일도 118MB였습니다. 글 하나 찾자고 받기에는 큰 크기입니다.
더 찾아보니 이 모델을 한국어 질문과 문단 쌍으로 다시 학습한 작은 판이 있었습니다. 모델 파일이 38.3MB입니다. 후보는 셋으로 추렸습니다.
| 후보 | 받는 파일 | 라이선스 | 특징 |
|---|---|---|---|
| KoEn-E5-Tiny | 약 40MB | Apache 2.0 | 한국어 미세 조정 |
| multilingual-e5-small | 약 144MB | MIT | 가장 널리 쓰임 |
| granite-embedding-97m | 약 120MB | Apache 2.0 | 같은 크기대에서 벤치마크 최고라고 소개 |
embeddinggemma-300m은 전용 약관이라 뺐습니다.
질문 34개로 재 보기
모델 카드의 점수는 이 블로그의 글로 잰 값이 아닙니다. 그래서 결과를 보기 전에 질문 34개와 정답 글을 먼저 적어 두었습니다. 17개는 글에 없는 표현으로 물었습니다. 세 겹 정육면체 퍼즐을 스무 번 안에 푸는 법의 정답은 루빅스 큐브 글입니다.
글은 섹션마다 400자씩 잘라 303개 조각으로 만들었고, 조각 앞에 글 제목과 소제목을 붙였습니다. 질문과 가장 가까운 조각의 점수가 그 글의 점수입니다. 방금 고친 글자 검색도 같은 질문으로 돌렸습니다.
- 문단
- 400자로 나눔
- 제목 붙임
- 숫자로 바꿈
- 몬티홀 딜레마, 문을 바꾸면 당첨 확률이 두 배가 되는 이유 - 반반으로 보이는 이유:문 두 개가 남았을 때 반반이 되려면 두 문이 같은 처지여야 합니다. 여기서는 처지가 다릅니다. 내 문은 아무 정보 없이 고른 문이고, 남은 문은 사회자가 정답을 피해 열지 않은 문입니다. 남은 개수만 세면 이 차이가 보이지 않습니다. 처음 고른 문이 오답인 판에서는 사회자가 남긴 문이 언제나 정답이고, 처음 고른 문이 정답인 판에서만 남긴 문이 오답입니다. 앞의 경우가 세 판 중 두 판, 뒤의 경우가 세 판 중 한 판입니다. 문이 두 개 남았다는 사실은 그대로여도 두 문의 확률은 1/3과 2/3입니다. 사회자의 규칙이 바뀌면 답도 바뀝니다. 현실의 퀴즈 쇼에서 사회자가 정답을 모른 채 문을 열 일은 없지만, 조건의 차이를 보려고 따져 봅니다.364자
- 몬티홀 딜레마, 문을 바꾸면 당첨 확률이 두 배가 되는 이유 - 반반으로 보이는 이유:다. 현실의 퀴즈 쇼에서 사회자가 정답을 모른 채 문을 열 일은 없지만, 조건의 차이를 보려고 따져 봅니다. 사회자가 아무 문이나 열었는데 우연히 염소가 나왔다면, 그때는 정말 반반입니다. 이 변형 은 '몬티 폴'이라고 부릅니다. 정답이 열린 판을 무효로 치고 염소가 나온 판만 세면 몬티홀 문제와 같아 보입니다. 화면은 같아도 남는 판이 다릅니다. 처음 고른 문이 맞은 판은 사회자가 무엇을 열든 염소라서 그대로 남습니다. 처음 고른 문이 틀린 판은 사회자가 절반의 확률로 정답을 열어 무효가 됩니다. 그래서 남는 비율이 1/3 대 2/3 × 1/2 = 1/3로 같아집니다. 사회자가 정답을 알면 무효가 되는 판이 없어서 2/3가 그대로 남습니다.363자
| 방식 | 1위 적중 | 3위 안 적중 | 크기 |
|---|---|---|---|
| 글자 검색 | 2 | 2 | 0 |
| KoEn-E5-Tiny | 30 | 34 | 40MB |
| multilingual-e5-small | 31 | 34 | 144MB |
| granite-embedding-97m | 28 | 30 | 120MB |
글자 검색은 34개 중 32개에서 아무 글도 찾지 못했습니다. 문장으로 물으면 낱말이 전부 들어간 글이 거의 없기 때문입니다.
글자 검색
결과 없음
KoEn-E5-Tiny
- 1몬티홀
- 2Jev
- 3비트코인
e5-small
- 1몬티홀
- 2Jev
- 3코딩 테스트
granite
- 1드론 경로
- 2비트코인
- 3몬티홀
벤치마크와 달랐던 결과
의외였던 건 granite입니다. 공개 벤치마크에서 같은 크기대 최고라고 소개된 모델인데, 이 글들에서는 꼴찌였습니다. MBTI T랑 F 말투 구별에 스테레오 카메라 글을 1위로 냈습니다. 일반 벤치마크 점수만 보고 골랐다면 가장 나쁜 쪽을 골랐을 겁니다.
KoEn-E5-Tiny와 e5-small의 차이는 1위 적중 한 문제였습니다. 3위 안 적중은 같았고 크기는 3.6배 차이라 작은 쪽으로 정했습니다. 재기 전에 정한 기준도 '1위와 5%p 이내면 가장 작은 모델'이었습니다.
다만 글이 13편뿐이라 쉬운 시험입니다. 아무렇게나 골라도 3위 안에 들 확률이 23%나 됩니다. 글이 늘면 다시 재 봐야 합니다.
없는 내용을 물으면
벡터 검색은 무엇을 물어도 가장 가까운 글을 돌려줍니다. 파스타 만드는 법을 물어도 뭔가 내놓습니다. 그래서 블로그와 무관한 질문 10개를 따로 재 봤습니다.
무관한 질문의 최고 점수는 0.236~0.377이었고, 정답이 있는 질문은 0.412~0.685였습니다. 0.40을 기준으로 두면 이 표본에서는 깔끔하게 갈립니다. 표본이 작아서 강한 근거는 아니지만, 없다고 답할 기준은 생겼습니다.
- 블로그에 답이 있는 질문
- 블로그와 무관한 질문
여기서도 차이가 있었습니다. e5-small은 점수가 0.85 부근에 몰려 정답과 오답이 0.01 차이로 붙습니다. KoEn-E5-Tiny는 폭이 넓어 기준선을 긋기 쉬웠습니다.
답을 글로 지어내는 단계는 넣지 않았습니다. 작은 모델로 생성까지 하면 틀린 답이 나올 위험이 크고, 내 글을 잘못 옮기는 검색은 없느니만 못합니다. 그래서 엄밀히 말하면 RAG의 앞부분인 검색까지만 있습니다.
브라우저에서 돌리기
방법은 새로 만들 게 많지 않았습니다. Jev 글의 T/F 판정기가 이미 같은 구조입니다. 버튼을 누른 뒤에만 onnxruntime-web과 토크나이저를 불러오고, 받은 모델은 브라우저 Cache API에 저장합니다.
제 맥 스튜디오의 Chromium에서 잰 값은 세션 열기 약 0.6초, 질문당 13~17ms였습니다. WASM 한 스레드인데도 충분히 빠릅니다. 휴대폰 속도는 아직 재지 못했습니다.
툴팁 숫자는 처음 예상보다 커졌습니다. 모델과 토크나이저만 41MB인 줄 알았는데 실행 엔진 WASM이 14.2MB입니다. 합쳐서 55.4MB이고, 라이브러리까지 더해 약 56MB로 적었습니다. 압축되는 파일이 있어 실제 전송량은 더 적지만, 저장 공간 기준으로 큰 쪽을 적었습니다.
모델 파일을 어디서 내려줄지도 정해야 했습니다. 처음에는 모델을 올린 사람의 허깅페이스 저장소를 그대로 가리켰습니다. 그 사람이 저장소를 지우면 내 검색도 같이 망가집니다. 라이선스가 재배포를 허용하니 모델과 라이브러리를 이 사이트 안으로 옮겼고, 고지 파일을 함께 두었습니다. 옮긴 뒤 만든 벡터는 바이트까지 같았습니다.
글을 고치면
처음 든 걱정은 글을 고칠 때마다 도구를 다시 받아야 하느냐였습니다. 그럴 필요가 없습니다. 모델은 그대로고 바뀌는 건 글 벡터 파일뿐이라, 방문자는 검색할 때 최신 벡터만 받습니다. 지금 크기는 약 290KB입니다.

벡터는 처음에 빌드 때 만들 생각이었지만 바꿨습니다. Vercel 빌드마다 모델을 받아 돌리면 느리고 불안정합니다. 그래서 pnpm embed로 만들어 저장소에 넣습니다. 조각마다 해시를 두어 바뀐 조각만 다시 계산합니다.
계산에는 새 의존성 대신 테스트용으로 이미 있던 Playwright Chromium을 씁니다. 방문자 브라우저와 같은 WASM 경로를 타니 값이 어긋날 걱정이 없습니다. 처음 전체 계산은 약 65초 걸렸습니다. 벡터가 낡으면 테스트가 실패하므로 빠뜨리고 배포할 수 없습니다.
조각 크기
이 모델은 한 번에 512토큰까지 받습니다. 글자 수로는 내용에 따라 다릅니다. 이 블로그 글은 토큰당 1.92자라 약 980자, 영어 문장은 약 2,000자입니다. 토크나이저는 스스로 자르지 않습니다. 4,400자를 넣었더니 2,003토큰이 그대로 나왔습니다. 그래서 pnpm embed는 넘는 조각이 있으면 계산 전에 실패하게 했습니다.
지금 조각은 평균 176토큰, 가장 긴 것이 268토큰이라 여유가 있습니다. 한도에 맞춰 조각을 키우면 벡터 수는 줄지만 한 벡터에 여러 내용이 섞입니다. 그래서 400자를 유지하기로 했고, 앞으로 글도 문단을 400자 안쪽에 한 가지 내용만 담아 쓰기로 했습니다.
마치며
처음 판단은 서버를 전제로 한 것이었습니다. 그 전제를 빼니 "글이 50~100편은 쌓여야 한다"던 시점이 12편으로 당겨졌습니다. 남은 걱정은 56MB를 받을 사람이 얼마나 있느냐인데, 받지 않아도 글자 검색은 그대로 있습니다.
아직 모르는 것도 있습니다. 실제 휴대폰에서의 속도와 메모리, 글이 수십 편으로 늘었을 때의 정확도, 0.40 기준이 그때도 맞는지입니다.
COMMENTS
댓글을 불러오고 있습니다.