자바 언어를 위해 제공되는 클라이언트에는 두 가지 종류가 있다. 내부적으로 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 여성게
:
ChatBot 2018. 2. 5. 14:37

Spring프레임워크를 이용한 챗봇


이제 카카오톡 이전 자동응답 시스템은 deprecated 될 예정입니다. 현재 신규 사용도 불가하구요. 추후에 현재나온 카카오톡 챗봇 빌더를 이용하여 챗봇 개발하는 글을 올릴 예정이니 그때 다시 확인해주시면 감사하겠습니다. - 여성게 20190729


1.시작전



코딩 초보인 저자가 무엇을 만들까 생각 해보다가 생각한 것이 챗봇이었습니다. 초보인 제가 하루종일 헤매다가 만들 챗봇을 저처럼 코딩에 익숙치 않으신 분들도 쉽게 만들어 볼 수 있도록 글을 작성해보았습니다.(틀린부분 지적질좀 부탁드려요...)






2.개발환경



1.구름IDE(구름을 사용하는 이유는 서버를 단 몇번의 클릭으로 생성 할 수 있다. 편한만큼 불편한 점도 많다. 단순히 테스트용으로 사용하길..)

2.eclipse

3.Spring Framework 4.3.3








3.Jackson2 라이브러리 추가






4./keyboard 구현하기


/keyboard 에 매칭되는 기능입니다. 이용자가 최초로 채팅방에 들어 올때 기본으로 키보드 영역에 표시될 메뉴들을 설정하기 위함입니다.

카카오톡 관리자 홈페이지에서 API형 URL을 등록 할때 초기 테스트로 꼭 필요한 기능이니 꼭 구현해주셔야합니다.

키보드 응답구조입니다.

위 구조와 같이 플러스친구와 1:1대화 누르면 나오는 첫화면인 /keyboard 와 매칭되는 JSON 객체의 구조입니다.

type은 buttons형과 text형이 있지만 저자는 buttons형이 편했음으로..




KeyboardDTO 구현


json-simple 라이브러리를 받아 아예 컨트롤러 안에서 keyboard 객체를 json object 형태로 리턴해주어도 되지만 객체지향인만큼 keyboard객체를 선언해서 keyboard 객체를 리턴해주면 조금더 깔끔할 것 같기에... 여기 하나더 보아야 할것.. DTO의 변수와 JSON 구조에서의 변수명이 같은 것을 주목해주세요! (같아야 나중에 어노테이션으로 인한 자동매핑이 가능합니다.)



controller 구현


컨트롤러 구현 이전에 스프링 설정파일에 설정 해 놓아야할 것이 있다.


스프링설정파일 설정


위에 보듯이 컨트롤러에서 선언한 어노테이션을 인식하기 위한 설정들이다. 빈생성 및 json을 위한 어노테이션 인식을 위함이다.(자세한 것은 구글링...)


Controller-/keyboard 


위에서 주목해야 할 어노테이션은 @RestController 이다. @RestController=@Controller + @ResponseBody 라는 뜻이다. 즉, 모든 메소드에서 @ResponseBody를 추가 해줄 필요가 없다는 것이다. (여기서 @Controller가 붙은 클래스는 컨테이너에 의해서 컨트롤러로 인식됨과 동시에 객체를 자동 생성해주는 어노테이션이고, @ResponseBody는 리턴 값을 자동으로 JSON 객체로 변환해서 리턴해주는 기능의 어노테이션이다.!)




이제 /keyboard url이 요청되었을 때, 카카오톡이 아닌 웹에서의 결과를 보면, json 형태로 결과값들이 리턴되는 것을 볼수 있다.("여보 이거 눌러봐 !"는 여자친구를 위해...저것을 눌렀을 때, 사랑해라는 대답을 응답 하기위해...해놓은 소소한 이벤트였다.. 애교로 봐주시길..)




여러분들이 직접만든 플러스친구에게 1:1대화를 신청하면 첫 화면에서 이렇게 keyboard가 생성 된 것을 볼 수 있다.

