검색은 어떻게 빨라지는가: 역색인과 BM25, 즉시 검색의 구조
이 글의 목차
← LLM Wiki / 개발·기술 / 알고리즘

검색은 어떻게 빨라지는가: 역색인과 BM25, 즉시 검색의 구조

역색인과 BM25가 맡는 역할, Pagefind의 정적 검색 구조, 한국어 토큰화와 맞춤 재정렬, 입력 즉시 결과를 표시하는 설계를 공식 문서와 예제로 정리한다.

검색 시스템의 기본 구조로 역색인과 BM25를 사용하는 것은 보편적인 선택이다. 역색인은 검색어가 들어 있는 문서를 찾는 자료구조이고, BM25는 찾은 문서들의 순서를 정하는 점수 모델이다. Elasticsearch도 BM25를 기본 유사도 모델로 사용한다. Elasticsearch의 유사도 설정

입력하는 순간 결과가 나오는 경험에는 한 단계가 더 필요하다. 검색 계산이 빨라도 입력 대기, 첫 실행 준비, 네트워크 응답을 차례로 기다리면 화면은 늦게 바뀐다. 검색을 이해할 때는 단어를 만드는 과정, 후보를 찾는 과정, 순위를 정하는 과정, 화면에 보여주는 과정을 나누어 보는 편이 정확하다.

역색인은 문서를 읽는 방향을 뒤집는다

문서를 저장할 때는 보통 문서 번호에서 본문으로 접근한다. 검색할 때 필요한 방향은 반대다. 검색이라는 단어에서 출발해 이 단어를 포함한 문서 번호를 알아야 한다. 이를 미리 만들어 둔 것이 역색인(inverted index)이다. 검색어 목록인 사전(dictionary)과, 각 검색어가 등장하는 문서의 목록인 포스팅 리스트(postings list)가 핵심 구성이다. Manning, Raghavan, Schütze, Introduction to Information Retrieval 1장

세 문서를 공백으로 나누어 역색인을 만들어 보자.

문서본문
D1검색 색인 구현
D2검색 속도 개선
D3색인 저장 구조

단어마다 문서 번호를 모으면 다음과 같은 색인이 만들어진다.

검색 → D1, D2
색인 → D1, D3
구현 → D1
속도 → D2
개선 → D2
저장 → D3
구조 → D3

검색 색인에 두 단어가 모두 있어야 한다는 AND 조건을 적용하면, 검색의 목록과 색인의 목록을 교집합으로 합친다. 결과는 D1이다. 본문 전체를 매번 읽는 대신 해당 단어의 문서 목록을 읽는 것이다. 정렬된 두 목록의 기본 교집합 알고리즘은 양쪽 포인터를 앞으로 이동하며 처리한다. 흔한 단어의 목록은 길어질 수 있으므로 실제 검색 엔진은 압축, 건너뛰기, 상위 결과만 계산하는 최적화도 사용한다. 불리언 질의 처리

문서 번호에 등장 위치까지 저장하면 검색 색인처럼 두 단어가 붙어 있는 구문도 구별할 수 있다. 제목과 본문을 구별하거나 검색어가 등장한 부분을 표시할 때도 이런 부가 정보가 쓰인다. 위치 정보를 가진 색인

색인 생성 비용은 문서를 추가하거나 바꿀 때 치른다. 검색 요청이 반복될수록 같은 본문을 계속 훑는 비용을 줄일 수 있지만, 새 문서가 색인에 반영되는 시점과 삭제된 문서가 빠지는 시점도 관리해야 한다.

BM25는 어떤 문서를 먼저 보여줄지 정한다

검색어가 들어 있는 문서가 수백 개라면 포함 여부만으로는 순서를 정하기 어렵다. BM25는 크게 세 가지 정보를 사용한다.

  • 문서 안의 등장 횟수: 같은 검색어가 여러 번 나오면 관련성의 근거가 커진다. 반복 횟수가 늘수록 추가 점수의 증가 폭은 줄어든다.
  • 문서 집합에서의 희소성: 거의 모든 문서에 등장하는 단어보다 드문 단어가 문서를 구별하는 데 더 유용하다.
  • 문서 길이: 긴 문서는 여러 단어를 포함할 기회가 많다. 평균 문서 길이를 기준으로 이 영향을 보정한다.

