sqzass
English

검색

부분 문자열 색인, 언어마다 한 파일, 그리고 단어 색인이 아닌 이유

빌드할 때마다 언어마다 색인 하나를 씁니다.

public/
├── search-en.json
└── search-ko.json

각 행이 페이지 하나입니다. 제목, 설명, 섹션, URL, 본문 평문이 들어갑니다. 클라이언트는 누군가 검색을 처음 열 때 현재 언어의 파일을 받아 부분 문자열로 훑습니다.

왜 부분 문자열인가

흔한 방식은 단어 색인입니다. 빌드 때 본문을 단어로 쪼개 두고, 검색 때 질의의 단어를 찾습니다. 더 작고 더 빠릅니다. 그리고 한국어에서는 틀립니다.

한국어는 명사에 조사가 붙기 때문에 생성기가 본문에서는 생성기는, 생성기를, 생성기가로 나타납니다. 단어 색인도 접두 매칭이면 여기까지는 버팁니다. 버티지 못하는 건 한국어가 합성어를 붙여 쓴다는 사실입니다. 검색엔진최적화 안의 최적화는 그 무엇의 접두사도 아니고, 단어 색인은 이걸 영영 돌려주지 않습니다. 의존성이 안의 존성도 마찬가지입니다.

여기서 떠오르는 해법은 색인할 때 형태소 분석기를 돌리는 것인데, 오히려 더 나빠집니다. 사전은 범용 국어사전이라 외래어를 모르고, 기술 문서의 어휘는 대부분 외래어입니다. 템플릿은템플+릿+으로 돌아오고, 템플릿이라는 단어가 정작 템플릿을 다루는 페이지들과 매칭되지 않게 됩니다. 한국어 2000페이지 코퍼스에서 실측했을 때 템플릿의 recall이 1018페이지에서 27페이지로 떨어졌습니다.

브라우저가 이걸 보정해 줄 수도 없습니다. Intl.Segmenter("ko")는 한국어 어절을 통째로 돌려줍니다. ICU는 중국어·일본어·태국어용 사전 분절기를 싣고 한국어용은 싣지 않기 때문입니다. 그러니 형태소로 쪼갠 색인은 통째로 들어오는 질의와 맞을 길이 없습니다.

본문을 부분 문자열로 훑으면 이런 문제가 어떤 언어에서도 생기지 않습니다. 대가는 JSON 파일 하나입니다.

대가

색인은 모든 페이지의 본문입니다. 이 사이트 기준으로 언어당 80KB쯤이고, 첫 검색 때 한 번 받습니다. 문서가 늘면 선형으로 커지고, 어느 크기를 넘으면 이 설계가 틀린 선택이 되는 지점이 분명히 있습니다. 다만 그 지점은 문서 사이트보다 훨씬 뒤에 있고, 거기 도달하는 건 자기 문서의 단어를 못 찾는 검색을 배포하는 것보다 나은 문제입니다.

검색 UI가 없는 사이트라면 그 80KB도 낼 이유가 없습니다.

[search]
enabled = false

색인 생성을 통째로 건너뜁니다.

템플릿에는 같은 스위치가 site.search로 옵니다 — 현재 언어의 색인 URL이거나, 꺼져 있으면 없습니다:

{% if site.search %}
<button id="search-trigger" data-index="{{ site.search }}"></button>
{% endif %}

site.feed와 같은 규칙입니다. 이 if는 방어가 아니라 검색을 끈 사이트에서도 테마가 동작하게 하는 방법 그 자체입니다. 이 사이트의 검색 버튼과 팔레트, 푸터 링크가 전부 이 뒤에 있습니다 — 그리고 URL은 템플릿이 search-에 언어 코드를 이어 붙여 만드는 게 아니라 빌드가 줍니다.

순위

질의의 모든 단어가 그 행 어딘가에 나타나야 합니다. OR가 아니라 AND입니다. 제목에서 맞은 것이 설명에서 맞은 것보다, 설명이 본문보다 앞섭니다. 제목이 그 단어로 시작하면 단순히 포함하는 것보다 앞섭니다. 결과는 12개에서 자릅니다. 사람이 읽는 건 그 정도까지입니다.

코드 블록도 색인합니다. 문서에서 사람들은 어렴풋이 기억나는 명령어를 검색하니까요.

행 스키마

클라이언트는 직접 만드는 것이니, 무엇을 읽게 되는지 적어 둡니다. 페이지 하나가 한 행입니다.

t 제목 항상 있음
d 설명 비어 있으면 생략
u URL 항상 있음. 서브경로가 있으면 그것까지 포함
s 소속 섹션 제목 비어 있거나 자기가 섹션이면 생략
c 본문 평문, 코드 블록 포함 항상 있음

키가 한 글자인 건 행마다 반복되기 때문입니다. 수백 페이지에서 "t" 대신 "title"을 쓰면 수십 KB가 더 드는데, 사람이 손으로 읽을 파일도 아닙니다.

클라이언트

이 사이트의 search.js는 200줄 남짓이고 의존성이 없습니다. 대화상자는 <dialog>라서 Esc, 백드롭, 포커스 트랩, 닫을 때 트리거로 포커스를 되돌리는 것까지 전부 코드가 아니라 플랫폼의 동작입니다. Ctrl/ + /로 엽니다.

이건 도구의 기능이 아니라 예제 코드입니다. sqzass는 색인을 쓰고, 그 위에 무엇을 만들지는 직접 정합니다.