PostgreSQL 풀 텍스트 검색과 Elasticsearch 비교
하나의 데이터베이스 또는 진정한 검색 스택
실제 논쟁의 핵심은 PostgreSQL 이 텍스트를 검색할 수 있는지, 혹은 Elasticsearch 가 문서를 저장할 수 있는지 여부입니다. 둘 다 가능합니다. 흥미로운 질문은 검색의 복잡성이 어디에 존재해야 하는가입니다.
PostgreSQL 의 전체 텍스트 검색은 tsvector, tsquery, 사전 (dictionaries), 순위 매기기 (ranking), 그리고 GIN 인덱스를 갖춘 트랜잭션 관계형 데이터베이스 내부에 존재합니다. Elasticsearch 는 Lucene 을 기반으로 구축된 분산 검색 및 분석 엔진으로, 분석기 (analyzers), BM25 점수 계산, 샤드 기반 확장, 집계 (aggregations), 그리고 준실시간 인덱싱을 제공합니다.

이들은 기능 목록의 차이 이전에 운영 철학의 차이입니다.
이 선택을 저장소, 파이프라인, 운영에 매핑한다면, 이 데이터 인프라 개요 가 더 넓은 시스템 컨텍스트를 제공합니다.
이 비교가 실제로 다루는 것
저수준에서 양쪽 시스템 모두 역색인 (inverted-index) 아이디어에 의존하지만, 이를 패키징하는 방식은 매우 다릅니다. PostgreSQL 은 GIN 을 선호하는 텍스트 검색 인덱스 유형으로 권장하며, 이를 tsvector 값의 형태소 (lexemes) 위에 구축된 역색인으로 설명합니다. Elasticsearch 는 text 필드를 분석하여 전체 텍스트 검색을 위한 인덱스를 생성한 다음, 확장성을 위해 샤드와 노드로 인덱스를 분배합니다. 실제적으로 PostgreSQL 은 애플리케이션 데이터베이스에 내장된 검색처럼 느껴지는 반면, Elasticsearch 는 자체 런타임, 수명 주기, 확장 모델을 가진 전용 검색 플랫폼처럼 느껴집니다.
이 비교는 주로 네이티브 PostgreSQL 전체 텍스트 검색과 모호한 매칭 (fuzzy-ish matching) 을 위한 매우 일반적인 pg_trgm 헬퍼에 관한 것입니다. 그 범위가 중요한 이유는 더 넓은 PostgreSQL 생태계가 시간이 지남에 따라 검색에 더 중점을 두고 있기 때문입니다. RUM 과 같은 확장은 구문 검색과 순위 지향 스캔을 위한 더 풍부한 인덱스 동작을 추가하고, PGroonga 는 PostgreSQL 에 또 다른 전체 텍스트 인덱싱 경로를 확장합니다. 이것이 네이티브 PostgreSQL 이 Elasticsearch 와 동등하다는 것을 의미하지는 않지만, 많은 오래된 비교가 가정하는 것보다 경계가 덜 정적임을 의미합니다.
제 의견 있는 프레임은 간단합니다. 검색은 보통 기능이 될 때까지 제품 표면이 아닙니다. 검색이 여전히 기능인 동안에는 PostgreSQL 이 이기는 경향이 있습니다. 검색이 사용자가 가장 먼저 판단하는 것이 될 때 Elasticsearch 가 이기는 경향이 있습니다. 이는 브랜드 이름의 문제가 아니라, 관련성 로직, 인덱싱 정책, 운영상 고통이 어디에 존재할 수 있는지 허용하는지에 관한 것입니다.
PostgreSQL 전체 텍스트 검색의 작동 방식
PostgreSQL 전체 텍스트 검색은 원시 텍스트를 형태소 (lexemes) 로 변환하는 것으로 시작합니다. to_tsvector 는 텍스트를 토큰화하고, 구성된 사전을 통해 정규화하며, 중지어 (stop words) 를 제거하고, 살아남은 형태소를 위치와 함께 저장합니다. setweight 를 사용하면 문서의 다른 부분 (제목, 요약, 본문 등) 에서 온 형태소에 라벨을 붙여 각 부분이 순위 매기기에 다르게 영향을 미치도록 할 수 있습니다. PostgreSQL 은 여러 가지 사전 정의된 언어 구성을 지원하며, 파서와 사전을 사용하여 사용자 정의 구성을 만들 수 있습니다.
이러한 패턴을 구현하는 동안 간결한 SQL 참조가 필요하다면, 이 PostgreSQL 치트시트 와 가장 유용한 SQL 명령어가 포함된 이 SQL 치트시트 가 실용적인 동반자가 됩니다.
일반적인 프로덕션 패턴은 저장 생성된 (stored generated) tsvector 열과 GIN 인덱스입니다. PostgreSQL 문서에서는 실용적인 텍스트 검색에는 일반적으로 인덱스가 필요하다고 단호하게 명시하며, 저장 생성된 열이 GIN 인덱스에 공급되는 것을 명시적으로 보여줍니다. 이 패턴은 검증 중에 to_tsvector 를 재계산하는 것을 방지하고 쿼리 표면을 깔끔하게 유지합니다.
alter table posts
add column search_vector tsvector
generated always as (
setweight(to_tsvector('english', coalesce(title, '')), 'A') ||
setweight(to_tsvector('english', coalesce(summary, '')), 'B') ||
setweight(to_tsvector('english', coalesce(body, '')), 'D')
) stored;
create index posts_search_idx
on posts using gin (search_vector);
select id,
title,
ts_rank_cd(
search_vector,
websearch_to_tsquery('english', '"query planner" -mysql')
) as rank
from posts
where search_vector @@ websearch_to_tsquery('english', '"query planner" -mysql')
order by rank desc
limit 20;
쿼리 측면에서 PostgreSQL 은 사용자 입력이 엔지니어링 블로그에서 인정하는 것보다 더 혼란스럽기 때문에 여러 파서를 제공합니다. to_tsquery 는 명확하고 강력합니다. phraseto_tsquery 는 <-> 연산자를 사용하여 단어 순서를 보존합니다. websearch_to_tsquery 는 검색 엔진과 유사한 입력을 받아들이며, 따옴표로 묶인 구문, OR, - 부정 (negation) 을 이해하며, 원시 사용자 입력에 대해 문법 오류를 절대 발생시키지 않습니다. PostgreSQL 은 또한 to_tsquery 의 형태소에 * 를 붙여 접두어 매칭 (prefix matching) 을 지원합니다.
순위 매기기는 네이티브 PostgreSQL 이 강점과 한계를 모두 보여주는 곳입니다. ts_rank 와 ts_rank_cd 는 빈도, 근접성, 구조적 가중치를 사용할 수 있으며, 가중치 모델은 많은 애플리케이션 검색 작업에 놀라울 정도로 좋습니다. 동시에 PostgreSQL 의 자체 문서에서는 순위 매기기가 비용이 많이 들어갈 수 있으며, 내장 순위 함수가 글로벌 정보를 사용하지 않는다고 명시합니다. 이것이 네이티브 PostgreSQL 전체 텍스트 검색의 조용하지만 중요한 한계입니다. 순위 매기는 것은 가능하지만, 관련성 (relevance) 이 엔진의 중심은 아닙니다.
PostgreSQL 이 전체 텍스트 검색에 충분한 경우
PostgreSQL 은 전용 검색 벤더들이 원하는 것보다 더 자주 충분합니다. 검색이 트랜잭션 행, 조인 (joins), 권한, 그리고 최신 작성 (fresh writes) 에 매우 가깝게 머를 때 특히 매력적입니다. PostgreSQL 의 MVCC 모델은 트랜잭션 일관성과 스냅샷 기반 읽기를 제공하므로, 작성을 받는 것과 동일한 데이터베이스가 Elasticsearch 스타일의 리프레시 윈도우 없이도 검색에 응답할 수 있습니다. 검색 상자가 실제로는 “방금 편집한 앱 안의 기록을 찾아라"인 경우, 그 속성이 화려한 관련성 데모보다 더 중요합니다.
SQL 필터링이 기능의 절반일 때도 충분합니다. 상태 필터, 테넌트 격리, 게시 상태, 타임스탬프, 관계형 조인은 비즈니스 라인 시스템에서 키워드 관련성과 똑같이 중요할 때가 많습니다. 그러한 경우 PostgreSQL 전체 텍스트 검색은 별도의 플랫폼처럼 유지되어야 하는 것이 아니라, 관계형 쿼리 플랜의 또 다른 인덱스된 술어 (predicate) 처럼 동작합니다. 이는 지루한 아키텍처이지만, 지루함은 종종 올바른 종류의 빠름입니다.
Elasticsearch 가 검색 엔진으로서 작동하는 방식
Elasticsearch 는 매우 다르게 자신을 제시합니다. 자체 문서는 이를 Apache Lucene 을 기반으로 구축된 분산 검색 및 분석 엔진, 확장 가능한 데이터 저장소, 벡터 데이터베이스로 정의하며, 프로덕션 규모에서 속도와 관련성을 최적화하고 준실시간으로 운영된다고 명시합니다. Elasticsearch 는 각 인덱스를 샤드로 나누고, 그 샤드를 복제하여 노드에 분배하여 인덱싱 및 쿼리 용량을 증가시킵니다. 이것이 Elasticsearch 가 단순히 “인덱스"만은 아닌 이유입니다. 이는 클러스터 아키텍처입니다.
밑바닥에서 분석기 (analyzers) 가 대부분의 중부 역할을 담당합니다. Elasticsearch 분석기는 문자 필터, 토크나이저, 토큰 필터의 조합입니다. 내장 분석기, 언어 분석기, 사용자 정의 분석기가 있으며, 동의어 처리는 분석의 일류 (first-class) 부분입니다. 이는 검색 동작이 쿼리만과 관련된 것이 아니라는 것을 의미합니다. 또한 문서와 쿼리가 점수 계산이 시작되기 전에 어떻게 정규화되는지와도 관련이 있습니다.
이러한 패턴을 구현하는 동안 손으로 만져볼 수 있는 API 참조를 원한다면, 이 Elasticsearch 치트시트 가 필수 명령과 운영 단축키를 수집했습니다.
PUT posts
{
"mappings": {
"properties": {
"title": { "type": "text" },
"summary": { "type": "text" },
"body": { "type": "text" },
"tags": { "type": "keyword" }
}
}
}
GET posts/_search
{
"query": {
"bool": {
"must": [
{
"multi_match": {
"query": "query planner",
"fields": ["title^3", "summary^2", "body"],
"type": "best_fields"
}
}
],
"must_not": [
{ "match": { "body": "mysql" } }
]
}
},
"aggs": {
"by_tag": {
"terms": {
"field": "tags"
}
}
},
"highlight": {
"fields": {
"body": {}
}
}
}
쿼리 DSL 이 Elasticsearch 가 데이터베이스처럼 느껴지기보다 검색 네이티브처럼 느껴지기 시작하는 곳입니다. bool 은 must, should, filter, must_not 으로 절 (clauses) 을 결합합니다. multi_match 는 필드 부스트와 best_fields, most_fields, cross_fields, phrase, bool_prefix 와 같은 다양한 실행 모드로 여러 필드를 검색할 수 있습니다. 집계, 하이라이트, 필터는 모두 메인 쿼리와 동일한 요청에 함께 위치할 수 있습니다. BM25 는 기본 유사성 모델입니다.
신선도 모델도 명시적입니다. Elasticsearch 는 즉시 검색 일관성이 아닌 준실시간입니다. 최근 작업은 리프레시가 새 세그먼트를 열 때 검색에 표시됩니다. 기본적으로는 최근 검색된 인덱스에서 매 초마다 리프레시가 발생합니다. Elastic 의 문서는 또한 리프레시가 리소스 집약적이며, 워크플로우가 작성 후 읽기 검색 가시성을 필요로 할 경우 주기적 리프레시를 기다리거나 refresh=wait_for 를 사용하도록 권장한다고 경고합니다. 이는 PostgreSQL 과 매우 다른 계약입니다.
왜 Elasticsearch 가 복잡한 검색에서 일반적으로 더 잘 순위 매기는가
이것이 많은 팀이 결국 PostgreSQL 전체 텍스트 검색에서 Elasticsearch 로 이동하는 가장 깊은 기술적 이유입니다. PostgreSQL 의 내장 순위 함수는 글로벌 정보를 사용하지 않지만, Elasticsearch 는 기본적으로 BM25 를 사용하며 필드별 유사성 설정, 분석기, 다중 필드 쿼리 형태, 관련성 튜닝을 중심으로 설계된 검색 DSL 을 노출합니다. 검색이 “매칭되었는가"에서 “왜 이 열 결과가 이겼는가"로 더 많이 변하면, Elasticsearch 가 일반적으로 더 표현력이 넓은 공간을 가집니다.
Elasticsearch 는 또한 정규화되지 않은 문서 (denormalized documents) 로 명확한 편향을 가집니다. 조인 필드 문서는 관계형 스키마를 복제하기 위해 여러 수준의 관계를 모델링하는 것을 명시적으로 경고하고, 더 나은 검색 성능을 위해 정규화를 권장합니다. 이러한 설계 선택은 Elasticsearch 의 강점과 좌절의 많은 부분을 설명합니다. 이는 더 빠른 LIKE 를 가진 PostgreSQL 이 되려고 시도하는 것이 아닙니다. 이는 대규모 문서 컬렉션을 빠르게 점수 매기고 검색할 수 있는 검색 엔진이 되려고 시도합니다.
실제 기능에서 PostgreSQL 전체 텍스트 검색 대 Elasticsearch
오타 허용 (Typo tolerance) 은 두 시스템이 급격히 갈라지는 곳입니다. Elasticsearch 는 Levenshtein 편집 거리를 기반으로 한 퍼지 쿼리를 제공하며, 전용 제안 (suggestion) 및 입력 시 자동 완성 (as-you-type) 필드 유형도 제공합니다. PostgreSQL 네이티브 전체 텍스트 검색 자체로는 오타에 허용되지 않습니다. 일반적인 PostgreSQL 의 대답은 pg_trgm 으로, 삼중어 (trigram) 유사성, LIKE, ILIKE 를 위한 유사성 연산자와 인덱스 지원을 추가합니다. 이는 잘 작동하지만, 하나의 통합된 검색 엔진 기능 세트보다는 조합 전략입니다.
하이라이트는 두 스택에 모두 존재하지만, 구현 세부 사항이 이야기를 들려줍니다. PostgreSQL 은 ts_headline 을 사용하여 유용한 스니펫을 반환할 수 있지만, 문서에서는 원래 문서를 사용하며 느릴 수 있고 웹 페이지에 직접 삽입하기에 안전하지 않을 수 있다고 명시합니다. Elasticsearch 하이라이트는 포스팅 오프셋 (postings offsets) 이나 터미널 벡터 (term vectors) 를 사용할 수 있으며, 이는 대량 필드에서 특히 유용합니다. 왜냐하면 각 하이라이트 요청마다 전체 텍스트를 다시 분석하지 않기 때문입니다. 요약하자면, PostgreSQL 은 하이라이트할 수 있지만, Elasticsearch 는 규모에 따른 하이라이트를 위해 구축되었습니다.
패시트 (Facets) 와 검색 분석은 또 다른 결함선입니다. Elasticsearch 는 집계 (aggregations) 를 검색 모델의 일류 부분으로 취급하며, 메트릭, 버킷, 파이프라인 집계가 검색 응답에서 직접 사용 가능합니다. PostgreSQL 은 SQL 이기 때문에 당연히 집계할 수 있지만, 세분화된 버킷, 히스토그램, 조합 가능한 검색 분석이 검색 제품 자체의 일부가 되면 Elasticsearch 는 훨씬 더 네이티브하게 느껴집니다. 차이는 원칙적으로 능력이 아닙니다. 엔진이 해당 워크로드에 얼마나 많은 쿼리 사용성 (ergonomics) 과 성능 정책을 전념하는지에 관한 것입니다.
자동완성은 동일한 패턴을 따릅니다. PostgreSQL 은 to_tsquery 에서 접두어 매칭을 수행할 수 있으며, 이는 유용하고 내부 도구에는 종종 충분합니다. Elasticsearch 는 접두어 및 접미어 완성을 위해 여러 분석된 하위 필드를 자동으로 구축하는 search_as_you_type 필드로 더 나아갑니다. 또한 빠른 제안을 위해 특별히 구축된 제안자 (completion suggesters) 를 제공합니다. 그 격차는 관리자 패널에서는 미미하지만 사용자 중심 발견 표면에서는 큽니다.
운영 비용은 벤치마크 스크린샷보다 더 중요합니다
유혹적인 검색 엔진 질문은 “Elasticsearch 가 검색에 대해 PostgreSQL 보다 더 빠른가?“입니다. 정직한 대답은 “어떤 형태의 검색에 대한가?“입니다. Elasticsearch 는 샤드, 복제본, 일괄 인덱싱, 리프레시 정책, 수명 주기 관리 주변에 공학되었습니다. Elastic 의 자체 프로덕션 문서는 샤드 전략, 일괄 요청 크기, 인덱싱 처리량, 리프레시 간격, ILM 에 대해 깊이 다룹니다. PostgreSQL 은 두 번째 클러스터를 피하지만, GIN 유지 관리에는 비용이 듭니다. PostgreSQL 의 문서는 GIN 삽입이 느릴 수 있으며, 대기 중인 목록 정리 (pending-list cleanup) 가 응답 시간 변동의 원인이 될 수 있고, 인덱스가 많이 업데이트되는 경우 자동 진공 (autovacuum) 전략이 중요하다고 경고합니다.
이는 대부분의 비교 글이 인정하는 것보다 성능 이야기가 더 뉘앙스 있음을 의미합니다. Elasticsearch 는 아키텍처가 이러한 작업에 전념하고 있기 때문에 대규모 상위 N 개 어휘 검색, 패시트, 자동완성, 분산 읽기 볼륨에 대해 일반적으로 더 많은 여유 공간 (headroom) 을 가집니다. PostgreSQL 은 두 번째 데이터스토어, 리프레시 경계, 디버깅할 동기화 경로가 없기 때문에 엄격한 신선도 요구 사항이 있는 관계형 애플리케이션 쿼리에서 종종 더 빠르다고 느껴집니다. 승자는 일반적으로 워크로드의 형태이며, 벤치마크 스크린샷이 아닙니다. 이는 부분적으로 추론이지만, PostgreSQL 의 트랜잭션 MVCC 모델과 Elasticsearch 의 준실시간 샤드 기반 설계에서 직접적으로 따릅니다.
트랜잭션 데이터와 검색 인덱스가 동일한 시스템에 존재해야 합니까? 검색 관련성이 적당하지만 신선도, 권한, 트랜잭션 진실이 중요한 경우, 동일한 시스템 설계는 명백한 장점을 가집니다. 검색 품질, 패시트, 동의어 정책, 오타 허용, 수평적 검색 확장이 일류 제품 관심사가 될 때, 두 번째 시스템이 정당화되기 시작합니다. Elasticsearch 의 자체 샤드 크기 지침은 일률적인 전략이 없으며, 프로덕션 하드웨어에서 프로덕션 데이터를 벤치마크할 것을 권장한다고 말합니다. 그 문장이 트레이드를 완벽하게 포착합니다. Elasticsearch 는 더 많은 검색 전용 아키텍처를 운영하라고 요구함으로써 여유 공간을 삽니다.
실용적인 결론
PostgreSQL 전체 텍스트 검색은 놀랍도록 자주 처음 80 퍼센트를 이깁니다. 토큰화, 중지어, 어근 추출 (stemming), 구문 쿼리, 가중치, 순위 매기기, 하이라이트, 생성된 검색 벡터, GIN 인덱스, 삼중어 기반 유사성 헬퍼를 지원합니다. PostgreSQL 의 트랜잭션 의미론과 결합하여 많은 애플리케이션에 단순하고, 최신이며, 데이터에 가까운 검색 스택을 제공합니다. SaaS 백오피스, 내부 도구, 중간 콘텐츠 사이트, 애플리케이션 네이티브 검색에 대해 그 조합은 쉽게 무시할 수 없습니다.
Elasticsearch 는 검색이 단순히 필터가 아닌 제품 표면이 될 때 설득력이 생깁니다. 기본적으로 BM25, 사용자 정의 분석기, 동의어 필터, 퍼지 쿼리, 다중 필드 순위 매기기, 집계, 전용 자동완성 옵션, 대량 필드 하이라이트 전략, 분산 샤드 기반 확장은 사이드 기능이 아닙니다. 이는 엔진이 존재하는 이유입니다. 이것이 왜 원지 지연 (raw latency) 에만 초점을 맞춘 Elasticsearch 비교는 일반적으로 요점을 놓치는지입니다. 더 큰 차이는 엔진이 소유하려는 검색 제품 로직의 양입니다.
가장 명확한 정신 모델은 이것입니다. PostgreSQL 전체 텍스트 검색은 검색이 데이터베이스에 속할 때 훌륭합니다. Elasticsearch 는 데이터베이스가 검색 플랫폼에 공급해야 할 때 훌륭합니다. 대부분의 팀은 속도에 과잉 집중하고 실패 모드에 덜 집중합니다. 실제 트레이드는 관련성 튜닝, 데이터 신선도, 운영 복잡성이 어디에 거주할 수 있는지 허용하는가에 관한 것입니다.