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';

주의할 점

50% 규칙을 모르면 검색 결과가 비어 보인다

Natural Language Mode에서 전체 행의 50% 이상에 등장하는 단어는 자동으로 제외됩니다. 데이터가 적은 개발 환경에서 대부분의 행에 같은 단어가 있으면 검색 결과가 0건이 될 수 있습니다. Boolean Mode에서는 이 규칙이 적용되지 않습니다.

n-gram 없이 한글 검색을 하면 결과가 부정확하다

기본 파서는 공백 기반으로 토큰을 분리합니다. 한글은 공백 분리만으로 정확한 검색이 어려우므로, 반드시 WITH PARSER ngram을 지정해야 합니다.

DELETE 후에도 인덱스가 즉시 정리되지 않는다

InnoDB FTS는 DELETE 시 실제 인덱스 엔트리를 바로 삭제하지 않고 삭제 표시만 합니다. OPTIMIZE TABLE로 주기적으로 정리해야 인덱스 크기가 비대해지는 것을 방지할 수 있습니다.

정리

항목설명
역색인단어 → 행 목록 매핑, LIKE '%..%'보다 훨씬 빠름
Natural Language Mode관련도 기반 정렬, 50% 규칙 적용
Boolean Mode+, -, *, "" 연산자로 세밀한 조건 제어
n-gram 파서한글/CJK 검색 필수, 보통 n=2(bigram)
MySQL FTS 적합소규모, 간단한 키워드 검색, 별도 인프라 불필요
Elasticsearch 적합대규모, 형태소 분석, 복잡한 검색/집계
댓글 로딩 중...