ref. https://esbook.kimjmin.net/
위 링크의 내용을 요약, 정리합니다.
ES가 검색을 위해 텍스트 데이터를 어떻게 처리하고 어떤 과정으로 인덱싱되는지 살펴본다.
RDBMS :
ES :
ES에서는 위와같이 Inverted Index(역 인덱스) 구조를 만들어 저장한다. 이 때 추출된 각 키워드를 term이라고 부른다.
데이터가 늘어나도 찾아갈 행이 늘어나는 것이 아니라 역 인덱스가 가리키는 id의 배열값이 증가하는 것이므로 큰 속도 저하가 일어나지 않는다. 이런 역 인덱스를 데이터가 저장되는 과정에서 만드므로 데이터를 입력할 때 저장이 아닌 색인한다고 표현한다.
문자열 저장시 여러 단계의 처리 과정을 거친다.
TokenFilter
분석된 문장을 _analyze
API를 이용해 확인할 수 있다. 이 때 body에 토크나이저는 tokenizer
(하나만 적용 → key:value 형식), 토큰 필터를 filter
(여러개 적용 → key:array 형식)으로 입력하면 된다.
GET _analyze
{
"text": "The quick brown fox jumps over the lazy dog",
"tokenizer": "whitespace",
"filter": [
"lowercase",
"stop",
"snowball"
]
}
//response
{
"tokens" : [
{
"token" : "quick",
"start_offset" : 4,
"end_offset" : 9,
"type" : "word",
"position" : 1
},
{
"token" : "brown",
"start_offset" : 10,
"end_offset" : 15,
"type" : "word",
"position" : 2
},
{
"token" : "fox",
"start_offset" : 16,
"end_offset" : 19,
"type" : "word",
"position" : 3
},
{
"token" : "jump",
"start_offset" : 20,
"end_offset" : 25,
"type" : "word",
"position" : 4
},
{
"token" : "over",
"start_offset" : 26,
"end_offset" : 30,
"type" : "word",
"position" : 5
},
{
"token" : "lazi",
"start_offset" : 35,
"end_offset" : 39,
"type" : "word",
"position" : 7
},
{
"token" : "dog",
"start_offset" : 40,
"end_offset" : 43,
"type" : "word",
"position" : 8
}
]
}
여러 토큰 필터 입력 시, 순서대로 필터링에 사용되기 때문에 이를 고려하여 작성하도록 한다.
analyzer
항목을 이용해 tokenizer와 filter를 한번에 표현할 수 있다.
앞서 사용한
"tokenizer": "whitespace",
"filter": [
"lowercase",
"stop",
"snowball"
]
는 "analyzer": "snowball"
로 표현할 수 있으므로(ES에 사전정의 되어있다.) 다음처럼 요청을 보내도 결과가 동일하다.
GET _analyze
{
"text": "The quick brown fox jumps over the lazy dog",
"analyzer": "snowball"
}
아래는 인덱스를 생성할 때 "analyzer": "snowball"
를 추가하고 검색 쿼리를 날린 결과이다.
검색 결과로 출력된 도큐먼트에는 "jumping"이라는 단어가 없지만 snowball
애널라이저에 의해 "jumps"가 "jump"라는 텀으로 변환되어 저장되었을 것이다. 검색어 "jumping"또한 동일한 애널라이저를 거쳐 "jump"로 검색할 것이기 때문에 위와 같은 결과가 나타났다.
term
쿼리는 match
쿼리와 문법은 유사하나 입력한 검색어에 애널라이저가 적용되지 않는다.
동일하게 message : jumping
으로 검색했으나 match를 사용했을 때와 달리 hit한 도큐먼트가 없다.
애널라이저, 캐릭터 필터, 토크나이저, 토큰필터 목록은 공식 도큐먼트에서 확인이 가능하다.
_analyze API
로 애널라이저, 토크나이저, 토큰필터들의 테스트가 가능하나 실제로 인덱스에 저장되는 데이터에 대한 설정은 애널라이저만 적용할 수 있다. 📌
ES에서 사전에 만들어져있는 애널라이저들은 홈페이지의 공식 도큐먼트를 참고하길 바란다.
사용자 정의 애널라이저는 인덱스 settings의 "index": { "analysis":
부분에 정의한다. 생성한 다음에는 해당 인덱스의 GET
또는 POST <인덱스>/_analyze
명령으로 사용 가능하다.
my_index3에 my_custom_analyzer
라는 이름의 커스텀 analyzer를 추가한다.
PUT my_index3
{
"settings": {
"index": {
"analysis": {
"analyzer": {
"my_custom_analyzer": {
"type": "custom",
"tokenizer": "whitespace",
"filter": [
"lowercase",
"stop",
"snowball"
]
}
}
}
}
}
}
_analyze API에서 my_custom_analyzer
analyzer를 이용해 문장의 역인덱싱 테스트를 해보자.
GET my_index3/_analyze
{
"analyzer": "my_custom_analyzer",
"text": [
"The quick brown fox jumps over the lazy dog"
]
}
🔎 사용자 정의 토큰필터
토크나이저, 토큰필터의 경우에도 옵션을 지정하는 경우에도 이를 커스텀 토크나이저, 토큰필터로 만들어 추가해야 한다.
PUT my_index3
{
"settings": {
"index": {
"analysis": {
"analyzer": {
"my_custom_analyzer": {
"type": "custom",
"tokenizer": "whitespace",
"filter": [
"lowercase",
"my_stop_filter",
"snowball"
]
}
},
"filter": {
"my_stop_filter": {
"type": "stop",
"stopwords": [
"brown"
]
}
}
}
}
}
}
filter 부분에서 커스텀 필터를 정의하고 있다. "my_stop_filter"이라는 이름의 새로운 필터를 정의하고 있으며, 기존 토큰 필터인 "stop"의 형태를 띄며 "stopwords"로 "brown"을 추가한다.
"my_custom_analyzer"의 filter에 stop
대신 my_stop_filter
를 넣어준 결과를 보도록 하자.
🔎 매핑에 사용자 정의 애널라이저 적용
brown이 포함된 메세지를 가진 도큐먼트를 추가하였으나 커스컴 애널라이저가 적용되어 term에서 생략되기 때문에 검색되지 않는다.
인덱싱된 도큐먼트의 역 인덱스의 내용을 확인할 때 사용한다.
GET <인덱스>/_termvectors/<도큐먼트id>?fields=<필드명>
이전에 커스텀을 등록한 인덱스이므로 아래 저장된 terms에 "brown"이 없음을 확인할 수 있다.
{
"_index" : "my_index3",
"_type" : "_doc",
"_id" : "1",
"_version" : 1,
"found" : true,
"took" : 11,
"term_vectors" : {
"message" : {
"field_statistics" : {
"sum_doc_freq" : 7,
"doc_count" : 1,
"sum_ttf" : 8
},
"terms" : {
"dog" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 8,
"start_offset" : 40,
"end_offset" : 43
}
]
},
"fox" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 3,
"start_offset" : 16,
"end_offset" : 19
}
]
},
"jump" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 4,
"start_offset" : 20,
"end_offset" : 25
}
]
},
"lazi" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 7,
"start_offset" : 35,
"end_offset" : 39
}
]
},
"over" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 5,
"start_offset" : 26,
"end_offset" : 30
}
]
},
"quick" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 1,
"start_offset" : 4,
"end_offset" : 9
}
]
},
"the" : {
"term_freq" : 2,
"tokens" : [
{
"position" : 0,
"start_offset" : 0,
"end_offset" : 3
},
{
"position" : 6,
"start_offset" : 31,
"end_offset" : 34
}
]
}
}
}
}
}
char_filter
항목에 배열로 입력하며 순차적으로 적용된다.<>
로 된 태그를 제거
등의 HTML 문법 용어들도 해석html_strip
❗️ NOTE
애널라이저는 항상 최소 1개의 토크나이저를 필요로 하기 때문에 캐릭터 필터만 적용하면 오류가 발생하니 주의하자.
Mapping 캐릭터 필터를 이용하면 지정한 단어를 다른 단어로 치환 가능하다.
아래 요청 결과를 보면 "C"가 포함되어 있는데, 도큐먼트 색인 시 standard 애널라이저가 적용되며 "C++"의 특수문자 +
는 제거되고, 소문자로 바뀌어 저장되기 때문이다.
standard 뿐 아니라 대다수의 애널라이저들이 특수문자에 대해서는 불용어로 간주하고 제거한다.
따라서, 검색될 가능성이 있는 특수문자는 다른 문자로 치환해서 저장해줘야한다. 🤔
캐릭터 필터에 커스텀 필터를 정의해주고 이를 커스텀 애널라이저에 넣어준다.
_termvectors
API를 사용하여 "C++"가 어떤 term으로 저장되었는지 보자!
정규식을 이용해 좀 더 복잡한 패턴들을 치환 할 수 있는 캐릭터 필터이다.
필터 타입은 pattern_replace
이며 "pattern" 필드에 정규식을 넣어주면 된다. 위에서 정의한 "camel_filter"는 대문자가 시작하는 단위마다 공백을 삽입("replacement")하여 세부 단어별로 토크나이징 될 수 있도록 한다.
🙋🏻 데이터 색인 과정에서 검색 기능에 가장 큰 영향을 미치는 단계가 토크나이저이다.
tokenizer
항목에 단일값으로 설정분석할 문장 : "THE quick.brown_FOx jumped! @ 3.5 meters."
standard | letter | whitespace |
---|---|---|
공백으로 구분 | 알파벳을 제외한 모든 공백, 숫자, 기호들을 기준으로 구분 | 스페이스, 탭, 줄바꿈같은 공백(whitespace)만을 기준으로 텀을 분리 |
"@"와 같은 "일부" 특수문자제거 | ||
단어끝의 특수문자는 제거 | 단어끝의 특수문자 제거 | 단어끝의 특수문자도 남긴다. |
단어 중간의 마침표나 밑줄은 제거or분리 안됨 | ||
📌 보통 많이 사용됨 | 📌 검색 범위 넓어져 원하지 않는 결과 많이 나올 수 있음 | 📌 특수문자를 거르지 않으므로 정확하게 검색해야함 |
@
, /
같은 특수문자를 제거하고 분리하면 문제가 될 수 있다.standard tokenizer :
uax_url_email tokenizer :
pattern
항목에 인덱스 생성시 설정PUT pat_tokenizer
{
"settings": {
"analysis": {
"tokenizer": {
"my_pat_tokenizer": {
"type": "pattern",
"pattern": "/"
}
}
}
}
}
분석할 문장 : "/usr/share/elasticsearch/bin"
"pattern":"/" 로 정의된 커스텀 tokenizer를 이용한 경우 | path_hierarchy를 이용한 경우 |
---|---|
delimiter
: 경로 구분자를 지정 (default가 /
이다.)replacement
: 소스의 구분자를 다른 구분자로 대치해서 저장filter
(token_filter가 아님) 항목에 배열값으로 나열해서 지정stopwords
항목에 불용어로 지정할 단어들을 배열 형태로 나열하거나 "_english_"
, "_german_"
같이 특정 언어를 지정할 수도 있다.synonyms
항목에서 직접 동의어 목록을 입력하는 방법synonyms_path
로 지정하는 방법"A, B => C"
: A,B 대신 C "텀"을 저장. A, B로 C의 검색(match)은 가능하나 반대로는 불가"A, B"
: A,B 각 텀이 A와 B 두개의 텀을 모두 저장. A, B 모두 서로의 검색어로 검색 가능다음의 토큰 필터를 포함하는 인덱스가 있다고 했을 때,
"filter": {
"syn_aws": {
"type": "synonym",
"synonyms": [
"amazon => aws"
]
}
}
다음과 같이 두 개의 도큐먼트가 저장된 경우를 생각해보자.
PUT my_synonym/_doc/1
{ "message" : "Amazon Web Service" }
PUT my_synonym/_doc/2
{ "message" : "AWS" }
term에는 "amazon"이 존재하지 않는다. "_id":"1"인 도큐먼트에 대해서도 "amazon"대신 "aws" term이 저장된다. ( =>
를 사용했기 때문에... )
→ 따라서 1️⃣ term 쿼리로 aws를 검색하면 두 개의 도큐먼트가 모두 검색되고, amazon을 검색하면 검색이 되지 않는다.
match 쿼리에는 검색어 amazon에도 애널라이저가 적용되어 실제로는 aws로 변환하여 검색을 한다. 따라서 amazon으로 검색해도 두 데이터가 모두 선택된다.
=>
가 아니라 ,
를 사용하는 필터를 쓰는 경우에는
"filter": {
"syn_aws": {
"type": "synonym",
"synonyms": [
"amazon, aws"
]
}
}
"amazon"이나 "aws"를 포함하고 있다면 "amazon", "aws" 두 개의 텀들이 모두 저장될 것이다.
synonym 토큰 필터에는 추가적으로 다음과 같은 옵션들이 있다.
"expand": false
→ "synonyms": "A, B, C, ..."
인 경우에 모든 텀을 저장하지 않고 맨 처음에 명시된 텀만 저장한다."synonyms": "A, B, C, ... => A"
와 동일하게 작동한다."lenient": true
→ synonym 설정에 오류가 있어도 무시하고 실행NGram
"type": "nGram"
으로 설정"house"라는 단어의 2글자를 NGram(bigram)으로 처리하면 "ho", "ou", "us", "se" 총 4개의 토큰이 추출되고 모두 저장된다. → "ho" 라고만 검색해도 house를 포함한 도큐먼트는 물론, "ho"를 포함한 도큐먼트도 모두 찾아진다.
❗️ NOTE
텀의 개수 기하급수적 증가 및 일부만 포함하고 있어도 모두 검색되므로 일반적인 텍스트 검색에 사용하지 않기를 추천.
카테고리 목록이나 태그 목록같이 전체 개수가 많지 않은 데이터 집단에 자동완성 같은 기능을 구현하는데 적합하다.
✔️ 옵션
min_gram
(default 1) 최소 문자수의 토큰을 구분
max_gram
(defalut 2) 최대 문자수의 토큰을 구분
house를 "min_gram": 2, "max_gram": 3
으로 설정 :
Edge NGram
"type": "edgeNGram"
"min_gram": 2, "max_gram": 3
으로 설정하고 "house" 분석 시 "ho", "hou" 2개의 토큰이 생성된다.
PUT my_ngram
{
"settings": {
"analysis": {
"filter": {
"my_ngram_f": {
"type": "edgeNGram",
"min_gram": 2,
"max_gram": 3
}
}
}
}
}
Shingle
"type": "shingle"
✔️ 옵션
❗️ NOTE
위 세가지 모두 일반적인 텍스트 분석에 사용하기는 적합하지 않다.
자동 완성 기능을 구현하거나 프로그램 코드 내에서 문법이나 기능명을 검색하는 것과 같이 특수한 경우에 유용하다.
인덱스 생성 시 filter
에 배열로 unique
를 추가해주면 자동으로 중복되는 컴은 하나만 저장한다.
GET _analyze
{
"tokenizer": "standard",
"filter": [
"lowercase",
"unique"
],
"text": [
"white fox, white rabbit, white bear"
]
}
❗️ NOTE
match 쿼리를 사용해서 검색하는 경우, unique 토큰 필터가 적용되면 텀의 개수가 여러개인 경우에도 1개만 집계된다. → TF 값이 줄어들어 스코어 점수가 달라질 수 있음. 따라서 relevancy를 따져야하는 검색의 경우에는 unique 토큰 필터는 사용하지 않는 것이 바람직하다.
문법에 따른 단어 변형에 상관 없이 검색이 가능해야한다. → 텍스트 데이터 분석시 각 텀에 있는 단어의 기본 형태(어간)을 추출해야한다. 이 과정이 어간 추출(or 형태소 분석)이며 영어로 stemming이라고 한다.
ES는 다양한 형태소 분석기를 지원하며, 공식적이지 않더라도 플러그인 형태로 사용가능한 오픈소스들이 많이 있다.
설치
elasticsearch 홈 디렉토리에서 다음 명령을 실행한다.
$> bin/elasticsearch-plugin install analysis-nori
제거
$> bin/elasticsearch-plugin remove analysis-nori
🔎 nori_tokenizer 토크나이저
standard :
nori_tokenizer :
standard 토크나이저가 공백 외에 아무런 분리를 하지 못한 것과 달리 nori_tokenizer 토크나이저는 명사를 모두 분리한 것을 볼 수 있다.
✔️옵션
none
: 어근을 분리하지 않고 완성된 합성어만 저장discard
(디폴트) : 합성어를 분리하여 각 어근만 저장mixed
: 어근과 합성어를 모두 저장사용자 정의 사전에 우선순위가 높은 단어가 있는 경우 해당 단어가 term으로 분리되고 이것이 기준이 되서 나머지 term들을 생성한다.
예를 들어 "user_dictionary_rules": ["해물"]
이라는 옵션이 추가된 경우 "동해물과"는 "동", "해물", "과"로 나눠져 저장된다.
🔎 nori_part_of_speech 토큰 필터
🔎 nori_readingform 토큰 필터
❗️ NOTE
query 또는 _analuze API에서"explain": true
옵션을 추가하면 분석된 한글 형태소들의 품사 정보를 같이 볼 수 있다.