이번에 포스팅할 내용은 More Like This 입니다. More Like This 쿼리란 주어진 질의문과 가장 유사한 문서를 찾는 알고리즘입니다. 그리고 해당 알고리즘을 사용하지 위해서는 쿼리를 날릴 필드가 인덱싱되어야하며 분석 속도를 높이기 위하여 term_vector 속성을 지정해주는 것이 좋습니다. 필자를 참고로 간단한 Q&A를 위한 챗봇을 만들기 위하여 해당 알고리즘을 이용하였습니다. 물론 문맥을 이해하는 것이 아니라 유사도를 판단하는 것이지만 나름 성능이 나오는 알고리즘입니다. 모든 예제는 이전에 이용하였던 High Level Rest Client를 이용하였습니다.

 

Index

한글형태소 분석기가 포함된 인덱스를 생성하는 코드입니다. 참고로 질의의 대상이 되는 "question"필드의 term_vector 속성을 "yes"로 지정하였습니다. 생성 후에 몇개 예제로 문서를 색인해주세요.

 

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
103
104
105
106
107
108
@RequestMapping("/create")
    public Object createIndex() {
        
        boolean acknowledged = false;
        
        try(
                RestHighLevelClient client = createConnection()
        ){
            //index name
            String indexName = "sample-korean-index";
            //type name
            String typeName = "_doc";
            
            //settings
            XContentBuilder settingsBuilder = XContentFactory.jsonBuilder()
                    .startObject()
                        .field("number_of_shards",5)
                        .field("number_of_replicas",1)
                        
                        .startObject("analysis")
                            .startObject("tokenizer")
                                .startObject("sample-nori-tokenizer")
                                    .field("type","nori_tokenizer")
                                    .field("decompound_mode","mixed")
                                    .field("user_dictionary","user_dictionary.txt")
                                .endObject()
                            .endObject()
                            
                            .startObject("analyzer")
                                .startObject("sample-nori-analyzer")
                                    .field("type","custom")
                                    .field("tokenizer","sample-nori-tokenizer")
                                    .array("filter",new String[]{
                                        "sample-nori-posfilter",
                                        "nori_readingform",
                                        "sample-synonym-filter",
                                        "sample-stop-filter"
                                        }
                                    )
                                .endObject()
                            .endObject()
                            
                            .startObject("filter")
                                .startObject("sample-nori-posfilter")
                                    .field("type","nori_part_of_speech")
                                    .array("stoptaags",new String[] {
                                            "E","IC","J","MAG","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"
                                        }
                                    )
                                .endObject()
                                
                                .startObject("sample-synonym-filter")
                                    .field("type","synonym")
                                    .field("synonyms_path","synonymsFilter.txt")
                                .endObject()
                                
                                .startObject("sample-stop-filter")
                                    .field("type","stop")
                                    .field("stopwords_path","stopFilter.txt")
                                .endObject()
                            .endObject()
                        .endObject()
                    .endObject();
            
            //mapping info
            XContentBuilder indexBuilder = XContentFactory.jsonBuilder()
            .startObject()
                .startObject(typeName)
                    .startObject("properties")
                        .startObject("question")
                            .field("type","text")
                            .field("analyzer","sample-nori-analyzer")
                            .field("term_vector","yes")
                        .endObject()
                        .startObject("answer")
                            .field("type","keyword")
                        .endObject()
                    .endObject()
                .endObject()
            .endObject();
            
            //인덱스생성 요청 객체
            CreateIndexRequest request = new CreateIndexRequest(indexName);
            //세팅 정보
            request.settings(settingsBuilder);
            //매핑 정보
            request.mapping(typeName, indexBuilder);
            
            //별칭설정
            String aliasName = "chatbotInstance";
            request.alias(new Alias(aliasName));
            
            //인덱스생성
            CreateIndexResponse response = client.indices().create(request, RequestOptions.DEFAULT);
            
            acknowledged = response.isAcknowledged();
            
        }catch (Exception e) {
            e.printStackTrace();
            return "인덱스 생성에 실패하였습니다. - catch";
        }
        
        
        return acknowledged == true ? "인덱스가 생성되었습니다.":"인덱스생성에 실패하였습니다.";
    }
cs

 

More Like This Query

 

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
@RequestMapping("/mlt")
    public Object mlt() {
        String aliasName = "chatbotInstance";
        String typeName = "_doc";
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
        searchSourceBuilder.query(QueryBuilders.moreLikeThisQuery
                (new String[] {"question"},new String[]{"사용자 발화 입니다. "},null)
                .minTermFreq(2)
                .minDocFreq(1)
                );
        searchSourceBuilder.from(0);
        searchSourceBuilder.size(5);
        searchSourceBuilder.sort(new FieldSortBuilder("answer").order(SortOrder.DESC));
        
        SearchRequest request = new SearchRequest(aliasName);
        request.types(typeName);
        request.source(searchSourceBuilder);
        
        
        SearchResponse response = null;
        SearchHits searchHits = null;
        List<Answer> resultMap = new ArrayList<>();
        
        try(RestHighLevelClient client = createConnection();){
            response = client.search(request, RequestOptions.DEFAULT);
            searchHits = response.getHits();
            for( SearchHit hit : searchHits) {
                Map<String, Object> sourceAsMap = hit.getSourceAsMap();
                Answer a = new Answer();
                a.setQuestion(sourceAsMap.get("question")+"");
                a.setAnswer(sourceAsMap.get("answer")+"");
                resultMap.add(a);
            }
        }catch (Exception e) {
            e.printStackTrace();
            /*
             * 예외처리
             */
        }
        return resultMap; 
    }
    private class Answer{
        private String question;
        private String answer;
        private String score;
        
        public String getQuestion() {
            return question;
        }
        public void setQuestion(String question) {
            this.question = question;
        }
        public String getAnswer() {
            return answer;
        }
        public void setAnswer(String answer) {
            this.answer = answer;
        }
        public String getScore() {
            return score;
        }
        public void setScore(String score) {
            this.score = score;
        }
    }
cs

 

파라미터 설명

 

Parameters

 

Document Input Parameters

  - like

    작성된 문서 또는 text를 바탕으로 문서를 검색 합니다.

  - unlike

    작성된 문서 또는 text에서 제외 시킬 term을 작성 합니다.

  - fields

    문서에서 analyzed text 를 가져올 필드를 지정 합니다.

    이 필드를 대상으로 질의 수행이 이루어 집니다.

  - like_text

    like 와 더불어 문서를 검색 할떄 추가적으로 사용할 text를 작성 합니다.

  - ids or docs

    @deprecated

 

Term Selection Parameters

  - max_query_terms

    작성된 문서 또는 text에서 추출하여 사용할 최대 query term size 입니다. (default 25)

  - min_term_freq

    작성된 문서 또는 text의 최소 TF 값으로 이 값보다 작을 경우 작성된 문서와 text는 무시 됩니다. (default 2)

  - min_doc_freq

    입력된 개별 term들에 대해서 각각 matching 된 문서의 최소 크기로 해당 크기 보다 작은 term의 결과는 무시 됩니다. (default 5)

  - max_doc_freq

    입력된 개별 term들에 대해서 각각 matching 된 문서의 최대 크기로 해당 크기 보다 큰 term의 결과는 무시 됩니다. (default unbounded 0)

  - min_word_length

    입력된 개별 term들의 최소 길이로 정의한 값보다 작은 term은 무시 됩니다. (default 0)

  - max_word_length

    입력된 개별 term들의 최대 길이로 정의한 값보다 큰 term은 무시 됩니다. (default unbounded 0)

  - stop_words

    불용어 목록을 등록 합니다.

  - analyzer

    입력한 문서와 text에 대한 analyzer 를 지정 합니다. 지정 하지 않을 경우 first field 의 analyzer 를 사용하게 됩니다.

 

Query Formation Parameters

  - minimum_should_match

    작성된 문서 또는 text에서 추출된 term matching 에 대한 minimum_should_match 정보를 구성 합니다. (default 30%)

  - boost_terms

    tems boost value 를 지정 합니다.

  - include

    검색 결과로 입력 문서를 포함 할지 말지를 결정 합니다. (default false)

  - boost

    전체 질의에 대한 boost value 를 지정 합니다. (default 1.0)

 

posted by 여성게
:

 

https://www.elastic.co/guide/en/elasticsearch/client/java-rest/current/java-rest-high-query-builders.html

 

Building Queries | Java REST Client [7.1] | Elastic

 

www.elastic.co

엘라스틱서치 Rest Client API Doc Url입니다.

posted by 여성게
:

자바 언어를 위해 제공되는 클라이언트에는 두 가지 종류가 있다. 내부적으로 HTTP REST API를 사용해 통신하는 방식과 네티(Netty)모듈을 이용해 네이티브 클라이언트를 통해 통신하는 방식이다.

 

REST Client Transport Client(Netty)
  • Java High Level REST Client라고 불린다.
  • HTTP방식을 이용해 엘라스틱서치와 통신한다.
  • 내부적으로는 HttpClient 모듈을 사용한다.
  • HTTPS 사용이 가능하다.
  • Java Client라고도 불린다.
  • 초기부터 제공되던 클라이언트 방식으로, 상대적으로 빠른 속도를 보장한다.
  • 소켓을 이용해 엘라스틱서치와 통신한다.
  • 내부적으로는 Netty모듈을 사용한다.

 

초기버전의 엘라스틱서치에서는 소켓을 이용하는 Transport 클라이언트만 제공됬다. 일종의 엘라스틱서치 노드와 비슷한 방식으로 동작하기 때문에 속도는 빠르지만 엘라스틱서치가 버전업될 때마다 제공되는 기능이나 API의 명세에 따라 클래스나 메서드가 바뀌는 문제점이 있었다. 이러한 문제점을 해결하기 위해서 새로운 모듈인 REST 클라이언트가 도입됐다.

 

아직까지 Transport방식이 많이 사용되기는 하지만 엘라스틱서치 7.0부터는 해당 방식이 deprecated됬으므로, REST 클라이언트 방식을 사용하는 것이 나을 듯 싶지만, 최근 버전에서는 어떻게 될지 모르겠지만, REST 클라이언트에 지원이 안되는 Transport방식의 기능도 있으므로 용도에 따라 적절히 혼합하여 사용해야할 듯하다. 하지만 이번 포스팅에서는 High-Level Rest Client만 다룰 예정이다.

 

 

Elasticsearch connection

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
        RestHighLevelClient client = new RestHighLevelClient(
                RestClient.builder(
                    new HttpHost("host",9200,"http")
                    ,new HttpHost("host2",9200,"http")
                )
        );
        
        /*
         * 비지니스로직
         */
        
        /*
         * 사용후 꼭 close
         */
        client.close();
cs

 

High-Level Rest Client는 위와 같이 RestHighLevelClient 객체로 엘라스틱서치 노드와 연결한다. 그리고 매개변수로 빌더패턴 객체가 들어간다. 엘라스틱서치는 위와같이 여러개의 노드 연결이 가능하다. 중요한것은 반드시 사용후 close해주어야 한다는 것이다. 혹은 아래와 같이 자바1.7에 나온 기능인 try 괄호 안에 넣어주어서 auto close하는 방법을 사용해도 될듯하다.(try-resource-with) 따로 close를 호출해도 않아도 되니, 해당 기능은 Closeable을 구현하고 있는 클래스라면 습관적으로 써주는 것이 좋을 듯하다.

 

1
2
3
4
5
6
7
8
9
10
11
12
        try(RestHighLevelClient client = new RestHighLevelClient(
                RestClient.builder(
                    new HttpHost("host",9200,"http")
                    ,new HttpHost("host2",9200,"http")
                )
        );){
            /*
             * 비지니스로직
             */
        }
cs

 

우선 앞에서 대충 이야기는 했지만 현재 최신버전은 모르지만 최소한 6버전대에선 transport에서 지원하는 모든 기능을 high-level rest client에서 모두 지원하지는 않는다. 하지만 사용에 크게 무리없는 대부분의 기능은 지원하니 오늘은 그러한 기능 위주로 다루어볼 것이다.

그리고 편의상 모든 요청은 GET 방식으로 만들었다. 실 서비스 목적으로 만드려면 용도에 맞는 METHOD 방식으로 만드는 것이 좋을 듯싶다.

 

 

인덱스 생성 API

직접 postman tool이나 curl로 요청했었던 인덱스 생성 API를 클라이언트를 통하여 생성해볼 것이다. 생성할 인덱스는 커스텀 한글 형태소분석기 설정을 포함하며 매핑정보를 가진 인덱스 생성 API이다. 우선 이전에 설정하였던 것을 JSON으로 표현해본 것이다.

 

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
PUT -> http://localhost:9200/sample-index
{
    "settings":{
        "number_of_shards": 5,
        "number_of_replicas": 1,
        "analysis":{
            "tokenizer":{
                "korean_nori_tokenizer":{
                    "type":"nori_tokenizer",
                    "decompound_mode":"mixed",
                    "user_dictionary":"user_dictionary.txt"
                }
            },
            "analyzer":{
                "nori_analyzer":{
                    "type":"custom",
                    "tokenizer":"korean_nori_tokenizer",
                    "filter":[
                        "nori_posfilter",
                        "nori_readingform",
                        "synonym_filtering",
                        "stop_filtering"
                    ]
                }
            },
            "filter":{
                "nori_posfilter":{
                    "type":"nori_part_of_speech",
                    "stoptags":[
                        "E","IC","J","MAG","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"
                    ]
                },
                "synonym_filtering":{
                    "type":"synonym"
                    ,"synonyms_path":"synonymsFilter.txt"
                },
                "stop_filtering":{
                    "type":"stop"
                    ,"stopwords_path":"stopFilter.txt"
                }
            }
        }
    },"mappings":{
        "_doc":{
            "properties":{
                "answer":{
                    "type":"keyword"
                }
                ,"question":{
                    "type":"text"
                    ,"analyzer":"nori_analyzer"
                }
            }
        }
    }
}
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
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
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
@RestController
@RequestMapping("/es/index")
public class IndexAPIController {
    
    /*
     * connection create method
     */
    public RestHighLevelClient createConnection() {
        return new RestHighLevelClient(
                    RestClient.builder(
                            new HttpHost("127.0.0.1",9200,"http")
                    )
                );
    }
 
    @RequestMapping("/create")
    public Object createIndex() {
        
        boolean acknowledged = false;
        
        try(
                RestHighLevelClient client = createConnection()
        ){
            //index name
            String indexName = "sample-korean-index";
            //type name
            String typeName = "_doc";
            
            //settings
            XContentBuilder settingsBuilder = XContentFactory.jsonBuilder()
                    .startObject()
                        .field("number_of_shards",5)
                        .field("number_of_replicas",1)
                        
                        .startObject("analysis")
                            .startObject("tokenizer")
                                .startObject("sample-nori-tokenizer")
                                    .field("type","nori_tokenizer")
                                    .field("decompound_mode","mixed")
                                    .field("user_dictionary","user_dictionary.txt")
                                .endObject()
                            .endObject()
                            
                            .startObject("analyzer")
                                .startObject("sample-nori-analyzer")
                                    .field("type","custom")
                                    .field("tokenizer","sample-nori-tokenizer")
                                    .array("filter",new String[]{
                                        "sample-nori-posfilter",
                                        "nori_readingform",
                                        "sample-synonym-filter",
                                        "sample-stop-filter"
                                        }
                                    )
                                .endObject()
                            .endObject()
                            
                            .startObject("filter")
                                .startObject("sample-nori-posfilter")
                                    .field("type","nori_part_of_speech")
                                    .array("stoptaags",new String[] {
                                            "E","IC","J","MAG","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"
                                        }
                                    )
                                .endObject()
                                
                                .startObject("sample-synonym-filter")
                                    .field("type","synonym")
                                    .field("synonyms_path","synonymsFilter.txt")
                                .endObject()
                                
                                .startObject("sample-stop-filter")
                                    .field("type","stop")
                                    .field("stopwords_path","stopFilter.txt")
                                .endObject()
                            .endObject()
                        .endObject()
                    .endObject();
            
            //mapping info
            XContentBuilder indexBuilder = XContentFactory.jsonBuilder()
            .startObject()
                .startObject(typeName)
                    .startObject("properties")
                        .startObject("question")
                            .field("type","text")
                            .field("analyzer","sample-nori-analyzer")
                        .endObject()
                        .startObject("answer")
                            .field("type","keyword")
                        .endObject()
                    .endObject()
                .endObject()
            .endObject();
            
            //인덱스생성 요청 객체
            CreateIndexRequest request = new CreateIndexRequest(indexName);
            //세팅 정보
            request.settings(settingsBuilder);
            //매핑 정보
            request.mapping(typeName, indexBuilder);
            
            //별칭설정
            String aliasName = "chatbotInstance";
            request.alias(new Alias(aliasName));
            
            //인덱스생성
            CreateIndexResponse response = client.indices().create(request, RequestOptions.DEFAULT);
            
            acknowledged = response.isAcknowledged();
            
        }catch (Exception e) {
            e.printStackTrace();
            return "인덱스 생성에 실패하였습니다. - catch";
        }
        
        
        return acknowledged == true ? "인덱스가 생성되었습니다.":"인덱스생성에 실패하였습니다.";
    }
}
 
