-
Notifications
You must be signed in to change notification settings - Fork 1
Elasticsearch ( spring data )
자세히 보기
{
"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;
}
전체 코드
@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로 묶는다. 마지막으로 페이징과 함께 검색한다. ( 요구사항 변경으로 검색에 특수한 필터, 필수 검색어가 추가될 경우 must
와 filter
추가 예정 )
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":"해변"
}
}
}
]
}
}
Spring 임경완 |
---|
@ MoonDooo |