Elasticsearch에서 Dictionary를 사용하여 analyzer를 만들고 그를 사용해서 index에 Document를 인덱싱할 수 있다. 근데 Dictionary가 변경되면 analyzer를 변경하고 indexing된 document를 갱신하려면 어떻게 해야하는지 정리해보자.

Background 지식


Analyzer는 character filter, tokenizer, token filter 순서대로 적용한다. 기본적으로 anaylyzer는 indexing time과 search time에 적용된다. index time 분석 대상은 source data(원본 데이터)이고 search time 분석 대상은 query string이다. 그러므로 사전을 변경하는 것은 indexing, serching 두개 모두 영항을 준다.

사전 업데이트 방법


엘라스틱서치에서 analyzer는 index가 close/open될 때 사전을 읽는다. 그리고 일반적으로 로딩된 이후로는 다시 사전을 읽어 들이지 않는다. 그러므로 수정된 사전을 업데이트 하기 위해서는 dictionary file을 가지고 있는 node를 재시작하거나 index를 _close, _open해야한다.

예를들어 위메프라고 형태소를 나눴을 때, 위메프라는 명사를 알지 못해 다음과 같이 쪼개진다.

GET nori_sample/_analyze
{
"analyzer": "my_analyzer",
"text" : "위메프"
}

anaylze api 결과

그럼 명사라는걸 알려주기 위해서 사전에 위메프를 추가해보자.

그리고 다시 검색을 해보자.
하지만 결과는 처음과 같다. 위에 말한 것 처럼 반영해주기 위해서는 node를 재시작하거나 index를 닫았다가 열어야한다.

다시 검색한 검색결과는 똑같다.

그럼 index를 _close했다가 _open해보자.

POST nori_sample/_close
POST nori_sample/_open


그리고 결과를 다시 확인하면 잘 구분된걸 확인할 수 있다.

정상적으로 변경된 Dictionary가 반영된걸 볼 수있다.

하지만 이 방식으로 사전 업데이트는 이미 인덱싱된 document에는 적용되지는 않는다. 왜냐하면 document는 사전이 업데이트 되기전에 analyzer를 사용해서 인뎅싱 되기 때문이다. 그래서 사전이 업데이트 되었다고해서 사전이 적용되어서 검색결과가 변경되어 나오지는 않는다.

그럼 어떻게 변경된 사전정보를 이미 존재하는 indices에 적용할 수 있을까?

엘라스틱서치에서는 인덱스된 document가 업데이트 되었을 때, document는 제거되고 다시 생성된다. 이때 우리가 업데이트한 사전정보를 이용해서 document가 다시 인덱싱된다. 그렇기 때문에 update by query api를 사용하여 인덱스에 모든 정보를 업데이트해야한다.

update by query 사용방법은 다음과 같다.

update by query 사용방법

 https://www.elastic.co/guide/en/elasticsearch/reference/7.0/docs-update-by-query.html

 

 

출처 : https://www.elastic.co/kr/blog/dictionary-update-behavior-for-elasticsearch-cjk-language-analyzers

엘라스틱 서치를 쓰면서 기존에 형태소 분석기를 아리랑, 은전한닢, open korea등을 사용했었다.


근데 이번에 6.4버전이 출시 되면서 Elasticsearch에서 기본으로 제공하는 한글 형태소 분석기가 나왔다. 이름은 nori(노리)이다. 

노리는 놀이라는 뜻에서 가져왔으며 mecab-ko-dic 사전을 이용하지만 사전을 압축하므로 기존 형태소 분석기와 비교하여 메모리를 적게 쓰고 훨씬 빠르다.


그럼 Docker에 엘라스틱서치 6.4와 Kibana 6.4를 설치하고 Nori 플러그인을 설치해서 한글 형태소분석기를 사용해보자.


Docker에 Elasticsearch와 Kibana 설치