cs

 

이미 생성된 인덱스명이 존재해서 각종 이름만 변경된 것을 제외하면 동일한 설정값을 갖는다. 그리고 마지막에 인덱스 별칭을 주기 위하여 alias를 추가하였다. XContentBuilder가 우리가 JSON으로 표현할 설정 정보를 세팅하는 객체라고 보면 된다. 빌더패턴을 마치 JSON과 비슷한 느낌으로 사용하고 있으므로 설정에 어려운 부분은 없다고 생각이 든다. 마지막으로 CreateIndexRequest 객체를 이용하여 인덱스 생성 정보를 담은 요청객체를 만들고 해당 객체에 settings정보와 mapping정보 그리고 alias설정등을 넣어주고 마지막으로 CreateIndexResponse 객체로 요청의 결과를 받아온다. 처음에도 강조하였지만 중요한 것은 RestHighLevelClient을 닫아주는 것이다. 여기서는 try-resource-with 구문을 사용하여 자동으로 닫아주었다.

 

 

인덱스 삭제 API

 

1
DELETE -> http://localhost:9200/sample-korean-index
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
@RestController
@RequestMapping("/es/index")
public class IndexAPIController {
    
    @RequestMapping("/delete")
    public Object delete() {
        
        boolean acknowledged = false;
        
        try(
                RestHighLevelClient client = createConnection()
        ){
            
            //index name
            String indexName = "sample-korean-index";
            
            //인덱스 삭제 요청 객체
            DeleteIndexRequest request = new DeleteIndexRequest(indexName);
            
            DeleteIndexResponse response = client.indices().delete(request, RequestOptions.DEFAULT);
            
            acknowledged = response.isAcknowledged();
        }catch (Exception e) {
            // TODO: handle exception
            e.printStackTrace();
            return "인덱스 삭제에 실패하였습니다. - catch";
        }
        
        return acknowledged == true ? "인덱스 삭제가 완료되었습니다.":"인덱스 삭제에 실패하였습니다.";
    }
    
    /*
     * connection create method
     */
    public RestHighLevelClient createConnection() {
        return new RestHighLevelClient(
                    RestClient.builder(
                            new HttpHost("127.0.0.1",9200,"http")
                    )
                );
    }
}
 
cs

 

인덱스 open/close

운영중인 인덱스의 설정 정보를 변경하려면 다음과 같은 프로세스를 거쳐야한다.

인덱스 close -> 설정변경 요청 -> 인덱스 open 

 

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
POST -> http://localhost:9200/sample-korean-index/_close
POST -> http://localhost:9200/sample-korean-index/_open
 
@RestController
@RequestMapping("/es/index")
public class IndexAPIController {
    
    @RequestMapping("/open")
    public Object open() {
        
        boolean acknowledged = false;
        
        try(
                RestHighLevelClient client = createConnection();
        ){
            //index name
            String indexName = "sample-korean-index";
            
            //인덱스 open
            OpenIndexRequest request = new OpenIndexRequest(indexName);
            
            OpenIndexResponse response = client.indices().open(request, RequestOptions.DEFAULT);
            
            acknowledged = response.isAcknowledged();
        }catch (Exception e) {
            // TODO: handle exception
            e.printStackTrace();
            return "인덱스 open에 실패하였습니다. - catch";
        }
        
        return acknowledged == true ? "인덱스를 open 하였습니다.":"인덱스 open에 성공하였습니다.";
    }
    
    @RequestMapping("/close")
    public Object close() {
        boolean acknowledged = false;
        
        try(
                RestHighLevelClient client = createConnection();
        ){
            //index name
            String indexName = "sample-korean-index";
            
            //인덱스 close
            CloseIndexRequest request = new CloseIndexRequest(indexName);
            
            CloseIndexResponse response = client.indices().close(request, RequestOptions.DEFAULT);
            
            acknowledged = response.isAcknowledged();
        }catch (Exception e) {
            // TODO: handle exception
            e.printStackTrace();
            return "인덱스 close에 실패하였습니다. - catch";
        }
        
        return acknowledged == true ? "인덱스를 close 하였습니다.":"인덱스 close에 성공하였습니다.";
    }
    
    /*
     * connection create method
     */
    public RestHighLevelClient createConnection() {
        return new RestHighLevelClient(
                    RestClient.builder(
                            new HttpHost("127.0.0.1",9200,"http")
                    )
                );
    }
}
 
cs

 

직접 엘라스틱서치에 요청을 보낼때는 맨위 두개의 요청이다. 해당 요청들을 클라이언트를 이용하여 요청하면 아래 소스코드와 같아진다.

 

 

문서 생성

문서 색인 요청이다. 색인 요청에는 인덱스명, 타입명, 문서 ID가 필요하다. 필자가 검색엔진을 이용하여 개발을 하였을 때는 문서 ID는 따로 RDB에서 시퀀스로 관리하였고 DB에서 다음 ID 값을 불러와 문서를 생성하였었다. 예제는 해당 내용이 포함되어 있지 않지만 나중에 활용하면 좋을 듯 하다.

 

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
POST -> http://localhost:9200/sample-korean-index/_doc/2
 
{
    "question":"사용자 발화 예상문입니다.",
    "answer":"사용자에게 보여질 답변 혹은 의도명입니다."
}
 
 
<Client Code>
 
@RestController
@RequestMapping("/es/doc")
public class DocAPIController {
    
    /*
     * connection create method
     */
    public RestHighLevelClient createConnection() {
        return new RestHighLevelClient(
                    RestClient.builder(
                            new HttpHost("127.0.0.1",9200,"http")
                    )
                );
    }
    
    /*
     * 문서추가
     */
    @RequestMapping("/create")
    public Object create() {
        
        
        try(RestHighLevelClient client = createConnection();){
            
            //인덱스명 대신 별칭으로 색인한다.
            String indexName = "sample-korean-index";
            //타입명
            String typeName = "_doc";
            
            //문서 ID(대게 RDB에 문서 키값정도는 가져와서 저장한다. 만약 자동생성을 하게 된다면 엘라스틱서치는 UUID로 ID를 생성)
            String docId = "DOC_1";
            
            //문서 색인
            IndexRequest request = new IndexRequest(indexName,typeName,docId);
            
            request.source(
                XContentFactory.jsonBuilder()
                    .startObject()
                        .field("question","사용자 발화 예상문입니다.")
                        .field("answer","사용자에게 보여질 답변 혹은 의도명입니다.")
                    .endObject()
            );
            
            IndexResponse response = client.index(request, RequestOptions.DEFAULT);
            
            return response.status() ;
            
        }catch (ElasticsearchException e) {
            // TODO: handle exception
            e.printStackTrace();
            if(e.status().equals(RestStatus.CONFLICT)) {
                return "동일한 DOC_ID 문서가 존재합니다.";
            }
        }catch (Exception e) {
            // TODO: handle exception
            e.printStackTrace();
            return "문서 생성에 실패하였습니다.";
        }
        
        return null;
    }
}
 
 
cs

 

만약 응답결과로 "CREATED"가 오면 정상적으로 문서색인이 완료된 것이다. 예외처리의 CONFLICT는 문서 아이디가 중복된 것이 존재할 경우에 예외 발생을 시키기위한 처리이다.

 

 

문서조회

문서의 ID값을 이용해 조회하는 예제이다.

 

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
GET -> http://localhost:9200/chatbotInstance/_doc/DOC_1
 
@RestController
@RequestMapping("/es/doc")
public class DocAPIController {
    
    private Gson gson = new Gson();
    
    /*
     * connection create method
     */
    public RestHighLevelClient createConnection() {
        return new RestHighLevelClient(
                    RestClient.builder(
                            new HttpHost("127.0.0.1",9200,"http")
                    )
                );
    }
    
    /*
     * 문서 조회
     */
    @RequestMapping("/search")
    public Object search() {
        
        //인덱스 별칭
        String aliasName = "chatbotInstance";
        //문서 타입
        String typeName ="_doc";
        //문서 ID
        String docId = "DOC_1";
        
        //문서조회 요청
        GetRequest request = new GetRequest(aliasName,typeName,docId);
        
        //문서조회 결과
        GetResponse response = null;
        try(RestHighLevelClient client = createConnection();){
            
            response = client.get(request, RequestOptions.DEFAULT);
            
        }catch (Exception e) {
            // TODO: handle exception
            e.printStackTrace();
            return "문서조회에 실패하였습니다.";
        }
        Map<String, Object> sourceAsMap = null;
        if(response.isExists()) {
            long version = response.getVersion();
            sourceAsMap = response.getSourceAsMap();
        }
        
        return gson.toJson(sourceAsMap);
    }
}
 
cs

 

문서존재여부

문서가 존재하는 지 여부를 알기위한 exist query이다. 결과는 true & false 이다.

 

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
@RestController
@RequestMapping("/es/doc")
public class DocAPIController {
    
    private Gson gson = new Gson();
    
    /*
     * connection create method
     */
    public RestHighLevelClient createConnection() {
        return new RestHighLevelClient(
                    RestClient.builder(
                            new HttpHost("127.0.0.1",9200,"http")
                    )
                );
    }
    
    /*
     * 문서 존재 여부, exist
     */
    @RequestMapping("/exist")
    public Object exist() {
        
        String aliasName = "chatbotInstance";
        String typeName = "_doc";
        String docId = "DOC_1";
        
        GetRequest request = new GetRequest(aliasName,typeName,docId);
        boolean exist = false;
        try(RestHighLevelClient client = createConnection();){
            
            exist = client.exists(request, RequestOptions.DEFAULT);
            
        }catch (Exception e) {
            /*
             * 예외처리
             */
        }
        
        return exist;
    }
}
 
cs

 

문서 삭제

문서삭제 요청이다. 결과값으로 "DELETED"가 나온다면 성공적으로 삭제된 것이다.

 

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
@RestController
@RequestMapping("/es/doc")
public class DocAPIController {
    
    private Gson gson = new Gson();
    
    /*
     * connection create method
     */
    public RestHighLevelClient createConnection() {
        return new RestHighLevelClient(
                    RestClient.builder(
                            new HttpHost("127.0.0.1",9200,"http")
                    )
                );
    }
    
    /*
     * 문서삭제
     */
    @RequestMapping("/delete")
    public Object delete() {
        String aliasName = "chatbotInstance";
        String typeName = "_doc";
        String docId = "DOC_1";
        
        DeleteRequest request = new DeleteRequest(aliasName,typeName,docId);
        DeleteResponse response = null;
        try(RestHighLevelClient client = createConnection();){
            
            response = client.delete(request, RequestOptions.DEFAULT);
            
        }catch (Exception e) {
            /*
             * 예외처리
             */
        }
        
        return response.getResult();
    }
}
 
cs

 

문서 업데이트

문서 업데이트 요청은 여러가지 종류가 존재한다. 스크립트를 이용한 업데이트, Upsert, 부분 문서 수정 등 요구사항에 맞는 업데이트 기능을 사용하면 될듯하다.

 

문서 업데이트 - script

스크립트를 이용하여 문서를 수정하는 예제이다. 쿼리를 이용하여 매치된 문서에 대한 일괄수정도 가능하고 예제처럼 특정 문서 ID의 문서만 수정가능하다.

 

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
@RestController
@RequestMapping("/es/doc")
public class DocAPIController {
    
    private Gson gson = new Gson();
    
    /*
     * connection create method
     */
    public RestHighLevelClient createConnection() {
        return new RestHighLevelClient(
                    RestClient.builder(
                            new HttpHost("127.0.0.1",9200,"http")
                    )
                );
    }
    
    /*
     * 문서 수정 - script
     */
    @RequestMapping("/update-1")
    public Object update_1() {
        String aliasName = "chatbotInstance";
        String typeName = "_doc";
        String docId = "DOC_1";
        
        UpdateRequest request = new UpdateRequest(aliasName, typeName, docId);
        Map<String, Object> parameters = Collections.singletonMap("string"" - update");
        
        Script inline = new Script(ScriptType.INLINE,"painless","ctx._source.answer += params.string",parameters);
        
        request.script(inline);
        
        UpdateResponse response = null;
        
        try(RestHighLevelClient client = createConnection();){
            
            response = client.update(request, RequestOptions.DEFAULT);
            
        }catch (ElasticsearchException e) {
            e.printStackTrace();
            if(e.status().equals(RestStatus.NOT_FOUND)) {
                return "수정할 문서가 존재하지 않습니다.";
            }
        }catch (Exception e) {
            /*
             * 기타 예외처리
             */
        }
        
        return response.getResult();
    }
}
 
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
54
55
56
57
@RestController
@RequestMapping("/es/doc")
public class DocAPIController {
    
    private Gson gson = new Gson();
    
    /*
     * connection create method
     */
    public RestHighLevelClient createConnection() {
        return new RestHighLevelClient(
                    RestClient.builder(
                            new HttpHost("127.0.0.1",9200,"http")
                    )
                );
    }
    
    /*
     * 문서 수정 - 부분문서수정
     */
    @RequestMapping("/update-2")
    public Object update_2() {
        String aliasName = "chatbotInstance";
        String typeName = "_doc";
        String docId = "DOC_1";
        
        
        
        UpdateRequest request = null;
        
        UpdateResponse response = null;
        
        try(RestHighLevelClient client = createConnection();){
            XContentBuilder builder = XContentFactory.jsonBuilder()
                    .startObject()
                        .field("answer","수정된 답변입니다.")
                    .endObject();
            
            request = new UpdateRequest(aliasName, typeName, docId).doc(builder);
            
            response = client.update(request, RequestOptions.DEFAULT);
            
        }catch (ElasticsearchException e) {
            e.printStackTrace();
            if(e.status().equals(RestStatus.NOT_FOUND)) {
                return "수정할 문서가 존재하지 않습니다.";
            }
        }catch (Exception e) {
            /*
             * 기타 예외처리
             */
        }
        
        return response.getResult();
    }
}
 
cs

 

문서 업데이트 - upsert

만약 문서가 존재하면 update, 존재하지 않을 시 insert하는 요청이다. 만약 문서가 존재하지 않으면 IndexRequest에 담긴 Builder에 담긴 문서가 생성되고 문서가 존재한다면 UpsertRequest에 담긴 Builder 내용으로 수정된다.

 

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
@RestController
@RequestMapping("/es/doc")
public class DocAPIController {
    
