서론: 검색, 모든 개발자의 익숙한 딜레마
모든 개발자는 Postgres를 사랑합니다. 수백만 명의 개발자들이 매일 사용하고 있으며, Stripe, Instagram, Spotify와 같은 거대 기업부터 수많은 스타트업에 이르기까지 Postgres는 기술 스택의 핵심입니다.
검색 기능 또한 모든 애플리케이션의 필수 요소입니다. 제품 카탈로그, 기술 문서, 사용자 콘텐츠, 고객 지원 티켓 등 검색이 없는 서비스는 상상하기 어렵습니다. 최근에는 AI 에이전트와 RAG 파이프라인이 정답을 생성하기 전에 정확한 문서를 찾아야 하므로 검색의 중요성은 더욱 커졌습니다.
당연하게도 많은 개발자들이 이미 사용 중인 Postgres로 검색 기능을 구현하려 시도합니다. 그리고 금세 한계에 부딪히게 됩니다.
그다음 단계는 보통 Elasticsearch나 Algolia, Typesense 같은 별도의 검색 시스템을 도입하는 것입니다. 하지만 이 결정은 또 다른 복잡성을 낳습니다.
-
별도의 검색 클러스터를 구축하고 24시간 내내 운영해야 합니다.
-
Postgres와 검색 시스템 간의 데이터 동기화 파이프라인을 만들어야 합니다.
-
검색 결과가 오래되었거나 누락되는 이유를 디버깅해야 합니다.
-
장애 대응을 위한 온콜(on-call) 로테이션에 또 하나의 시스템이 추가됩니다.
-
매달 수천 달러의 관리형 서비스 비용을 내거나, 시스템을 운영할 전문가를 고용해야 합니다.
물론 페타바이트 규모의 로그 집계를 위한 Elasticsearch나 실시간 분석을 위한 Clickhouse처럼, 1%의 기업에게는 이러한 복잡성이 필요할 수 있습니다. 하지만 99%의 개발자들에게는 불필요한 복잡성입니다. 그들에게 필요한 것은 이미 사용하고 있는 데이터베이스 안에서 더 나은 검색 기능을 사용하는 것입니다.
만약 우리가 이미 사용하고 있는 Postgres의 검색 기능이 훨씬 더 좋아진다면 어떨까요? 이제 그것이 가능해졌습니다. 하지만 먼저, Postgres의 기본 검색 기능에 어떤 문제가 있었는지 살펴보겠습니다.
1. Postgres 기본 검색은 왜 우리를 실망시켰을까?
검색의 핵심 목표는 주어진 쿼리에 대해 가장 관련성 높고 유용한 결과를 반환하는 것입니다. 간단해 보이지만 현실은 그렇지 않습니다. 검색 쿼리와 문서의 특성에 따라 미묘하고 때로는 명백한 문제들이 발생하기 시작합니다.
이 문제들을 구체적으로 살펴보기 위해, 다음과 같은 문서들이 있다고 가정해 봅시다.
데이터베이스 커넥션 풀링 가이드: “데이터베이스 커넥션 풀링은 애플리케이션 성능을 향상시킵니다. 풀은 재사용 가능한 커넥션을 유지합니다. 워크로드에 따라 풀 크기를 구성하세요.”
PostgreSQL 인증 설정: “PostgreSQL 데이터베이스 인증 방법을 설정합니다. pg_hba.conf를 구성하여 암호, 인증서 및 LDAP 인증을 설정하세요.”
일반 블로그 포스트(스팸성): “데이터베이스 데이터베이스 데이터베이스. 데이터베이스에 대해 알아보세요. 데이터베이스는 중요합니다. 데이터베이스 데이터베이스 데이터베이스. 더 많은 데이터베이스 정보.”
EXPLAIN ANALYZE 팁 (15단어): “느린 PostgreSQL 쿼리를 찾으려면 EXPLAIN ANALYZE를 사용하세요. 실행 계획과 실제 시간을 보여줍니다.”
PostgreSQL 쿼리 튜닝 가이드 (80단어): “이 종합 PostgreSQL 가이드는 쿼리 튜닝을 다룹니다. PostgreSQL 쿼리 성능은 EXPLAIN 및 EXPLAIN ANALYZE의 적절한 사용에 달려 있습니다. 느린 쿼리에 EXPLAIN ANALYZE를 실행하세요…”
이 문서들을 대상으로 검색할 때 Postgres 기본 검색은 다음과 같은 문제들을 드러냅니다.
-
키워드 스터핑 (Keyword Stuffing) "database"를 검색하면, Postgres 기본 검색은 키워드 등장 횟수를 기준으로 순위를 매깁니다. 그 결과, "database"라는 단어를 12번이나 반복한 스팸성 문서가 1위를 차지하고, 정작 유용한 가이드는 더 낮은 순위로 밀려납니다.
-
일반적인 단어의 과대평가 "database authentication"을 검색할 때, "database"는 10개 이상의 문서에 등장하는 흔한 단어인 반면, "authentication"은 단 하나의 문서에만 등장하는 희귀한 단어입니다. 어떤 단어가 사용자의 의도를 더 잘 나타낼까요? 당연히 "authentication"입니다. 하지만 Postgres 기본 검색은 두 단어를 동등하게 취급하여 검색 결과의 정확도를 떨어뜨립니다.
-
긴 문서의 유리함 "EXPLAIN ANALYZE"를 검색하면, 80단어 분량의 긴 가이드는 이 용어를 8번 언급하고, 15단어 분량의 짧은 팁은 2번 언급합니다. Postgres 기본 검색은 단순히 언급 횟수가 많은 긴 문서를 더 높은 순위에 올립니다. 하지만 실제로는
EXPLAIN ANALYZE라는 주제에만 온전히 집중한 짧은 팁이 사용자에게 훨씬 더 유용한 결과입니다. -
전부 아니면 전무 (All-or-Nothing) 매칭 "database connection pooling"을 검색하면, Postgres 기본 검색은
AND연산자를 사용합니다. 세 단어가 모두 포함된 문서만 결과로 반환되어, 15개 문서 중 단 2개만 찾을 수 있습니다. 만약OR연산자로 바꾸면 13개의 결과를 얻지만, 대부분의 문서가 동일한 점수를 받아 어떤 것이 더 관련성 높은지 판단할 방법이 없습니다.
2. 해답은 최신 기술이 아닌, 업계 표준 ‘BM25’
다행히도 이 문제들은 검색 업계에서 1990년대에 이미 해결되었습니다. 그 해답은 바로 BM25(Best Matching 25) 라는 알고리즘입니다. BM25는 Elasticsearch, Solr, Lucene 등 사실상 모든 주요 검색 시스템의 핵심 기술입니다. 그리고 이 강력한 기술이 드디어 pg_textsearch라는 확장 기능으로 Postgres에 추가되었습니다.
BM25는 위에서 언급한 문제들을 다음과 같이 해결합니다.
-
용어 빈도 포화 (Term Frequency Saturation) 한 단어를 12번 언급한다고 해서 관련성이 12배 높아지는 것은 아닙니다. BM25는 특정 횟수 이상 단어가 반복되어도 관련도 점수에 미치는 영향을 제한하여 스팸성 문서가 높은 순위를 차지하는 것을 방지합니다.
-
역문서 빈도 (Inverse Document Frequency) 희귀한 단어일수록 더 중요합니다. "database"처럼 모든 문서에 등장하는 단어는 노이즈에 가깝지만, "authentication"처럼 드물게 나타나는 단어는 강력한 신호가 됩니다. BM25는 이 원리를 이용해 희귀한 단어에 더 높은 가중치를 부여합니다.
-
문서 길이 정규화 (Length Normalization) 문서의 길이를 고려하여 점수를 보정합니다. 덕분에 검색어와 관련 없는 내용이 많은 긴 문서보다, 검색어에 대해 집중적으로 다루는 짧은 문서가 더 높은 점수를 받을 수 있습니다.
-
순위 기반 검색 (Ranked Retrieval) 단순히 ‘일치’ 또는 '불일치’로 결과를 나누는 대신, 모든 문서에 의미 있는 관련도 점수를 부여합니다. 이를 통해 부분적으로 일치하는 문서들도 관련도에 따라 순위가 매겨져 결과에 표시될 수 있습니다.
이제 pg_textsearch 확장을 통해 Postgres에서 이 모든 것을 간단하게 사용할 수 있습니다.
-- 확장 기능 활성화
CREATE EXTENSION pg_textsearch;
-- BM25 인덱스 생성
CREATE INDEX ON articles USING bm25(content);
-- BM25를 사용한 검색
SELECT * FROM articles
ORDER BY content <@> to_bm25query('database performance')
LIMIT 10;
3. AI 에이전트를 위한 최종 병기: 하이브리드 검색
이제 검색은 AI 에이전트와 RAG 파이프라인의 핵심 요소가 되었습니다. 하지만 이 영역에서는 BM25만으로는 해결할 수 없는 새로운 문제가 등장합니다.
사용자가 "데이터베이스가 왜 느린가요?"라고 질문했을 때, "쿼리 최적화"나 "인덱스 튜닝"과 같은 관련 문서에는 '느리다’라는 키워드가 직접적으로 포함되어 있지 않을 수 있습니다. 이 경우 키워드 기반인 BM25는 아무런 결과도 찾지 못하고 AI 에이전트는 실패하게 됩니다.
벡터 검색은 이러한 의미 기반 검색에 강점이 있습니다. "느린 데이터베이스"와 "성능 최적화"가 의미적으로 관련 있다는 것을 이해할 수 있습니다. 하지만 벡터 검색은 반대의 문제를 가집니다. 바로 '모호함’입니다. 예를 들어 "PG-1234"와 같은 특정 오류 코드를 검색하면, 정확한 코드가 포함된 문서를 찾는 대신 일반적인 오류 관련 문서를 반환할 수 있습니다.
최상의 해결책은 이 둘을 함께 사용하는 하이브리드 검색입니다.
쿼리: error PG-1234
-
BM25: 정확한 오류 코드가 포함된 문서를 찾아냅니다.
-
벡터 검색: 일반적인 오류 관련 문서를 찾아냅니다.
-
하이브리드 검색: 정확한 오류 코드 문서를 최상위로 보여줍니다.
쿼리: why is my database slow
-
BM25: 일치하는 키워드가 없어 아무것도 찾지 못합니다.
-
벡터 검색: “성능 최적화” 관련 문서를 찾아냅니다.
-
하이브리드 검색: “성능 최적화” 관련 문서를 정확하게 찾아줍니다.
쿼리: fix connection timeout
-
BM25: 타임아웃 설정 관련 문서를 찾아냅니다.
-
벡터 검색: 문제 해결 가이드를 찾아냅니다.
-
하이브리드 검색: 두 종류의 문서를 모두 찾아 관련도 순으로 보여줍니다.
이것이 바로 모든 주요 AI 검색 시스템이 하이브리드 검색을 사용하는 이유입니다. LangChain은 Reciprocal Rank Fusion을 사용하는 EnsembleRetriever로 BM25와 벡터를 결합합니다. Cohere는 Rerank API의 1차 리트리버로 BM25를 추천하며, Pinecone은 희소 벡터(키워드 기반)와 밀집 벡터(의미 기반)를 결합한 하이브리드 검색을 추가했습니다.
그리고 이제 Postgres에서도 pg_textsearch (BM25)와 pgvector (벡터 검색)를 결합하여 강력한 하이브리드 검색을 직접 구현할 수 있습니다.
-- Reciprocal Rank Fusion을 사용한 하이브리드 검색
WITH bm25 AS (
-- 1단계: BM25로 상위 20개 결과를 검색하고 순위를 매김
SELECT id, ROW_NUMBER() OVER (ORDER BY content <@> to_bm25query($1)) as rank
FROM docs LIMIT 20
),
vector AS (
-- 2단계: 벡터 검색으로 상위 20개 결과를 검색하고 순위를 매김
SELECT id, ROW_NUMBER() OVER (ORDER BY embedding <=> $2) as rank
FROM docs LIMIT 20
)
-- 3단계: 두 결과 집합을 결합하고 RRF 공식으로 최종 점수 계산
SELECT
id,
-- Reciprocal Rank Fusion: 1/(k+rank). k는 보통 60으로 설정
1.0/(60+bm25.rank) + 1.0/(60+vector.rank) as score
FROM bm25
FULL JOIN vector USING (id)
ORDER BY score DESC
LIMIT 10;
결론: 하나의 데이터베이스, 모든 검색
“Postgres는 어디에나 있고, 검색도 어디에나 있습니다. 이제 강력한 검색 기능이 Postgres 안에 있습니다.”
개발자들은 더 이상 복잡한 외부 검색 시스템을 운영하고 동기화하느라 애쓸 필요가 없습니다. 우리가 이미 사용하고 신뢰하는 Postgres 안에서 정확한 키워드 검색, 유연한 의미 검색, 그리고 이 둘을 결합한 최강의 하이브리드 검색까지 모두 해결할 수 있게 되었습니다.
지금 바로 시작해보세요
pg_textsearch는 PostgreSQL 라이선스 하에 완전히 오픈 소스입니다. 아래 리소스를 통해 지금 바로 사용해 보세요.
-
데모 앱: pgtextsearchdemo.vercel.app에서 네이티브 검색, BM25, 벡터, 하이브리드 검색 결과를 나란히 비교해 보세요.
-
GitHub 저장소: github.com/rajaraodv/pg_textsearch_demo에서 전체 소스 코드와 문서를 확인하세요.
기존 Postgres 데이터베이스에 추가하는 방법은 간단합니다.
-- 1. 확장 기능 활성화
CREATE EXTENSION pg_textsearch;
-- 2. 검색할 컬럼에 BM25 인덱스 생성
CREATE INDEX ON your_table USING bm25(content);
-- 3. 검색 실행!
SELECT * FROM your_table
ORDER BY content <@> to_bm25query('your search')
LIMIT 10;
검색 시스템을 관리하는 복잡함에서 벗어나게 된 지금, 당신은 그 시간에 무엇을 더 만들고 싶으신가요?