1
2
docker run --9200:9200 -9300:9300 --name elastic -"discovery.type=single-node" docker.elastic.co/elasticsearch/elasticsearch:6.4.0
docker run ---link elastic:elastic-url -"ELASTICSEARCH_URL=http://elastic-url:9200" -5601:5601 --name kibana docker.elastic.co/kibana/kibana:6.4.0
cs

설치를 완료하고 docker process를 확인하여 정상 동작하는지 확인한다.



Elasticsearch에 Nori 플러그인 설치

엘라스틱서치 bash셀에 접근해서 anaysis-nori를 설치한다.

1
2
3
4
5
6
// elasticsearch의 bash셀 접근
docker exec -it elastic /bin/bash
 
 
// 노리 플러그인 설치
bin/elasticsearch-plugin install analysis-nori
cs


설치가 완료되면 elasticsearch 프로세스를 재 시작 한다. 그러면서 프로세스가 올라올때 Nori 플러그인이 정상적으로 올라오는지 확인해보자.

1
[2018-10-13T01:26:05,498][INFO ][o.e.p.PluginsService     ] [dFC4eSy] loaded plugin [analysis-nori]
cs


그리고 Kibana를 사용해서 분석 플러그인 동작을 확인하자

1
2
3
4
5
6
7
GET _analyze
{
  "analyzer": "nori",
  "text" : "안녕 나는 바보 위들이야."
}
 
 
cs




사전 추가하기

위에 결과를 보면 위들이라는건 하나의 대명사로써 내 별칭인데 '들'이라는 조사를 제거하고 보여주느라 위들이라는 단어가 사라졌다. 이를 해결하기위해 사전을 만들어서 위들을 추가해보자.


먼저 사전에 사용될 사전은 mecab-ko-dic을 사용한다. 플러그인 설치했던것처럼 Elasticsearch에 접속하여 config/userdict_ko.txt를 생성한다. 그리고 그 txtx파일에 "위들"을 추가한다.


그리고 해당 사전을 사용하도록 anaylzer를 생성한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
PUT nori_sample
{
  "settings": {
    "index": {
      "analysis": {
        "tokenizer": {
          "nori_user_dict": {
            "type": "nori_tokenizer",
            "decompound_mode": "mixed",
            "user_dictionary": "userdict_ko.txt"
          }
        },
        "analyzer": {
          "my_analyzer": {
            "type": "custom",
            "tokenizer": "nori_user_dict"
          }
        }
      }
    }
  }
}
cs


그리고 다시한번 조회해보자.

1
2
3
4
5
6
GET nori_sample/_analyze
{
  "analyzer": "my_analyzer",
  "text" : "안녕 나는 바보 위들이야."
}
 
cs

결과를 확인해보면 정상적으로 위들이 형태소분석기에 의해 잘 나누어지는 것을 볼 수 있다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
{
  "tokens": [
    {
      "token": "안녕",
      "start_offset": 0,
      "end_offset": 2,
      "type": "word",
      "position": 0
    },
    {
      "token": "나",
      "start_offset": 3,
      "end_offset": 4,
      "type": "word",
      "position": 1
    },
    {
      "token": "는",
      "start_offset": 4,
      "end_offset": 5,
      "type": "word",
      "position": 2
    },
    {
      "token": "바보",
      "start_offset": 6,
      "end_offset": 8,
      "type": "word",
      "position": 3
    },
    {
      "token": "위들",
      "start_offset": 9,
      "end_offset": 11,
      "type": "word",
      "position": 4
    },
    {
      "token": "이",
      "start_offset": 11,
      "end_offset": 12,
      "type": "word",
      "position": 5
    },
    {
      "token": "야",
      "start_offset": 12,
      "end_offset": 13,
      "type": "word",
      "position": 6
    }
  ]
}
cs



참고

https://www.elastic.co/guide/en/elasticsearch/plugins/6.4/analysis-nori.html

https://www.elastic.co/kr/blog/nori-the-official-elasticsearch-plugin-for-korean-language-analysis