    private Gson gson = new Gson();
    
    /*
     * connection create method
     */
    public RestHighLevelClient createConnection() {
        return new RestHighLevelClient(
                    RestClient.builder(
                            new HttpHost("127.0.0.1",9200,"http")
                    )
                );
    }
    
    /*
     * 문서 수정 - upsert
     */
    @RequestMapping("/update-3")
    public Object update_3() {
        String aliasName = "chatbotInstance";
        String typeName = "_doc";
        String docId = "DOC_3";
        
        
        IndexRequest index = null;
        UpdateRequest request = null;
        UpdateResponse response = null;
        
        try(RestHighLevelClient client = createConnection();){
            index = new IndexRequest(aliasName,typeName,docId);
            index.source(XContentFactory.jsonBuilder()
                    .startObject()
                        .field("question","주문할게요")
                        .field("answer","어떤걸 주문하시게요?")
                    .endObject()
            );
            XContentBuilder builder = XContentFactory.jsonBuilder()
                    .startObject()
                        .field("answer","어떤걸 주문하시게요?")
                    .endObject();
            
            request = new UpdateRequest(aliasName, typeName, docId).doc(builder).upsert(index);
            
            response = client.update(request, RequestOptions.DEFAULT);
            
        }catch (ElasticsearchException e) {
            e.printStackTrace();
            if(e.status().equals(RestStatus.NOT_FOUND)) {
                return "수정할 문서가 존재하지 않습니다.";
            }
        }catch (Exception e) {
            /*
             * 기타 예외처리
             */
        }
        
        return response.getResult();
    }
}
 
cs

 

Bulk API

여러건의 문서를 Create,Update,Delete 할 수 있는 요청이다. 만약 여러개의 문서를 반복문으로 하나씩 요청하고 있다면 해당 기능을 이용하면 깔끔하게 하나의 요청으로 완료될 것이다.

 

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
@RestController
@RequestMapping("/es/doc")
public class DocAPIController {
    
    private Gson gson = new Gson();
    
    /*
     * connection create method
     */
    public RestHighLevelClient createConnection() {
        return new RestHighLevelClient(
                    RestClient.builder(
                            new HttpHost("127.0.0.1",9200,"http")
                    )
                );
    }
    /*
     * Bulk API
     */
    @RequestMapping("/bulk")
    public Object bulk() {
        String aliasName = "chatbotInstance";
        String typeName = "_doc";
        
        BulkRequest request = new BulkRequest();
        BulkResponse response = null;
        
        try(RestHighLevelClient client = createConnection();){
            
            request.add(new IndexRequest(aliasName,typeName,"DOC_4")
                    .source(XContentFactory.jsonBuilder()
                            .startObject()
                                .field("question","벌크 질문 - add")
                                .field("answer","벌크 답변 - add")
                            .endObject()
                    )
            );
            
            request.add(new IndexRequest(aliasName,typeName,"DOC_5")
                    .source(XContentFactory.jsonBuilder()
                            .startObject()
                                .field("question","벌크 질문 - add2")
                                .field("answer","벌크 답변 - add2")
                            .endObject()
                    )
            );
            
            request.add(new UpdateRequest(aliasName,typeName,"DOC_4")
                    .doc(XContentFactory.jsonBuilder()
                            .startObject()
                                .field("question","벌크 질문 - add2")
                                .field("answer","벌크 답변 - add2")
                            .endObject()
                    )
            );
            
            request.add(new DeleteRequest(aliasName,typeName,"DOC_5"));
            
            response = client.bulk(request, RequestOptions.DEFAULT);
            
        }catch (Exception e) {
            /*
             * 예외처리
             */
            e.printStackTrace();
        }
        
        List<DocWriteResponse> results = new ArrayList<>();
        
        for(BulkItemResponse bulkItemResponse : response) {
            
            DocWriteResponse itemResponse = bulkItemResponse.getResponse();
            
            if(bulkItemResponse.getOpType().equals(DocWriteRequest.OpType.INDEX)) {
                IndexResponse indexResponse = (IndexResponse)itemResponse;
                results.add(indexResponse);
            }else if(bulkItemResponse.getOpType().equals(DocWriteRequest.OpType.UPDATE)) {
                UpdateResponse updateResponse = (UpdateResponse)itemResponse;
                results.add(updateResponse);
            }else if(bulkItemResponse.getOpType().equals(DocWriteRequest.OpType.DELETE)) {
                DeleteResponse deleteResponse = (DeleteResponse)itemResponse;
                results.add(deleteResponse);
            }
        }
        
        List<Object> resultList = results.stream().map(i->i.getResult()).collect(Collectors.toList());
        
        return Arrays.toString(resultList.toArray());
    }
}
 
cs

 

 

검색(Search) API

검색 API는 SearchRequest 클래스를 이용해 검색 요청을 할 수 있다. 쿼리에는 Tranport 클라이언트에서 사용하던 Query DSL 클래스를 동일하게 사용할 수 있다.

 

match-all Query

 

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
@RestController
@RequestMapping("/es/search")
public class SearchAPIController {
    
    private Gson gson = new Gson();
    
    /*
     * connection create method
     */
    public RestHighLevelClient createConnection() {
        return new RestHighLevelClient(
                    RestClient.builder(
                            new HttpHost("127.0.0.1",9200,"http")
                    )
                );
    }
    
    @RequestMapping("/match-all")
    public String matchAll(){
        String aliasName = "chatbotInstance";
        String typeName = "_doc";
        
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
        searchSourceBuilder.query(QueryBuilders.matchAllQuery());
        searchSourceBuilder.from(0);
        searchSourceBuilder.size(5);
        searchSourceBuilder.sort(new FieldSortBuilder("answer").order(SortOrder.DESC));
        
        SearchRequest request = new SearchRequest(aliasName);
        request.types(typeName);
        request.source(searchSourceBuilder);
        
        
        SearchResponse response = null;
        SearchHits searchHits = null;
        List<Answer> resultMap = new ArrayList<>();
        
        try(RestHighLevelClient client = createConnection();){
            
            response = client.search(request, RequestOptions.DEFAULT);
            searchHits = response.getHits();
            for( SearchHit hit : searchHits) {
                Map<String, Object> sourceAsMap = hit.getSourceAsMap();
                Answer a = new Answer();
                a.setQuestion(sourceAsMap.get("question")+"");
                a.setAnswer(sourceAsMap.get("answer")+"");
                resultMap.add(a);
            }
            
        }catch (Exception e) {
            /*
             * 예외처리
             */
            e.printStackTrace();
        }
        
        return gson.toJson(resultMap); 
    }
    
    private class Answer{
        private String question;
        private String answer;
        
        public String getQuestion() {
            return question;
        }
        public void setQuestion(String question) {
            this.question = question;
        }
        public String getAnswer() {
            return answer;
        }
        public void setAnswer(String answer) {
            this.answer = answer;
        }
    }
}
 
cs

 

Full Text Query API - Match Query

 

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
@RestController
@RequestMapping("/es/search")
public class SearchAPIController {
    
    private Gson gson = new Gson();
    
    /*
     * connection create method
     */
    public RestHighLevelClient createConnection() {
        return new RestHighLevelClient(
                    RestClient.builder(
                            new HttpHost("127.0.0.1",9200,"http")
                    )
                );
    }
    
    @RequestMapping("/match")
    public String match(){
        String aliasName = "chatbotInstance";
        String fieldName = "question";
        String typeName = "_doc";
        
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
        searchSourceBuilder.query(QueryBuilders.matchQuery(fieldName, "질문"));
        searchSourceBuilder.from(0);
        searchSourceBuilder.size(5);
        searchSourceBuilder.sort(new FieldSortBuilder("answer").order(SortOrder.DESC));
        
        SearchRequest request = new SearchRequest(aliasName);
        request.types(typeName);
        request.source(searchSourceBuilder);
        
        
        SearchResponse response = null;
        SearchHits searchHits = null;
        List<Answer> resultMap = new ArrayList<>();
        
        try(RestHighLevelClient client = createConnection();){
            
            response = client.search(request, RequestOptions.DEFAULT);
            searchHits = response.getHits();
            for( SearchHit hit : searchHits) {
                Map<String, Object> sourceAsMap = hit.getSourceAsMap();
                Answer a = new Answer();
                a.setQuestion(sourceAsMap.get("question")+"");
                a.setAnswer(sourceAsMap.get("answer")+"");
                resultMap.add(a);
            }
            
        }catch (Exception e) {
            /*
             * 예외처리
             */
            e.printStackTrace();
        }
        
        return gson.toJson(resultMap); 
    }
    
    private class Answer{
        private String question;
        private String answer;
        
        public String getQuestion() {
            return question;
        }
        public void setQuestion(String question) {
            this.question = question;
        }
        public String getAnswer() {
            return answer;
        }
        public void setAnswer(String answer) {
            this.answer = answer;
        }
    }
}
 
cs

 

common term query

많이 검색되는 단어와 적게 검색되는 단어 중 어떤 단어가 더 중요한지를 판단해서 검색 스코어링을 변경하는 알고리즘을 가지고 있는 쿼리이다. 예를 들어 "the red fox"라는 문장이 있을 경우 the,red,fox라는 텀으로 문장을 자르고 검색할때 각각을 검색해서 최종적으로 스코어를 계산한다. the라는 단어는 너무 흔한 단어로서 맨 나중에 검색되도록 가중치를 조절할 수 있다.

 

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
@RestController
@RequestMapping("/es/search")
public class SearchAPIController {
    
    private Gson gson = new Gson();
    
    /*
     * connection create method
     */
    public RestHighLevelClient createConnection() {
        return new RestHighLevelClient(
                    RestClient.builder(
                            new HttpHost("127.0.0.1",9200,"http")
                    )
                );
    }
    
    @RequestMapping("/commonTerm")
    public Object commmonTerm() {
        String aliasName = "chatbotInstance";
        String fieldName = "question";
        String typeName = "_doc";
        
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
        searchSourceBuilder.query(QueryBuilders.commonTermsQuery(fieldName, "질문"));
        searchSourceBuilder.from(0);
        searchSourceBuilder.size(5);
        searchSourceBuilder.sort(new FieldSortBuilder("answer").order(SortOrder.DESC));
        
        SearchRequest request = new SearchRequest(aliasName);
        request.types(typeName);
        request.source(searchSourceBuilder);
        
        
        SearchResponse response = null;
        SearchHits searchHits = null;
        List<Answer> resultMap = new ArrayList<>();
        
        try(RestHighLevelClient client = createConnection();){
            response = client.search(request, RequestOptions.DEFAULT);
            searchHits = response.getHits();
            for( SearchHit hit : searchHits) {
                Map<String, Object> sourceAsMap = hit.getSourceAsMap();
                Answer a = new Answer();
                a.setQuestion(sourceAsMap.get("question")+"");
                a.setAnswer(sourceAsMap.get("answer")+"");
                resultMap.add(a);
            }
        }catch (Exception e) {
            e.printStackTrace();
            /*
             * 예외처리
             */
        }
        return gson.toJson(resultMap); 
    }
    
    private class Answer{
        private String question;
        private String answer;
        private float score;
        
        public String getQuestion() {
            return question;
        }
        public void setQuestion(String question) {
            this.question = question;
        }
        public String getAnswer() {
            return answer;
        }
        public void setAnswer(String answer) {
            this.answer = answer;
        }
        public float getScore() {
            return score;
        }
        public void setScore(float score) {
            this.score = score;
        }
    }
}
 
cs

 

Query String Query

검색할 때 연산자를 따로 지정하는 것이 아니라 쿼리문 자체에 AND, OR 절을 사용하고 싶을때 사용하는 쿼리이다. 혹은 특정 키워드를 필수 조건으로 빼거나 넣거나 할 수도 있다.

 

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
@RestController
@RequestMapping("/es/search")
public class SearchAPIController {
    
    
    /*
     * connection create method
     */
    public RestHighLevelClient createConnection() {
        return new RestHighLevelClient(
                    RestClient.builder(
                            new HttpHost("127.0.0.1",9200,"http")
                    )
                );
    }
    
    @RequestMapping("/queryString")
    public Object queryString() {
        String aliasName = "chatbotInstance";
        String fieldName = "question";
        String typeName = "_doc";
        String question = "+질문 -벌크";
        
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
//        searchSourceBuilder.query(QueryBuilders.queryStringQuery("질문 OR 벌").field(fieldName));
        searchSourceBuilder.query(QueryBuilders.queryStringQuery(question).field(fieldName));
        searchSourceBuilder.from(0);
        searchSourceBuilder.size(5);
        searchSourceBuilder.sort(new FieldSortBuilder("answer").order(SortOrder.DESC));
        
        SearchRequest request = new SearchRequest(aliasName);
        request.types(typeName);
        request.source(searchSourceBuilder);
        
        
        SearchResponse response = null;
        SearchHits searchHits = null;
        List<Answer> resultMap = new ArrayList<>();
        
        try(RestHighLevelClient client = createConnection();){
            response = client.search(request, RequestOptions.DEFAULT);
            searchHits = response.getHits();
            for( SearchHit hit : searchHits) {
                Map<String, Object> sourceAsMap = hit.getSourceAsMap();
                Answer a = new Answer();
                a.setQuestion(sourceAsMap.get("question")+"");
                a.setAnswer(sourceAsMap.get("answer")+"");
                resultMap.add(a);
            }
        }catch (Exception e) {
            e.printStackTrace();
            /*
             * 예외처리
             */
        }
        return resultMap; 
    }
    
    private class Answer{
        private String question;
        private String answer;
        private String score;
        
        public String getQuestion() {
            return question;
        }
        public void setQuestion(String question) {
            this.question = question;
        }
        public String getAnswer() {
            return answer;
        }
        public void setAnswer(String answer) {
            this.answer = answer;
        }
        public String getScore() {
            return score;
        }
        public void setScore(String score) {
            this.score = score;
        }
    }
}
 
cs

 

Term Query

지정된 필드에 정확한 텀이 들어있는 문서를 찾을 때 사용한다. Keyword 타입으로 설정된 필드에서만 검색가능하다.

 

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
@RequestMapping("/term")
    public Object term() {
        String aliasName = "chatbotInstance";
        String fieldName = "question";
        String typeName = "_doc";
        
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
        searchSourceBuilder.query(QueryBuilders.termQuery("keywordFiled","keyword"));
        searchSourceBuilder.from(0);
        searchSourceBuilder.size(5);
        searchSourceBuilder.sort(new FieldSortBuilder("answer").order(SortOrder.DESC));
        
        SearchRequest request = new SearchRequest(aliasName);
        request.types(typeName);
        request.source(searchSourceBuilder);
        
        
        SearchResponse response = null;
        SearchHits searchHits = null;
        List<Answer> resultMap = new ArrayList<>();
        
        try(RestHighLevelClient client = createConnection();){
            response = client.search(request, RequestOptions.DEFAULT);
            searchHits = response.getHits();
            for( SearchHit hit : searchHits) {
                Map<String, Object> sourceAsMap = hit.getSourceAsMap();
                Answer a = new Answer();
                a.setQuestion(sourceAsMap.get("question")+"");
                a.setAnswer(sourceAsMap.get("answer")+"");
                resultMap.add(a);
            }
        }catch (Exception e) {
            e.printStackTrace();
            /*
             * 예외처리
             */
        }
        return resultMap; 
    }
cs

 

Bool Query

