Postgres 전문검색이 느리면 ts_headline 부터 의심하라 (스니펫 입력 캡)
전문검색이 느린 원인은 인덱스가 아니라 스니펫을 만드는 ts_headline 이었습니다. 입력을 앞 8KB 로 캡해 266ms 를 3ms 로 줄인 과정을 정리했습니다.
전문검색(FTS)에 결과 미리보기 스니펫(매칭어 하이라이트)을 붙였더니, 동시 사용자가 늘면서 검색 지연이 비정상적으로 치솟았습니다. 단일 검색 쿼리 하나가 약 265ms 였습니다.
인덱스가 문제가 아니었습니다
인덱스가 없나 싶어 EXPLAIN (ANALYZE, BUFFERS) 를 돌렸는데 의외였습니다.
Limit (actual time=131..266 rows=2)
└ Result (ts_headline) 약 265ms <- 거의 전부
└ Sort + Index/Seq Scan 약 1.4ms <- 스캔은 사실상 공짜스캔과 정렬은 1ms 대인데 ts_headline()(스니펫 생성) 노드 하나가 265ms 를 먹고 있었습니다.
이유는 ts_headline 이 하이라이트 위치를 찾으려고 원본 문서 전체를 다시 토크나이즈하기 때문입니다. body 컬럼은 평균 100KB 대(추출된 오피스, PDF 본문)였고 최대는 수백 KB 였습니다. 행당 100ms 이상이 나왔고, 이게 CPU 를 태워 동시성 아래서 DB 가 포화됐습니다.
입력을 잘라도 결과는 그대로입니다
어떤 행이 검색되는지는 tsvector GIN 인덱스가 결정합니다. ts_headline 은 오직 미리보기 스니펫 텍스트만 만듭니다.
그러면 입력을 LEFT(body, N) 으로 잘라도 검색 결과 집합은 그대로고, 스니펫이 문서 앞부분만 본다는 정도의 차이만 생깁니다.
입력을 앞 8KB 정도로 캡하니 266ms 가 3ms 가 됐습니다. 약 88배입니다.
-- before: 문서 전체를 재토큰화하니 느리다
SELECT id,
ts_headline('simple', body,
plainto_tsquery('simple', $1)) AS snippet
FROM documents
WHERE search_tsv @@ plainto_tsquery('simple', $1) -- 매칭은 여기(GIN)서 결정
ORDER BY ts_rank(search_tsv, plainto_tsquery('simple', $1)) DESC
LIMIT 50;
-- after: 스니펫 입력만 캡한다. 결과는 동일하고 88배 빠르다
SELECT id,
ts_headline('simple', LEFT(body, 8000), -- 스니펫 입력만 제한
plainto_tsquery('simple', $1)) AS snippet
FROM documents
WHERE search_tsv @@ plainto_tsquery('simple', $1)
ORDER BY ts_rank(search_tsv, plainto_tsquery('simple', $1)) DESC
LIMIT 50;여기서 더 줄일 수도 있습니다. 스니펫을 과다 인출한 후보 전부에 만들지 말고, 권한 필터와 페이지를 적용한 뒤 실제로 보여 줄 행에만 만들면 호출 횟수 자체가 줄어듭니다.
FTS 가 느리면 보통 인덱스부터 의심하게 되는데, 스니펫 하이라이팅 비용은 문서 길이에 비례합니다. 큰 문서를 다룬다면 ts_headline 입력부터 캡해 볼 만합니다. 매칭 정확도는 인덱스가 책임지니까요.
참고
- PostgreSQL, Highlighting Results (ts_headline): https://www.postgresql.org/docs/current/textsearch-controls.html#TEXTSEARCH-HEADLINE