33P by GN⁺ 9일전 | ★ favorite | 댓글 2개
  • 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는 공백을 기준으로 분할하며, 수동으로 불용어와 어간을 사용할 수 있음
    • 필드 가중치에 기반한 검색 점수 매기기가 불가능함
    • 대안과 비교했을 때 장난감 시스템임

Hacker News 의견 무섭네영... "천만개? 장난?"