must, mustNot, should, filter 등을 조합해서 쿼리를 구성한다.

 

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
@RequestMapping("/bool")
    public Object bool() {
        String aliasName = "chatbotInstance";
        String typeName = "_doc";
        
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
        searchSourceBuilder.query(QueryBuilders.boolQuery()
                .must(QueryBuilders.termQuery("keywordField""value"))
                .mustNot(QueryBuilders.termQuery("keywordField2""value2"))
                .should(QueryBuilders.termQuery("keywordField3""value3"))
                .filter(QueryBuilders.termQuery("keywordField4""value4")));
        searchSourceBuilder.from(0);
        searchSourceBuilder.size(5);
        searchSourceBuilder.sort(new FieldSortBuilder("answer").order(SortOrder.DESC));
        
        SearchRequest request = new SearchRequest(aliasName);
        request.types(typeName);
        request.source(searchSourceBuilder);
        
        
        SearchResponse response = null;
        SearchHits searchHits = null;
        List<Answer> resultMap = new ArrayList<>();
        
        try(RestHighLevelClient client = createConnection();){
            response = client.search(request, RequestOptions.DEFAULT);
            searchHits = response.getHits();
            for( SearchHit hit : searchHits) {
                Map<String, Object> sourceAsMap = hit.getSourceAsMap();
                Answer a = new Answer();
                a.setQuestion(sourceAsMap.get("question")+"");
                a.setAnswer(sourceAsMap.get("answer")+"");
                resultMap.add(a);
            }
        }catch (Exception e) {
            e.printStackTrace();
            /*
             * 예외처리
             */
        }
        return resultMap; 
    }
cs

 

기타 많은 쿼리들을 있지만 이번 포스팅은 여기까지만 다룬다. 이번 포스팅의 목적은 엘라스틱서치의 기능이 목적은 아니고 엘라스틱서치 클라이언트를 적응하는 것이 목적이었다.

 

posted by 여성게
:

 

이번 포스팅에서 다루어볼 내용은 이전 포스팅에서 다룬 한글 형태소분석기를 이용한 기타 고급검색 기법에 대해 다루어볼 것이다. 물론 해당 고급검색에는 간략한 내용만 다루어보고 집계쿼리나 기타 유사도검색 기반 검색들은 다른 포스팅에서 다루어볼 것이다. 이번에 다루어볼 내용들이다.

 

  1. 검색 결과 하이라이팅
  2. 스크립트를 이용한 동적필드 추가
  3. 검색 템플릿을 이용한 동적 쿼리 제공
  4. 별칭을 이용하여 항상 최신 인덱스 유지하기
  5. 스냅샷을 이용한 백업과 복구

위 내용들을 다루어볼 것이다. 검색의 성능을 향상시키거나 그런 내용은 아니지만 알아두면 아주 유용한 기능들이기에 다루어볼 것이다.

 

 

검색결과 하이라이팅

하이라이팅은 문서 검색 결과에 사용자가 입력한 검색어를 강조하는 기능이다. 해당 기능을 통해 사용자가 입력한 검색어가 어디에 일치되는 지는 쉽게 눈으로 확인가능하다. 즉, 사용자의 편의를 위한 기능중 한가지라고 볼 수 있을 것이다.

 

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
POST http://localhost:9200/movie_search/_search
 
{
    "size":1,
    "query":{
        "match":{
            "movieNm":"그대 장미"
        }
    },
    "highlight":{
        "fields":{
            "movieNm":{}
        }
    }
}
 
result =>
{
    "took": 2,
    "timed_out": false,
    "_shards": {
        "total": 5,
        "successful": 5,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": 49,
        "max_score": 10.243236,
        "hits": [
            {
                "_index": "movie_search",
                "_type": "_doc",
                "_id": "cz3KqmkBjjM-ebDbBcoO",
                "_score": 10.243236,
                "_source": {
                    "movieCd": "2009A438",
                    "movieNm": "장미",
                    "movieNmEn": "",
                    "prdtYear": "",
                    "openDt": "",
                    "typeNm": "",
                    "prdtStatNm": "기타",
                    "nationAlt": "",
                    "genreAlt": "",
                    "repNationNm": "",
                    "repGenreNm": "",
                    "directors": [],
                    "companys": []
                },
                "highlight": {
                    "movieNm": [
                        "<em>장미</em>"
                    ]
                }
            }
        ]
    }
}
cs

 

크게 어렵지 않다. 위처럼 쿼리를 날리게 되면 결과로 highlight가 나오는 걸 볼 수 있다. 화면에 뿌려줄때에는 highlight부분을 검색결과 링크로 뿌려주어도 될 것 같다. 그리고 em 태그 말고 다른 태그로 하이라이팅을 하고 싶을 수도 있다. 그럴때에는 몇개의 옵션만 추가해주면된다.

 

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
POST http://localhost:9200/movie_search/_search
 
{
    "size":1,
    "query":{
        "match":{
            "movieNm":"그대 장미"
        }
    },
    "highlight":{
        "pre_tags":["<string>"],
        "post_tags":["</string>"],
        "fields":{
            "movieNm":{}
        }
    }
}
 
result =>
{
    "took": 2,
    "timed_out": false,
    "_shards": {
        "total": 5,
        "successful": 5,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": 49,
        "max_score": 10.243236,
        "hits": [
            {
                "_index": "movie_search",
                "_type": "_doc",
                "_id": "cz3KqmkBjjM-ebDbBcoO",
                "_score": 10.243236,
                "_source": {
                    "movieCd": "2009A438",
                    "movieNm": "장미",
                    "movieNmEn": "",
                    "prdtYear": "",
                    "openDt": "",
                    "typeNm": "",
                    "prdtStatNm": "기타",
                    "nationAlt": "",
                    "genreAlt": "",
                    "repNationNm": "",
                    "repGenreNm": "",
                    "directors": [],
                    "companys": []
                },
                "highlight": {
                    "movieNm": [
                        "<em>장미</em>"
                    ]
                }
            }
        ]
    }
}
cs

 

위와 같이 pre_tags,post_tags 옵션을 주어 하이라이팅할 단어에 태그를 지정해줄 수도 있다.

 

 

스크립트를 이용하여 동적으로 필드 추가하기

엘라스틱서치는 스크립트를 이용해 사용자가 특정 로직을 삽입하는 것이 가능하다. 이러한 방식을 스크립팅이라고 한다. 스크립팅을 이용하면 두 개 이상의 필드 스코어를 하나로 합하거나 계산된 스코어를 특정 수식으로 재계산하는 등의 작업이 가능해진다. 사용자는 이를 활용해 검색 요청시 특정 필드를 선택적으로 반환하거나 필드의 특정 요소를 수정하는 등 광범위한 작업을 할 수 있다.

 

엘라스틱서치에서는 스크립팅을 지원하는 두가지 방식이 존재한다.

  1. config 폴더에 스크립팅을 저장하여 콜하는 방식
  2. 요청바디에 스크립트를 포함하는 방식

그리고 지금 엘라스틱서치에서는 스크립팅 전용 언어인 페인리스(painless)가 개발되어 사용된다. 엘라스틱서치는 기본적으로 업데이트를 허용하지 않는다. 재색인을 통해 설정한 _id의 문서를 삭제하고 다시 생성할 뿐이다. 하지만 _update API를 제공하고 있는데, 이는 내부적으로 스크립팅을 사용하여 업데이트가 이루어지는 것이다. 오늘 다루어볼 스크립트는 문서의 필드를 동적으로 추가하고 삭제하는 예제이다.

 

1
2
3
4
5
6
7
8
9
PUT http://localhost:9200/movie_script/_doc/1
 
{
    "movieList":{
        "Death_Wish":5.5,
        "About_Time":7,
        "Suits":3.5
    }
}
cs

 

 

인덱스 및 문서를 하나 생성해준다.

 

1
2
3
4
5
POST http://localhost:9200/movie_script/_doc/1/_update
 
{
    "script":"ctx._source.movieList.Black_Panther=3.7"
}
cs

 

위와 같이 _update API를 이용하여 내부적으로 스크립트를 작성해 존재하지 않는 필드를 하나 생성해준다. ctx._source는 스크립트에서 제공하는 특수한 문법이다. 이는 색인된 문서에 접근하기 위한 문법이라고 생각하면 된다.

 

1
2
3
4
5
POST http://localhost:9200/movie_script/_doc/1/_update
 
{
    "script":"ctx._source.movieList.remove(\"Suits\")"
}
cs

 

기존에 존재하고 있는 Suits라는 필드를 삭제한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
GET http://localhost:9200/movie_script/_doc/1
 
result =>
{
    "_index": "movie_script",
    "_type": "_doc",
    "_id": "1",
    "_version": 4,
    "found": true,
    "_source": {
        "movieList": {
            "Death_Wish": 5.5,
            "About_Time": 7,
            "Black_Panther": 3.7
        }
    }
}
cs

 

결과값으로 Suits 필드가 삭제되고 Black_Panther가 추가된 것을 볼 수 있다. 이와 같이 스크립트를 이용하면 필드를 동적으로 추가,제거할 수도 있고, 어떠한 결과값을 재정의 하는 것도 가능해진다.

 

 

검색 템플릿을 이용한 동적 쿼리 제공

검색 템플릿은 엘라스틱서치 1.1 버전에 추가된 오래된 기능이다. 하지만 이를 통해 복잡한 검색 로직을 템플릿화하여 저장하고 활용할 수 있기때문에 매우 유용하다. 검색 템플릿의 필드명과 파라미터를 사용해서 쿼리를 전송하고 템플릿에 제공한 파라미터로 실제 검색이 이뤄진다.즉, 검색 템플릿을 사용하여 클라이언트쪽 코드가 아주 간략해지는 것이다.

 

또한 클라이언트 프로그램을 열어 검색의 요구사항이 변경될때마다 코드를 수정하고 다시 배포하는 것이 아니라 엘라스틱서치에 저장돼 있는 템플릿의 기존 쿼리를 수정하고 새 쿼리를 작성하면 되므로 이점이 있다. 검색 템플릿은 Mustache라는 템플릿 엔진을 사용해서 표현된다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
POST http://localhost:9200/_scripts/movie_search_template
 
{
    "script":{
        "lang":"mustache",
        "source":{
            "query":{
                "match":{
                    "movieNm":"{{movie_nm}}"
                }
            }
        }
    }
}
cs

 

검색 템플릿 하나를 생성해준다. 생성이 완료되면 밑의 요청을 보내 검색템플릿이 정상적으로 생성되었는지 정보를 확인한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
GET http://localhost:9200/_scripts/movie_search_template
 
result =>
{
    "_id": "movie_search_template",
    "found": true,
    "script": {
        "lang": "mustache",
        "source": "{\"query\":{\"match\":{\"movieNm\":\"{{movie_nm}}\"}}}",
        "options": {
            "content_type": "application/json; charset=UTF-8"
        }
    }
}
cs

 

이제는 해당 템플릿을 이용하여 검색을 해보자.

 

1
2
3
4
5
6
7
8
9
10
11
POST http://localhost:9200/movie_search/_doc/_search/template
 
{
    "id":"movie_search_template",
    "params":{
        "movie_nm":"그대"
    }
}
 
 
 
cs

 

아이디 값으로 검색 템플릿 이름을 주고 파라미터에 정의된 파라미터 명을 쓰고 값을 넣어주면 된다. 그리고 추후에 검색 템플릿의 변경이 생긴다면 생성과 동일한 요청으로 변경된 템플릿 요청을 보내주면 수정이 완료된다. 이렇게 클라이언트 코드의 수정이 없이도 쿼리의 변경이 가능하니 아주 편리한 기능이다.

 

 

**별칭을 이용해 항상 최신 인덱스 유지하기

엘라스틱서치 클러스터를 운영하는 중에 인덱스 매핑 설정이 변경되거나 인덱스가 깨진다면 기존에 생성된 인덱스를 삭제하고 다시 생성해야 할것이다. 그런데 운영중인 클러스터라면 인덱스 변경을 위하여 인덱스가 삭제되어있는 중에 사용자의 쿼리 요청을 받게되면 인덱스가 존재하지 않는다는 에러메시지를 인덱스가 다시 생성될 때까지 사용자는 보게 될 것이다. 이것이 운영중인 클러스터 상황에 나와야하는 상황일까? 절대 아니다. 절대 나와서는 안되는 상황인 것이다. 이러한 문제를 방지하기 위하여 엘라스틱서치에는 별칭(Alias)라는 기능을 제공한다. 이러한 기능은 Apache Solr에도 거의 동일하게 존재하는 기능이 있다. 필자는 솔라를 이용할때 해당 별칭기능을 이용하여 Active/Standby 컬렉션을 운영했던 경험이 있다. 아마 엘라스틱서치도 비슷한 구성으로 운영가능하지 않을까 생각한다. 그리고 하나의 인덱스에만 별칭을 줄 수 있는 것은 아니다. 여러개의 인덱스에 동일한 별칭을 주어 여러개의 인덱스를 하나의 별칭으로 검색 할 수 있도록 할 수도 있다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST http://localhost:9200/_aliases
 
{
    "actions":[
        {
            "add":{"index":"movie_search","alias":"movie_alias"}
        },
        {
            "add":{"index":"movie_search2","alias":"movie_alias"}
        }
    ]
}
 
 
 
cs

 

위와 같이 하나 이상의 인덱스를 동일한 별칭으로 설정할 수 있다. 멀티테넌시 기능을 이용하기 보다는 해당 별칭기능을 이용하는 편이 훨씬 나을 듯하다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
POST http://localhost:9200/movie_alias/_search
 
{
    "size":1,
    "query":{
        "match":{
            "movieNm":"그대 장미"
        }
    }
}
 
 
 
cs

 

별칭으로 검색하는 예제이다. 이와같이 변경이 잦은 인덱스는 별칭을 이용하는 것이 좋다. 예를 들어 매일같이 변경되는 인덱스가 있다고 생각해보자. 그러면 인덱스명을 "인덱스명_타임스탬프" 형식으로 생성하고 다음날에는 이전 인덱스의 별칭을 삭제함과 동시에 새로운 인덱스에 별칭을 주면 사용자 입장에서는 변경되는 것을 알아차리지 못하고 별칭을 이용하여 새로운 인덱스에 요청을 보내게 된다. 이렇게 실무에서는 별칭을 이용하여 항상 최신의 인덱스를 유지하여 사용할 수 있다. 그리고 위에서 잠깐 이야기했지만, Active/StandBy 인덱스를 나누어서 사용해야할 일이 있다면 항상 Active에 별칭을 주고, Switch되는 이벤트가 발생하여 StandBy 인덱스가 Active로 가야한다면 별칭만 StandBy로 추가해주고 Active의 별칭을 삭제하면 Active<->StandBy 스위치가 이러나게 되는 것이다. 이렇게 별칭을 여러가지 용도로 사용가능하다.

 

1
2
3
4
5
6
7
8
9
10
11
12
POST http://localhost:9200/_aliases
 
{
    "actions":[
        {
            "remove":{"index":"movie_search","alias":"movie_alias"}
        }
    ]
}
 
 
 
cs

 

마지막으로 별칭을 삭제하는 요청이다. 해당 요청에는 위와 동일하게 add,remove를 같이 배열로 요청을 보낼 수 있으므로, 인덱스 변경시에는 add와 remove를 동시에 요청을 보내면 될듯하다.

 

 

**스냅샷을 이용한 백업&복구

엘라스틱서치 클러스터를 관리하다 보면 항상 하게 되는 고민 중 하나인 데이터 백업 정책이다. 검색 서버가 항상 안정적이라는 보장은 없기때문에 항상 백업과 복구를 염려해야하는 것은 당연한 것이다. 단순히 데이터 소스를 제공하는 시스템에서 다시 데이터를 Full 색인하면 된다라는 생각을 버려야한다. 데이터가 아주 크다면 아주 오랜시간 동안 색인 작업이 이루어질 것이고, 그러면 그 시간동안 서비스는 불가능 할 것이다. 이러한 고민을 해결해주기 위해 엘라스틱서치는 _snapshot API를 제공해준다. 해당 기능을 이용하여 개별 인덱스를 백업할 수도 있고, 클러스터 전체를 스냅샷으로 만드는 것도 가능하다. 그리고 스냅 샷은 점진적으로 가져온다. 즉, 인덱스의 스냅 샷을 만들 때, Elasticsearch는 동일한 인덱스의 이전 스냅 샷의 일부로 이미 리포지토리에 저장된 데이터를 복사하지 않도록한다. 따라서 클러스터의 스냅 샷을 자주 가져 오는 것이 효율적일 수 있다.

 

