---
title: "Postgres 전문검색이 느리면 ts_headline 부터 의심하라 (스니펫 입력 캡)"
description: "전문검색이 느린 원인은 인덱스가 아니라 스니펫을 만드는 ts_headline 이었습니다. 입력을 앞 8KB 로 캡해 266ms 를 3ms 로 줄인 과정을 정리했습니다."
date: 2026-10-17
updated: 2026-10-17T06:36:07.989Z
tags: [postgresql, full-text-search, performance]
canonical: https://blog.wooncloud.com/posts/postgres-ts-headline-input-cap
---

전문검색(FTS)에 결과 미리보기 스니펫(매칭어 하이라이트)을 붙였더니, 동시 사용자가 늘면서 검색 지연이 비정상적으로 치솟았습니다. 단일 검색 쿼리 하나가 약 265ms 였습니다.

![ts_headline이 문서 전체를 재토큰화하면 스니펫 생성에 265ms가 걸리지만, 입력을 앞부분 8KB로 캡해도 매칭은 GIN 인덱스가 그대로 결정해 3ms로 끝난다.](/images/posts/postgres-ts-headline-input-cap/0dedb5ca-ea35-4c14-9f17-2f857cfb7964.webp)

## 인덱스가 문제가 아니었습니다

인덱스가 없나 싶어 `EXPLAIN (ANALYZE, BUFFERS)` 를 돌렸는데 의외였습니다.

```text
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)` 으로 잘라도 검색 결과 집합은 그대로고, 스니펫이 문서 앞부분만 본다는 정도의 차이만 생깁니다.

![ts_headline 입력을 캡하지 않으면 문서 전체를 재토큰화해 약 266ms 가 걸리지만, 앞 8KB 만 넘기면 매칭은 GIN 인덱스가 정하므로 같은 결과를 약 3ms 에 얻는다.](/images/posts/postgres-ts-headline-input-cap/4dcdf6eb-057f-4005-8514-81aa68253d27.svg)

입력을 앞 8KB 정도로 캡하니 266ms 가 3ms 가 됐습니다. 약 88배입니다.

```sql
-- 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