하지만 이 버튼들중 무엇을 눌러도 에러가 발생 할 것이다. 왜냐? 버튼을 눌러 플러스친구에게 메시지를 보내면 그 메시지에 대한 응답을 할 메소드를 만들지 않았기 때문...




사용자 요청 구조 


이것은 이용자가 보낸 메시지가 서버에 저런 json 구조로 값이 들어 온다는 겁니다.

사용자 요청에 따른 챗봇의 응답구조



이것은 서버측에서 이용자가 보낸 메시지를 토대로 응답을 보낼때의 json의 구조입니다. 보시면 json객체 안에서 message & keyboard 객체가 또 message 객체 안에 여러 객체가 들어가 있는 것이 보입니다. 반드시 이 객체들을 모두 구현 할 필요는 없습니다. 예를 들어 텍스트로만 응답을 보내고 싶으면 message 객체안의 text라는 것만 객체에 담아서 보내도 됩니다. 그 예는 조금 있다 보실수 있습니다.




MessageButtonDTO,MessageDTO,PhotoDTO,RequestMessageDTO,ResponseMessageDTO구현





여기서 RequestMessageDTO 객체는 사용자로부터 들어오는 요청에 대한 DTO이고(위 구조 참고), ResponseMessageDTO는 사용자의 요청에 대한 응답을 담아서 보낼 DTO 객체입니다. 그 외의 DTO들은 ResponseMessageDTO에 담길 객체들입니다.(이것 또한 위의 구조참고) 이것들을 활용한 controller의 구현입니다.




Controller-/message



여기서 주목해야 할것은 /message 요청이 들어왔을 때, 수행되는 메소드의 매개변수 입니다. @RequestBody 어노테이션을 이용해 json객체 형태의 사용자의 요청을 RequestMessageDTO 객체에 매핑을 시켜주는 겁니다. 그래서 위에서 설명했듯이 JSON 구조와 DTO 객체의 인스턴스 변수명을 똑같이 맞춰주는 겁니다. 그래야 자동으로 JSON객체를 RequstMesaageDTO 객체의 형태로 자동으로 바꿔줘서 매개변수에 들어오는 겁니다. 그리고 if 문 안의 dto.getContent() 메소드는 요청에 담긴 사용자의 String 형태의 채팅내용을 가져오는 겁니다.(위 요청 구조에서 content라는 것이 있었던 거 기억하죠?)

이런식으로 사용자가 한 채팅내용에 따라 적절히 분기 시켜 각각의 응답을 결정합니다....(원래 진정한 챗봇은 이런 구조를 갖지 않겠죠..? 자연어 처리가 들어가야하는데 필자는 그정도의 능력이 안되 간단히 단어만 추출해 답변을 정하는 정도..)

마지막으로 어떠한 조건문에 걸려 응답에 대한 객체들을 적당히 만들어주고 마지막에 ResponseMessageDTO 객체에 담아서 리턴해주면 JSON 형태로 응답이 가게 됩니다.(위에 보면 응답구조에 따른 모든 json객체를 다 포함해서 리턴하지 않습니다. 자신이 텍스트만 보내고 싶으면 텍스트만 리턴해주면 되고 사진도 보내고 싶으면 사진 객체도 포함시키고 개발자의 마음입니다.)





마치며.. 




필자는 코딩을 잘하지 않습니다.. 다만 제가 개발하면서 정말 헤매었던 부분, 어려웠던 부분을 위주로 저와같은 초보도 알아 듣기 쉽게 하기 위해 노력했습니다... 잘못된 부분은 지적해주시면 감사합니다. 부족한 필자의 글을 보고 아주 조금이나마 도움이 되셨다면 좋겠습니다...


혹시나 코드가 필요하신분들이 있다면 말씀해주세요~!





'ChatBot' 카테고리의 다른 글

Chatbot - 1.개요_ 우리말 한글 챗봇 구성요소  (1) 2019.07.06
posted by 여성게
: