엘라스틱서치 검색 유사도 높이는 방법 / 인덱스 템플릿 생성 / 형태소 분석 테스트 쿼리

형태소 분석기 (Analyzer)

형태소 분석기 구성요소

캐릭터 필터 : 불필요한 문자열 제거 > 토크나이저 : 문자열을 토큰으로 분리 > 토큰 필터 : 토큰 변경/추가/삭제

엘라스틱서치는 글로벌 오픈소스라서 영어 문법에 특화된 standard 분석기가 기본 분석기로 적용되어 있습니다.
standard 분석기는 한글을 단어가 아닌 공백으로만 분리하여 저장하기 때문에 한글 분석이 안 됩니다.
한글 검색 정확도를 높이려면 한글 형태소 분석에 특화된 Nori 형태소 분석기 등을 따로 설치해서 적용해야 합니다.

Nori 형태소 분석기 설치 방법

cd /usr/share/elasticsearch

엘라스틱서치 실행 파일 경로 이동 후 플러그인을 사용하여 nori 형태소 분석기를 설치합니다.

./bin/elasticsearch-plugin install analysis-nori

엘라스틱서치에서 공식 지원하여 명령어로 설치 가능합니다.
형태소 분석기는 elasticsearch 재시작 시 사용할 수 있습니다.

설치된 형태소 분석기 목록 확인

./bin/elasticsearch-plugin list

Nori 토크나이저를 컬럼에 매핑한 인덱스 생성 쿼리

