PostgreSQL 전문검색(full-text search)은 문서를 단어 단위로 쪼갠 tsvector와 검색어를 표현한 tsquery를 매칭시켜, LIKE '%키워드%'보다 빠르고 똑똑하게 텍스트를 찾는 내장 기능이다. GIN 인덱스를 얹으면 수백만 건에서도 밀리초 단위로 동작한다.
별도의 검색 엔진을 붙이기 전에, 나는 웬만하면 PostgreSQL의 전문검색을 먼저 시도한다. Elasticsearch를 세우는 순간 동기화·운영 부담이 따라오는데, 중소 규모 검색은 DB 안에서 끝내는 게 훨씬 속 편하다.
LIKE의 한계부터
LIKE '%foo%'는 두 가지가 아프다. 첫째, 앞에 %가 붙으면 인덱스를 못 타서 풀 스캔이다. 둘째, 단어의 어형을 모른다. "running"으로 검색해도 "run"은 안 걸리고, 대소문자·어간 처리를 전부 손으로 해야 한다.
전문검색은 텍스트를 토큰으로 정규화해서 이 문제를 푼다. 어간 추출(stemming), 불용어 제거, 정규화를 거친 tsvector를 만들어 두고, 그걸 인덱싱한다.
-- 문서를 tsvector로 변환 (영어 설정)
SELECT to_tsvector('english', 'The quick brown foxes are running');
-- 'brown':3 'fox':4 'quick':2 'run':6
-- foxes → fox, running → run 으로 어간 처리, the/are는 불용어라 제거
-- 검색어는 tsquery로
SELECT to_tsvector('english', 'a running fox')
@@ to_tsquery('english', 'fox & run');
-- true
@@ 연산자가 매칭 여부를 판단한다. 핵심은 저장할 때와 검색할 때 같은 텍스트 설정(configuration)을 써야 한다는 것. 저장은 english로 해놓고 검색은 simple로 하면 어간이 안 맞아 엉뚱하게 놓친다.
인덱스로 빠르게
본격적으로 쓰려면 tsvector를 생성 컬럼으로 저장하고 GIN 인덱스를 건다. PostgreSQL 12부터 생성 컬럼(generated column)으로 깔끔하게 만들 수 있다.
ALTER TABLE articles
ADD COLUMN search_vec tsvector
GENERATED ALWAYS AS (
to_tsvector('english', coalesce(title,'') || ' ' || coalesce(body,''))
) STORED;
CREATE INDEX idx_articles_search ON articles USING GIN (search_vec);
-- 검색
SELECT id, title
FROM articles
WHERE search_vec @@ websearch_to_tsquery('english', 'postgres index tuning')
ORDER BY ts_rank(search_vec, websearch_to_tsquery('english', 'postgres index tuning')) DESC
LIMIT 20;
websearch_to_tsquery는 구글처럼 자연스러운 입력을 받아준다. 따옴표 구문, or, 마이너스 제외까지 알아서 파싱해줘서 사용자 입력을 그대로 넣기 좋다. ts_rank로 관련도 정렬까지 붙이면 그럴듯한 검색이 된다.
GIN vs GiST
| 항목 | GIN | GiST |
|---|---|---|
| 검색 속도 | 빠름 | 상대적으로 느림 |
| 인덱스 크기 | 큼 | 작음 |
| 갱신 비용 | 높음(fastupdate로 완화) | 낮음 |
| 추천 상황 | 읽기 많은 정적 문서 | 갱신 잦은 데이터 |
실무에서 검색은 십중팔구 GIN이 정답이다. 문서는 자주 안 바뀌고 검색은 잦으니까. 갱신이 아주 빈번한 특수 케이스에서만 GiST를 고려하면 된다.
한국어는 어떻게
여기서 솔직히 말하면, PostgreSQL 기본 설정에는 한국어 형태소 분석기가 없다. english 설정으로 한국어를 넣으면 공백 단위로만 쪼개져 조사가 붙은 단어를 못 나눈다. 방법은 두 갈래다.
- 간이 방식:
simple설정 +pg_trgm(트라이그램)을 병행해 부분일치·오타 허용을 챙긴다. 소규모엔 이 조합이 가성비가 좋다. - 본격 방식:
mecab-ko기반 형태소 분석기 확장을 붙인다. 설치·운영 손이 가지만 검색 품질은 확실히 올라간다.
나는 처음엔 pg_trgm으로 시작해서 검색 품질 불만이 쌓이면 형태소 분석기로 넘어가는 단계적 접근을 선호한다. 처음부터 무겁게 갈 필요는 없다.
자주 묻는 질문
tsvector 컬럼을 꼭 따로 저장해야 하나요?
쿼리마다 to_tsvector를 즉석에서 계산할 수도 있지만, 그러면 GIN 인덱스를 탈 수 없어 느립니다. 생성 컬럼으로 저장하고 인덱싱하는 편이 성능·관리 모두 유리합니다.
ts_rank로 정렬하면 인덱스를 못 타지 않나요?
맞습니다. 랭킹 계산은 매칭된 행에 대해 수행되므로, WHERE로 후보를 좁힌 뒤 그 결과에만 ts_rank를 적용하는 구조가 중요합니다. 매칭 자체는 GIN 인덱스가 처리하고, 랭킹은 남은 소수 행에만 걸리게 하면 됩니다.
부분 단어 검색(오토컴플리트)은 전문검색으로 되나요?
어간 기반 전문검색은 완결된 단어 매칭에 강하고, 접두어 검색은 to_tsquery('foo:*')로 어느 정도 됩니다. 다만 임의 위치의 부분일치·오타 허용은 pg_trgm이 더 적합해서 둘을 함께 쓰는 경우가 많습니다.
언제 Elasticsearch로 넘어가야 하나요?
수천만 문서 이상, 복잡한 관련도 튜닝, 다국어 분석기, 검색 전용 확장성이 필요해질 때입니다. 그 전까지는 PostgreSQL 전문검색으로 충분한 경우가 많고, 별도 엔진의 동기화 비용을 아낄 수 있습니다.

댓글 0