본문 바로가기

카테고리 없음

[ElasticSearch - springboot] (Script) 여행지 제목과 매핑되는 정보 제공

목차

    위키 제목 == 여행지 제목과 매핑되는 정보 제공 이슈

    # 기존 코드
    GET scrap_wiki_1102/_search
    {
      "_source": ["attraction_name", "content_id", "wiki_title", "wiki_content"],
      "query": {
        "bool": {
          "must": [
            {
              "script": {
                "script": {
                  "source": "if (doc['attraction_name.keyword'].size() != 0 && doc['wiki_title.keyword'].size() != 0) { return doc['attraction_name.keyword'].value == doc['wiki_title.keyword'].value; } return false;",
                  "lang": "painless"
                }
              }
            }
          ]
        }
      }
    }

    해당 코드의 경우 공백의 문제로 매핑이 되지 않는다. (스크립한 제목과, 기존 DB의 제목이 공백 차이로 검색이 되지 않는 것을 확인하였다. 

     

    이 문제를 해결하기 위해 공백을 제거하여 리턴의 방식을 고민해야 할 것 같다.

     

    1. trim analyzer (맞지 않음)

     

    https://stackoverflow.com/questions/67331196/how-to-trim-all-whitespace-in-an-elasticsearch-normalizer

     

    How To Trim All Whitespace In an Elasticsearch Normalizer

    I found out that the normalizer with the trim filter is not trimming all whitespace characters, for example \u2007 is not trimmed. Is there a way how to trim all whitespace characters in a normaliz...

    stackoverflow.com

     

    주로 trim 필터를 사용하는 듯 한데, 기존 인덱스에 걸지는 않고 쿼리 검색 시에만 분석기를 적용해보고자 한다.

    근데 분석기를 추가할 경우 기본적으로 분석기가 모든 인덱스에 적용되기 때문에 모든 검색어에 문제가 생기게 된다.

     

    기본적으로 원했던 색인할 때는 기존대로 하고, 검색할 때만 별도의 분석기를 사용하는 방법은 Elasticsearch에서 기본적으로 지원하지 않는다고 한다.

    따라서 해당 방법은 사용할 수 없다.

     

    2. 검색 쿼리에서 스크립트 사용

     

    https://www.elastic.co/guide/en/elasticsearch/reference/8.2/modules-scripting.html

     

    Scripting | Elasticsearch Guide [8.2] | Elastic

    With scripting, you can evaluate custom expressions in Elasticsearch. For example, you can use a script to return a computed value as a field or evaluate a custom score for a query. The default scripting language is Painless. Additional lang plugins are av

    www.elastic.co

     

    https://www.elastic.co/guide/en/elasticsearch/painless/current/painless-guide.html

     

    Painless Guide | Painless Scripting Language [8.10] | Elastic

    Painless is a simple, secure scripting language designed specifically for use with Elasticsearch. It is the default scripting language for Elasticsearch and can safely be used for inline and stored scripts. For a jump start into Painless, see A Brief Painl

    www.elastic.co

     

    https://www.elastic.co/guide/en/elasticsearch/painless/7.11/index.html

     

    Painless Scripting Language [7.11] | Elastic

     

    www.elastic.co

     

    스크립트와 painless 문법을 사용하면 기존 Java에서 사용하던 것처럼 구문을 작성할 수 있다.

    다만 모든 문법을 제공하지는 않는 것 같았다. replaceAll의 경우 ClassCastException 에러가 발생했다. Painless에서는 기본적으로 일부 자바 기능이 제한되어 있어서 모든 자바 메서드가 그대로 작동하는 것은 아니라고 한다.

     

    # 공백 제거
    GET scrap_wiki_1102/_search
    {
      "_source": ["attraction_name", "content_id", "wiki_title"],
      "query": {
        "bool": {
          "must": [
            {
              "script": {
                "script": {
                  "source": """
                      if (doc['attraction_name.keyword'].size() != 0 && doc['wiki_title.keyword'].size() != 0) { 
                            String attName = doc['attraction_name.keyword'].value.replace(' ', '');
                            String wikiTitle = doc['wiki_title.keyword'].value
                              .replace(' ', '');
                            return attName.equals(wikiTitle);
                      } return false;""",
                  "lang": "painless"
                }
              }
            }
          ]
        }
      }
    }
    
    #실제 쿼리에서는 검색어를 여기 받을 것이다.
    # 공백 제거
    GET scrap_wiki_1102/_search
    {
      "_source": ["attraction_name", "content_id", "wiki_title"],
      "query": {
        "bool": {
          "must": [
            {
              "script": {
                "script": {
                  "source": """
                      if (doc['attraction_name.keyword'].size() != 0 && doc['wiki_title.keyword'].size() != 0) { 
                            String attName = doc['attraction_name.keyword'].value.replace(' ', '');
                            String wikiTitle = doc['wiki_title.keyword'].value
                              .replace(' ', '');
                            if(attName.equals(wikiTitle)) {
                              return attName.equals("입력 검색어");
                            }
                      } return false;""",
                  "lang": "painless"
                }
              }
            }
          ]
        }
      }
    }

     

     

    Springboot

    이를 Springboot로 이식하면 다음과 같다.

     

    https://senoritadeveloper.medium.com/spring-boot-and-elasticsearch-update-documents-with-scripting-2f237b6816e5

     

    Spring Boot and Elasticsearch — Update Documents with Scripting

    Write Painless Scripts to Update Documents in ElasticSearch

    senoritadeveloper.medium.com

     

    package com.sch.sch_elasticsearch.domain.wiki.service;
    
    import com.sch.sch_elasticsearch.domain.wiki.entity.Wiki;
    import lombok.RequiredArgsConstructor;
    import lombok.extern.slf4j.Slf4j;
    import org.elasticsearch.index.query.BoolQueryBuilder;
    import org.elasticsearch.index.query.QueryBuilder;
    import org.elasticsearch.index.query.QueryBuilders;
    
    import org.elasticsearch.script.Script;
    import org.elasticsearch.script.ScriptType;
    import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate;
    import org.springframework.data.elasticsearch.core.SearchHits;
    import org.springframework.data.elasticsearch.core.query.FetchSourceFilter;
    import org.springframework.data.elasticsearch.core.query.NativeSearchQuery;
    import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
    import org.springframework.stereotype.Service;
    
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    
    /**
     * Script 등 2차 가공 이상 복잡한 쿼리 로직의 집합입니다.
     */
    
    @Service
    @RequiredArgsConstructor
    @Slf4j
    public class WikiServiceExtend {
        private final ElasticsearchRestTemplate elasticsearchRestTemplate;
        private final ToolsForWikiService toolsForWikiService;
    
        /**
         * Attraction_name과 Wiki_Title이 입력 검색어와 같은 쿼리 반환s
         * @return List<Wiki>
         */
        public List<Wiki> getSameAttNameAndWikiTitle(String inputString) {
    
            // Script 문자열을 정의한다.
            String scriptStr = "if (doc['attraction_name.keyword'].size() != 0 && doc['wiki_title.keyword'].size() != 0) { " +
                    "String attName = doc['attraction_name.keyword'].value.replace(' ', ''); " +
                    "String wikiTitle = doc['wiki_title.keyword'].value.replace(' ', ''); " +
                    "return attName.equals(wikiTitle) && attName.equals(params.inputString); " +
                    "} return false;";
    
            // 파라미터 맵을 생성하고 inputString을 추가한다. painless에서는 이렇게 param을 통해 값을 넣어야한다. (pstmt를 지원하지 않는다..)
            Map<String, Object> params = new HashMap<>();
            params.put("inputString", inputString);
    
            // Script 객체를 생성한다. (params를 넘겨줘야 해서 인자를 변경해줄 것)
            Script script = new Script(ScriptType.INLINE, "painless", scriptStr, params);
    
            // 스크립트 쿼리 빌더를 생성한다.
            QueryBuilder scriptQueryBuilder = QueryBuilders.scriptQuery(script);
    
            // Elasticsearch의 bool 쿼리를 생성한다.
            BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery().must(scriptQueryBuilder);
    
            // NativeSearchQuery를 생성한다.
            NativeSearchQuery searchQuery = new NativeSearchQueryBuilder()
                    .withQuery(boolQueryBuilder)
                    .withSourceFilter(new FetchSourceFilter(new String[]{"pk_id","attraction_name", "content_id", "wiki_title", "wiki_content"}, null)) // 필요한 필드만 가져오기 위한 설정
                    .build();
    
            // Elasticsearch에서 쿼리 실행 후 결과값 가져오기
            SearchHits<Wiki> searchHits = elasticsearchRestTemplate.search(searchQuery, Wiki.class);
            return toolsForWikiService.getListBySearchHits(searchHits);
        }
    }