1)백업 디렉토리 생성

스냅샷 기능을 이용하기 위해서는 스냅샷이 저장될 물리적인 디렉토리를 생성해주어야 한다.원하는 위치에 디렉토리를 하나 생성해준다.

 

2)백업 디렉토리 등록

엘라스틱서치가 물리적인 백업용 디렉토리를 인식할 수 있도록 elasticsearch.yml 파일에 설정해준다.

백업디렉토리는 위와 같이 배열로 여러 경로를 지정해줄 수 있다. 용도별 스냅샷 디렉토리를 구분할 필요가 있다면 여러개 설정해주어도 될듯하다. 엔진을 재시작한다.

 

3)레포지토리 생성

다음은 스냅샷들을 저장하는 논리적인 저장공간을 만들것이다. 이를 레포지토리라고 부른다. 레포지토리는 설정 내에 물리적인 디렉토리 내부를 저장소로 이용한다. 또한 레포지토리를 생성할 때 다양한 설정 값을 지정할 수 있다. 스냅샷을 백업&복구하는 작업은 시스템 리소스를 많이 사용하기에 몇가지 옵션을 잘 조합해서 사용하는 것이 좋다. 즉, 해당 기능은 엘라스틱서치가 파일시스템을 백업과 복구를 이용하기 위해 등록하는 과정이라고 보면된다.

 

매개변수 설정
location 스냅샷의 물리적인 저장 경로설정.
compress 스냅샷 생성이 압축 수행여부. 이때 데이터 자체는 압축되지 않고 메타데이터만 압축 대상이 된다.
chunk_size 생성되는 파일을 특정 크기로 나누어서 생성할 수 있다. 기본적으로 스냅샷은 하나의 파일로 생성된다.
max_restore_bytes_per_sec 스냅샷 복원 시 속도를 설정한다. 기본적으로 초당 40MB의 속도이다.
max_snapshot_bytes_per_sec 스냅샷 생성(백업)시 속도를 설정한다. 기본적으로 초당 40MB의 속도이다.
readonly 레포지토리를 읽기 전용으로 생성한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
PUT http://localhost:9200/_snapshot/movie_data_backup
 
{
    "type":"fs",
    "settings":{
        "location":"/Users/yun-yeoseong/elasticsearch-file/elasticsearch-6.4.3/backup_snapshot",
        "compress":true
    }
}
 
 
 
cs

 

위 요청으로 레포지토리를 생성해준다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
GET http://localhost:9200/_snapshot/movie_data_backup
 
result=>
{
    "movie_data_backup": {
        "type": "fs",
        "settings": {
            "compress": "true",
            "location": "/Users/yun-yeoseong/elasticsearch-file/elasticsearch-6.4.3/backup_snapshot"
        }
    }
}
 
 
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
GET http://localhost:9200/_snapshot/search*,agg*,movie*
 
result=>
{
    "searchexam": {
        "type": "fs",
        "settings": {
            "compress": "true",
            "location": "/Users/yun-yeoseong/elasticsearch-file/book_backup/search_example"
        }
    },
    "aggexam": {
        "type": "fs",
        "settings": {
            "compress": "true",
            "location": "/Users/yun-yeoseong/elasticsearch-file/book_backup/agg_example"
        }
    },
    "movie_data_backup": {
        "type": "fs",
        "settings": {
            "compress": "true",
            "location": "/Users/yun-yeoseong/elasticsearch-file/elasticsearch-6.4.3/backup_snapshot"
        }
    }
}
 
 
cs

 

또한 위와 같이 "*"를 이용하여 여러개의 레포지토리를 조회할 수도 있다.

 

4)스냅샷 생성(백업)

레포지토리가 정상적으로 생성되면 스냅샷을 생성한다. 기본적으로 스냅샷 대상이 되는 인덱스는 더 이상 변경이 없는 인덱스여야만 한다. 변경이 일어나는 도중에 스냅샷이 생성되면 문제가 발생할 수 있다. 그리고 스냅샷 이름은 클러스터 내에서 꼭 유일한 이름이여야한다. 만약 스냅샷 이름이 동일한 것이 존재한다면 에러를 내뱉을 것이다.

 

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
PUT http://localhost:9200/_snapshot/movie_data_backup/movie_snapshot_20190511?wait_for_completion=true
 
{
    "indices":"movie_search",
    "ignore_unavailable":true,
    "include_global_state":false
}
 
result=>
{
    "snapshot": {
        "snapshot": "movie_snapshot_20190511",
        "uuid": "m9glqYcyS_mNiv7ZTJmZ8Q",
        "version_id": 6040399,
        "version": "6.4.3",
        "indices": [
            "movie_search"
        ],
        "include_global_state": false,
        "state": "SUCCESS",
        "start_time": "2019-05-11T06:37:08.496Z",
        "start_time_in_millis": 1557556628496,
        "end_time": "2019-05-11T06:37:08.880Z",
        "end_time_in_millis": 1557556628880,
        "duration_in_millis": 384,
        "failures": [],
        "shards": {
            "total": 5,
            "failed": 0,
            "successful": 5
        }
    }
}
cs

 

wait_for_completion=true 옵션을 이용하면 스냅샷 생성이 완료될 때까지 기다린다. 만약 false로 설정하면 스냅샷 초기화가 완료된 직후에 결과를 반환한다. 그리고 내부적으로 백업할 인덱스명을 옵션으로 줄 수 있다.(indices) 하지만 해당 옵션을 사용하지 않으면 기본값으로 해당 클러스터 내부의 모든 인덱스를 백업대상으로 삼는다. 인덱스명은 ","구분으로 여러개를 지정할 수 있다. 그리고 ignore_unavailable 옵션을 true로 설정하면 명시한 indices 옵션에 설정한 인덱스가 존재하지 않는다면 해당 인덱스를 무시하고 다른 인덱스들 백업을 하게 된다. 만약 false로 설정하면 백업을 지정한 인덱스가 존재하지 않으면 에러를 내뱉는다. include_global_state는 클러스터 글로벌 설정을 백업의 일부로 포함시키지 않는다. 만약 해당 설정을 true로 설정하면 모든 샤드에 대해 글로벌 설정이 맞지 않으면 백업이 실패한다. 그리고 만약 스냅샷의 이름의 일부로 날짜등의 데이터를 넣어야 한다면 날짜 표현식을 넣을 수도 있다. 날짜를 넣는다면 데일리 백업에 있어 구분하기 쉬운 방법이 될 것이다. 그리고 배치등을 이용하여 다시 복구할때 소스의 수정없이 날짜 표현식을 이용하면 된다. 해당 표현식은 반드시 URL 엔코딩이 된 문자열이여야한다.

 

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
# PUT /_snapshot/my_backup/<snapshot-{now/d}>
PUT http://localhost:9200/_snapshot/movie_data_backup/%3Csnapshot-%7Bnow%2Fd%7D%3E?wait_for_completion=true
 
{
    "indices":"movie_search",
    "ignore_unavailable":true,
    "include_global_state":false
}
 
result=>
{
    "snapshot": {
        "snapshot": "snapshot-2019.05.11",
        "uuid": "k3GeN2rrS2meMFptYPfAOA",
        "version_id": 6040399,
        "version": "6.4.3",
        "indices": [
            "movie_search"
        ],
        "include_global_state": false,
        "state": "SUCCESS",
        "start_time": "2019-05-11T06:55:51.344Z",
        "start_time_in_millis": 1557557751344,
        "end_time": "2019-05-11T06:55:51.366Z",
        "end_time_in_millis": 1557557751366,
        "duration_in_millis": 22,
        "failures": [],
        "shards": {
            "total": 5,
            "failed": 0,
            "successful": 5
        }
    }
}
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
GET http://localhost:9200/_snapshot/movie_data_backup/*
GET http://localhost:9200/_snapshot/movie_data_backup/_all
GET http://localhost:9200/_snapshot/movie_data_backup/snapshot*
 
result=>
{
    "snapshots": [
        {
            "snapshot": "movie_snapshot_20190511",
            "uuid": "m9glqYcyS_mNiv7ZTJmZ8Q",
            "version_id": 6040399,
            "version": "6.4.3",
            "indices": [
                "movie_search"
            ],
            "include_global_state": false,
            "state": "SUCCESS",
            "start_time": "2019-05-11T06:37:08.496Z",
            "start_time_in_millis": 1557556628496,
            "end_time": "2019-05-11T06:37:08.880Z",
            "end_time_in_millis": 1557556628880,
            "duration_in_millis": 384,
            "failures": [],
            "shards": {
                "total": 5,
                "failed": 0,
                "successful": 5
            }
        },
        {
            "snapshot": "snapshot-2019.05.11",
            "uuid": "k3GeN2rrS2meMFptYPfAOA",
            "version_id": 6040399,
            "version": "6.4.3",
            "indices": [
                "movie_search"
            ],
            "include_global_state": false,
            "state": "SUCCESS",
            "start_time": "2019-05-11T06:55:51.344Z",
            "start_time_in_millis": 1557557751344,
            "end_time": "2019-05-11T06:55:51.366Z",
            "end_time_in_millis": 1557557751366,
            "duration_in_millis": 22,
            "failures": [],
            "shards": {
                "total": 5,
                "failed": 0,
                "successful": 5
            }
        }
    ]
}
cs

 

마지막으로 결과값에 포함된 state의 값의 의미이다.

설명
IN_PROGRESS 스냅샷 생성중.
SUCCESS 스냅샷이 정상적으로 생성됨.
FAILED 스냅샷이 정상적으로 생성되지 못하였으며, 어떠한 데이터도 저장되지 않음.
PARTIAL 글로벌 클러스터 상태가 저장되었지만 하나 이상의 샤드의 데이터가 성공적으로 저장되지 않음.
INCOMPATIBLE

스냅 샷은 이전 버전의 Elasticsearch로 작성되었으므로 현재 버전의 클러스터와 호환되지 않음.

 

공식도큐먼트 글

인덱스 스냅 샷을 만드는 과정에서 Elasticsearch는 이미 리포지토리에 저장된 인덱스 파일 목록을 분석하고 마지막 스냅 샷 이후에 작성되었거나 변경된 파일 만 복사합니다. 따라서 여러 스냅 샷을 압축 된 형태로 저장소에 보존 할 수 있습니다. 스냅 샷 프로세스는 비 차단 방식으로 실행됩니다. 스냅 샷을 생성하는 인덱스에 대해 모든 인덱싱 및 검색 작업을 계속 실행할 수 있습니다. 그러나 스냅 샷은 스냅 샷이 생성 된 시점의 인덱스의 특정 시점보기를 나타내므로 스냅 샷 프로세스가 시작된 후 인덱스에 추가 된 레코드는 스냅 샷에 표시되지 않습니다. 스냅 샷 프로세스는 시작되었지만 현재 재배치되지 않은 기본 샤드에 대해 즉시 시작됩니다. 버전 1.2.0 이전에는 클러스터에 스냅 샷에 참여하는 색인의 재배치 또는 초기화 기본이있는 경우 스냅 샷 작업이 실패합니다. 버전 1.2.0부터 Elasticsearch는 Shard의 재배치 또는 초기화가 스냅 샷을 만들기 전에 완료 될 때까지 대기합니다.

각 인덱스의 복사본을 만드는 것 외에도 스냅 샷 프로세스는 영구 클러스터 설정 및 템플릿을 포함하는 글로벌 클러스터 메타 데이터를 저장할 수 있습니다. 일시적 설정 및 등록 된 스냅 샷 저장소는 스냅 샷의 일부로 저장되지 않습니다.

언제든지 하나의 스냅 샷 프로세스 만 클러스터에서 실행할 수 있습니다. 특정 샤드의 스냅 샷이 생성되는 동안이 샤드는 다른 노드로 이동할 수 없으므로 균형 조정 프로세스 및 할당 필터링을 방해 할 수 있습니다. Elasticsearch는 스냅 샷이 완료되면 샤드를 다른 노드로 이동시킬 수 있습니다 (현재 할당 필터링 설정 및 재조정 알고리즘에 따라).

 

스냅샷 복구

1
POST http://localhost:9200/_snapshot/movie_data_backup/movie_snapshot_20190511/_restore
cs

 

위와 같은 요청으로 스냅샷 복구가 가능하다. 하지만 여기서 주의해야 할것은 이미 스냅샷에 저장된 인덱스가 존재할 경우에는 복구에 실패하게 된다. 이말은 즉, 동일한 인덱스가 존재한다면 삭제하고 복구해야한다는 것이다.

 

스냅샷 삭제

 

1
POST http://localhost:9200/_snapshot/movie_data_backup/movie_snapshot_20190511
cs

위의 요청을 보내면 스냅샷이 삭제됨과 동시에 물리적인 디렉토리에서도 삭제되는 것을 볼 수 있다.

 

자세한 설명은 공식 Reference를 참고하면 될듯하다. 스냅샷의 여러 옵션등을 참고 할 수 있다.

 

Snapshot and Restore | Elasticsearch Reference [7.0] | Elastic

The snapshot format can change across major versions, so if you have clusters on different versions trying to write the same repository, snapshots written by one version may not be visible to the other and the repository could be corrupted. While setting t

www.elastic.co

 

지금까지 다양한 검색과 백업/복구 등의 예제를 다루었다. 사실 이것보다 훨씬 다양하고 많은 옵션,기능이 존재한다. 모든 것을 다 다룰 수는 없기때문에 필요한 것은 도큐먼트를 참고하거나 혹은 엘라스틱서치 커뮤니티를 이용해도 좋을 것 같다.

posted by 여성게
:

 

엘라스틱서치 혹은 솔라와 같은 검색엔진들은 모두 한글에는 성능을 발휘하기 쉽지 않은 검색엔진이다. 그 이유는 한글은 다른 언어와 달리 조사나 어미의 접미사가 명사,동사 등과 결합하기 때문에 기본 형태소분석기로는 분석하기 쉽지 않다. 그렇기 때문에 검색엔진을 한글에 적용하기 위해서 별도의 한글 형태소 분석기가 필요하다. 솔라도 물론 가능하고 엘라스틱서치도 역시 한글 형태소 분석기를 내장할 수 있다. 이번 포스팅에서 다루어볼 한글 형태소 분석기는 요즘 뜨고 있는 Nori 형태소분석기를 플러그인 할 것이다. Nori 형태소 분석기는 루씬 프로젝트에서 공식적으로 제공되는 한글 형태소 분석기로써 엘라스틱서치 6.4버전에서 공식적으로 배포됬다. 내부적으로 세종 말뭉치와 mecab-ko-dic 사전을 사용하며, 기존 형태소 분석기와는 다르게 사전을 모두 압축하여 사용하고 있기 때문에 30%이상 빠르고 메모리 사용량도 현저하게 줄었으며, 시스템 전반적으로 영향을 주지 않게 최적화하였다.(아이러니하게 이 형태소분석기를 만든 사람은 한국 사람이 아니다..아마 일본어 형태소 분석기를 만든 사람으로 알고 있다.) 공식적으로 릴리스되었음에도 불구하고 Nori는 기본 플러그인으로 포함되어있지 않기때문에 직접 수동으로 플러그인해야한다(아마 최신에는 기본 내장되어 있을 수도 있다. 만약 그렇다면 댓글 부탁드린다.)

 

Nori 형태소 분석기 플러그인 설치

 

 

플러그인을 위와 같이 설치해준다. 설치가 완료되었다면 plugins 디렉토리에 Nori Analyzer가 설치되어 있을 것이다.

(혹은 ./elasticsearch-plugin list로 analysis-nori가 설치 되었는지 확인해보자. 설치 이후에 엘라스틱서치를 재시작하고 노리 플러그인이 잘 로드되는지 스타트업 로그를 확인하자)

 

직접 사용해보기전에 Nori 형태소 분석기에 대략적인 설명을 하면, Nori 형태소 분석기는 하나의 토크나이저와 두 개의 토큰 필터로 구성돼 있으며 사용자 설정 사전과 불용어 처리를 위한 stoptags를 지원한다.

 

  • nori_tokenizer : 토크나이저
  • nori_part_of_speech : 토큰필터
  • nori_readingform : 토큰필터

 

norir_tokenizer 토크나이저

토크나이저는 루씬에서 사용자 질의 혹은 색인되는 문장을 형태소(토큰)형태로 분리하는 데 사용된다. Nori에서는 해당 토크나이저를 사용할 때에 두 가지 파라미터를 지원한다.

 

  1. decompound_mode : 복합명사를 토크나이저가 처리하는 방식
  2. user_dictionary : 사용자 사전 정의

 

decompound_mode

decompound_mode는 토크나이저가 복합명사를 처리하는 방식을 결정한다. 복합명사가 있을 경우 단어를 어떻게 분리할지를 결정한다.

 

파라미터명 파라미터 값 설명 예제
decompound_mode none 복합명사로 분리하지 않는다 잠실역
사회보장제도
discard 복합명사로 분리하고 원본 데이터는 삭제한다. 잠실역=>[잠실,역]
사회보장제도=>[사회,보장,제도]
mixed 복합명사로 분리하고 원본 데이터도 유지한다. 잠실역=>[잠실,역,잠실역]
사회보장제도=>[사회,보장,제도,사회보장제도]

 

user_dictionary

Nori 형태소 분석기는 위에서도 말햇듯이 시스템사전으로 세종 말뭉치와 mecab-ko-dic 사전을 압축된 형태로 사용한다. 이 사전들 이외로 사용자 사전을 등록하여 사용할 수 있다.(명사,복합명사) 사전 파일은 config 디렉토리 밑에 user_dictionary.txt 와 같은 txt 파일을 만들어 사용한다. 사전파일의 경로는 인덱스 매핑 시 분석기의 파라미터로 지정한다. 

 

사전 등록 방법(config/user_dictionary.txt)

  • 삼성전자
  • 삼성전자 삼성 전자
  • 잠실역

첫번째는 명사 등록이고, 두번째는 복합명사 등록이다. 물론 해당 Nori 사용자 사전 이외에도 엘라스틱서치(루씬의)의 토큰 필터인 synonym filter의 동의어 사전과 stop filter의 불용어 사전등을 관리할 수도 있다. 그렇지만 이것은 사실 Nori 형태소 분석 이후 토큰에 대한 필터이기에 한글 형태소 분석기에 내장된 기능이라고는 볼 수없기에 마지막쯤에 따로 다루겠다.

 

 

Nori Tokenizer를 사용하는 커스텀 분석기 설정

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
설정) PUT http://localhost:9200/korean_analyzer
 
{
    "settings":{
        "number_of_shards": 5,
        "number_of_replicas": 1,
        "analysis":{
            "tokenizer":{
                "korean_nori_tokenizer":{
                    "type":"nori_tokenizer",
                    "decompound_mode":"mixed",
                    "user_dictionary":"user_dictionary.txt"
                }
            },
            "analyzer":{
                "nori_analyzer":{
                    "type":"custom",
                    "tokenizer":"korean_nori_tokenizer"
                }
            }
        }
    }
}
cs

 

korean_analyzer라는 인덱스에 Nori tokenizer를 사용하는 분석기를 사용하는 설정이다. decompound_mode는 mixed이며 config/user_dictionary.txt를 사용하는 nori_tokenizer를 가진 분석기를 설정한것이다. 사실 전처리 필터, 토큰 필터등 더 다양한 옵션을 지정할 수 있다. 지금부터 해당 옵션들을 하나하나 다루어볼 것이다. 우선 위의 분석기가 잘 작동하는지 확인하자.

 

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
설정) POST http://localhost:9200/_analyze
 
{
    "analyzer":"nori",
    "text":"미아역은 어디에 있죠?"
}
 
result =>
 
{
    "tokens": [
        {
            "token": "미아",
            "start_offset": 0,
            "end_offset": 2,
            "type": "word",
            "position": 0
        },
        {
            "token": "역",
            "start_offset": 2,
            "end_offset": 3,
            "type": "word",
            "position": 1
        },
        {
            "token": "어디",
            "start_offset": 5,
            "end_offset": 7,
            "type": "word",
            "position": 3
        },
        {
            "token": "있",
            "start_offset": 9,
            "end_offset": 10,
            "type": "word",
            "position": 5
        }
    ]
}
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
54
55
설정) POST http://localhost:9200/korean_analyzer/_analyze
 
{
    "analyzer":"nori_analyzer",
    "text":"미아역은 어디에 있죠?"
}
 
result =>
 
{
    "tokens": [
        {
            "token": "미아역",
            "start_offset": 0,
            "end_offset": 3,
            "type": "word",
            "position": 0
        },
        {
            "token": "은",
            "start_offset": 3,
            "end_offset": 4,
            "type": "word",
            "position": 1
        },
        {
            "token": "어디",
            "start_offset": 5,
            "end_offset": 7,
            "type": "word",
            "position": 2
        },
        {
            "token": "에",
            "start_offset": 7,
            "end_offset": 8,
            "type": "word",
            "position": 3
        },
        {
            "token": "있",
            "start_offset": 9,
            "end_offset": 10,
            "type": "word",
            "position": 4
        },
        {
            "token": "죠",
            "start_offset": 10,
            "end_offset": 11,
            "type": "word",
            "position": 5
        }
    ]
}
cs

 

여러가지로 결과가 다른 것을 볼 수 있다. 일단 사용자 정의사전이 적용되어 복합명사로 분리되지 않고 하나의 명사로 인식된것만 확인하자.

 

nori_part_of_speech 토큰 필터

nori_part_of_speech 토큰 필터는 품사 태그 세트와 일치하는 토큰을 찾아 제거하는 토큰 필터이다. 즉, 모든 명사를 역색인으로 생성하는 것이 아니라, 역색인 문서를 생성하기 전에 명시한 품사 태그와 일치하는 토큰을 제거하여 역색인을 생성한다. 이러한 삭제할 품사 태그는 stoptags라는 파라미터를 이용하여 지정한다.

 

앞에서 만들었던 korean_analyzer 인덱스의 nori_analyzer에 토큰 필터를 추가할 것이다. 그전에 우리는 이미 생성한 인덱스를 변경하려면 인덱스를 close상태로 만들었다가 인덱스를 변경하고 다시 open해주는 과정을 거처야한다.

 

1
2
3
설정) POST http://localhost:9200/korean_analyzer/_close
 
 
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
54
55
56
57
58
59
60
61
62
63
설정) PUT http://localhost:9200/korean_analyzer/_settings
 
{
        "analysis":{
            "tokenizer":{
                "korean_nori_tokenizer":{
                    "type":"nori_tokenizer",
                    "decompound_mode":"mixed",
                    "user_dictionary":"user_dictionary.txt"
                }
            },
            "analyzer":{
                "nori_analyzer":{
                    "type":"custom",
                    "tokenizer":"korean_nori_tokenizer",
                    "filter":[
                        "nori_posfilter"    
                    ]
                }
            },
            "filter":{
                "nori_posfilter":{
                    "type":"nori_part_of_speech",
                    "stoptags":[
                        "E",
                        "IC",
                        "J",
                        "MAG",
                        "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"
                    ]
                }
            }
        }
}
 
POST http://localhost:9200/korean_analyzer/_open
 
 
cs

 

거의 대부분의 품사를 제거하도록 stoptags에 설정해주었다.  분석기 설정을 변경한 후에 인덱스의 상태를 open으로 바꾸어준다.

 

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
설정) POST http://localhost:9200/korean_analyzer/_analyze
 
{
    "analyzer":"nori_analyzer",
    "text":"미아역은 어디에 있죠?"
}
 
result =>
 
{
    "tokens": [
        {
            "token": "미아역",
            "start_offset": 0,
            "end_offset": 3,
            "type": "word",
            "position": 0
        },
        {
            "token": "어디",
            "start_offset": 5,
            "end_offset": 7,
            "type": "word",
            "position": 2
        }
    ]
}
 
 
cs

 

이전과 동일한 분석요청을 보냈음에도 불구하고 변경된 분석기 설정에 의해 대부분의 토큰이 제거된 것을 볼 수 있다. 이렇게 pos 필터를 이용하여 불필요한 토큰들을 제거할 수 있다. 하지만 너무 과한 토큰 삭제는 분석과정에 영향을 미치니 신중하게 결정해야한다. 품사 태그는 아마 엘라스틱서치 공식 홈페이지에 가면 볼 수 있을 것이다. 하지만 중요한 것은 매번 이렇게 close-open 해가며 품사 제거를 테스트하기는 번거롭다. 필자는 직접 Lucene 라이브러리를 임포트해서 자바클래스에서 테스트를 하니 훨씬 편했다. 소스가 필요하다면 댓글남겨주시면 될 것같다.

 

<품사 태그 의미>

예시가 담긴 정보는 아래 테이블 참고(참조: https://prohannah.tistory.com/73)

값(tag) 영문명 한글명 예시
E Verbal endings 어미 사랑/하(E)/다
IC Interjection 감탄사 와우(IC), 맙소사(IC)
J Ending Particle 조사 나/는(J)/너/에게(J)
MAG General Adverb 일반 부사 빨리(MAG)/달리다, 과연(MAG)/범인/은/누구/인가
MAJ Conjunctive adverb 접속 부사 그런데(MAJ), 그러나(MAJ)
MM (*) ES:Modifier(한정사), 루씬 API:Determiner(관형사) 설명이 다름 맨(MM)/밥
NA Unknown 알 수 없음  
NNB Dependent noun (following nouns) 의존명사  
NNBC Dependent noun 의존명사(단위를 나타내는 명사)  
NNG General Noun 일반 명사 강아지(NNG)
NNP Proper Noun 고유 명사 비숑(NNP)
NP Pronoun 대명사 그것(NP), 이거(NP)
NR Numeral 수사 하나(NR)/밖에, 칠(NR)/더하기/삼(NR)
SC(*) Separator (· / :) 구분자 nori_tokenizer가 특수문자 제거
SE(*) Ellipsis 줄임표(...) nori_tokenizer가 특수문자 제거
SF(*) Terminal punctuation (? ! .) 물음표, 느낌표, 마침표 nori_tokenizer가 특수문자 제거
SH Chinese character 한자 中國(SH)
SL Foreign language 외국어 hello(SL)
SN Number 숫자 1(SN)
SP Space 공백  
SSC(*) Closing brackets 닫는 괄호 ),] nori_tokenizer가 특수문자 제거
SSO(*) Opening brackets 여는 괄호 (,[ nori_tokenizer가 특수문자 제거
SY Other symbol 심벌  
UNA Unknown 알 수 없음  
UNKNOWN Unknown 알 수 없음  
VA Adjective 형용사 하얀(VA)/눈
VCN Negative designator 부정 지정사(서술격조사) 사람/이/아니(VCN)/다
VCP Positive designator 긍정 지정사(서술격조사) 사람/이(VCN)/다
VSV Unknown 알 수 없음  
VV Verb 동사 움직이(VV)/다,먹(VV)/다
VX Auxiliary Verb or Adjective 보조 용언 가지/고/싶(VX)/다, 먹/어/보(VX)/다
XPN(*) Prefix 접두사(체언 접두사?) ES에서 매핑되는 단어를 찾지 못함
XR(*) Root 어근 ES에서 매핑되는 단어를 찾기 못함
XSA Adjective Suffix 형용사 파생 접미사 멋/스럽(XSA)/다
XSN(*) Noun Suffix 명사 파생 접미사 ES에서 매핑되는 단어를 찾기 못함
XSV(*) Verb Suffix 동사 파생 접미사 ES에서 매핑되는 단어를 찾기 못함

 

nori_readingform 토큰 필터

사용자 질의에 존재하는 한자를 한글로 변경하는 역할을 하는 필터이다. 위와 같이 동일한 close-open 과정을 통해 분석기를 변경해보자.

 

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
설정) PUT http://localhost:9200/korean_analyzer/_settings
 
{
        "analysis":{
            "tokenizer":{
                "korean_nori_tokenizer":{
                    "type":"nori_tokenizer",
                    "decompound_mode":"mixed",
                    "user_dictionary":"user_dictionary.txt"
                }
            },
            "analyzer":{
                "nori_analyzer":{
                    "type":"custom",
                    "tokenizer":"korean_nori_tokenizer",
                    "filter":[
                        "nori_posfilter",
                        "nori_readingform"
                    ]
                }
            },
            "filter":{
                "nori_posfilter":{
                    "type":"nori_part_of_speech",
                    "stoptags":[
                        "E",
                        "IC",
                        "J",
                        "MAG",
                        "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"
                    ]
                }
            }
        }
}
 
 
POST http://localhost:9200/korean_analyzer/_analyze
{
    "analyzer":"nori_analyzer",
    "text":"中國은 어디에 있죠?"
}
result=>
 
{
    "tokens": [
        {
            "token": "중국",
            "start_offset": 0,
            "end_offset": 2,
            "type": "word",
            "position": 0
        },
        {
            "token": "어디",
            "start_offset": 4,
            "end_offset": 6,
            "type": "word",
            "position": 2
        }
    ]
}
cs

 

한자가 한글로 바뀐 것을 볼 수 있다. 마지막으로는 동의어필터, 불용어 필터를 옵션으로 넣어 조금더 풍부한 분석기를 만들어 볼 것이다. 분석기 설정은 이전에 설정한 것을 수정하려고 했으나, 이미 생성된 인덱스에 추가적으로 필터를 등록하면 예외를 내뱉는다.. 이유는 모르지만 어쨌든 새로 인덱스를 생성하였다.(혹시나 이미 생성된 인덱스에 커스텀 필터를 추가하는 방법을 아시는 분은 꼭 댓글로 남겨주시길 바랍니다...)  ->이미 생성된 인덱스도 추가적으로 필터를 추가하는 것이 되는 것으로 확인(테스트 완료)

 

Nori Tokenizer + Nori POS Filter + Nori readingform Filter + Synonym Filter + Stopword Filter

사실 토큰 필터 말고도 전처리 필터를 적용할 수도 있지만 이번 예제는 생략한다. 인덱스를 close-open하는 과정은 동일함으로 생략한다

.

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
설정) PUT http://localhost:9200/custom_korean_analyzer
 
{
    "settings":{
        "number_of_shards": 5,
        "number_of_replicas": 1,
        "analysis":{
            "tokenizer":{
                "korean_nori_tokenizer":{
                    "type":"nori_tokenizer",
                    "decompound_mode":"mixed",
                    "user_dictionary":"user_dictionary.txt"
                }
            },
            "analyzer":{
                "nori_analyzer":{
                    "type":"custom",
                    "tokenizer":"korean_nori_tokenizer",
                    "filter":[
                        "nori_posfilter",
                        "nori_readingform",
                        "synonym_filtering",
                        "stop_filtering"
                    ]
                }
            },
            "filter":{
                "nori_posfilter":{
                    "type":"nori_part_of_speech",
                    "stoptags":[
                        "E","IC","J","MAG","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"
                    ]
                },
                "synonym_filtering":{
                    "type":"synonym"
                    ,"synonyms_path":"synonymsFilter.txt"
                },
                "stop_filtering":{
                    "type":"stop"
                    ,"stopwords_path":"stopFilter.txt"
                }
            }
        }
    }
}
 
 
cs

 

