Skip to content

Elasticsearch ( spring data )

MoonDooo edited this page Jul 6, 2024 · 14 revisions

매핑 및 세팅

자세히 보기
{
  "analysis": {
    "analyzer": {
      "korean": {
        "type": "nori"
      }
    }
  }
}

{
  "properties": {
    "id": {
      "type": "long"
    },
    "title": {
      "type": "text",
      "analyzer": "korean"
    },
    "info": {
      "type": "text",
      "analyzer": "korean"
    },
    "blindInfo": {
      "type": "text",
      "analyzer": "korean"
    },
    "thumbnailUrl": {
      "type": "keyword"
    },
    "keywordList": {
      "type": "text",
      "analyzer": "korean"
    },
    "address": {
      "type": "text",
      "analyzer": "korean"
    },
    "contentType": {
      "type": "keyword"
    },
    "category": {
      "properties": {
        "largeCategory": {
          "type": "keyword"
        },
        "middleCategory": {
          "type": "keyword"
        },
        "smallCategory": {
          "type": "keyword"
        }
      }
    }
  }
}

매핑 객체

자세히 보기
@Document(indexName = "e_destination")
@Getter
@Mapping(mappingPath = "elastic/destination-mapping.json")
@Setting(settingPath = "elastic/destination-setting.json")
@AllArgsConstructor
@Builder
public class EDestination {
    @Id
    private Long id;
    private String title;
    private List<String> keywordList;
    private String thumbnailUrl;
    private String info;
    private String blindInfo;
    private String address;
    private Category category;
    private ContentType contentType;

    @Field(type = FieldType.Dense_Vector, dims = 100)
    private double[] embedding;
}

repository

전체 코드
@RequiredArgsConstructor
@Repository
@Slf4j
public class EDestinationCustomRepository {

    private final ElasticsearchOperations elasticsearchOperations;

    public List<EDestination> findAllByUserKeywordAndGPTKeywordList(String userKeyword, List<String> gptKeywordList, Pageable pageable){
        Query userQuery = getUserKeywordQuery(userKeyword);
        List<Query> keywordMatchQueryList = getGptKeywordQuery(gptKeywordList);
        Query query = QueryBuilders.bool().should(userQuery).should(keywordMatchQueryList).build()._toQuery();
        SearchHits<EDestination> response = getSearchHits(pageable, query);
        return response.getSearchHits().stream().map(
                SearchHit::getContent
        ).collect(Collectors.toList());
    }

    private static Query getUserKeywordQuery(String userKeyword) {
        return NativeQuery.builder()
                          .withQuery(
                                                   q-> q.multiMatch(MultiMatchQuery.of(builder->
                                                           builder
                                                                   .query(userKeyword)
                                                                   .fields("title^2", "info", "blind_info", "address")
                                                                   .boost(1.5f)
                                                                   .operator(Operator.Or)
                                                                   .type(TextQueryType.MostFields)
                                                   ))
                                           ).getQuery();
    }
    private static List<Query> getGptKeywordQuery(List<String> gptKeywordList) {
        return gptKeywordList.stream().map(
                keyword->
                        NativeQuery.builder().withQuery(
                                p->p.match(builder ->
                                        builder
                                                .field("keywordList")
                                                .query(keyword)
                                                .operator(Operator.Or)
                                )
                        ).getQuery()
        ).toList();
    }
    private SearchHits<EDestination> getSearchHits(Pageable pageable, Query query) {
        NativeQuery nativeQuery = NativeQuery.builder()
                        .withQuery(query)
                        .withPageable(pageable)
                .build();

        SearchHits<EDestination> response =  elasticsearchOperations.search(nativeQuery, EDestination.class);
        return response;
    }
}
 public List<EDestination> findAllByUserKeywordAndGPTKeywordList(String userKeyword, List<String> gptKeywordList, Pageable pageable){
        Query userQuery = getUserKeywordQuery(userKeyword);
        List<Query> keywordMatchQueryList = getGptKeywordQuery(gptKeywordList);
        Query query = QueryBuilders.bool().should(userQuery).should(keywordMatchQueryList).build()._toQuery();
        SearchHits<EDestination> response = getSearchHits(pageable, query);
        return response.getSearchHits().stream().map(
                SearchHit::getContent
        ).collect(Collectors.toList());
    }