흔히 설명에 사용하는 BM25 형태는 다음과 같다. 여기서는 질의 안에서 같은 단어가 반복되는 경우를 단순화했다.

score⁡(D,Q)=∑t∈QIDF⁡(t)f(t,D)(k1+1)f(t,D)+k1(1−b+b∣D∣avgdl⁡)\operatorname{score}(D,Q)=\sum_{t\in Q}\operatorname{IDF}(t)\frac{f(t,D)(k_1+1)}{f(t,D)+k_1\left(1-b+b\frac{|D|}{\operatorname{avgdl}}\right)}

f(t,D)는 문서 D에서 단어 t의 등장 횟수이고, |D|와 avgdl은 문서 길이와 평균 문서 길이다. k₁은 반복 횟수에 따른 점수 포화를, b는 길이 보정 강도를 조절한다. IDF는 단어가 드물수록 커지는 값이다. Lucene은 문서 수를 N, 해당 단어가 있는 문서 수를 n이라 할 때 다음 형태를 사용한다.

IDF⁡(t)=log⁡(1+N−n+0.5n+0.5)\operatorname{IDF}(t)=\log\left(1+\frac{N-n+0.5}{n+0.5}\right)

이 설명에서 중요한 부분은 반복의 포화다. 길이가 같은 문서 둘을 비교할 때 검색어가 한 번에서 두 번으로 늘어나는 효과와, 백 번에서 백한 번으로 늘어나는 효과가 같지 않다. 문서를 길게 만들고 단어를 반복한 만큼 점수를 계속 얻는 구조를 완화한다. Lucene의 기본값은 k₁=1.2, b=0.75다. 수식의 표현과 상수 처리, 필드 길이 계산은 구현에 따라 달라질 수 있으므로 엔진 사이의 점수를 그대로 비교하기보다는 같은 검색 안의 순위로 읽는다. Lucene BM25Similarity 문서

Pagefind는 이 구조를 정적 사이트에 맞게 배치한다

Pagefind는 빌드가 끝난 HTML에서 검색용 데이터를 만들고, 이를 정적 파일과 함께 배포한다. 방문자의 브라우저는 검색에 필요한 색인 조각과 결과 정보를 불러온다. 검색 요청마다 별도의 검색 서버가 본문을 조회하는 구성을 만들지 않아도 된다. 공개 문서가 빌드 단위로 갱신되는 블로그나 문서 사이트에 잘 맞는 이유다. Pagefind 소개, 시작하기

문서 작성 → HTML 빌드 → 검색 색인 생성 → 정적 파일 배포
                                           ↓
사용자 입력 → 필요한 색인 조각 로드 → 후보 검색과 점수 계산 → 결과 표시

Pagefind 1.5.2의 검색 코드는 단어별 문서 정보를 조회하고 BM25 계열 점수를 계산한다. 기본 순위 설정에는 단어 빈도, 문서 길이, 반복 포화, 검색어와 일치 단어의 길이 차이에 관한 조절 값이 있다. termSaturation 기본값은 1.4, pageLength는 0.75다. 제목 등 특정 부분의 가중치를 높이는 기능도 제공한다. Pagefind 1.5.2 검색 구현, 순위 조절, 내용 가중치

검색 실행 위치와 공개 범위는 함께 결정해야 한다. 브라우저에 내려보낸 공개 색인은 사용자가 열어볼 수 있는 데이터다. 비공개 본문을 색인에 넣고 화면에서만 숨기는 방식은 접근 제어가 되지 않는다. 개인 문서를 함께 찾는 서비스라면 서버에서 요청자의 권한을 확인한 뒤 허용된 결과만 반환하는 경로가 필요하다.

작은 목록의 문자열 검색도 역할이 있다

메뉴나 서비스 이름처럼 항목 수가 적고 텍스트가 짧은 목록은 전체를 순회하면서 문자열 포함 여부를 검사해도 충분할 수 있다. 구현이 단순하고 별도 색인의 동기화가 필요 없다는 장점이 있다. 문서가 많아지거나 본문이 길어지면 요청마다 확인할 텍스트도 늘어난다. 이때 역색인으로 후보를 줄이는 효과가 커진다.

