DB Study

6장. Text Analysis — Analyzer 파이프라인과 한국어 처리

"Quick"으로 검색했는데 "quick"이 담긴 문서가 나오는 이유. 문자열이 토큰으로 잘게 부서져 역색인의 키가 되는 과정 — 검색의 매칭 규칙은 전부 여기서 결정됩니다.

중급 ⏱ 약 30분 🎬 애니메이션 3개 선수 지식: 5장 매핑
🧭
이 장에서 배우는 것

① 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단계로 구성됩니다.

공식 문서의 유명한 예시 문자열로 전 과정을 따라가 봅시다.

"<p>The 2 Quick Brown-Foxes!</p>" ① Char filter html_strip ② Tokenizer standard (단어 경계) ③ Token filter lowercase "The 2 Quick Brown-Foxes!" 태그 제거 The 2 Quick Brown Foxes 5개 토큰 the 2 quick brown foxes 📖 역색인 (inverted index) the → [doc1] quick → [doc1] brown → [doc1] foxes → [doc1] ...
💡
standard analyzer에 stop filter는 기본 꺼져 있다

기본 analyzer인 standard는 "the" 같은 불용어(stopword)도 그대로 색인합니다. 제거하고 싶으면 stop token filter를 명시적으로 추가해야 합니다.

6.2 내장 Analyzer — 조립 완제품들

ES는 자주 쓰는 조합을 미리 만들어 제공합니다. 어떤 analyzer가 어떤 결과를 내는지는 _analyze API로 언제든 직접 확인할 수 있습니다.

Analyzer동작용도
standard (기본)유니코드 텍스트 분할 + lowercase다국어 일반 검색
simple영문자 외 모두 분할 + lowercase단순 영문
whitespace공백으로만 분할, lowercase 안 함토큰 원형 보존 (코드 등)
stopsimple + 불용어 제거영문 단순 검색
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 형태소 단위 분해 검색 엔진 공부 ㅂ니다 nori_part_of_speech 조사(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" }
    }
  }
}
⚠️
user_dictionary는 "핫 리로드"가 안 된다

파일 기반 사전(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입니다.

📥 색인 시 (edge_ngram) "검색엔진" 검색 검색엔 검색엔진 ← prefix 토큰들이 역색인에 🔎 검색 시 (standard) 입력: "검색" 검색 토큰 1개 그대로 검색 ✅ 매칭! "검색엔진" 문서가 자동완성 후보로 ⚠️ 검색 시에도 edge_ngram을 쓰면? 입력: "검색" 검색 "검"으로 시작하는 모든 문서가 매칭 — 과매칭 폭발 ❌
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"
      }
    }
  }
}
💡
대안: completion suggester

검색창 자동완성 전용으로는 FST 기반의 completion 필드 타입도 있습니다. 매우 빠르지만 색인 시점에 후보를 미리 등록해야 합니다. 7장에서 간단히 다룹니다.

6.7 _analyze API — 매칭이 안 될 때 첫 번째 디버깅 도구

"분명히 있는 문서인데 검색이 안 돼요"의 답은 거의 항상 토큰에 있습니다. 디버깅의 황금 패턴:

  1. 검색어를 _analyze로 토큰화해 본다
  2. 색인된 문서의 해당 필드도 _analyze로 토큰화해 본다
  3. 두 토큰 집합의 교집합을 확인한다 — 교집합이 비어 있으면 매칭이 안 되는 게 당연
// 특정 필드에 지정된 analyzer로 테스트
POST /korean_news/_analyze
{
  "field": "title",
  "text":  "검색엔진을",
  "explain": true    // 단계별 토큰 변화·품사 태그까지 출력
}

explain: true를 붙이면 tokenizer → 각 filter 단계별 토큰 변화와 position, offset, 품사 태그까지 볼 수 있어 동의어·품사 필터 디버깅에 특히 유용합니다.

⚠️
복잡한 analyzer는 색인 처리량을 깎는다

analyzer는 text 필드 1개당, 문서 1건당 1회 실행됩니다. 필드 수 × 문서 수만큼 비용이 곱해지므로, analyzer 변경 전후 bulk indexing 처리량을 벤치마크로 비교해 영향을 측정하세요.

✍️ 이해도 체크

Analyzer 파이프라인의 3단계를 올바른 순서로 나열한 것은?
✅ 문자 단위 전처리(Char filter, 0개 이상) → 토큰 분해(Tokenizer, 정확히 1개) → 토큰 단위 변환(Token filter, 0개 이상) 순서입니다. Tokenizer가 정확히 1개만 온다는 것도 함께 기억하세요.
자동완성용 edge_ngram 구성에서, 검색 시점 analyzer로 standard를 따로 지정해야 하는 이유는?
✅ 검색 시에도 edge_ngram을 쓰면 "검색"이 [검, 검색]으로 분해되어 "검"만 있는 문서까지 전부 매칭됩니다. 색인은 prefix를 넓게 깔아두고(edge_ngram), 검색어는 그대로(standard) 매칭하는 비대칭 구성이 자동완성의 정석입니다.
📚
원문으로 더 깊이

이 장의 원문 문서: chapters/ch06_text_analysis.md — Synonyms API 상세, 한·영 혼용 multi-fields 전략, analyzer 비용 측정 방법이 담겨 있습니다.