동의어 필터와 불용어 필터를 추가해주었다. 각 경로의 사전의 내용은 아래와 같다.

 

  • 동의어 사전 : 여성게->주인  *여성게를 주인으로 변환해준다. 만약 "여성게,주인" 형태로 사전을 작성한다면 여성게라는 토큰을 날리는 것이 아니고 여성게라는 토큰이 들어오면 역색인에 여성게와 주인 두개의 토큰을 색인한다.
  • 불용어 사전 : 바보  *바보라는 단어가 들어오면 불용어 필터에 걸려 토큰을 날린다.
  • 사용자 사전 : 여성게  *여성게라는 단어를 명사로 인식시켰다.

혹시나 인덱스를 생성하고 사전내용을 추가하였다면 꼭 사전파일을 인식시키기위해 close-open 과정을 추가적으로 수행한다!

 

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
설정) POST http://localhost:9200/custom_korean_analyzer/_analyze
 
{
    "analyzer":"nori_analyzer",
    "text":"中國은 어디에 있죠? 그리고 여성게은 누구죠? 바보"
}
 
result =>
{
    "tokens": [
        {
            "token": "중국",
            "start_offset": 0,
            "end_offset": 2,
            "type": "word",
            "position": 0
        },
        {
            "token": "어디",
            "start_offset": 4,
            "end_offset": 6,
            "type": "word",
            "position": 2
        },
        {
            "token": "그리고",
            "start_offset": 12,
            "end_offset": 15,
            "type": "word",
            "position": 6
        },
        {
            "token": "주인",
            "start_offset": 16,
            "end_offset": 19,
            "type": "SYNONYM",
            "position": 7
        },
        {
            "token": "누구",
            "start_offset": 21,
            "end_offset": 23,
            "type": "word",
            "position": 9
        }
    ]
}
 
 
cs

 

중국이라는 한자를 한글로 바꿔주었고, 품사 태그 리스트에 해당되면 모두 토큰을 삭제했고, 여성게를 주인으로 동의어 처리를 하였으며 마지막으로 바보라는 토큰을 불용어 처리하여 삭제하였다.

 

이렇게 엘라스틱서치에서 Nori 형태소분석기를 사용하는 방법을 간략히 다루어보았다. 사실 이보다 더 응용할 것이 넘치고 넘친다. 토큰 필터만 수십개이고 다양한 조합을 통해 더욱 용도에 맞는 형태소 분석기로 거듭날 수 있다.

posted by 여성게
:

 

엘라스틱서치는 대량의 데이터를 처리하기 위해 기본적으로 데이터를 분산해서 처리한다. 검색요청이 발생하면 엘라스틱서치는 모든 샤드에게 브로드캐스트 방식으로 동시에 요청을 보내고 각각 샤드들이 데이터를 검색한후 결과를 반환하면 엘라스틱서치는 모든 결과를 취합하여 사용자에게 검색 결과를 전달한다. 이러한 동작 방식 때문에 제공되는 부가적인 환경설정값이 있다.

 

동적 분배 방식의 샤드 선택

엘라스틱서치는 부하 분산과 장애처리를 위하여 원본 샤드 + 복제 리플리카 샤드를 운영한다. 물론 원본 샤드와 복제 리플리카 샤드는 각각 다른 노드에 위치하게 된다. 그렇다면 위에서 엘라스틱서치는 검색요청시 모든 샤드에 브로드캐스트 방식으로 검색요청을 보낸다 했는데, 원본 샤드와 복제 리플리카 샤드 두개 모두에게 검색 요청이 갈까? 만약 그렇다면 중복된 답변을 사용자에게 내보낼 것이다. 하지만 엘라스틱서치는 내부적으로 기본 라운드로빈 방식으로 원본,복제 샤드를 번갈아가며 요청을 보낸다. 하지만 이 라운드로빈 방식 이외에도 동적 분배 방식의 알고리즘도 제공한다. 동적 분배 방식은 검색 요청의 응답시간, 검색 요청을 수행하는 스레드 풀의 크기등을 고려해 최적의 샤드를 동적으로 결정하는 방식이다.

 

1
2
3
4
5
6
7
설정) PUT http://localhost:9200/_cluster/settings
 
{
    "transient":{
        "cluster.routing.use_adaptive_replica_selection":true
    }
}
cs

 

글로벌 타임아웃 설정

이전 포스팅에서 쿼리 요청시 응답까지의 타임아웃 설정을 할 수 있다는 것을 다루어봤다. 하지만 모든 쿼리마다 타임아웃 시간을 보내기는 번거로울 수가 있다. 그럴때 사용하는 것이 글로벌 타임아웃 설정이다.

 

 

Elasticsearch - 2.검색 API(Elasticsearch Query DSL)

엘라스틱서치는 인덱스에 저장된 문서를 검색할 수 있도록 다양한 검색기능을 제공한다. 문서는 색인시 설정한 Analyzer에 의해 분석과정을 거쳐 토큰으로 분리되는데, 이러한 Analyzer는 색인 시점 말고도 검색..

coding-start.tistory.com

 

1
2
3
4
5
6
7
설정) PUT http://localhost:9200/_cluster/settings
 
{
    "transient":{
        "search.default_search_timeout":"3s"    
    }
}
cs

 

Count Search API

결과값으로 전문이 아닌 문서 매칭 개수를 알고 싶다면 해당 쿼리를 사용하면 된다.

 

1
2
3
4
5
6
7
8
9
설정) POST http://localhost:9200/movie_search/_count
 
{
    "query":{
        "match":{
            "movieNm":"그대"
        }
    }
}
cs

 

Validate API

작성한 쿼리에 문법적인 오류가 있는지 확인할 수 있는 요청이다.

 

1
2
3
4
5
설정) POST http://localhost:9200/movie_search/_validate/query?rewrite=true
 
{
    "validation 할 쿼리작성"
}
cs

 

Explain API

쿼리 결과로 나온 점수에 대한 상세한 설명이 필요하다면 해당 쿼리를 이용한다.

 

1
2
3
4
5
6
7
8
9
설정) POST http://localhost:9200/movie_search/_doc/문서ID/_explain
 
{
    "query":{
        "term":{
            "typeNm":"장편"
        }
    }
}
cs

 

Profile API

요청한 질의를 실행하는 과정에서 각 샤드별로 얼마나 많은 시간이 소요됐는지의 등의 정보를 각 샤드별로 보고 싶다면 해당 쿼리를 이용한다.

 

1
2
3
4
5
6
7
8
9
10
설정) POST http://localhost:9200/movie_search/_search
 
{
    "profile":true,
    "query":{
        "term":{
            "typeNm":"장편"
        }
    }
}
cs

 

여기까지 일부 환경설정 및 부가적인 쿼리 API에 대해 다루었다. 

posted by 여성게
:

엘라스틱서치는 인덱스에 저장된 문서를 검색할 수 있도록 다양한 검색기능을 제공한다. 문서는 색인시 설정한 Analyzer에 의해 분석과정을 거쳐 토큰으로 분리되는데, 이러한 Analyzer는 색인 시점 말고도 검색 시점에도 이용된다. 특정 문장이 검색어로 요청되면 분석기를 통해 분석된 토큰의 일치 여부를 판단하여 그 결과에 Score을 매긴다. 이러한 엘라스틱서치에서는 다양한 검색 조건을 주기위하여 Query DSL이라는 특수한 쿼리 문법을 제공한다.

 

1. 검색 API

문장은 색인 시점에 텀으로 분리된다. 검색 시에는 이 텀을 일치시켜야 검색이 가능하다. 엘라스틱서치는 루씬기반이기 때문에 색인 시점에 Analyzer를 통해 분석된 텀을 Term, 출현빈도, 문서번화와 같이 역색인 구조로 만들어 내부적으로 저장한다. 검색 시점에는 Keyword 데이터 타입과 같은 분석이 되지 않는 데이터와 Text 데이터 타입과 같은 분석이 가능한 데이터를 구분해서 분석이 가능할 경우 분석기를 이용해 분석을 수행한다. 이를 통해 검색 시점에도 형태소분석이 되든 되지 않든 텀을 얻을 수 있으며, 해당 텀으로 색인 시점에 생긴 역색인 테이블을 뒤져 문서를 찾고 스코어를 계산하여 결과로 제공한다.

 

1.1. 검색 질의 방식

엘라스틱서치에서 제공하는 검색 API는 기본적으로 Query기반으로 동작한다. 검색 질의에는 검색하고자 하는 각종 Query 조건을 명시할 수 있으며, 동일한 조건을 두 가지 방식으로 표현 가능하다.

 

  1. URI 검색(루씬 스타일)
  2. Request Body 검색

첫번째로 우선 URI방식을 살펴본다. 

 

1.1.1. URI 검색 방식

URI를 이용하는 방식은 HTTP GET 요청을 활용한다. 즉, 쿼리파라미터로 Query를 작성하는 것이다. 간단한 쿼리는 쉽지만 쿼리파라미터의 표현한계로 인해 복잡한 질의는 불가능하다.

 

예시) GET http://localhost:9200/인덱스명/_search?q=필드명:검색값

 

1.1.2. Request Body 검색 방식

Request Body 방식은 HTTP 요청 시 Body에 검색할 칼럼과 검색어를 미리 정의된 JSON 구조로 표현하여 질의하는 방식이다. JSON 구조의 표현을 조금더 효율적으로 하기 위해 엘라스틱서치는 Query DSL이라는 미리 정의된 특별한 문법을 지원한다. URI 검색방식보다 더욱 풍부한 조건의 질의가 가능하다.

 

1
2
3
4
5
6
7
8
9
예시) POST http://localhost:9200/인덱스명/_search
     Header Content-Type:application/json
Request Body {
                "query":{
                   "term":{
                      "typeNm":"장편"
                   }
                }
             }
cs

 

그러면 각 검색 표현법의 장단점은 무엇일까?

 

1.2.1. URI 검색 방식

URI 검색은 Request Body 검색에 비해 단순하고 사용하기 편하지만 복잡한 질의문을 입력하기 힘들다는 치명적인 단점이 있다. 또한 URI 검색을 이용할 경우에는 엘라스틱서치에서 제공하는 모든 검색 API 옵션을 사용할 수 없다. 이유는 쿼리파라미터 표현법에는 표현상 한계가 있기 때문이다. 하지만 간단한 쿼리 혹은 테스트에서는 간편함을 제공하기도 함으로 URI 검색 방식의 주요 파라미터를 알아보자.

 

파라미터 기본값 설명
q - 검색을 수행할 쿼리 문자열 조건을 지정.
df - 쿼리에 검색을 수행할 필드가 지정되지 않았을 경우 기본값으로 검색할 필드를 지정한다.
analyzer 검색 대상 필드에 설정된 형태소 분석기(색인 시점 분석기) 쿼리 문자열을 형태소 분석할때 사용할 분석기 지정
analyze_wildcard false 접두어/와일드카드 검색 활성화 여부 지정
default_operator OR 두 개 이상의 검색 조건이 q에 포함된 경우 검색 조건 연산자를 설정한다.
_source true 검색 결과에 문서 본문 포함 여부를 지정
sort - 정렬 기준 필드 지정
from - 검색 시작 위치 지정
size - 반환 결과 개수 지정

 

q 옵션에는 기본적으로 '필드명:검색어' 형태로 입력할 수 있으며, 여러개의 필드를 검색 할때는 공백을 입력한 후에 추가적인 필드명과 검색어를 입력한다. URI 검색 방식의 q 옵션은 Request Body 검색에서 제공하는 Query String Search 옵션과 동일하게 동작한다.

 

예시) http://localhost:9200/인덱스명/_search?q=필드명1:필드값1 AND 필드명2:필드값2&

        analyze_wildcard=true&from=0&size=5&sort=_score:desc,필드명:asc&

       _source_includes=필드명,필드명,필드명,필드명

 

 

1.2.2. Request Body 검색 방식

위의 URI 검색방식 예시를 그대로 Request Body 검색 방식으로 바꾸어보겠다.

 

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
POST http://localhost:9200/인덱스명/_search
Header Content-Type : application/json
<Request Body>
{
    "query":{
        "query_string":{
            "default_field":"필드명"
            ,"query":"필드명1:필드값1 AND 필드명2:필드값2"
        }
    }
    ,"from":0
    ,"size":5
    ,"sort":[{
        "_score":{
            "order":"desc"
        }
        ,"필드명":{
            "order":"asc"
        }
    }]
    ,"_source":[
        "필드명"
        ,"필드명"
        ,"필드명"
        ,"필드명"
    ]
}
cs

 

간단한 검색 조건인 경우에는 URI 검색방식이 편해보일 수도 있지만, 구조화된 JSON 구조를 파악하면 더욱 깔끔한 질의작성이 가능하다.

 

 

2. Request Body 검색 방식 - Query DSL 

엘라스틱서치로 검색 질의를 요청할때는 Request Body 방식과 URI 방식 모두 _search API를 이용한다. 하지만 Query DSL 검색을 이용하면 여러개의 질의를 조합하거나 질의 결과에 대해 또 검색을 수행하는 등의 기존 URI 방식 보다 강력한 검색이 가능하다. 엘라스틱서치의 Query DSL은 구조화된 JSON 형태를 사용한다.

 

2.1. Query DSL 구조

 

<요청>

1
2
3
4
5
6
7
8
9
10
{
    "size" : "-->반환받는 결과 개수"
    ,"from" : "-->몇번째 문서부터 가져올지 지정. 기본값은 0이다."
    ,"timeout" : "-->검색 요청시 결과를 받는 데까지 걸리는 시간. timeout을 너무 작게하면
                  전체 샤드에서 timeout을 넘기지 않은 문서만 결과로 출력된다. 기본 값은 무한이다."
    ,"_source" : "-->검색시 필요한 필드만 출력하고 싶을 때 사용"
    ,"query" : "-->검색 조건문이 들어가는 공간"
    ,"aggs" : "-->통계 및 집계 데이터를 사용할때 사용하는 공간"
    ,"sort" : "-->문서 결과의 정렬 조건이 들어가는 공간"
}
cs

 

엘라스틱서치로 쿼리가 요청되면 해당 쿼리를 파싱해서 문법에 맞는 요청인지 검사한 후에 파싱에 성공하면 해당 쿼리를 기반으로 검색을 수행하고, 결과를 JSON으로 돌려준다.

 

<응답>

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
    "took" : "-->쿼리 실행 시간"
    ,"timed_out" : "-->쿼리 시간이 초과할 경우를 나타낸다"
    ,"_shards" : {
        "total" : "-->쿼리를 요청한 전체 샤드 개수"
        ,"successful" : "-->검색 요청에 성공적으로 응답한 샤드 개수"
        ,"failed" : "-->검색 요청에 실패한 샤드 개수"
    }
    ,"hits" : {
        "total" : "-->질의에 매칭된 문서의 전체 개수"
        ,"max_score" : "-->일치하는 문서의 스코어 값중 가장 높은 값"
        ,"hits" : [결과 문서 정보와 스코어 값등을 보여줌]
    }
}
cs

 

만약 JSON문법의 오류가 있다면 오류 결과를 JSON으로 넘겨준다.

 

 

2.2. Query DSL 쿼리와 필터

Query DSL을 이용해 검색 질의를 작성할 때 조금만 조건이 복잡해 지더라도 여러개의 질의를 조합해 사용해야 한다. 이때 작성되는 작은 질의들을 두 가지 형태로 나눌 수 있다. 실제 분석기에 의한 전문 분석이 필요한 경우와 단순히 "YES/NO"로 판단할 수 있는 조건 검색의 경우다. 엘라스틱서치에서는 전자를 쿼리 컨텍스트라고 하고, 후자를 필터 컨텍스트라는 용어로 구분한다.

 

  쿼리 컨텍스트 필터 컨텍스트