2026년 10월 24일 기준 Readiz의 통합 검색은 이런 선택을 함께 사용한다. 공개 글은 Pagefind에서 후보를 찾고, 작은 서비스 목록은 정규화한 이름과 별칭 등의 문자열을 비교한다. 결과 목록에서는 제목의 정확한 일치, 접두어 일치, 태그와 별칭 일치 같은 서비스별 규칙으로 순서를 다시 조정한다.

여기서 BM25 검색과 맞춤 재정렬은 서로 다른 단계다. 예를 들어 문서 검색에서 상위 30개를 받은 뒤 제목 일치 점수를 높이면, 그 30개 안의 순서는 바뀐다. 최초 후보에 없던 31번째 문서를 재정렬만으로 되살릴 수는 없다. 제목 검색을 더 잘하려면 가중치뿐 아니라 후보 수와 검색 대상 필드도 함께 살펴야 한다.

정확한 메뉴 이름을 입력했을 때 그 메뉴가 먼저 나오는 편이 좋은지, 본문 설명이 풍부한 글이 먼저 나오는 편이 좋은지는 제품의 목적에 따라 달라진다. 맞춤 점수는 서비스의 검색 목적에 맞춰 정하고 실제 검색 예제로 검증한다.

한국어는 단어를 만드는 과정부터 확인한다

같은 색인과 점수 모델을 사용해도 토큰을 어떻게 만들었는지에 따라 결과가 달라진다. 검색, 검색을, 검색하다를 어떤 관계로 취급할지, 붙여 쓴 문장을 어떻게 나눌지가 먼저 결정되어야 한다. 문자열을 소문자로 바꾸고 공백을 정리하는 처리와, 단어의 어형을 분석하는 처리는 별개다.

Pagefind의 한국어 지원 표에는 UI 번역이 제공되고 어간 추출(stemming)은 지원하지 않는 것으로 표시되어 있다. 확장 빌드는 공백으로 분리되지 않은 언어의 단어 분할(segmentation)을 지원한다. 조사 처리, 띄어쓰기 변형, 초성 검색, 오타 교정은 필요한 질의별로 직접 시험한다. Pagefind 다국어 검색

SQLite FTS5로 검색을 옮길 때도 같은 문제가 남는다. 기본 unicode61 토크나이저는 유니코드 문자 규칙을 이용하고, porter는 영어용 어간 추출이다. 한국어 형태소 분석이 자동으로 추가되는 것은 아니다. 부분 문자열 검색을 위해 trigram을 선택하면 세 글자 단위 색인을 활용할 수 있지만, 세 유니코드 문자보다 짧은 문자열은 전문 검색 질의에서 매칭되지 않는다. 검색 같은 두 글자 질의를 반드시 시험해야 하는 이유다. SQLite FTS5 토크나이저

선택 기준은 실제로 찾고 싶은 표현이다. 제목의 시작 부분을 찾는 기능, 본문 단어 검색, 문장 중간의 부분 문자열 검색은 서로 다른 요구다. 한 가지 색인을 선택하기 전에 이 요구를 예제 질의로 고정해 두면 누락을 빨리 발견할 수 있다.

입력 즉시 표시하려면 기다리는 이유를 줄인다

체감 응답 시간에는 검색 계산 외의 비용이 들어간다. 검색창을 처음 열면서 JavaScript와 WebAssembly를 준비하고, 색인과 목록을 내려받고, 서버 결과를 기다리는 시간이 겹칠 수 있다. 모든 출처가 끝난 뒤 한꺼번에 보여주는 구성이라면 가장 느린 출처가 첫 화면의 시점까지 결정한다.

다음은 지연이 생기는 지점별 개선 방법이다.

지연이 생기는 지점대응함께 지켜야 할 조건
검색창을 연 뒤 엔진을 처음 준비포커스 등 사용 의도가 생기면 준비 시작불필요한 선로딩 비용 확인
입력할 때마다 고정 시간 대기로컬 검색은 바로 실행느린 서버 호출은 별도로 조절
모든 출처의 완료를 기다림준비된 결과부터 점진적으로 표시남은 검색 상태와 선택 항목 유지
이전 질의가 뒤늦게 도착요청 취소와 질의 세대 번호 검사최신 입력의 결과만 화면에 반영
반복되는 공개 목록 요청공개 데이터에 짧은 캐시 적용갱신 시점과 만료 기준 명확화
한글 조합이 끝날 때까지 대기조합 중 문자열로도 검색조합용 Enter와 방향키를 결과 조작으로 가로채지 않기

