From a10ac17edf7fbcdc1bc1e71e917c9437c9ac2272 Mon Sep 17 00:00:00 2001 From: "musab.bozkurt" Date: Thu, 15 Feb 2024 00:42:15 +0300 Subject: [PATCH] elastic fuzzy-search is implemented --- .../api/controller/CarController.java | 9 +++++ .../api/filter/ApiCarFilter.java | 19 ++++++++++ .../livedataservice/config/CacheConfig.java | 2 +- .../data/filter/CarFilter.java | 19 ++++++++++ .../data/model/elastic/Car.java | 3 +- .../mb/livedataservice/mapper/CarMapper.java | 17 +++++++++ .../livedataservice/service/CarService.java | 4 ++ .../service/impl/CarServiceImpl.java | 19 ++++++++++ .../util/ElasticSearchUtil.java | 38 +++++++++++++++++++ .../{utils => util}/RedisConstants.java | 2 +- .../controller/TutorialControllerTest.java | 15 ++++++++ 11 files changed, 144 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/mb/livedataservice/api/filter/ApiCarFilter.java create mode 100644 src/main/java/com/mb/livedataservice/data/filter/CarFilter.java create mode 100644 src/main/java/com/mb/livedataservice/util/ElasticSearchUtil.java rename src/main/java/com/mb/livedataservice/{utils => util}/RedisConstants.java (88%) diff --git a/src/main/java/com/mb/livedataservice/api/controller/CarController.java b/src/main/java/com/mb/livedataservice/api/controller/CarController.java index cb45d1f..6df65de 100644 --- a/src/main/java/com/mb/livedataservice/api/controller/CarController.java +++ b/src/main/java/com/mb/livedataservice/api/controller/CarController.java @@ -1,5 +1,6 @@ package com.mb.livedataservice.api.controller; +import com.mb.livedataservice.api.filter.ApiCarFilter; import com.mb.livedataservice.api.request.ApiCarRequest; import com.mb.livedataservice.api.response.ApiCarResponse; import com.mb.livedataservice.mapper.CarMapper; @@ -10,6 +11,8 @@ import org.springframework.data.domain.Pageable; import org.springframework.web.bind.annotation.*; +import java.util.List; + @Slf4j @RestController @RequiredArgsConstructor @@ -42,4 +45,10 @@ public void delete(@PathVariable String id) { log.info("Received a request to delete. delete - id: {}", id); carService.deleteCarById(id); } + + @GetMapping("/fuzzy-search") + public List fuzzySearch(ApiCarFilter apiCarFilter) { + log.info("Received a request to do fuzzy search. fuzzySearch - ApiCarFilter: {}", apiCarFilter); + return carMapper.map(carService.fuzzySearch(carMapper.map(apiCarFilter))); + } } diff --git a/src/main/java/com/mb/livedataservice/api/filter/ApiCarFilter.java b/src/main/java/com/mb/livedataservice/api/filter/ApiCarFilter.java new file mode 100644 index 0000000..0abf14c --- /dev/null +++ b/src/main/java/com/mb/livedataservice/api/filter/ApiCarFilter.java @@ -0,0 +1,19 @@ +package com.mb.livedataservice.api.filter; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ApiCarFilter { + + private String model; + + private Integer yearOfManufacture; + + private String brand; +} diff --git a/src/main/java/com/mb/livedataservice/config/CacheConfig.java b/src/main/java/com/mb/livedataservice/config/CacheConfig.java index 8d1c7c5..ccb86a6 100644 --- a/src/main/java/com/mb/livedataservice/config/CacheConfig.java +++ b/src/main/java/com/mb/livedataservice/config/CacheConfig.java @@ -1,6 +1,6 @@ package com.mb.livedataservice.config; -import com.mb.livedataservice.utils.RedisConstants; +import com.mb.livedataservice.util.RedisConstants; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; diff --git a/src/main/java/com/mb/livedataservice/data/filter/CarFilter.java b/src/main/java/com/mb/livedataservice/data/filter/CarFilter.java new file mode 100644 index 0000000..0f78ac8 --- /dev/null +++ b/src/main/java/com/mb/livedataservice/data/filter/CarFilter.java @@ -0,0 +1,19 @@ +package com.mb.livedataservice.data.filter; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class CarFilter { + + private String model; + + private Integer yearOfManufacture; + + private String brand; +} diff --git a/src/main/java/com/mb/livedataservice/data/model/elastic/Car.java b/src/main/java/com/mb/livedataservice/data/model/elastic/Car.java index 6157f41..e538daf 100644 --- a/src/main/java/com/mb/livedataservice/data/model/elastic/Car.java +++ b/src/main/java/com/mb/livedataservice/data/model/elastic/Car.java @@ -11,7 +11,8 @@ public record Car(@Id String id, - @Field(type = FieldType.Text, name = "model") String model, + @Field(type = FieldType.Text, name = "model") + String model, @Field(type = FieldType.Integer, name = "year") Integer yearOfManufacture, diff --git a/src/main/java/com/mb/livedataservice/mapper/CarMapper.java b/src/main/java/com/mb/livedataservice/mapper/CarMapper.java index cd507b4..7f99edc 100644 --- a/src/main/java/com/mb/livedataservice/mapper/CarMapper.java +++ b/src/main/java/com/mb/livedataservice/mapper/CarMapper.java @@ -1,11 +1,18 @@ package com.mb.livedataservice.mapper; +import co.elastic.clients.elasticsearch.core.SearchResponse; +import co.elastic.clients.elasticsearch.core.search.Hit; +import com.mb.livedataservice.api.filter.ApiCarFilter; import com.mb.livedataservice.api.request.ApiCarRequest; import com.mb.livedataservice.api.response.ApiCarResponse; +import com.mb.livedataservice.data.filter.CarFilter; import com.mb.livedataservice.data.model.elastic.Car; import org.mapstruct.Mapper; import org.mapstruct.Mapping; +import java.util.ArrayList; +import java.util.List; + @Mapper(componentModel = "spring") public interface CarMapper { @@ -13,4 +20,14 @@ public interface CarMapper { @Mapping(target = "id", ignore = true) Car map(ApiCarRequest apiCarRequest); + + CarFilter map(ApiCarFilter apiCarFilter); + + default List map(SearchResponse searchResponse) { + List carResponses = new ArrayList<>(); + for (Hit hit : searchResponse.hits().hits()) { + carResponses.add(this.map(hit.source())); + } + return carResponses; + } } diff --git a/src/main/java/com/mb/livedataservice/service/CarService.java b/src/main/java/com/mb/livedataservice/service/CarService.java index 508b0d9..784df2c 100644 --- a/src/main/java/com/mb/livedataservice/service/CarService.java +++ b/src/main/java/com/mb/livedataservice/service/CarService.java @@ -1,5 +1,7 @@ package com.mb.livedataservice.service; +import co.elastic.clients.elasticsearch.core.SearchResponse; +import com.mb.livedataservice.data.filter.CarFilter; import com.mb.livedataservice.data.model.elastic.Car; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -13,4 +15,6 @@ public interface CarService { Page findAll(Pageable pageable); void deleteCarById(String id); + + SearchResponse fuzzySearch(CarFilter carFilter); } diff --git a/src/main/java/com/mb/livedataservice/service/impl/CarServiceImpl.java b/src/main/java/com/mb/livedataservice/service/impl/CarServiceImpl.java index 4d46141..3c2021f 100644 --- a/src/main/java/com/mb/livedataservice/service/impl/CarServiceImpl.java +++ b/src/main/java/com/mb/livedataservice/service/impl/CarServiceImpl.java @@ -1,22 +1,31 @@ package com.mb.livedataservice.service.impl; +import co.elastic.clients.elasticsearch.ElasticsearchClient; +import co.elastic.clients.elasticsearch._types.query_dsl.Query; +import co.elastic.clients.elasticsearch.core.SearchResponse; +import com.mb.livedataservice.data.filter.CarFilter; import com.mb.livedataservice.data.model.elastic.Car; import com.mb.livedataservice.data.repository.CarRepository; import com.mb.livedataservice.exception.BaseException; import com.mb.livedataservice.exception.LiveDataErrorCode; import com.mb.livedataservice.service.CarService; +import com.mb.livedataservice.util.ElasticSearchUtil; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; +import java.io.IOException; +import java.util.function.Supplier; + @Slf4j @Service @RequiredArgsConstructor public class CarServiceImpl implements CarService { private final CarRepository carRepository; + private final ElasticsearchClient elasticsearchClient; @Override public Car save(Car car) { @@ -37,4 +46,14 @@ public Page findAll(Pageable pageable) { public void deleteCarById(String id) { carRepository.deleteById(id); } + + @Override + public SearchResponse fuzzySearch(CarFilter carFilter) { + Supplier supplier = ElasticSearchUtil.createSupplierQuery(carFilter); + try { + return elasticsearchClient.search(s -> s.index("car_index").query(supplier.get()), Car.class); + } catch (IOException e) { + throw new BaseException(LiveDataErrorCode.UNEXPECTED_ERROR); + } + } } diff --git a/src/main/java/com/mb/livedataservice/util/ElasticSearchUtil.java b/src/main/java/com/mb/livedataservice/util/ElasticSearchUtil.java new file mode 100644 index 0000000..f31af9a --- /dev/null +++ b/src/main/java/com/mb/livedataservice/util/ElasticSearchUtil.java @@ -0,0 +1,38 @@ +package com.mb.livedataservice.util; + +import co.elastic.clients.elasticsearch._types.query_dsl.FuzzyQuery; +import co.elastic.clients.elasticsearch._types.query_dsl.Query; +import com.mb.livedataservice.data.filter.CarFilter; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import lombok.val; +import org.apache.commons.lang3.StringUtils; + +import java.util.Objects; +import java.util.function.Supplier; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class ElasticSearchUtil { + + public static Supplier createSupplierQuery(CarFilter carFilter) { + return () -> Query.of(builder -> builder.fuzzy(createFuzzyQuery(carFilter))); + } + + private static FuzzyQuery createFuzzyQuery(CarFilter carFilter) { + val fuzzyQuery = new FuzzyQuery.Builder(); + + if (StringUtils.isNotBlank(carFilter.getModel())) { + fuzzyQuery.field("model").value(carFilter.getModel()); + } + + if (Objects.nonNull(carFilter.getYearOfManufacture())) { + fuzzyQuery.field("yearOfManufacture").value(carFilter.getYearOfManufacture()); + } + + if (StringUtils.isNotBlank(carFilter.getBrand())) { + fuzzyQuery.field("brand").value(carFilter.getBrand()); + } + + return fuzzyQuery.build(); + } +} diff --git a/src/main/java/com/mb/livedataservice/utils/RedisConstants.java b/src/main/java/com/mb/livedataservice/util/RedisConstants.java similarity index 88% rename from src/main/java/com/mb/livedataservice/utils/RedisConstants.java rename to src/main/java/com/mb/livedataservice/util/RedisConstants.java index 8a5d40b..7bb6b97 100644 --- a/src/main/java/com/mb/livedataservice/utils/RedisConstants.java +++ b/src/main/java/com/mb/livedataservice/util/RedisConstants.java @@ -1,4 +1,4 @@ -package com.mb.livedataservice.utils; +package com.mb.livedataservice.util; import lombok.AccessLevel; import lombok.NoArgsConstructor; diff --git a/src/test/java/com/mb/livedataservice/integration_tests/api/controller/TutorialControllerTest.java b/src/test/java/com/mb/livedataservice/integration_tests/api/controller/TutorialControllerTest.java index 78662d2..f35ece6 100644 --- a/src/test/java/com/mb/livedataservice/integration_tests/api/controller/TutorialControllerTest.java +++ b/src/test/java/com/mb/livedataservice/integration_tests/api/controller/TutorialControllerTest.java @@ -2,6 +2,7 @@ import com.mb.livedataservice.api.request.ApiTutorialRequest; import com.mb.livedataservice.api.request.ApiTutorialUpdateRequest; +import com.mb.livedataservice.api.response.ApiCarResponse; import com.mb.livedataservice.api.response.ApiTutorialResponse; import com.mb.livedataservice.base.BaseUnitTest; import com.mb.livedataservice.client.jsonplaceholder.DeclarativeJSONPlaceholderRestClient; @@ -35,7 +36,9 @@ import org.testcontainers.junit.jupiter.Testcontainers; import org.testcontainers.utility.DockerImageName; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Objects; import static org.assertj.core.api.Assertions.assertThat; @@ -244,4 +247,16 @@ void shouldCreateNewPost() { PostResponse post = declarativeJSONPlaceholderRestClient.createPost(newPost); assertThat(post.title()).isEqualTo("new title"); } + + @Test + void shouldDoFuzzySearchByFilter() { + Map queryParams = new HashMap<>(); + queryParams.put("model", "model"); + queryParams.put("yearOfManufacture", "2000"); + queryParams.put("brand", "brand"); + + ApiCarResponse[] tutorials = restTemplate.getForObject("/cars/fuzzy-search?model={model}&yearOfManufacture={yearOfManufacture}&brand={brand}", ApiCarResponse[].class, queryParams); + + assertThat(tutorials.length).isNotNegative(); + } }