용도 전문 검색 시 사용 조건 검색 시 사용
특징

1.분석기에 의해 분석이 수행됨.                    2.연관성 관련 점수 계산                              3.루씬 레벨에서 분석 과정을 거처야 하므로 상대적으로 느림.

1.YES/NO로 단순 판별 가능                           2.연관성 관련 점수 계산 안함                         3.엘라스틱서치 레벨에서 처리가 가능하므로 상대적으로 빠름

사용 예 "삼성전자의 본사는 어디있죠?" 같은 문장 분석

"created_year" 필드값이 2018년도인지 여부 "status" 필드에 "user"라는 코드 포함여부

 

대부분의 경우 쿼리 방식과 필터 방식 중 어떤 방식으로 검색 질의를 표현하더라도 같은 결과를 얻을 수도 있다. 하지만 둘의 용도와 성능차이가 있기 때문에 반드시 용도에 맞는 사용을 하는 것이 좋다.

 

2.2.1. 쿼리 컨텍스트

 

  • 문서가 쿼리와 얼마나 유사한지를 점수로 계산
  • 질의가 요청될 때마다 엘라스틱서치에서 내부의 루씬을 이용해 계산을 수행(결과 캐싱 x)
  • 일반적으로 전문 검색에 이용
  • 캐싱되지 않고 디스크 연산을 수행하기에 상대적으로 느림

 

2.2.2. 필터 컨텍스트

 

  • 쿼리의 조건과 문서가 일치하는지를 구분
  • 별도로 점수 계산은 하지 않고 단순 매칭 여부만 검사
  • 자주 사용되는 필터의 결과는 내부적으로 캐싱
  • 기본적으로 메모리 연산이기에 속도 빠름

 

2.3.1. Query DSL 주요 파라미터

 

Multi Index 검색

기본적으로 모든 검색 요청은 Multi Index, Multi Type 검색이 가능하다. 즉, 여러 인덱스를 검색할때 한번의 요청으로 검색요청을 보낼 수 있다. 검색 요청시 ","로 다수의 인덱스명을 입력한다.

 

예시) POST http://localhost:9200/인덱스명1,인덱스명2/_search ...

 

두개의 인덱스명이 공통적인 필드만 갖고 있다면 두개가 서로다른 스키마 구조이더라도 한번에 검색할 수 있다. 검색 요청시 인덱스명에 "*" 입력이 가능하기 때문에 더욱 손쉬운 멀티 인덱스 검색이 가능하다. 예를 들어 매일매일 날짜별로 인덱스를 생성하여 데이터를 색인하는 로그 수집으로 엘라스틱 서치를 이용한다고 생각해보면, "log-2019-01-01" 형태라고 가정시 "log-2019-*"로 손쉽게 멀티 인덱스 검색이 가능하다. 솔라 검색엔진과 비교하면 아주 강력한 기능이다. 보통 솔라는 엘라스틱서치와 달리 인덱스로 도메인을 구분하지 않고 인스턴스로 도메인을 구분하기 때문에 멀티테넌시를 제공하기 쉽지 않다. 그 말은 즉, 도메인 인스턴스별로 같은 질문을 여러번 요청을 보내야 엘라스틱서치의 멀티 인덱스 검색과 같은 결과를 얻을 수 있는 것이다.(최소한 필자 사용시는 그랬음...솔라도 비슷한 기능이 있을 수도 있음)

 

쿼리 결과 페이징

페이징을 하기 위해서는 두가지 파라미터를 이용한다. from과 size이다. 각 기본값은 0,5로 설정되어 있다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
예시) POST http://localhost:9200/인덱스명/_search - 1Page
{
    "from":0,
    "size":5,
    "query":{
        "term":{
            "fieldName":"value"
        }
    }
}
 
예시) POST http://localhost:9200/인덱스명/_search - 2Page
{
    "from":5,
    "size":5,
    "query":{
        "term":{
            "fieldName":"value"
        }
    }
}
cs

 

엘라스틱서치는 관계형 데이터베이스와는 조금 다르게 페이징된 범위의 문서만 가져오는 것이 아니라, 모든 데이터를 읽고 그 중 필터링하여 몇개의 문서를 가져오는 것이기 때문에 페이지 번호가 높아질수록 쿼리 비용은 비례하여 높아진다.

 

 

쿼리 결과 정렬

엘라스틱서치는 기본적으로 점수를 내림차순으로 결과정렬은 한다. 하지만 점수 이외에 정렬 조건을 주고 싶다면 sort 파라미터를 이용한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
예시) POST http://localhost:9200/인덱스명/_search
{
    "query":{
        "term":{
            "fieldName":"value"
        }
    }
    ,"sort":{
        "_score":{
            "order":"desc"
        }
        ,"fieldName":{
            "order":"asc"
        }
    }
}
 
cs

 

위 예제는 점수가 동일할 시에 추가 정렬 조건으로 특정 필드 값으로 오름차순 정렬을 한다.

 

_source 필드 필터링

검색의 결과로 _source의 모든 항목을 결과로 반환한다.(_source의 모든 항목은 문서의 모든 필드를 포함한 결과) 하지만 모든 필드를 결과로 내보낼 필요가 없을 때도 있다. 그때 사용하는 것이 _source 파라미터이다. 필요한 필드만 결과값으로 출력한다면 네트워크 사용량을 줄여 응답 속도도 빨라질 수 있다.

 

1
2
3
4
5
6
7
8
9
10
예시) POST http://localhost:9200/인덱스명/_search
{
    "query":{
        "term":{
            "fieldName":"value"
        }
    }
    ,"_source":["fieldName1","fieldName2"]
}
 
cs

 

범위검색

숫자나 날짜 등을 특정한 숫자나 날짜가 아니라 범위를 지정하여 질의 요청을 할 수도 있다.

 

문법 연산자 설명
lt < 피연산자보다 작음
gt > 피연산자보다 큼
lte <= 피연산자보다 작거나 같음
gte >= 피연산자보다 크거나 같음

 

2016년부터 2019년까지의 데이터 조회 예시이다.

 

1
2
3
4
5
6
7
8
9
10
11
12
예시) POST http://localhost:9200/인덱스명/_search
{
    "query":{
        "range":{
            "date":{
                "gte":"2016",
                "lte":"2019"
            }
        }
    }
}
 
cs

 

 

operator 설정

엘라스틱서치는 검색 시 문장이 들어올 경우 기본적으로 OR연산으로 동작한다. 하지만 실무에서 더 정확도 높은 결과를 위하여 AND연산을 사용하여 검색할때도 있다. 그때 사용하는 것이 operator 파라미터이다.(기본값은 OR이다)

 

1
2
3
4
5
6
7
8
9
10
11
12
예시) POST http://localhost:9200/인덱스명/_search
{
    "query":{
        "match":{
            "fieldName":{
                "query":"coding start",
                "operator":"and"
            }
        }
    }
}
 
cs

 

coding start가 분석기에 의해 분리되고, 두개의 텀을 and 연산으로 검색을 하게 된다. 즉, 둘다 포함된 문서가 결과로 나올 것이다. 만약 OR 연산이라면 coding과 start라는 단어 둘 중 최소한 하나만 포함이 되어도 결과로 반환될 것이다.

 

minimum_should_match 설정

바로 위에서 보았던 operator 설정중 OR연산을 사용하면 너무 많은 결과가 나오게 된다. 이 경우 텀의 개수가 몇 개 이상 매칭될 때만 검색 결과로 나오게 할 수 있는데 이것이 바로 minimum_should_match 파라미터를 이용하는 것이다. 즉, OR연산자로 AND연산과 비슷한 효과를 낼 수 있게 된다.

 

1
2
3
4
5
6
7
8
9
10
11
12
예시) POST http://localhost:9200/인덱스명/_search
{
    "query":{
        "match":{
            "fieldName":{
                "query":"this is coding start",
                "minimum_should_match" : 3
            }
        }
    }
}
 
cs

 

위의 쿼리의 뜻은 OR연산을 사용하되 4개의 텀중 최소한 3개가 일치해야 결과로 내보낸다라는 쿼리이다.

 

 

fuzziness 설정

fuzziness 파라미터를 이용하면 완벽히 동일한 텀으로 문서를 찾는 Match Query를 유사한 텀의 형태로 검색을 하는 Fuzzy Query로 변경할 수 있다. 이는 레벤슈타인 편집 거리 알고리즘을 기반으로 문서의 필드 값을 여러번 변경하는 방식으로 동작한다. 유사한 검색 결과를 찾기 위해 허용 범위의 텀으로 변경해 가며 문서를 찾아 결과로 출력한다. 예를 들어, 편집 거리수를 2로 설정하면 오차범위가 두 글자 이하인 검색 결과까지 포함해서 결과를 준다. 하지만 한국어에는 적용하기 쉽지 않다.(만약 키워드성 질의가 많은 검색이라면 어느정도는 한국어도 가능하기는 한것같다.) 오차범위 값으로 0,1,2,AUTO 총 4가지 값을 사용가능하다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
예시) POST http://localhost:9200/인덱스명/_search
{
    "query":{
        "match":{
            "movieNm":{
                "query":"곤지엠",
                "fuzziness":1
            }
        }
    }
}
 
 
cs

 

 

위와 같이 질의를 날리면 "곤지암"이라는 영화 제목을 결과값으로 반환받을 수 있다.

 

 

boost 설정

boost 설정으로 관련성이 높은 필드나 키워드에 가중치를 더 줄 수 있게 해준다.

 

1
2
3
4
5
6
7
8
9
10
11
12
예시) POST http://localhost:9200/인덱스명/_search
{
    "query":{
        "multi_match":{
            "query":"Fly",
            "fields":["field1^3","field2"]
        }
    }
}
 
 
 
cs

 

위의 쿼리는 Fly라는 단어가 field1에 있을 경우 해당 문서의 가중치에 곱하기 3을 하여 더 높은 점수를 얻게 된다.

 

 

2.3.2. Query DSL 주요 쿼리

 

Match All Query

match_all 파라미터를 사용하여 색인의 모든 문서를 검색하는 쿼리이다.

 

1
2
3
4
5
6
7
8
9
10
예시) POST http://localhost:9200/인덱스명/_search
{
    "query":{
        "match_all":{}
    }
}
 
 
 
 
cs

 

Match Query

Match Query는 텍스트, 숫자, 날짜 등이 포함된 문장을 형태소 분석을 통해 텀으로 분리한 후 이 텀들을 이용해 검색 질의를 수행한다. 즉, 검색어가 분리돼야 할 경우에 사용해야한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
예시) POST http://localhost:9200/인덱스명/_search
{
    "query":{
        "match":{
            "fieldName":"coding start"
        }
    }
}
 
 
 
 
 
cs

 

위의 쿼리는 형태소분석에 의해 coding, start라는 두개의 텀으로 분리되고 별도의 operator가 없기에 OR연산으로 fieldName이라는 필드에 coding,start 텀을 OR연산으로 검색한다.

 

 

Multi Match Query

multi_match 파라미터를 이용하여 하나의 필드가 아니라 여러개의 필드에 대해서 검색을 수행할 수 있다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
예시) POST http://localhost:9200/인덱스명/_search
{
    "query":{
        "multi_match":{
            "query":"가족",
            "fields":["fieldName1","fieldName2"]
        }
    }
}
 
 
 
 
 
 
cs

 

위에서도 다루어봤지만 multi_match를 사용하면서 특정 필드에 가중치를 부여할 수도 있다.

 

 

Term Query

텍스트 형태의 값을 검색하기 위해 엘라스틱서치는 두 가지 매핑 유형을 지원한다.

 

타입 설명
Text 타입 필드에 데이터가 저장되기 전에 데이터가 Analyzer에 의해 분석되어 역색인 구조로 저장됨.
Keyword 타입 데이터가 분석되지 않고 문장이라도 하나의 텀(분석되지 않은)으로 저장됨

 

바로 이전에 다룬 Match Query는 검색어 매칭 전에 검색 텍스트에 대해 형태소 분석이 들어간다. 하지만 Term Query는 별도의 형태소 분석없이 검색 문자 그대로 해당 문자와 동일한 문서가 있는지 찾는다. 즉, Keyword 데이터 타입을 사용하는 필드를 검색하기 위해서는 Term Query를 사용한다. 그리고 정확히 일치하지 않으면 검색 결과로 나오지 않는다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
예시) POST http://localhost:9200/인덱스명/_search
{
    "query":{
        "term":{
            "fieldName":"코미디"
        }
    }
}
 
 
 
 
 
 
 
cs

 

fieldsName이라는 필드에 코미디라는 정확한 단어가 포함된 문서가 있다면 결과로 돌려준다.

 

 

Bool Query

RDB에서는 다양하게 AND,OR,NOT 등의 조건을 여러개 WHERE절에 사용할 수 있다. 이처럼 엘라스틱서치도 여러개의 조건 절을 연결하여 더욱 정확한 검색결과를 얻을 수 있는 쿼리가 있는데, 바로 boot query이다. 

 

엘라스틱서치 SQL 설명
must : [] AND 칼럼=조건 반드시 조건에 만족하는 문서만 검색
must_not : [] AND 칼럼!=조건 조건을 만족하지 않는 문서가 검색
should : [] OR 칼럼=조건 여러 조건 중 하나 이상을 만족하는 문서 검색
filter : [] 칼럼 IN (조건) 조건을 포함하고 있는 문서를 출력. 해당 파라미터를 사용하면 점수별로 정렬하지 않는다.

 

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
예시) POST http://localhost:9200/인덱스명/_search
{
    "query":{
        "bool":{
            "must":[
                {
                    "term":{
                        "repGenreNm":"코미디"    
                    }
                },
                {
                    "match":{
                        "repNationNm":"한국"
                    }
                }
            ],
            "must_not":[
                {
                    "match":{
                        "typeNm":"단편"
                    }
                }
            ]
        }
    }
}
 
 
 
 
 
 
 
 
cs

 

해당 쿼리는 장르가 코미디이면서 제작사 나라가 한국이라는 단어를 포함하고 있으며, 종류가 단편이 아닌 영화를 조회하는 쿼리이다.

 

 

Prefix Query

Prefix Query는 해당 접두어가 있는 모든 문서를 검색하는데 사용한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
예시) POST http://localhost:9200/인덱스명/_search
{
    "query":{
        "prefix":{
            "movieNm":"자전"
        }
    }
}
 
 
 
 
 
 
 
 
 
cs

 

영화제목에 자전이라고 시작하는 영화 제목을 검색 결과로 모두 가져온다.

 

 

Exists Query

필드 값이 null 인  데이터를 제외하고 검색한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
예시) POST http://localhost:9200/인덱스명/_search
{
    "query":{
        "exists":{
            "field":"movieNm"
        }
    }
}
 
 
 
 
 
 
 
 
 
cs

 

Wildcard Query

검색어가 와일드카드와 일치하는 구문을 찾는다. 이때 입력된 검색어는 형태소 분석이 이뤄지지 않는다.

 

와일드카드 옵션 설명
* 문자의 길이와 상관없이 와일드카드와 일치하는 모든 문서를 찾는다
? 지정된 위치의 한 글자가 다른 경우의 문서를 찾는다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
예시) POST http://localhost:9200/인덱스명/_search
{
    "query":{
        "wildcard":{
            "fieldName":"여?"
        }
    }
}
 
 
 
 
 
 
 
 
 
cs

 

여기까지 간단한 엘라스틱서치 검색 API에 대해 다루어보았다. 다음 포스팅에서는 더 다루지 못한 검색 API 나머지 부분을 다룰 예정이다. 혹시 잘못된 점이나 모르는 점 등이 있다면 댓글을 꼭 부탁드린다..

posted by 여성게
: