한국어 검색이 LIKE ‘%키워드%’로 버틸 때, pg_bigm과 전문검색 비교

한국어 검색이 LIKE

게시판 검색창에 LIKE '%키워드%' 조건을 넣어두고 서비스를 시작했는데, 글이 몇만 건쯈 쌓이자 검색 버튼만 누르면 화면이 몇 초씩 멈추는 경험을 겪어보신 분들이 적지 않을 겁니다. 한국어 검색이 LIKE ‘%키워드%’로 버티는 구간은 의외로 금방 끝나는데, 이때 바로 풀 텍스트 검색 엔진으로 갈아타기보다 pg_bigm 확장을 올려서 기존 LIKE 쿼리에 인덱스만 태우는 선택지를 먼저 검토해 볼 만합니다. 이 글에서는 PostgreSQL 환경을 기준으로 pg_bigm과 tsvector 기반 전문 검색이 실제로 어떻게 다르게 동작하는지, 어느 쪽을 언제 골라야 하는지 정리해 보겠습니다.

LIKE ‘%키워드%’ 검색, 데이터가 쌓이면 왜 느려질까요

WHERE title LIKE '%키워드%'처럼 패턴 앞뒤에 %가 붙은 조건은 일반적인 B-tree 인덱스를 전혀 쓰지 못합니다. 인덱스는 값이 정렬된 상태를 전제로 앞부분부터 비교하는데, 검색어가 문자열 중간 어디에 올지 모르는 상태에서는 그 정렬을 활용할 방법이 없기 때문입니다.

그래서 PostgreSQL은 이런 조건을 만나면 테이블의 모든 행을 순서대로 읽어 하나씩 패턴을 대조하는 Seq Scan을 선택합니다. 글 수가 수천 건일 때는 체감이 안 되지만, 수십만 건을 넘기면 조회 한 번에 디스크 I/O와 CPU 비교 연산이 누적되면서 응답 시간이 눈에 띄게 늘어납니다.

pg_bigm 설치와 바이그램 인덱스 생성 과정

pg_bigm은 문자열을 두 글자씩 겹쳐서 잘라낸 바이그램 단위로 색인을 만드는 확장입니다. 영어권에서 흔히 쓰는 pg_trgm(3글자 단위 트라이그램)이 한글·한자·일본어 같은 멀티바이트 문자에서는 색인 효율이 떨어지는 문제가 있어, 이를 보완하려고 2글자 단위로 쪼개는 방식을 택한 모듈입니다.

설치는 확장 등록과 GIN 인덱스 생성만으로 끝납니다.

PostgreSQL 데이터베이스 쿼리 성능 최적화 화면

CREATE EXTENSION pg_bigm;

CREATE INDEX idx_posts_title_bigm
  ON posts USING gin (title gin_bigm_ops);

GIN 대신 GiST를 쓰고 싶다면 gist_bigm_ops 연산자 클래스로 바꿀 수 있습니다. GIN은 조회 속도가 빠르고 GiST는 인덱스 생성·갱신 부담이 상대적으로 가볍다는 차이가 있어, 쓰기가 잦은 테이블인지 읽기가 잦은 테이블인지에 따라 고르시면 됩니다.

쿼리 문법은 그대로 두고 실행 계획만 바뀝니다

pg_bigm의 장점은 애플리케이션 코드를 건드리지 않아도 된다는 점입니다. LIKE와 ILIKE 문법을 그대로 두고 인덱스만 추가하면, 옵티마이저가 자동으로 바이그램 인덱스를 활용하도록 계획을 바꿉니다.

-- 인덱스 적용 전
EXPLAIN SELECT id, title FROM posts WHERE title LIKE '%키워드%';
  Seq Scan on posts
    Filter: (title ~~ '%키워드%'::text)

-- pg_bigm 인덱스 적용 후
EXPLAIN SELECT id, title FROM posts WHERE title LIKE '%키워드%';
  Bitmap Heap Scan on posts
    Recheck Cond: (title ~~ '%키워드%'::text)
    ->  Bitmap Index Scan on idx_posts_title_bigm
          Index Cond: (title ~~ '%키워드%'::text)

실제 비용(cost) 숫자는 테이블 크기와 통계값에 따라 달라지지만, 핵심은 실행 계획 노드가 Seq Scan에서 Bitmap Index Scan으로 바뀐다는 점입니다. 바이그램 단위라서 글자가 2개 이상 연속될 때 효과가 분명하고, 한 글자짜리 검색어는 색인 선택도가 낮아 상대적으로 덜 유리합니다.

전문 검색(tsvector)으로 바꾸면 한국어 검색이 좋아질까요

