PostgreSQL 풀 텍스트 검색: 제대로 하면 빠르다(느리다는 오해 해소)
(blog.vectorchord.ai)- PostgreSQL의 기본 Full-Text Search(FTS)는 느리다는 인식이 있지만, 적절한 최적화만 하면 매우 빠르게 동작함
- Neon의 블로그에서는 Rust 기반
pg_search
확장과 기본 FTS를 비교하여 후자가 느리다고 주장함 - 하지만 이 비교는 PostgreSQL FTS에 필수적인 기본 최적화 작업들이 누락된 상태에서 이루어졌을 가능성이 큼
- 본 글에서는 기본 FTS 설정에 단순한 최적화만 적용해도 50배 성능 향상이 가능함을 수치로 입증함
벤치마크 설정 개요
- 1천만 개의 로그 데이터를 가진 테이블을 기반으로 테스트 수행
CREATE TABLE benchmark_logs ( id SERIAL PRIMARY KEY, message TEXT, country VARCHAR(255), severity INTEGER, timestamp TIMESTAMP, metadata JSONB );
- 문제의 쿼리 구조:
SELECT country, COUNT(*) FROM benchmark_logs WHERE to_tsvector('english', message) @@ to_tsquery('english', 'research') GROUP BY country ORDER BY country;
- to_tsvector()를 쿼리 내에서 실행 → 매우 비효율적
- GIN 인덱스가 있어도 제대로 활용되지 않음
테스트 환경 (기본 설정 복제)
- EC2 i7ie.xlarge 인스턴스, 로컬 NVMe SSD 사용
- 4 vCPUs, PostgreSQL 16(Docker) 사용
- 주요 PostgreSQL 설정:
-c shared_buffers=8GB -c maintenance_work_mem=8GB -c max_parallel_workers=4 -c max_worker_processes=4
- 병렬 처리 제한: max_parallel_workers_per_gather = 2 (Neon은 8 사용)
성능 저하 요인 1: 실시간 tsvector 계산
- to_tsvector()를 쿼리 내에서 실행 시:
- 텍스트 파싱, 형태소 분석 등을 매번 수행
- 인덱스를 전혀 활용할 수 없음
-
해결책: tsvector 컬럼 사전 생성 및 인덱싱
- 1. tsvector 컬럼 추가
ALTER TABLE benchmark_logs ADD COLUMN message_tsvector tsvector;
- 2. 데이터 채우기
UPDATE benchmark_logs SET message_tsvector = to_tsvector('english', message);
- 3. 인덱스 생성 (fastupdate 비활성화)
CREATE INDEX idx_gin_logs_message_tsvector ON benchmark_logs USING GIN (message_tsvector) WITH (fastupdate = off);
- 4. 쿼리 수정
SELECT country, COUNT(*) FROM benchmark_logs WHERE message_tsvector @@ to_tsquery('english', 'research') GROUP BY country ORDER BY country;
성능 저하 요인 2: GIN 인덱스 fastupdate=on 설정
- fastupdate=on은 쓰기 성능엔 유리하지만, 검색 성능에는 악영향
- 읽기 전용 또는 검색 중심의 데이터셋에는 fastupdate=off가 필수
- 인덱스가 더 작고 빠르며 pending list 처리 불필요
-
최적화된 GIN 인덱스 생성법
CREATE INDEX idx_gin_logs_message_tsvector ON benchmark_logs USING GIN (message_tsvector) WITH (fastupdate = off);
성능 향상 수치: 50배 이상 개선
- 최적화 전: 약 41.3초 (41,301 ms)
- 최적화 후: 약 0.88초 (877 ms)
- 약 50배의 성능 향상을 보여줌
- 병렬 처리 수가 적은 환경에서도 이 성능 달성 가능
ts_rank 성능은 실제로 느릴 수 있음
- ts_rank 또는 ts_rank_cd는 모든 결과를 평가한 뒤 정렬하므로 상대적으로 느릴 수 있음
- 특히 대량 결과를 다룰 때는 CPU/IO 부담이 큼
고급 순위 기능: VectorChord-BM25 확장
- 정렬 정확도 및 속도가 중요한 경우에는 전용 확장 사용이 더 효과적
- VectorChord-BM25는 PostgreSQL용 확장으로, BM25 알고리즘 기반의 순위 평가 기능 제공
- Elasticsearch보다 3배 빠름이라는 보고도 있음
VectorChord-BM25의 장점
- BM25 알고리즘: TF-IDF보다 발전된 검색 순위 알고리즘
- 전용 인덱스 형식: Block WeakAnd 등 고속 검색 최적화
- bm25vector 타입 제공: 토크나이즈된 표현 저장
- 검색 정확도 및 속도 모두 향상
결론: PostgreSQL 기본 FTS도 충분히 빠름
- tsvector 컬럼과 적절한 GIN 인덱스(fastupdate=off) 사용 시, 기본 FTS로도 매우 빠른 검색 가능
- 성능 비교는 최적화된 기준으로 이루어져야 함
- 고급 순위 기능이 필요할 경우엔 VectorChord-BM25와 같은 확장 도구 활용 고려
- 핵심 메시지: 도구가 느린 것이 아니라, 설정이 문제일 수 있음
Hacker News 의견
-
pg_search의 유지보수자로서, Postgres 문서에 따르면 Neon/ParadeDB 기사와 여기서 사용된 전략 모두 유효한 대안으로 제시됨
- Postgres FTS의 문제는 단일 쿼리를 최적화하는 것이 아니라 다양한 실제 쿼리에 대해 Elastic 수준의 성능을 제공하는 것임
- pg_search는 후자의 문제를 해결하기 위해 설계되었으며, 벤치마크도 이를 반영함
- Neon/ParadeDB 벤치마크는 총 12개의 쿼리를 포함하며, 현실적인 사용 사례에서는 비현실적임
- pg_search는 다양한 "Elastic 스타일" 쿼리와 Postgres 타입에 대해 간단한 인덱스 정의만으로 작동함
-
tsvector를 실시간으로 계산하는 것은 큰 실수임
- Postgres FTS를 개인 프로젝트에 구현했을 때, 문서를 읽고 지침을 따랐음
- 문서는 기본 비최적화 사례를 만들고 최적화하는 과정을 명확히 설명함
- 이 실수를 저지른 사람은 문서를 읽지 않았거나 Postgres FTS를 잘못 표현하려는 의도가 있는 것 같음
-
모든 것을 Postgres에 넣으려는 경향을 이해하지 못하겠음
-
Postgres-native의 전체 텍스트 검색 구현을 더 많이 보게 되어 기쁨
- 대안 솔루션(lucene/tantivy)은 불변 세그먼트에 맞춰 설계되어 Postgres 힙 테이블과 결합하면 더 나쁜 솔루션이 될 수 있음
-
설명 계획이 없어서 무슨 일이 일어나는지 이해하기 어려움
- 쿼리가 인덱스를 사용하면 실시간 tsvector 재검사는 일치 항목에만 적용되며 벤치마크 쿼리는 LIMIT 10이므로 재검사가 적음
- 쿼리 조건이 2개의 gin 인덱스에 조건을 가지고 있어, 계획자가 모든 일치 항목을 먼저 재검사하는 것 같음
-
몇 년 전, 네이티브 FTS를 사용하고 싶었으나 실패했음
- 수천 개의 삽입/초가 있는 테이블에서 전체 업데이트가 느려져 트랜잭션이 시간 초과됨
- 인덱스를 추가했으나 두 번째 인덱스가 완료되자 시스템에서 시간 초과가 발생함
- 인덱스를 다시 삭제해야 했고, 실제 FTS 성능을 테스트할 기회를 얻지 못했음
-
pg_search와 vchord_bm25 확장 RPM/DEB를 패키징했음
- 스스로 벤치마크를 하고 싶은 사람들을 위해 링크를 제공함
-
많은 팀이 Elasticsearch나 Meilisearch로 바로 이동하는 것을 보았음
- 적절히 사용하면 네이티브 PG FTS에서 많은 성능을 얻을 수 있음
- SQLite + FTS5 + Wasm을 사용하여 브라우저에서 유사한 성능을 얻을 수 있을지 궁금함
-
1천만 개의 레코드는 장난감 데이터셋임
- 전체 Wikipedia나 2022년 이전 Reddit 댓글과 같은 큰 텍스트 데이터셋이 벤치마크에 더 적합함
-
2008년경 처음으로 pg 전체 텍스트를 사용했음
- Postgres 전체 텍스트 검색의 문제는 너무 느리다는 것이 아니라 너무 유연하지 않다는 것임
- 간단한 검색을 추가하는 데는 좋지만 검색을 조정하려면 부족함
- Solr와 Elasticsearch는 복잡한 인덱스와 검색 처리를 설정할 수 있음
- Postgres는 이러한 기능을 채택할 수 있지만, 현재는 아무것도 제공하지 않음
- Postgres는 공백을 기준으로 분할하며, 수동으로 불용어와 어간을 사용할 수 있음
- 필드 가중치에 기반한 검색 점수 매기기가 불가능함
- 대안과 비교했을 때 장난감 시스템임