PUT 신규인덱스명
{
  "settings": {
    "analysis": {
      "analyzer": {
        "nori" : {
          "tokenizer" : "nori_tokenizer"
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "Nori분석기적용컬렴명" : {
        "type": "text",
        "analyzer": "nori"
      }
    }
  }
}

형태소 분석기를 컬럼에 mapping한 인덱스 생성 후
logstash로 인덱스 데이터를 수집하면 자동으로 형태소 분석기가 적용되어 형태소 분리 저장됩니다.
생성된 인덱스의 analysis는 변경 불가하며, 변경 시 인덱스 삭제 후 재생성 또는 리인덱싱이 필요합니다.

커스텀 형태소 분석기를 컬럼에 매핑한 인덱스 생성 쿼리

PUT 신규인덱스명
{
  "settings": {
    "analysis": {
      "filter": {
        "custom_stopwords": {
          "type": "stop",
          "stopwords": ["제외단어1", "제외단어2"]
        },
        "custom_stoptags": {
          "type": "nori_part_of_speech",
          "stoptags": ["제외품사1", "제외품사2"] # SN은 숫자 품사
        }
      },
      "analyzer": {
        "custom_analyzer": {
          "type": "custom",
          "char_filter": ["html_strip"], # html 태그를 발라낼 경우 추가
          "tokenizer": "nori_tokenizer", # Nori 형태소 분석기 설치 후 추가
          "filter": ["custom_stopwords", "custom_stoptags"]
        }
      }
    }
  },
  "mappings": {
	"properties": {
	  "커스텀분석기적용컬럼명": {"type": "text", "an alyzer": "custom_analyzer"}
	}
  }
}

Nori 토크나이저에 제외단어, 제외품사 필터를 적용하여 인덱스를 생성해 보았습니다.

명사 이외 모든 품사 제거
“E”, “IC”, “J”, “MAG”, “MAJ”, “MM”, “NA”, “NR”, “SC”, “SE”, “SF”, “SH”, “SL”, “SN”, “SP”, “SSC”, “SSO”, “SY”, “UNA”, “UNKNOWN”, “VA”, “VCN”, “VCP”, “VSV”, “VV”, “VX”, “XPN”, “XR”, “XSA”, “XSN”, “XSV” 등이 제거됩니다.
단점은 일부 영어, 숫자 등이 검색되지 않을 수 있습니다.

커스텀 형태소 분석기를 기본 분석기로 사용하는 인덱스 생성 쿼리

PUT 신규인덱스명
{
  "settings": {
    "analysis": {
      "analyzer": {
        "default": { # 기본 분석기로 사용하여 모든 컬럼에 매핑
          "type": "custom",
          "tokenizer": "nori_tokenizer" # Nori 형태소 분석기 설치 후 추가
        }
      }
    }
  }
}

defualt로 설정한 analyzer가 모든 컬럼에 자동 적용되니 컬럼마다 따로 매핑해주지 않아도 됩니다.

형태소 분석기 테스트 쿼리

post _analyze
{
  "tokenizer": "nori_tokenizer 또는 standard",
  "text" : "형태소 분리 테스트 텍스트"
}

“explain”: true 속성도 넣고 검색하면 분리된 token들의 품사(leftPOS) 정보를 확인할 수 있습니다.

한글 형태소 품사 태그표
http://kkma.snu.ac.kr/documents/?doc=postag

형태소 분석기를 적용한 인덱스 템플릿 생성

PUT _index_template/인덱스패턴명
{
  "index_patterns": ["test*"], # 인덱스명이 test로 시작하는 모든 인덱스 생성 시 인덱스 패턴 적용
  "priority": 1, # 템플릿 우선순위 (숫자가 높은 템플릿이 우선 적용됨)
  "template": { # 인덱스 템플릿 코드 (settings, mappings)
    "settings": {
      "analysis": {
        "analyzer": {
          "default": {
            "type": "custom",
            "tokenizer": "nori_tokenizer"
          }
        }
      }
    }
  }
}

테스트 또는 수집 쿼리 문제로 인덱스 삭제 후 재생성하여 데이터를 수집할 때마다,
수집 전에 형태소 분석기 매핑 & 인덱스 생성 쿼리를 다시 날려줘야 하는 번거로움이 있어서
인덱스 템플릿을 생성해 인덱스 생성과 동시에 커스텀 형태소 분석기가 기본값으로 적용되게 했습니다.

데이터 양이 많으면 성능 최적화를 위해 날짜별로 인덱스를 분리(파티셔닝) 하려고 인덱스 템플릿을 이용하기도 합니다.

적용되는 우선순위가 같은 인덱스 템플릿 생성 시 에러메세지

{
  "error": {
    "root_cause": [
      {
        "type": "illegal_argument_exception",
        "reason": "index template [인덱스템플릿명] has index patterns [인덱스명] matching patterns from existing templates [nori_template] with patterns (nori_template => [nori_*]) that have the same priority [1], multiple index templates may not match during index creation, please use a different priority"
      }
    ],
    "type": "illegal_argument_exception",
    "reason": "index template [인덱스템플릿명] has index patterns [인덱스명] matching patterns from existing templates [nori_template] with patterns (nori_template => [nori_*]) that have the same priority [1], multiple index templates may not match during index creation, please use a different priority"
  },
  "status": 400
}

인덱스 템플릿 리스트 및 코드 확인

GET _index_template/인덱스템플릿명*

인덱스 템플릿 삭제

DELETE _index_template/인덱스템플릿명

인덱스템플릿 생성 및 삭제 시, 기존에 생성된 인덱스들은 영향을 받지 않습니다.


딕셔너리 > 동의어 사전

생성된 인덱스의 매핑 정보를 변경할 수 없기 때문에 동의어 사전 파일을 미리 생성하고 매핑해두면 좋습니다.

동의어 사전 txt 파일 만들기

cd /etc/elasticsearch
mkdir analysis
cd analysis
touch synonym.txt
vi synonym.txt

아래와 같은 내용으로 저장합니다.

elasticsearch, 엘라스틱서치 # elasticsearch 검색 시 엘라스틱서치도 나올 수 있게 동의어 토큰 추가
Harry => 해리 # Harry 토큰을 해리 토큰으로 치환

동의어 사전 txt를 필터로 매핑한 인덱스템플릿 생성 쿼리

PUT _index_template/인덱스템플릿명
{
  "index_patterns": ["인덱스명*"],
  "priority": 1,
  "template": {
    "settings": {
      "analysis": {
        "analyzer": {
          "default": {
            "type": "custom",
            "tokenizer": "nori_tokenizer",
            "filter" : ["lowercase", "synonym"] # 대소문자 모두 검색 가능하게 lowercase 필터 먼저 적용
          }
        },
        "filter" : {
            "synonym" : {
                "type" : "synonym",
                "synonyms_path" : "analysis/synonym.txt" # 동의어 파일 생성 후 경로
            }
        }
      }
    }
  }
}

인덱스템플릿 생성 후 인덱스 재생성 해야 적용됩니다.

동의어 사전 txt 수정 후 규칙 적용

POST nori_item/_close 후
POST nori_item/_open

동의어 사전 내용 변경 후에는, 이렇게 인덱스 reload 시 적용됩니다.


딕셔너리 > 사용자 사전

사용자 사전 txt 파일 생성

cd /etc/elasticsearch/analysis
touch user_dict.txt

사용자 사전 적용 인덱스템플릿 생성 쿼리 (최종 공통 템플릿)

PUT _index_template/인덱스템플릿명
{
  "index_patterns": ["인덱스명*"],
  "priority": 1,
  "template": {
    "settings": {
      "analysis": {
        "analyzer": {
          "default": {
            "type": "custom",
            "tokenizer": "custom_tokenizer",
            "filter" : ["custom_stoptags", "lowercase", "synonym"]
          }
        },
        "tokenizer": {
        "custom_tokenizer": {
         "type": "nori_tokenizer",
         "decompound_mode": "mixed",
         "discard_punctuation": "false",
         "user_dictionary": "analysis/user_dict.txt"
         }
        },
        "filter" : {
            "synonym" : {
                "type" : "synonym",
                "synonyms_path" : "analysis/synonym.txt"
            },
            "custom_stoptags": { 
              "type": "nori_part_of_speech",
              "stoptags": [
                    "E",
                    "IC",
                    "J", 
                    "MAG", 
                    "MAJ", 
                    "MM", 
                    "SP", 
                    "SSC", 
                    "SSO", 
                    "SC", 
                    "SE", 
                    "XPN", 
                    "XSA", 
                    "XSN", 
                    "XSV", 
                    "UNA", 
                    "NA", 
                    "VSV",
                    "VA",
                    "VV",
                    "VX"]
            }
        }
      }
    }
  }
}

설정한 사용자 사전 txt에 등록된 단어는 형태소 분리되어도 검색됩니다.

인덱스에 적용한 형태소 분석기 테스트 쿼리

GET 인덱스명/_analyze
{
  "analyzer": "default",
  "text": ["삼성전자"]
}

검색 결과

{
  "tokens": [
    {
      "token": "삼성전자",
      "start_offset": 0,
      "end_offset": 4,
      "type": "word",
      "position": 0,
      "positionLength": 2
    },
    {
      "token": "삼성",
      "start_offset": 0,
      "end_offset": 2,
      "type": "word",
      "position": 0
    },
    {
      "token": "전자",
      "start_offset": 2,
      "end_offset": 4,
      "type": "word",
      "position": 1
    }
  ]
}

커스텀 토크나이저에 “decompound_mode”: “mixed” 설정을 주고 인덱스를 생성했기 때문에 위와 같은 결과가 나옵니다.


검색 알고리즘 (유사도 알고리즘 모델)

엘라스틱서치는 TF-IDF 알고리즘에서 발전한 BM25 알고리즘을 기본 검색 알고리즘으로 사용합니다.
DFR, DFI, IB, LM Dirichlet, LM Jelinek Mercer 등 다른 알고리즘은 _settings을 변경하여 사용 가능합니다.

검색 쿼리의 유사도 점수 확인 방법

"explain": true

GET 쿼리 속성에 추가시키면 아래 정보들이 상세하게 나옵니다.

1. score
score(점수)가 높을수록 유사도 높은 document입니다.

score = boost * idf * tf

2. boost
관련성이 높은 필드나 키워드에 더하는 가중치이며, fields 속성의 필드명 뒤에 ^2 등 붙여서 가중치 부여 가능합니다.

3. IDF
인덱스의 전체 document 내 용어 등장 빈도 (역문서 빈도) 이며, 낮을수록 연관성이 높습니다.

IDF = freq / (freq + k1 * (1 - b + b * dl / avgdl))

  • freq : 해당 document 내 용어 등장 횟수
  • k1, b : 알고리즘을 정규화 하기 위한 가중치 (디폴트 상수)
  • dl : 해당 document 필드 길이
  • avgdl : 전체 document 평균 필드 길이

4. TF
해당 document 내 용어 등장 빈도 (용어빈도) 이며, 높을수록 연관성이 높습니다.

TF = log(1 + (N - n + 0.5) / (n + 0.5))

  • n : 용어 등장 document 수
  • N : 인덱스의 전체 document 수

Leave a comment