https://www.elastic.co/guide/en/elasticsearch/plugins/6.4/analysis-nori-tokenizer.html

엘라스틱 서치에서 일반적인 검색 기능은 특정 인덱스에 문서를 저장하고, 쿼리에 매칭되는 문서를 불러오는 방식으로 수행된다.

하지만 percolating 쿼리 방식은 그 반대로 동작한다. 쿼리를 사전에 저장하고, 새로 유입된 문서가 매칭되는 쿼리가 있는지 확인해 매칭되는 쿼리를 반환한다.

업무적으로 필요한 기능이어서 알아보던 중 알게되어서 정리해본다.

https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-percolate-query.html



인덱스 생성

아래 인덱스생성에 보면 두 가지 필드를 볼 수있다. 먼저 message 필드는 percolator에서 정의된 문서를 임시 인덱스로 인덱싱하기 전에 사전 처리하는 데 사용되는 필드이다. query 필드는 쿼리 문서를 인덱싱하는 데 사용된다. 실제 Elasticsearch 쿼리를 나타내는 json 객체를 보유한다. query 필드는 쿼리 dsl을 이해하고 이후에 percolate 쿼리에 정의 된 문서와 일치시키기 위해 쿼리를 저장한다.

이해하기 어려운데, 자세한 내용은 사용방법을 더 보면 알 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
PUT wedul_product
{
  "mappings": {
    "seat": {
      "properties": {
        "message": {
          "type": "text"
        },
        "query": {
          "type": "percolator"
        }
      }
    }
  }
}
 
cs


쿼리 삽입
상품명을 가지고 있는 문서에서 특정 지역들에 대한 정보가 들어있는지 확인하기 위해서 지역정보 쿼리를 미리 넣어둔다. (춘천, 서울, 등등...)

1
2
3
4
5
6
7
8
POST /wedul_product/seat/?refresh
{
    "query" : {
        "match" : {
            "message" : "춘천"
        }
    }
}
cs



문서 매칭되는 쿼리 찾아보기
'위들아이패드 서울 지점'과 '맥북 부산시 AS 지점' 두 개를 검색해보고 매칭 결과를 확인해보자. 쿼리는 score를 고려하는 것과 percolate를 사용해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
#쿼리 (score 없는 filter 사용)
GET /wedul_product/_search
{
  "query": {
    "constant_score": {
      "filter": {
        "percolate": {
          "field": "query",
          "document": {
            "message": "위들아이패드 서울 지점"
          }
        }
      }
    }
  }
}
 
#결과
{
  "took": 55,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": 1,
    "max_score": 1,
    "hits": [
      {
        "_index": "wedul_product",
        "_type": "seat",
        "_id": "7dAz8mUBIvIDO7uZfj1s",
        "_score": 1,
        "_source": {
          "query": {
            "match": {
              "message": "서울"
            }
          }
        },
        "fields": {
          "_percolator_document_slot": [
            0
          ]
        }
      }
    ]
  }
}
 
# score가 고려된 percolate사용
GET /wedul_product/_search
{
  
  "query" : {
        "percolate" : {
            "field": "query",
            "document" : {
                "message" : "맥북 부산 시 AS 지점"
            }
        }
    }
}
 
#결과
{
  "took": 3,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": 1,
    "max_score": 0.2876821,
    "hits": [
      {
        "_index": "wedul_product",
        "_type": "seat",
        "_id": "BtAz8mUBIvIDO7uZkz5Q",
        "_score": 0.2876821,
        "_source": {
          "query": {
            "match": {
              "message": "부산"
            }
          }
        },
        "fields": {
          "_percolator_document_slot": [
            0
          ]
        }
      }
    ]
  }
}
cs


이를 이용해서 다양한 것을 할 수 있을 것 같다.

네이버에서도 이 기능을 이용해서 로그 알림 기능을 만들었다. 참고하면 좋을 것 같다.
https://d2.naver.com/helloworld/1044388

+ Recent posts