예를 들어 검을 검색한 요청보다 나중에 보낸 검색 요청이 먼저 끝날 수 있다. 이전 요청을 취소하고, 결과를 표시하기 직전에 현재 질의 번호와 같은지 확인하면 뒤늦은 응답이 화면을 덮는 일을 막을 수 있다. 브라우저의 AbortController는 fetch 요청 등의 중단을 지원한다. 취소할 수 없는 계산이나 이미 끝난 비동기 처리까지 고려하면 결과 반영 단계의 검사도 유용하다. MDN AbortController

디바운스는 입력이 잠시 멈춘 뒤 실행하여 요청 수를 줄이는 방식이다. 서버 호출 비용을 줄이는 데 도움이 되지만, 모든 결과에 같은 대기 시간을 걸면 로컬에서 이미 준비된 결과도 늦어진다. 공개 로컬 검색과 개인 문서 서버 검색의 실행 시점을 따로 조절할 수 있다. 개인 결과는 권한 확인을 마친 응답만 표시하고, 로그아웃이나 권한 변경 이후에도 예전 결과가 남는 캐시를 만들지 않아야 한다.

무엇을 측정하면 다음 개선을 고를 수 있을까

첫 결과가 보이는 시간과 전체 결과가 모이는 시간을 따로 측정하면 병목을 구별하기 쉽다. 캐시가 없는 첫 검색과 같은 세션의 반복 검색도 나눈다. 평균뿐 아니라 느린 경우의 지연을 함께 보면 일부 질의에서만 생기는 문제를 놓치지 않는다.

속도 측정과 함께 대표 질의별 기대 결과를 남긴다. 정확한 제목, 제목 일부, 본문에만 있는 단어, 두 단어 질의, 두 글자 한국어, 조합 중 입력, 결과 없는 질의 정도부터 시작할 수 있다. 검색 결과가 빨리 나와도 원하는 문서가 후보에 빠지면 사용자에게는 실패다.

다음 선택은 관측한 병목에서 출발한다. 작은 서비스 목록의 순회가 충분히 빠르면 그대로 유지한다. 본문을 전부 읽는 서버 검색이 느려지면 FTS5 같은 전문 검색 색인을 검토한다. 정적 공개 문서의 첫 검색만 느리면 엔진 교체 전에 초기 로드와 색인 조각의 전송을 확인한다. 여러 출처를 합치는 화면이 늦으면 부분 결과 표시와 질의 취소를 먼저 점검한다.

역색인은 검색 범위를 줄이고, BM25는 후보의 순서를 정하며, 비동기 응답 설계는 사용자가 기다리는 방식을 바꾼다. 이 세 역할을 구분하면 검색 품질과 체감 속도를 각각 개선할 수 있다.

자료를 다시 볼 때

확인할 질문원문과 활용 범위
역색인은 어떤 자료구조인가Introduction to Information Retrieval: 사전, 포스팅 리스트, 질의 처리의 기초
BM25는 무엇을 조절하는가Lucene BM25Similarity: 기본값과 IDF 정의
실제 엔진의 기본값인가Elasticsearch Similarity: 기본 유사도와 설정 범위
정적 사이트에 어떻게 적용하는가Pagefind 문서: 빌드, 브라우저 검색, 순위와 언어 지원
SQLite로 옮기면 무엇이 달라지는가SQLite FTS5: 전문 검색, 토크나이저, BM25와 부분 문자열 검색

Pagefind의 구현 설명은 1.5.2, Lucene API 설명은 10.5.2를 기준으로 확인했다. 실제 도입이나 업그레이드 때는 해당 버전의 설정과 한국어 질의를 다시 대조하면 된다. 지연을 단계별로 나누어 해석하는 다른 사례로는 AI 서빙의 지연과 처리 용량을 함께 볼 수 있다.

AI로 읽기 · Markdown
Markdown 열기 ↗
curl -fsSL 'https://www.readiz.com/llm-wiki/inverted-index-bm25-pagefind-search-design/index.md'

서비스 열기

열기 방식을 선택해 주세요.

새 브라우저 탭에서 열기현재 화면을 유지하며 별도 탭에서 사용합니다.

로그인이 필요하거나 창 안에서 열리지 않는 서비스는 새 브라우저 탭에서 열어 주세요.