repozine은 GitHub Discussions에 쓴 글을 Astro 블로그로 만들어 준다. repozine으로 만든 사이트 중 제일 큰 곳이 zeikar.dev/leetcode다. 2021년부터 쓴 LeetCode 풀이 노트 165개가 올라가 있다. 9월 14일에 검색을 붙였다. 정적 사이트 검색이라면 보통 Pagefind를 쓰니까 나도 그렇게 했다. 빌드가 끝나면 pagefind --site dist가 결과물을 읽어 인덱스를 만들고, 브라우저는 필요한 조각만 받아서 검색한다.

그런데 “복잡도”를 검색했더니 노트가 8개밖에 안 나왔다. 그 단어를 쓴 노트는 훨씬 많은데.

Pagefind는 단어 앞부분만 맞춘다

페이지 세 개짜리 사이트로 재현해 봤다. Pagefind 1.5.2, lang="ko"다.

페이지 본문 “복잡도” “시간” “잡도”
시간복잡도는 O(n)이다. 안 나옴 나옴 안 나옴
복잡도는 O(n log n)이다. 나옴   안 나옴
공간 복잡도를 줄였다. 나옴   안 나옴

“복잡도”로 검색하면 “복잡도는”, “복잡도를”은 찾지만 “시간복잡도는”은 못 찾는다. “잡도”로는 아무것도 안 나온다. 공백으로 자른 단어의 앞부분만 본다는 얘기다. 영어는 “complex”로 “complexity”가 걸리니 이걸로 된다. 한국어도 조사가 뒤에 붙는 건 괜찮지만, “시간복잡도”처럼 명사끼리 붙여 쓰면 뒤쪽 명사로는 찾을 수가 없다.

Pagefind 문서를 보면 단어를 따로 잘라 주는 언어는 중국어, 일본어, 태국어뿐이다. 한국어는 목록에 없으니 영어처럼 공백으로만 자르고, 인덱싱할 때 “ko는 어간 추출을 지원하지 않는다”는 안내만 나온다. 단어 중간을 맞추는 옵션도 문서에서 찾지 못했다.

실제 노트로 세어 보니 차이가 꽤 컸다. 165개 중 35개에 “복잡도”가 들어 있는데, 그게 단어 맨 앞에 오는 노트는 8개뿐이다. 나머지는 “시간복잡도”나 “공간복잡도”라고 썼다.

단어 중간까지 찾는 검색

Pagefind 인덱스는 단어 앞부분 기준이고, 설정으로 바꿀 방법도 찾지 못했다. 그래서 9월 17일에 Pagefind를 뺐다. 대신 빌드할 때 모든 글의 제목과 본문 텍스트를 search.json 하나로 내보내고, 브라우저가 그걸 받아서 직접 찾는다. 지금 165개 글에 221KB, gzip으로 70KB다. 이 정도면 검색 페이지를 열 때 한 번 받아도 부담이 없다. 글이 훨씬 많아지면 그때는 인덱스를 다시 둬야 한다.

검색어를 공백으로 나누고, 그 단어가 전부 글 어딘가에 들어 있으면 결과에 넣는다. 대소문자는 가리지 않는다. 단어 중간도 맞으니 “복잡도”로 “시간복잡도는”이 걸리고, “해시 맵”으로도 “해시맵”이 걸린다. 빌드 단계가 하나 줄어서 npm run dev에서도 검색이 된다.

단어 중간에서 걸린 결과는 뒤로

중간까지 다 맞추면 반대 문제가 생긴다. “합을”을 검색하면 “두 수의 합을 구한다”와 함께 “조합을 센다”도 걸린다. 둘 다 나오는 건 괜찮은데, 조합 쪽이 위로 올라오면 곤란하다.

맞은 위치에 따라 점수를 다르게 줬다. 단어 맨 앞에서 맞으면 1점, 단어 중간에서 맞으면 0.25점이다. 제목은 본문보다 훨씬 무겁게 쳐서, 단어 앞이면 20점, 중간이면 10점을 준다. 본문은 같은 단어가 여러 번 나와도 점수가 1에 가까워질 뿐 계속 늘지는 않는다.

입력하는 중에도 결과가 사라지지 않게

한국어 검색에는 문제가 하나 더 있다. IME가 마지막 글자를 조합하는 동안 검색창에는 완성되지 않은 글자가 들어 있다. “배열”을 치면 검색창 값이 ㅂ, 배, 뱅, 배여, 배열 순으로 바뀐다. 입력할 때마다 검색하면 “뱅”이나 “배여” 단계에서 결과가 통째로 사라졌다가 다시 나타난다.

그래서 조합 중인 마지막 글자는 거기서 이어질 수 있는 글자를 모두 후보로 놓고 맞췄다. “뱅”은 그대로 “뱅”일 수도 있고, 모음이 이어지면 “배여”처럼 ㅇ으로 시작하는 글자로 넘어갈 수도 있어서 둘 다 맞게 했다. 겹받침이나 ㅘ 같은 겹모음으로 바뀔 수 있는 경우도 넣었다. 다만 이렇게 넓게 맞추면 “시간”을 치는 도중 지나가는 “식”이 “식” 들어간 글을 잔뜩 끌어온다. 친 그대로 맞는 글을 먼저 보여 주고, 후보 글자로만 맞는 글은 그 뒤로 미뤘다.

마지막으로 크롬에서는 조합 중에 결과 링크를 눌러도 이동하지 않는 일이 있었다. 링크를 누르는 순간 검색창이 포커스를 잃으면서 조합이 끝나는데, 이때 크롬이 검색창을 화면에 보이게 하려고 스크롤한다. 그 바람에 클릭이 끝나기 전에 링크가 밀려난다. 결과 링크의 mousedown에서 preventDefault()로 포커스를 검색창에 남겨 두니 해결됐다.