PostgreSQL 전문 검색은 tsvector라는 전용 자료형과 GIN 인덱스를 쓰는 구조입니다. PostgreSQL 12 이상에서는 생성 컬럼 구문으로 색인 대상 컬럼을 자동으로 만들 수 있습니다.

ALTER TABLE posts
  ADD COLUMN search_vector tsvector
  GENERATED ALWAYS AS (to_tsvector('simple', title)) STORED;

CREATE INDEX idx_posts_search ON posts USING gin (search_vector);

SELECT id, title FROM posts
WHERE search_vector @@ to_tsquery('simple', '키워드');

여기서 짚어야 할 지점이 있습니다. 'simple' 설정은 공백과 구두점 기준으로만 토큰을 나누고 소문자로 바꿔줄 뿐, 한국어 형태소를 인식하지 않습니다. 그래서 “키워드”와 “키워드를”은 서로 다른 토큰으로 취급되어 매칭되지 않습니다. 영어처럼 어간 추출이 되는 언어와 달리, 한국어는 조사·어미가 토큰 경계에 그대로 붙어버리는 셈입니다. 제대로 된 형태소 분석을 쓰려면 별도의 한국어 사전 기반 확장이 필요하고, 이 부분은 PostgreSQL 기본 배포판에 포함되어 있지 않습니다.

pg_bigm과 전문 검색을 가르는 선택 기준

항목 LIKE (인덱스 없음) pg_bigm (바이그램 인덱스) 전문 검색 (tsvector, ‘simple’)
적용 난이도 추가 작업 없음 EXTENSION 설치 + GIN 인덱스 추가 컬럼 추가, 인덱스, 토큰화 설정 필요
기존 쿼리 문법 그대로 LIKE / ILIKE 그대로 유지 @@ 연산자로 변경 필요
한국어 토큰 경계 인식 없음 없음 (글자 단위 색인) 공백 기준 분리, 형태소 미인식
부분 문자열 검색 가능하지만 느림 가능하고 빠름 토큰 전체 일치 위주, 부분 매칭 약함
적합한 상황 데이터가 적을 때 기존 LIKE 코드를 그대로 쓰며 속도만 올릴 때 공백 단위 키워드, 랭킹·하이라이트가 필요할 때

기존 서비스 코드에 LIKE '%키워드%'가 이미 깊게 박혀 있고 당장 속도만 개선하고 싶다면 pg_bigm이 수정 범위가 가장 작습니다. 반대로 검색 결과에 관련도 순위를 매기거나 여러 단어를 조합한 질의(AND/OR)를 지원해야 한다면 tsvector 쪽 구조가 더 맞습니다. 다만 관리형 데이터베이스를 쓰신다면, 해당 서비스가 제공하는 확장 목록에 pg_bigm이 포함되어 있는지 먼저 확인하시는 게 안전합니다.

오탈자·유사어 검색에서는 두 방식 모두 한계가 있습니다

문자를 두 글자씩 겹쳐서 쪼개는 바이그램 방식이기 때문에 pg_bigm은 띄어쓰기나 형태소와 무관하게 글자가 포함된 행을 모두 찾아낼 수 있고, 이런 n-그램 구조는 오래전부터 문자열 매칭 알고리즘에서 쓰여온 방식입니다. 하지만 오탈자 교정이나 동의어 처리, 초성(ㄱㅅ) 검색까지는 커버하지 못합니다. tsvector의 'simple' 설정도 마찬가지로 정확히 같은 토큰이어야 매칭되기 때문에 사정은 비슷합니다.

이런 기능까지 필요해지는 순간이 오면 pg_bigm이나 기본 전문 검색만으로는 부족하고, 별도의 토크나이저를 쓰는 확장(예: pgroonga)이나 Elasticsearch·OpenSearch 같은 외부 검색엔진 도입을 검토하게 됩니다. PostgreSQL 공식 문서의 전문 검색 챕터에도 토큰화 방식과 설정(configuration) 교체 방법이 정리되어 있으니, 어느 단계까지 PostgreSQL 안에서 해결할지 판단할 때 참고하시면 됩니다.

지금 당장 할 일은 명확합니다. 데이터가 수만 건을 넘기면서 LIKE 검색이 느려졌다면, 먼저 CREATE EXTENSION pg_bigm과 GIN 인덱스 하나로 코드 변경 없이 속도를 확인해 보시고, 랭킹이나 다중 키워드 조합이 필요해질 때 tsvector로 넘어가는 순서가 안전합니다.

벡터 DB를 pgvector로 버틸 수 있는 규모, 어디까지일까요

참고: 한국어 검색이 LIKE — 위키백과

Leave a Comment