Skip to content

Commit 71b3bcb

Browse files
committed
feature elastic search
1 parent a0486e5 commit 71b3bcb

6 files changed

Lines changed: 92 additions & 10 deletions

File tree

build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ dependencies {
5858

5959
// Elasticsearch
6060
implementation("org.springframework.boot:spring-boot-starter-data-elasticsearch")
61+
implementation("co.elastic.clients:elasticsearch-java:8.17.2")
6162

6263
// Docker
6364
//developmentOnly("org.springframework.boot:spring-boot-docker-compose")

src/main/java/com/ll/commars/domain/restaurant/restaurantDoc/controller/ApiV1RestaurantDocController.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import lombok.RequiredArgsConstructor;
88
import org.springframework.web.bind.annotation.*;
99

10+
import java.io.IOException;
1011
import java.util.List;
1112
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
1213

@@ -34,8 +35,8 @@ public class ApiV1RestaurantDocController {
3435

3536
@GetMapping("/search")
3637
@Operation(summary = "식당 검색")
37-
public List<RestaurantDoc> search(@RequestParam("keyword") String keyword) {
38-
return restaurantDocService.searchByKeyword(keyword);
38+
public List<RestaurantDoc> search(@RequestParam("keyword") String keyword, @RequestParam("lat") String lat, @RequestParam("lng") String lng) throws IOException {
39+
return restaurantDocService.searchByKeyword(keyword, Double.parseDouble(lat), Double.parseDouble(lng), "50km");
3940
}
4041

4142
@GetMapping("/sort/rate")

src/main/java/com/ll/commars/domain/restaurant/restaurantDoc/document/RestaurantDoc.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
package com.ll.commars.domain.restaurant.restaurantDoc.document;
22

3+
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
4+
import com.fasterxml.jackson.annotation.JsonProperty;
35
import lombok.*;
46
import org.springframework.data.annotation.Id;
57
import org.springframework.data.elasticsearch.annotations.*;
68

9+
@JsonIgnoreProperties(ignoreUnknown = true)
710
@Document(indexName = "es_restaurants", createIndex = true)
811
@Setting(settingPath = "elasticsearch/settings.json")
912
@Mapping(mappingPath = "elasticsearch/mappings.json")
@@ -22,6 +25,7 @@ public class RestaurantDoc {
2225
@Field(type = FieldType.Text)
2326
private String details;
2427

28+
@JsonProperty("average_rate")
2529
@Field(name = "average_rate", type = FieldType.Double)
2630
private Double averageRate;
2731

src/main/java/com/ll/commars/domain/restaurant/restaurantDoc/service/RestaurantDocService.java

Lines changed: 62 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,25 @@
11
package com.ll.commars.domain.restaurant.restaurantDoc.service;
22

3+
import co.elastic.clients.elasticsearch.ElasticsearchClient;
4+
import co.elastic.clients.elasticsearch._types.GeoLocation;
5+
import co.elastic.clients.elasticsearch._types.LatLonGeoLocation;
6+
import co.elastic.clients.elasticsearch._types.query_dsl.*;
7+
import co.elastic.clients.elasticsearch.core.SearchRequest;
8+
import co.elastic.clients.elasticsearch.core.SearchResponse;
39
import com.ll.commars.domain.restaurant.restaurantDoc.document.RestaurantDoc;
410
import com.ll.commars.domain.restaurant.restaurantDoc.repository.RestaurantDocRepository;
511
import lombok.RequiredArgsConstructor;
612
import org.springframework.stereotype.Service;
713

14+
import java.io.IOException;
815
import java.util.List;
16+
import java.util.stream.Collectors;
917

1018
@Service
1119
@RequiredArgsConstructor
1220
public class RestaurantDocService {
1321
private final RestaurantDocRepository restaurantDocRepository;
22+
private final ElasticsearchClient elasticsearchClient;
1423

1524
public RestaurantDoc write(String name, String details, Double averageRate, Double lat, Double lng) {
1625
RestaurantDoc restaurantDoc = RestaurantDoc.builder()
@@ -27,11 +36,60 @@ public void truncate() {
2736
restaurantDocRepository.deleteAll();
2837
}
2938

30-
public List<RestaurantDoc> searchByKeyword(String keyword) {
31-
return restaurantDocRepository.searchByKeyword(keyword);
32-
}
39+
public List<RestaurantDoc> searchByKeyword(String keyword, double userLat, double userLng, String distance) throws IOException {
40+
Query matchNameQuery = MatchQuery.of(m -> m
41+
.field("name")
42+
.query(keyword)
43+
.fuzziness("AUTO")
44+
)._toQuery();
45+
46+
Query matchDetailsQuery = MatchQuery.of(m -> m
47+
.field("details")
48+
.query(keyword)
49+
.fuzziness("AUTO")
50+
)._toQuery();
51+
52+
// ✅ 사용자 위치 기준 반경 검색 (Geo Distance)
53+
Query geoDistanceQuery = GeoDistanceQuery.of(g -> g
54+
.field("location") // ES의 GeoPoint 필드
55+
.distance(distance) // 검색 반경 (예: "50km")
56+
.location(GeoLocation.of(l -> l.latlon(LatLonGeoLocation.of(ll -> ll.lat(userLat).lon(userLng))))) // ✅ 수정된 부분
57+
)._toQuery();
58+
59+
// 키워드 + 거리 필터 조합
60+
Query boolQuery = BoolQuery.of(b -> b
61+
.should(matchNameQuery)
62+
.should(matchDetailsQuery)
63+
.filter(geoDistanceQuery) // ✅ 거리 필터 추가
64+
)._toQuery();
65+
66+
// Function Score Query (평점 높은 곳 우선)
67+
FunctionScoreQuery functionScoreQuery = FunctionScoreQuery.of(f -> f
68+
.query(boolQuery)
69+
.functions(FunctionScore.of(fs -> fs
70+
.fieldValueFactor(FieldValueFactorScoreFunction.of(fv -> fv
71+
.field("average_rate")
72+
.factor(1.5)
73+
.modifier(FieldValueFactorModifier.Sqrt)
74+
))
75+
))
76+
);
77+
78+
// 검색 요청
79+
SearchRequest searchRequest = SearchRequest.of(s -> s
80+
.index("es_restaurants")
81+
.query(functionScoreQuery._toQuery())
82+
);
83+
84+
// 검색 실행
85+
SearchResponse<RestaurantDoc> response = elasticsearchClient.search(searchRequest, RestaurantDoc.class);
86+
87+
return response.hits().hits().stream()
88+
.map(hit -> hit.source())
89+
.collect(Collectors.toList());
90+
}
3391

34-
public List<RestaurantDoc> showSortByRate() {
92+
public List<RestaurantDoc> showSortByRate() {
3593
return restaurantDocRepository.findAllByOrderByAverageRateDesc();
3694
}
3795

src/main/resources/elasticsearch/mappings.json

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,21 @@
22
"properties": {
33
"name": {
44
"type": "text",
5-
"analyzer": "korean"
5+
"analyzer": "ngram_analyzer",
6+
"search_analyzer": "korean_analyzer"
67
},
78
"body": {
89
"type": "text",
9-
"analyzer": "korean"
10+
"analyzer": "ngram_analyzer",
11+
"search_analyzer": "korean_analyzer"
1012
},
1113
"rate": {
1214
"type": "integer"
1315
},
1416
"details": {
1517
"type": "text",
16-
"analyzer": "korean"
18+
"analyzer": "ngram_analyzer",
19+
"search_analyzer": "korean_analyzer"
1720
},
1821
"average_rate": {
1922
"type": "double"
@@ -22,4 +25,4 @@
2225
"type": "geo_point"
2326
}
2427
}
25-
}
28+
}

src/main/resources/elasticsearch/settings.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,23 @@
1313
"nori_posfilter",
1414
"nori_readingform"
1515
]
16+
},
17+
"ngram_analyzer": {
18+
"type": "custom",
19+
"tokenizer": "ngram_tokenizer",
20+
"filter": ["lowercase"]
1621
}
1722
},
1823
"tokenizer": {
1924
"nori_tokenizer": {
2025
"type": "nori_tokenizer",
2126
"decompound_mode": "mixed"
27+
},
28+
"ngram_tokenizer": {
29+
"type": "ngram",
30+
"min_gram": 2,
31+
"max_gram": 3,
32+
"token_chars": ["letter", "digit"]
2233
}
2334
},
2435
"filter": {
@@ -29,6 +40,10 @@
2940
"SSC", "SSO", "SC", "SE", "XPN", "XSA",
3041
"XSN", "XSV", "UNA", "NA", "VSV"
3142
]
43+
},
44+
"korean_stop": {
45+
"type": "stop",
46+
"stopwords": ["", "", "", "그리고", "하지만"]
3247
}
3348
}
3449
}

0 commit comments

Comments
 (0)