6장. Text Analysis — Analyzer 파이프라인과 한국어 처리
"Quick"으로 검색했는데 "quick"이 담긴 문서가 나오는 이유. 문자열이 토큰으로 잘게 부서져 역색인의 키가 되는 과정 — 검색의 매칭 규칙은 전부 여기서 결정됩니다.
① Char filter → Tokenizer → Token filter 3단계 파이프라인 ② 내장 analyzer와 custom analyzer 조립법 ③ 한국어 형태소 분석기 Nori와 decompound_mode ④ 동의어(synonym_graph)와 자동완성(edge_ngram) 실전 패턴
6.1 Analyzer 파이프라인 — 문자열이 토큰이 되기까지
text 필드에 넣은 문자열은 그대로 저장되지 않습니다. analyzer(분석기)라는 파이프라인을 거쳐 토큰(token)들로 분해되고, 각 토큰이 역색인(inverted index)의 키가 됩니다. 검색할 때 질의어도 같은 analyzer를 거치기 때문에, analyzer 설계는 곧 "무엇이 매칭되고 무엇이 매칭되지 않는지"를 결정합니다.
모든 analyzer는 정확히 3단계로 구성됩니다.
- Character filter (0개 이상) — 문자 단위 전처리. HTML 태그 제거, 문자 치환 등
- Tokenizer (정확히 1개) — 문자열을 토큰으로 쪼개는 핵심 단계
- Token filter (0개 이상) — 토큰 단위 변환. 소문자화, 불용어 제거, 동의어, 어간 추출 등
공식 문서의 유명한 예시 문자열로 전 과정을 따라가 봅시다.
원본 문자열 "<p>The 2 Quick Brown-Foxes!</p>"이 analyzer 파이프라인에 들어갑니다. HTML 태그가 섞여 있네요.
① Char filter(html_strip)가 문자 단위 전처리로 HTML 태그를 벗겨냅니다. 아직 토큰이 아니라 "문자열"인 상태입니다.
② Tokenizer(standard)가 유니코드 단어 경계로 문자열을 쪼갭니다. 하이픈으로 연결된 Brown-Foxes도 두 토큰으로 분리 — [The, 2, Quick, Brown, Foxes].
③ Token filter(lowercase)가 각 토큰을 소문자로 변환합니다. 덕분에 "Quick"으로 검색하든 "quick"으로 검색하든 같은 키를 찾게 됩니다.
완성된 토큰들이 역색인의 키로 저장됩니다. 검색 시 질의어도 같은 파이프라인을 거치므로 "색인과 검색이 같은 언어로 대화"하게 됩니다.
기본 analyzer인 standard는 "the" 같은 불용어(stopword)도 그대로 색인합니다. 제거하고 싶으면 stop token filter를 명시적으로 추가해야 합니다.
6.2 내장 Analyzer — 조립 완제품들
ES는 자주 쓰는 조합을 미리 만들어 제공합니다. 어떤 analyzer가 어떤 결과를 내는지는 _analyze API로 언제든 직접 확인할 수 있습니다.
| Analyzer | 동작 | 용도 |
|---|---|---|
standard (기본) | 유니코드 텍스트 분할 + lowercase | 다국어 일반 검색 |
simple | 영문자 외 모두 분할 + lowercase | 단순 영문 |
whitespace | 공백으로만 분할, lowercase 안 함 | 토큰 원형 보존 (코드 등) |
stop | simple + 불용어 제거 | 영문 단순 검색 |
keyword | 분석하지 않음 — 입력 전체가 토큰 1개 | text인데 분석을 끄고 싶을 때 |
pattern | 정규식으로 분할 | 커스텀 구분자 |
language | 언어별 stemmer 등 (english 등) | 언어 특화 |
POST /_analyze
{
"analyzer": "standard",
"text": "The 2 Quick Brown-Foxes!"
}
// 결과 토큰: the, 2, quick, brown, foxes
6.3 Custom Analyzer — 부품을 직접 조립하기
내장 analyzer로 부족하면 char filter / tokenizer / token filter를 직접 조합합니다. "HTML을 벗기고, 표준 분할 후, 소문자화 + 영어 불용어 제거 + 어간 추출"을 하는 블로그용 analyzer는 이렇게 만듭니다.
PUT /blog
{
"settings": {
"analysis": {
"char_filter": {
"strip_html": { "type": "html_strip" }
},
"filter": {
"english_stop": { "type": "stop", "stopwords": "_english_" },
"english_stemmer": { "type": "stemmer", "language": "english" }
},
"analyzer": {
"blog_text": {
"type": "custom",
"char_filter": ["strip_html"],
"tokenizer": "standard",
"filter": ["lowercase", "english_stop", "english_stemmer"]
}
}
}
},
"mappings": {
"properties": {
"body": { "type": "text", "analyzer": "blog_text" }
}
}
}
핵심 원칙: analyzer는 색인 시점과 검색 시점 모두에 적용됩니다. 의도적으로 다르게 하려면 search_analyzer를 별도로 지정합니다.
"body": {
"type": "text",
"analyzer": "blog_text", // 색인 시
"search_analyzer": "blog_search" // 검색 시
}
대표적인 비대칭 사용 사례가 자동완성(색인 시 edge_ngram, 검색 시 standard)입니다. 6.6에서 애니메이션으로 봅니다.
6.4 한국어 분석 — Nori 형태소 분석기
영어는 대체로 "공백 = 단어 경계"지만 한국어는 다릅니다. "검색엔진을"은 "검색엔진" + "을(조사)"로 분해되어야 "검색엔진"으로 검색했을 때 매칭됩니다. 이렇게 단어를 의미 단위(형태소)로 쪼개는 것이 형태소 분석(morphological analysis)이며, ES의 공식 한국어 플러그인이 Nori(mecab-ko-dic 기반)입니다.
# 모든 노드에 설치 후 재시작 필요
bin/elasticsearch-plugin install analysis-nori
nori analyzer는 세 부품의 조합입니다: nori_tokenizer(형태소 분해) + nori_part_of_speech(조사·어미 등 제거) + nori_readingform(한자→한글 변환). 문장이 어떻게 처리되는지 봅시다.
한국어를 공백으로만 자르면 검색엔진을처럼 조사가 붙은 덩어리가 색인됩니다. "검색엔진"으로 검색하면 매칭 실패!
nori_tokenizer가 문장을 형태소 단위로 분해합니다. 복합명사 검색엔진도 검색 + 엔진으로 나뉩니다(decompound).
nori_part_of_speech filter가 검색에 의미 없는 품사 — 조사 을(J), 어미 ㅂ니다(E) — 를 stoptags 목록에 따라 제거합니다.
최종 토큰 [검색, 엔진, 공부, 하]가 역색인에 저장됩니다. 이제 "검색", "엔진", "공부" 어느 것으로 검색해도 이 문서가 나옵니다.
decompound_mode — 복합명사 처리 방식
nori_tokenizer의 가장 중요한 파라미터입니다. 복합명사를 어떻게 쪼갤지 결정합니다.
| 모드 | 입력 가곡역 | 의미 |
|---|---|---|
none | [가곡역] | 분해 안 함 — 부분 매칭 불가 (정밀도↑) |
discard (기본) | [가곡, 역] | 분해하고 원본은 버림 (재현율↑) |
mixed | [가곡역, 가곡, 역] | 분해 + 원본도 유지 — 운영 베이스라인 권장 |
PUT /korean_news
{
"settings": {
"analysis": {
"tokenizer": {
"korean_nori_tokenizer": {
"type": "nori_tokenizer",
"decompound_mode": "mixed",
"user_dictionary": "userdict_ko.txt" // 신조어·고유명사 사전
}
},
"filter": {
"korean_pos_filter": {
"type": "nori_part_of_speech",
"stoptags": ["E", "IC", "J", "MAG", "MAJ", "MM",
"SP", "SSC", "SSO", "SC", "SE",
"XPN", "XSA", "XSN", "XSV", "UNA", "NA", "VSV"]
}
},
"analyzer": {
"korean_analyzer": {
"type": "custom",
"tokenizer": "korean_nori_tokenizer",
"filter": ["korean_pos_filter", "nori_readingform", "lowercase"]
}
}
}
},
"mappings": {
"properties": {
"title": { "type": "text", "analyzer": "korean_analyzer" },
"body": { "type": "text", "analyzer": "korean_analyzer" }
}
}
}
파일 기반 사전(config/userdict_ko.txt)은 변경 시 노드 재시작이, user_dictionary_rules(매핑 내 직접 기재)는 인덱스 재생성이 필요합니다. 운영에서는 ILM rollover 시 신규 인덱스에 새 사전이 적용되는 패턴을 흔히 사용합니다.
6.5 동의어 — synonym_graph와 검색 시점 적용
"노트북"과 "랩탑"을 같은 것으로 취급하려면 synonym filter를 씁니다. 두 종류가 있는데, multi-word 동의어(new york 등)를 정확히 처리하는 synonym_graph를 권장합니다.
"filter": {
"my_synonyms": {
"type": "synonym_graph",
"synonyms": [
"노트북, 랩탑, laptop",
"휴대폰, 핸드폰, 스마트폰",
"ai => 인공지능" // 단방향 매핑
]
}
}
동의어를 어느 시점에 적용할지가 운영의 핵심 결정입니다.
색인 시점 적용
+ 검색이 빠름 (이미 확장됨)
− 동의어 변경 시 재색인 필요
− 사전이 색인에 박혀 있음
검색 시점 적용 (권장)
+ 동의어 변경 즉시 반영
+ 운영 친화적
− 검색 시 매번 확장 → 약간 느림
8.x부터는 Synonyms API로 사전을 클러스터에 등록하고, updateable: true filter를 search_analyzer에만 사용하면 재색인 없이 POST /index/_reload_search_analyzers 한 번으로 사전 변경이 반영됩니다. 이것이 8.x 운영 표준입니다.
6.6 자동완성 — Edge N-gram과 비대칭 analyzer
자동완성은 "검"만 입력해도 "검색엔진"이 나와야 합니다. 비결은 색인 시점에 접두사(prefix) 토큰을 미리 만들어 두는 edge_ngram입니다.
- N-gram:
"검색" → [검, 색, 검색]— 모든 위치의 조각. 강력하지만 색인 크기 폭증 - Edge N-gram:
"검색엔진" → [검, 검색, 검색엔, 검색엔진]— 시작 부분의 prefix만. 자동완성에 최적
색인 시: "검색엔진"이 edge_ngram tokenizer를 거쳐 [검, 검색, 검색엔, 검색엔진] — 접두사 토큰들이 미리 역색인에 저장됩니다.
검색 시: 사용자가 "검색"까지 입력한 순간, 검색어는 standard analyzer로 처리되어 토큰 [검색] 하나만 만듭니다.
토큰 검색이 역색인의 prefix 토큰과 정확히 매칭 → "검색엔진" 문서가 자동완성 후보로 반환됩니다. 색인은 넓게, 검색은 좁게!
만약 검색 시에도 edge_ngram을 쓰면 "검색"이 [검, 검색]으로 분해되어 "검"만 담긴 문서까지 전부 매칭됩니다. search_analyzer 분리가 필수인 이유입니다.
PUT /autocomplete
{
"settings": {
"analysis": {
"tokenizer": {
"edge_ngram_tokenizer": {
"type": "edge_ngram",
"min_gram": 1, "max_gram": 20,
"token_chars": ["letter", "digit"]
}
},
"analyzer": {
"edge_ngram_index": { "tokenizer": "edge_ngram_tokenizer", "filter": ["lowercase"] },
"edge_ngram_search": { "tokenizer": "standard", "filter": ["lowercase"] }
}
}
},
"mappings": {
"properties": {
"name": {
"type": "text",
"analyzer": "edge_ngram_index",
"search_analyzer": "edge_ngram_search"
}
}
}
}
검색창 자동완성 전용으로는 FST 기반의 completion 필드 타입도 있습니다. 매우 빠르지만 색인 시점에 후보를 미리 등록해야 합니다. 7장에서 간단히 다룹니다.
6.7 _analyze API — 매칭이 안 될 때 첫 번째 디버깅 도구
"분명히 있는 문서인데 검색이 안 돼요"의 답은 거의 항상 토큰에 있습니다. 디버깅의 황금 패턴:
- 검색어를
_analyze로 토큰화해 본다 - 색인된 문서의 해당 필드도
_analyze로 토큰화해 본다 - 두 토큰 집합의 교집합을 확인한다 — 교집합이 비어 있으면 매칭이 안 되는 게 당연
// 특정 필드에 지정된 analyzer로 테스트
POST /korean_news/_analyze
{
"field": "title",
"text": "검색엔진을",
"explain": true // 단계별 토큰 변화·품사 태그까지 출력
}
explain: true를 붙이면 tokenizer → 각 filter 단계별 토큰 변화와 position, offset, 품사 태그까지 볼 수 있어 동의어·품사 필터 디버깅에 특히 유용합니다.
analyzer는 text 필드 1개당, 문서 1건당 1회 실행됩니다. 필드 수 × 문서 수만큼 비용이 곱해지므로, analyzer 변경 전후 bulk indexing 처리량을 벤치마크로 비교해 영향을 측정하세요.
✍️ 이해도 체크
이 장의 원문 문서: chapters/ch06_text_analysis.md — Synonyms API 상세, 한·영 혼용 multi-fields 전략, analyzer 비용 측정 방법이 담겨 있습니다.