먼저 getUserKeywordQuery는 multi match를 이용해서 사용자가 입력한 키워드로 검색한다. 또한 GPT의 답변으로 받은 키워드들이 존재한다면 getGptKeywordQuery에서 쿼리로 묶는다. 이 두 쿼리를 boolean 쿼리의 should로 묶는다. 마지막으로 페이징과 함께 검색한다. ( 요구사항 변경으로 검색에 특수한 필터, 필수 검색어가 추가될 경우 mustfilter추가 예정 )

private static Query getUserKeywordQuery(String userKeyword) {
        return NativeQuery.builder()
                          .withQuery(
                                  q-> q.multiMatch(MultiMatchQuery.of(builder-> 
                                                  builder
                                                          .query(userKeyword)
                                                          .fields("title^2", "info", "blind_info", "address", "keywordList")
                                                          .boost(1.5f)
                                                          .operator(Operator.Or)
                                                          .type(TextQueryType.MostFields)
                                                   ))
                                           ).getQuery();
    }

getUserKeywordQuery는 상술하듯 user가 작성한 키워드를 통해서 검색하는 부분이다. 먼저 multi match 를 사용하기 위해서 NativeQuery를 사용하였다. NativeQuery 사용자가 결과 값을 예상하고 검색했을 때를 위하여 title을 더 중요하게 다루며 해당 multi match 는 다른 추가적인 쿼리보다 1.5배 더 중요하게 설정하였다. multi match의 기본 타입인 BestFields는 가장 잘 매칭되는 필드를 기준으로 매칭되는데 GPT 답변으로 오는 키워드가 제대로 반영되지 않을 가능성이 있다. 따라서 MostFields를 이용해 모든 필드에 대한 score를 계산하도록 했다.

private static List<Query> getGptKeywordQuery(List<String> gptKeywordList) {
        return gptKeywordList.stream().map(
                keyword->
                        NativeQuery.builder().withQuery(
                                p->p.match(builder ->
                                        builder
                                                .field("keywordList")
                                                .query(keyword)
                                                .operator(Operator.Or)
                                )
                        ).getQuery()
        ).toList();
    }

GPT의 답변을 이용하는 해당 부분은 NativeQuery를 사용하여 match 쿼리 List를 만든다. 필드는 text rank 로 추출한 keywordList에 검색한다. GPT의 답변이 많을 수 있으므로 (최대 10개) 많은 수의 문서를 검색하는 것은 성능 저하를 일으킬 수 있어 keywordList에 한정했다.

GPT 답변이 적용되지 않은 쿼리
"Query":{
   "bool":{
      "should":[
         {
            "multi_match":{
               "boost":1.5,
               "fields":[
                  "title^2",
                  "info",
                  "blind_info",
                  "address"
               ],
               "operator":"or",
               "query":"검색어",
               "type":"most_fields"
            }
         }
      ]
   }
}
GPT 답변이 적용된 쿼리
"Query":{
   "bool":{
      "should":[
         {
            "multi_match":{
               "boost":1.5,
               "fields":[
                  "title^2",
                  "info",
                  "blind_info",
                  "address"
               ],
               "operator":"or",
               "query":"검색어",
               "type":"most_fields"
            }
         },
         {
            "match":{
               "keywordList":{
                  "operator":"or",
                  "query":"여행"
               }
            }
         },
         {
            "match":{
               "keywordList":{
                  "operator":"or",
                  "query":"관광지"
               }
            }
         },
         {
            "match":{
               "keywordList":{
                  "operator":"or",
                  "query":"호텔"
               }
            }
         },
         {
            "match":{
               "keywordList":{
                  "operator":"or",
                  "query":"음식"
               }
            }
         },
         {
            "match":{
               "keywordList":{
                  "operator":"or",
                  "query":"문화"
               }
            }
         },
         {
            "match":{
               "keywordList":{
                  "operator":"or",
                  "query":"휴양지"
               }
            }
         },
         {
            "match":{
               "keywordList":{
                  "operator":"or",
                  "query":"자연"
               }
            }
         },
         {
            "match":{
               "keywordList":{
                  "operator":"or",
                  "query":"역사"
               }
            }
         },
         {
            "match":{
               "keywordList":{
                  "operator":"or",
                  "query":"도시"
               }
            }
         },
         {
            "match":{
               "keywordList":{
                  "operator":"or",
                  "query":"해변"
               }
            }
         }
      ]
   }
}