Theme:

LIKE '%검색어%'로 검색하면 테이블 전체를 스캔하는데, 수백만 건의 텍스트에서 키워드를 빠르게 찾으려면 어떻게 해야 할까요?

텍스트 검색은 일반 인덱스로 해결할 수 없는 영역입니다. MySQL의 풀텍스트 인덱스는 역색인(Inverted Index) 을 사용하여 대용량 텍스트에서도 빠른 검색을 제공합니다. 이 글에서는 풀텍스트 인덱스의 구조, 검색 모드, 한글 검색까지 살펴보겠습니다.

개념 정의

풀텍스트 인덱스는 텍스트 컬럼의 내용을 단어(토큰) 단위로 분리하여 역색인을 만드는 인덱스입니다.

PLAINTEXT
일반 인덱스:  행 → 데이터
역색인:      단어 → 해당 단어가 포함된 행 목록
PLAINTEXT
원본 데이터:
  행1: "MySQL은 오픈소스 데이터베이스입니다"
  행2: "PostgreSQL도 오픈소스입니다"
  행3: "MySQL 성능 최적화 가이드"

역색인:
  "MySQL"       → [행1, 행3]
  "오픈소스"     → [행1, 행2]
  "데이터베이스" → [행1]
  "성능"        → [행3]
  "최적화"      → [행3]
  ...

풀텍스트 인덱스 생성

SQL
-- 테이블 생성 시
CREATE TABLE articles (
    id INT AUTO_INCREMENT PRIMARY KEY,
    title VARCHAR(200) NOT NULL,
    content TEXT NOT NULL,
    FULLTEXT INDEX ft_idx (title, content)
) ENGINE=InnoDB;

-- 기존 테이블에 추가
ALTER TABLE articles ADD FULLTEXT INDEX ft_title (title);

-- 또는
CREATE FULLTEXT INDEX ft_content ON articles(content);

지원 타입: CHAR, VARCHAR, TEXT (InnoDB, MyISAM)

Natural Language Mode

기본 검색 모드로, 검색어와의 관련도(Relevance) 를 계산하여 정렬합니다.

SQL
-- 기본 사용
SELECT id, title,
       MATCH(title, content) AGAINST('MySQL 성능') AS relevance
FROM articles
WHERE MATCH(title, content) AGAINST('MySQL 성능');

-- IN NATURAL LANGUAGE MODE는 기본값이므로 생략 가능
SELECT * FROM articles
WHERE MATCH(title, content) AGAINST('MySQL 성능' IN NATURAL LANGUAGE MODE);

관련도 계산 요소:

  • 검색어가 문서에 등장하는 빈도
  • 검색어가 전체 문서에서 얼마나 희귀한지 (IDF)
  • 문서의 길이

주의사항

  • 50% 규칙: 전체 행의 50% 이상에 등장하는 단어는 검색에서 제외됩니다 (너무 흔한 단어)
  • 최소 단어 길이: 기본적으로 3글자 미만 단어는 무시됩니다 (InnoDB: innodb_ft_min_token_size)
  • 불용어(Stopword): 'the', 'is' 등 일반적인 단어는 인덱싱에서 제외됩니다
SQL
-- 최소 토큰 크기 확인 (InnoDB 기본: 3)
SHOW VARIABLES LIKE 'innodb_ft_min_token_size';

-- 불용어 목록 확인
SELECT * FROM information_schema.INNODB_FT_DEFAULT_STOPWORD;

Boolean Mode

+, -, *, ~ 등 연산자로 검색 조건을 세밀하게 제어합니다.

SQL
-- + : 반드시 포함
SELECT * FROM articles
WHERE MATCH(title, content) AGAINST('+MySQL +성능' IN BOOLEAN MODE);

-- - : 제외
SELECT * FROM articles
WHERE MATCH(title, content) AGAINST('+MySQL -PostgreSQL' IN BOOLEAN MODE);

-- * : 와일드카드 (접두사 매칭)
SELECT * FROM articles
WHERE MATCH(title, content) AGAINST('optim*' IN BOOLEAN MODE);

-- " " : 구문 검색 (정확한 구문)
SELECT * FROM articles
WHERE MATCH(title, content) AGAINST('"성능 최적화"' IN BOOLEAN MODE);

-- > < : 관련도 가중치 조절
SELECT * FROM articles
WHERE MATCH(title, content) AGAINST('+MySQL +(>성능 <기초)' IN BOOLEAN MODE);
-- 성능이 포함된 문서가 기초보다 높은 관련도
연산자의미예시
+반드시 포함+MySQL
-제외-PostgreSQL
*와일드카드optim*
""구문 검색"성능 최적화"
>관련도 증가>성능
<관련도 감소<기초
~부정적 기여~초보
()그룹핑+(MySQL PostgreSQL)

Boolean Mode의 특징:

  • 50% 규칙이 적용되지 않습니다
  • 관련도 0인 결과도 반환될 수 있습니다
  • 풀텍스트 인덱스 없이도 동작하지만, 매우 느립니다

Query Expansion Mode

검색 결과를 자동으로 확장하는 모드입니다.

SQL
SELECT * FROM articles
WHERE MATCH(title, content) AGAINST('데이터베이스' WITH QUERY EXPANSION);

동작 방식:

  1. 1차 검색: '데이터베이스'로 검색
  2. 1차 결과에서 자주 등장하는 단어를 추출
  3. 2차 검색: 원래 검색어 + 추출된 단어로 다시 검색

주의: 노이즈가 많이 포함될 수 있으므로, 신중하게 사용해야 합니다.

n-gram 파서 — 한글 검색

영어는 공백으로 단어를 분리할 수 있지만, 한글/중국어/일본어(CJK)는 공백 기반 분리가 부정확합니다.

n-gram 파서는 텍스트를 n글자 단위로 잘라서 토큰을 생성합니다.

PLAINTEXT
n=2 (bigram)일 때 "MySQL 성능 최적화":
→ "My", "yS", "SQ", "QL", "성능", "능 ", " 최", "최적", "적화"

사용 방법

SQL
-- n-gram 파서로 풀텍스트 인덱스 생성
CREATE TABLE posts (
    id INT AUTO_INCREMENT PRIMARY KEY,
    title VARCHAR(200),
    content TEXT,
    FULLTEXT INDEX ft_content (title, content) WITH PARSER ngram
) ENGINE=InnoDB;

-- n-gram 토큰 크기 설정 (기본 2, 서버 시작 시 설정)
-- my.cnf: ngram_token_size = 2
SHOW VARIABLES LIKE 'ngram_token_size';

n-gram 검색

SQL
-- 한글 검색
SELECT * FROM posts
WHERE MATCH(title, content) AGAINST('데이터베이스' IN BOOLEAN MODE);

-- n=2일 때 '데이터베이스'는 다음 토큰으로 분리됩니다:
-- "데이", "이터", "터베", "베이", "이스"
-- 이 토큰 중 하나라도 포함된 행이 검색됩니다

n-gram 크기 선택

n 값장점단점
1모든 글자 검색 가능인덱스 크기 매우 큼, 정확도 낮음
2한글 검색에 적합1글자 검색 불가
3오탐률 감소2글자 이하 검색 불가

한글에는 n=2 (bigram) 가 가장 일반적입니다.

InnoDB FTS 아키텍처

InnoDB의 풀텍스트 인덱스는 내부적으로 보조 테이블(Auxiliary Tables) 을 사용합니다.

PLAINTEXT
┌─────────────────────────────────┐
│         원본 테이블               │
│  (articles)                      │
├─────────────────────────────────┤
│    FTS Index Cache (메모리)      │
│    (최근 변경 사항 버퍼링)         │
├─────────────────────────────────┤
│    6개의 보조 테이블 (디스크)      │
│    (역색인 데이터)                │
├─────────────────────────────────┤
│    FTS_DOC_ID                    │
│    (문서 식별자)                  │
└─────────────────────────────────┘

FTS_DOC_ID

SQL
-- 명시적으로 FTS_DOC_ID 컬럼을 추가하면 성능이 향상됩니다
CREATE TABLE articles (
    FTS_DOC_ID BIGINT UNSIGNED AUTO_INCREMENT NOT NULL,
    title VARCHAR(200),
    content TEXT,
    PRIMARY KEY (FTS_DOC_ID),
    FULLTEXT INDEX ft_idx (title, content)
);

FTS Index Cache

INSERT/UPDATE 시 변경 사항을 즉시 디스크에 쓰지 않고 메모리 캐시에 모아두었다가 한꺼번에 플러시합니다.

SQL
-- FTS 캐시 크기 (기본 8MB)
SHOW VARIABLES LIKE 'innodb_ft_cache_size';

-- 수동 동기화
SET GLOBAL innodb_optimize_fulltext_only = ON;
OPTIMIZE TABLE articles;
SET GLOBAL innodb_optimize_fulltext_only = OFF;

풀텍스트 인덱스 vs Elasticsearch

비교 항목MySQL FTSElasticsearch
설치/운영MySQL에 내장별도 클러스터 필요
형태소 분석제한적 (n-gram)다양한 분석기
확장성단일 서버 한계수평 확장 용이
실시간성즉시 반영near real-time
복잡한 검색제한적매우 강력
관련도 조절제한적세밀한 제어
적합한 규모수십만~수백만 행수억 행 이상

MySQL FTS가 적합한 경우

  • 별도 인프라를 추가하기 어려운 소규모 프로젝트
  • 간단한 키워드 검색만 필요한 경우
  • 실시간 데이터 동기화가 중요한 경우

Elasticsearch가 적합한 경우

  • 대규모 텍스트 검색 (로그, 문서 등)
  • 형태소 분석, 동의어 처리가 필요한 경우
  • 복잡한 검색 조건과 집계가 필요한 경우

성능 최적화 팁

SQL
-- 1. 불용어 커스텀 (불필요한 단어 제거)
-- my.cnf에서 설정
-- innodb_ft_server_stopword_table = 'my_db/my_stopwords'

-- 2. 최소 토큰 크기 조절
-- innodb_ft_min_token_size = 2 (한글 2글자 검색 허용)

-- 3. 인덱스 최적화 주기적 실행
OPTIMIZE TABLE articles;

-- 4. 삭제된 문서 정리
-- InnoDB FTS는 DELETE 시 즉시 인덱스를 정리하지 않음
SHOW VARIABLES LIKE 'innodb_ft_num_word_optimize';

정리

  • 풀텍스트 인덱스는 역색인 구조로 LIKE '%keyword%'보다 훨씬 빠른 텍스트 검색을 제공합니다
  • Natural Language Mode는 관련도 기반, Boolean Mode는 연산자 기반 검색입니다
  • 한글 검색에는 n-gram 파서(보통 n=2)가 필수입니다
  • 소규모 프로젝트에서는 MySQL FTS로 충분하지만, 대규모/복잡한 검색에는 Elasticsearch를 고려합니다
  • OPTIMIZE TABLE로 주기적으로 FTS 인덱스를 정리해야 합니다
댓글 로딩 중...