Elasticsearch에서 text 타입 필드에 문서를 인덱싱하면, 원본 텍스트가 그대로 저장되는 것이 아니다. Analyzer라는 텍스트 처리 파이프라인을 거쳐 토큰으로 분해된 후 역색인에 저장된다. "어떤 Analyzer를 사용하느냐"가 곧 검색 품질을 결정한다.
영어는 공백과 어간 추출(Stemming)만으로도 합리적인 검색이 가능하지만, 한국어는 교착어(조사, 어미가 붙는 언어)이기 때문에 단순 공백 분리로는 제대로 된 검색이 불가능하다. 이 글에서는 Analyzer의 구조를 이해하고, 한국어 검색을 위한 Nori 분석기 설정을 다룬다.
Analyzer의 구조
Analyzer는 세 가지 구성 요소로 이루어진다.
입력 텍스트
↓
[Character Filter] → 문자 단위 전처리
↓
[Tokenizer] → 토큰으로 분리
↓
[Token Filter] → 토큰 후처리
↓
토큰 목록 (역색인에 저장)
Character Filter
텍스트가 토큰화되기 전에 문자 수준에서 변환을 수행한다. 0개 이상 적용 가능하다.
html_strip: HTML 태그를 제거한다.<b>검색</b>→검색mapping: 문자를 다른 문자로 치환한다.&→andpattern_replace: 정규식으로 문자를 치환한다
Tokenizer
텍스트를 토큰(term) 으로 분리하는 핵심 단계이다. Analyzer당 정확히 1개만 지정한다.
| Tokenizer | 동작 | 예시 입력 → 출력 |
|---|---|---|
standard | 유니코드 단어 경계로 분리 | "Hello World" → [Hello, World] |
whitespace | 공백 기준으로 분리 | "New York" → [New, York] |
keyword | 분리하지 않음 (전체가 하나의 토큰) | "New York" → [New York] |
ngram | N-gram으로 분리 | "abc" → [a, ab, abc, b, bc, c] |
nori_tokenizer | 한국어 형태소 분석 | "한국어를 분석한다" → [한국어, 를, 분석, 한다] |
Token Filter
토큰화된 결과에 후처리를 적용한다. 0개 이상, 순서대로 적용된다.
| Token Filter | 동작 |
|---|---|
lowercase | 소문자 변환 |
stop | 불용어(a, the, is 등) 제거 |
stemmer | 어간 추출 (running → run) |
synonym | 동의어 처리 (NY → New York) |
nori_part_of_speech | 한국어 품사 기반 필터링 |
기본 제공 Analyzer
Elasticsearch는 여러 Built-in Analyzer를 제공한다.
Standard Analyzer (기본값)
"The Quick Brown Fox"
→ [the, quick, brown, fox]
Standard Tokenizer + Lowercase Token Filter. 영어에서 가장 무난한 선택이지만, 한국어에서는 부적합하다.
"한국어 검색 엔진"
→ [한국어, 검색, 엔진] (공백 기준 분리만 수행)
"한국어를"로 검색하면 "한국어"와 매칭되지 않는다. 조사 "를"이 붙어있기 때문이다.
분석 결과 확인: _analyze API
Analyzer의 동작을 직접 확인할 수 있다.
POST _analyze
{
"analyzer": "standard",
"text": "Elasticsearch는 강력한 검색 엔진이다"
}
응답으로 각 토큰의 텍스트, 위치, 오프셋 정보가 반환된다. 커스텀 Analyzer를 설계할 때 반드시 이 API로 검증해야 한다.
Nori: 한국어 형태소 분석기
Nori는 Elasticsearch에 공식 포함된 한국어 형태소 분석 플러그인이다. 은전한닢(MeCab-ko) 사전을 기반으로 한다.
설치
# Elasticsearch 플러그인으로 설치
bin/elasticsearch-plugin install analysis-nori
기본 사용
PUT /korean-index
{
"settings": {
"analysis": {
"analyzer": {
"korean": {
"type": "custom",
"tokenizer": "nori_tokenizer"
}
}
}
},
"mappings": {
"properties": {
"title": {
"type": "text",
"analyzer": "korean"
}
}
}
}
분석 결과를 확인해 보자.
POST /korean-index/_analyze
{
"analyzer": "korean",
"text": "한국어를 분석한다"
}
// 결과: [한국어, 를, 분석, 한다]
"한국어를"이 "한국어" + "를"로 분리되었다. 이제 "한국어"로 검색해도 "한국어를 분석한다"가 매칭된다.
nori_tokenizer 옵션
"tokenizer": {
"my_nori": {
"type": "nori_tokenizer",
"decompound_mode": "mixed",
"discard_punctuation": true,
"user_dictionary": "userdict_ko.txt"
}
}
decompound_mode: 복합어 처리 방식을 결정한다.
| 모드 | "삼성전자" 분석 결과 |
|---|---|
none | [삼성전자] |
discard | [삼성, 전자] |
mixed | [삼성전자, 삼성, 전자] |
mixed가 가장 유연하다. 복합어 전체로도, 분리된 단어로도 검색할 수 있다.
user_dictionary: 기본 사전에 없는 단어를 추가한다. 브랜드명, 전문 용어 등을 등록할 때 사용한다.
# config/userdict_ko.txt
삼성전자
카카오페이
쿠팡로켓배송
nori_part_of_speech: 품사 기반 필터링
조사, 어미 등 검색에 불필요한 품사를 제거한다.
"filter": {
"my_pos_filter": {
"type": "nori_part_of_speech",
"stoptags": [
"E", // 어미
"J", // 조사
"IC", // 감탄사
"MAJ", // 접속 부사
"SP", // 문장 부호
"SSC", // 닫는 괄호
"SSO", // 여는 괄호
"SC", // 구분자
"SE", // 줄임표
"XSN", // 명사 접미사
"XSV", // 동사 접미사
"UNA", // 알 수 없는
"NA", // 분석 불능
"VSV" // 긍정 지정사
]
}
}
"한국어를 분석한다"에 이 필터를 적용하면 [한국어, 분석]만 남는다. 조사 "를"과 어미 "한다"가 제거된다.
실전 커스텀 Analyzer 구성
한국어 블로그 검색용 Analyzer
PUT /blog-index
{
"settings": {
"analysis": {
"tokenizer": {
"nori_mixed": {
"type": "nori_tokenizer",
"decompound_mode": "mixed"
}
},
"filter": {
"nori_pos": {
"type": "nori_part_of_speech",
"stoptags": ["E", "J", "IC", "MAJ", "SP", "SSC", "SSO", "SC", "SE"]
},
"nori_readingform": {
"type": "nori_readingform"
}
},
"analyzer": {
"korean_search": {
"type": "custom",
"tokenizer": "nori_mixed",
"filter": [
"nori_readingform",
"lowercase",
"nori_pos",
"nori_number"
]
}
}
}
},
"mappings": {
"properties": {
"title": {
"type": "text",
"analyzer": "korean_search",
"fields": {
"keyword": { "type": "keyword" }
}
},
"content": {
"type": "text",
"analyzer": "korean_search"
},
"tags": {
"type": "keyword"
}
}
}
}
nori_readingform은 한자를 한글 독음으로 변환한다. "東京" → "동경".
검색어 자동완성용 Analyzer
자동완성에는 Edge N-gram 방식이 효과적이다. 입력 도중의 부분 문자열로도 검색되어야 하기 때문이다.
"analyzer": {
"autocomplete_index": {
"type": "custom",
"tokenizer": "nori_mixed",
"filter": ["lowercase", "nori_pos", "edge_ngram_filter"]
},
"autocomplete_search": {
"type": "custom",
"tokenizer": "nori_mixed",
"filter": ["lowercase", "nori_pos"]
}
},
"filter": {
"edge_ngram_filter": {
"type": "edge_ngram",
"min_gram": 1,
"max_gram": 10
}
}
인덱싱 시에는 Edge N-gram을 적용하고(autocomplete_index), 검색 시에는 적용하지 않는다(autocomplete_search). 이렇게 인덱스 Analyzer와 검색 Analyzer를 분리하는 것이 자동완성의 핵심 패턴이다.
"title": {
"type": "text",
"analyzer": "autocomplete_index",
"search_analyzer": "autocomplete_search"
}
동의어(Synonym) 처리
검색에서 동의어 처리는 사용자 경험에 큰 영향을 미친다.
"filter": {
"synonym_filter": {
"type": "synonym",
"synonyms": [
"ES, Elasticsearch, 엘라스틱서치",
"DB, Database, 데이터베이스",
"K8s, Kubernetes, 쿠버네티스"
]
}
}
또는 파일로 관리할 수 있다.
"filter": {
"synonym_filter": {
"type": "synonym",
"synonyms_path": "analysis/synonyms.txt"
}
}
동의어 필터는 보통 Token Filter 체인의 마지막에 배치한다. 형태소 분석과 소문자 변환이 완료된 토큰에 동의어를 매칭하는 것이 정확도가 높다.
정리
- Analyzer는 Character Filter → Tokenizer → Token Filter의 파이프라인이다
- 한국어 검색에는 Nori 플러그인이 필수이며,
decompound_mode: mixed와 품사 필터를 함께 사용한다 - 인덱스/검색 Analyzer 분리: 자동완성처럼 인덱싱과 검색에 다른 분석이 필요한 경우
search_analyzer를 별도 지정한다 - 동의어 처리로 사용자가 다양한 표현으로 검색해도 원하는 결과를 찾을 수 있게 한다
_analyzeAPI로 항상 분석 결과를 검증하고, 기대한 대로 토큰이 생성되는지 확인한다