From 550c941fa96ce58aececd948e2ebf2da3bdbc8fd Mon Sep 17 00:00:00 2001 From: Paulo Gomes da Cruz Junior Date: Tue, 8 Oct 2024 10:12:08 -0700 Subject: [PATCH] feat(FSADT1-1528): updating complex search from predictive (#1219) * feat(FSADT1-1528): added external pagination info for predictive * feat(FSADT1-1528): updated predictive endpoint to complex - Updated endpoint to be just search - Updated the controller to be able to search for latest - Updated query to include pagination - Added default page values for predictive - Changed tests * fix(FSADT1-1528): fixing search * test: fixing tests * Made code reviews --------- Co-authored-by: Maria Martinez --- .../controller/ClientSearchController.java | 25 +++-- .../repository/ForestClientRepository.java | 47 ++++++-- .../gov/app/service/ClientSearchService.java | 21 ++-- ...ClientSearchControllerIntegrationTest.java | 105 ++++++++++++------ .../ClientSearchServiceIntegrationTest.java | 7 +- 5 files changed, 142 insertions(+), 63 deletions(-) diff --git a/legacy/src/main/java/ca/bc/gov/app/controller/ClientSearchController.java b/legacy/src/main/java/ca/bc/gov/app/controller/ClientSearchController.java index ae1dabc9c5..296b225e93 100644 --- a/legacy/src/main/java/ca/bc/gov/app/controller/ClientSearchController.java +++ b/legacy/src/main/java/ca/bc/gov/app/controller/ClientSearchController.java @@ -10,6 +10,8 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.BooleanUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.data.domain.PageRequest; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -137,14 +139,19 @@ public Flux findByClientName( return service.findByClientName(clientName); } - @GetMapping("/predictive") - public Flux findByPredictiveSearch( - @RequestParam String value - ){ - log.info("Receiving request to search by predictive search {}", value); - return service.predictiveSearch(value); - } - - + @GetMapping + public Flux findByComplexSearch( + @RequestParam(required = false) String value, + @RequestParam(required = false, defaultValue = "0") Integer page, + @RequestParam(required = false, defaultValue = "5") Integer size) { + if (StringUtils.isNotBlank(value)) { + log.info("Receiving request to do a complex search by {}", value); + return service.complexSearch(value, PageRequest.of(page, size)); + } else { + log.info("Receiving request to search the latest entries"); + return service.latestEntries(PageRequest.of(page, size)); + } + } + } diff --git a/legacy/src/main/java/ca/bc/gov/app/repository/ForestClientRepository.java b/legacy/src/main/java/ca/bc/gov/app/repository/ForestClientRepository.java index 9602653894..1ddb15072f 100644 --- a/legacy/src/main/java/ca/bc/gov/app/repository/ForestClientRepository.java +++ b/legacy/src/main/java/ca/bc/gov/app/repository/ForestClientRepository.java @@ -72,15 +72,15 @@ Flux findClientByIncorporationOrName( cl.city as city, ctc.description as client_type, csc.description as client_status, - ( - CASE WHEN c.client_number = :value THEN 112 ELSE 0 END + - CASE WHEN c.CLIENT_ACRONYM = :value THEN 111 ELSE 0 END + - (UTL_MATCH.JARO_WINKLER_SIMILARITY(c.client_name, :value)+10) + - (UTL_MATCH.JARO_WINKLER_SIMILARITY(c.legal_first_name, :value)+9) + - (UTL_MATCH.JARO_WINKLER_SIMILARITY(dba.doing_business_as_name, :value)+7) + - CASE WHEN c.client_identification = :value THEN 106 ELSE 0 END + - UTL_MATCH.JARO_WINKLER_SIMILARITY(c.legal_middle_name, :value) - ) AS score + ( + CASE WHEN c.client_number = :value THEN 112 ELSE 0 END + + CASE WHEN c.CLIENT_ACRONYM = :value THEN 111 ELSE 0 END + + (UTL_MATCH.JARO_WINKLER_SIMILARITY(c.client_name, :value)+10) + + (UTL_MATCH.JARO_WINKLER_SIMILARITY(c.legal_first_name, :value)+9) + + (UTL_MATCH.JARO_WINKLER_SIMILARITY(dba.doing_business_as_name, :value)+7) + + CASE WHEN c.client_identification = :value THEN 106 ELSE 0 END + + UTL_MATCH.JARO_WINKLER_SIMILARITY(c.legal_middle_name, :value) + ) AS score FROM the.forest_client c LEFT JOIN the.CLIENT_DOING_BUSINESS_AS dba ON c.client_number = dba.client_number LEFT JOIN the.CLIENT_TYPE_CODE ctc ON c.client_type_code = ctc.client_type_code @@ -97,10 +97,35 @@ Flux findClientByIncorporationOrName( OR dba.doing_business_as_name LIKE '%' || :value || '%' OR c.client_identification = :value OR UTL_MATCH.JARO_WINKLER_SIMILARITY(c.legal_middle_name,:value) >= 90 + OR c.legal_middle_name LIKE '%' || :value || '%' ) AND cl.CLIENT_LOCN_CODE = '00' ORDER BY score DESC - FETCH FIRST 5 ROWS ONLY""") - Flux findByPredictiveSearch(String value); + OFFSET :offset ROWS FETCH NEXT :limit ROWS ONLY""") + Flux findByPredictiveSearch(String value, int limit, long offset); + + @Query(""" + SELECT + c.client_number, + c.CLIENT_ACRONYM as client_acronym, + c.client_name, + c.legal_first_name as client_first_name, + dba.doing_business_as_name as doing_business_as, + c.client_identification, + c.legal_middle_name as client_middle_name, + cl.city as city, + ctc.description as client_type, + csc.description as status_code, + 100 as score + FROM the.forest_client c + LEFT JOIN the.CLIENT_DOING_BUSINESS_AS dba ON c.client_number = dba.client_number + LEFT JOIN the.CLIENT_TYPE_CODE ctc ON c.client_type_code = ctc.client_type_code + LEFT JOIN the.CLIENT_LOCATION cl ON c.client_number = cl.client_number + LEFT JOIN the.CLIENT_STATUS_CODE csc ON c.client_status_code = csc.client_status_code + WHERE + cl.CLIENT_LOCN_CODE = '00' + ORDER BY c.ADD_TIMESTAMP DESC + OFFSET :offset ROWS FETCH NEXT :limit ROWS ONLY""") + Flux findByEmptyFullSearch(int limit, long offset); } diff --git a/legacy/src/main/java/ca/bc/gov/app/service/ClientSearchService.java b/legacy/src/main/java/ca/bc/gov/app/service/ClientSearchService.java index 279505e863..34b5f0eb9c 100644 --- a/legacy/src/main/java/ca/bc/gov/app/service/ClientSearchService.java +++ b/legacy/src/main/java/ca/bc/gov/app/service/ClientSearchService.java @@ -27,6 +27,7 @@ import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.r2dbc.core.R2dbcEntityTemplate; import org.springframework.data.relational.core.query.Criteria; @@ -532,18 +533,22 @@ public Flux findByClientName(String clientName) { ); } - public Flux predictiveSearch(String value){ - log.info("Predictive search for value {}", value); + public Flux complexSearch(String value, Pageable page) { + // This condition is for predictive search, and we will stop here if no query param is provided if (StringUtils.isBlank(value)) { return Flux.error(new MissingRequiredParameterException("value")); } return forestClientRepository - .findByPredictiveSearch(value.toUpperCase()) - .doOnNext(dto -> log.info("Found predictive search for value {} as {} {} with score {}", - value, - dto.clientNumber(), dto.name(), dto.score() - ) - ); + .findByPredictiveSearch(value.toUpperCase(), page.getPageSize(), page.getOffset()) + .doOnNext(dto -> log.info("Found complex search for value {} as {} {} with score {}", + value, dto.clientNumber(), dto.name(), dto.score())); + } + + public Flux latestEntries(Pageable page) { + return forestClientRepository + .findByEmptyFullSearch(page.getPageSize(), page.getOffset()) + .doOnNext(dto -> log.info("Found complex empty search as {} {} with score {}", + dto.clientNumber(), dto.name(), dto.score())); } /** diff --git a/legacy/src/test/java/ca/bc/gov/app/controller/ClientSearchControllerIntegrationTest.java b/legacy/src/test/java/ca/bc/gov/app/controller/ClientSearchControllerIntegrationTest.java index 91c5567b4b..279ec17a2d 100644 --- a/legacy/src/test/java/ca/bc/gov/app/controller/ClientSearchControllerIntegrationTest.java +++ b/legacy/src/test/java/ca/bc/gov/app/controller/ClientSearchControllerIntegrationTest.java @@ -10,6 +10,7 @@ import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; @@ -281,6 +282,8 @@ void shouldSearchClientName( @DisplayName("Search using the predictive search") void shouldSearchPredicatively( String searchValue, + Integer page, + Integer size, String expectedClientNumber, String expectedClientName ) { @@ -290,8 +293,10 @@ void shouldSearchPredicatively( .get() .uri(uriBuilder -> uriBuilder - .path("/api/search/predictive") - .queryParam("value", Optional.ofNullable(searchValue)) + .path("/api/search") + .queryParamIfPresent("value", Optional.ofNullable(searchValue)) + .queryParamIfPresent("page", Optional.ofNullable(page)) + .queryParamIfPresent("size", Optional.ofNullable(size)) .build(new HashMap<>()) ) .header("Content-Type", MediaType.APPLICATION_JSON_VALUE) @@ -306,14 +311,41 @@ void shouldSearchPredicatively( .jsonPath("$[0].clientName").isNotEmpty() .jsonPath("$[0].name").isEqualTo(expectedClientName) .consumeWith(System.out::println); - }else{ + } else { response.expectStatus().isOk() - .expectBody() + .expectBody() .consumeWith(System.out::println).json("[]"); } } + @Test + @DisplayName("Search using the predictive search") + void shouldSearchEmpty() { + + ResponseSpec response = + client + .get() + .uri(uriBuilder -> + uriBuilder + .path("/api/search") + .queryParam("page", 0) + .queryParam("size", 10) + .build(new HashMap<>()) + ) + .header("Content-Type", MediaType.APPLICATION_JSON_VALUE) + .exchange(); + + response + .expectStatus().isOk() + .expectBody() + .jsonPath("$[0].clientNumber").isNotEmpty() + .jsonPath("$[0].clientName").isNotEmpty() + .jsonPath("$.length()").isEqualTo(10) + .consumeWith(System.out::println); + + } + private static Stream byEmail() { return Stream.concat( @@ -497,39 +529,48 @@ private static Stream clientName() { private static Stream doingBusinessAs() { return - Stream - .of( - Arguments.of(null,null, null, MissingRequiredParameterException.class), - Arguments.of(null,false, null, MissingRequiredParameterException.class), - Arguments.of(null,true, null, MissingRequiredParameterException.class), - Arguments.of(StringUtils.EMPTY, null, null, MissingRequiredParameterException.class), - Arguments.of(StringUtils.EMPTY, false, null, MissingRequiredParameterException.class), - Arguments.of(StringUtils.EMPTY, true, null, MissingRequiredParameterException.class), - Arguments.of(" ", null, null, MissingRequiredParameterException.class), - Arguments.of(" ", false, null, MissingRequiredParameterException.class), - Arguments.of(" ", true, null, MissingRequiredParameterException.class), - - Arguments.of("BORIS AND BORIS INC.", null, "00000003", null), - Arguments.of("BORIS AND BORIS INC.", false, "00000003", null), - Arguments.of("BORIS AND BORIS", true, "00000003", null), - - Arguments.of("ELARICHO", null, "00000005", null), - Arguments.of("ELARICHO", false, "00000005", null), - Arguments.of("ELARICHO", true, "00000005", null), - - Arguments.of("ELARICO", true, "00000005", null), - Arguments.of("ELACHO", true, StringUtils.EMPTY, null), - Arguments.of("ELARICO", false, StringUtils.EMPTY, null) - ); + Stream + .of( + Arguments.of(null, null, null, MissingRequiredParameterException.class), + Arguments.of(null, false, null, MissingRequiredParameterException.class), + Arguments.of(null, true, null, MissingRequiredParameterException.class), + Arguments.of(StringUtils.EMPTY, null, null, + MissingRequiredParameterException.class), + Arguments.of(StringUtils.EMPTY, false, null, + MissingRequiredParameterException.class), + Arguments.of(StringUtils.EMPTY, true, null, + MissingRequiredParameterException.class), + Arguments.of(" ", null, null, MissingRequiredParameterException.class), + Arguments.of(" ", false, null, MissingRequiredParameterException.class), + Arguments.of(" ", true, null, MissingRequiredParameterException.class), + + Arguments.of("BORIS AND BORIS INC.", null, "00000003", null), + Arguments.of("BORIS AND BORIS INC.", false, "00000003", null), + Arguments.of("BORIS AND BORIS", true, "00000003", null), + + Arguments.of("ELARICHO", null, "00000005", null), + Arguments.of("ELARICHO", false, "00000005", null), + Arguments.of("ELARICHO", true, "00000005", null), + + Arguments.of("ELARICO", true, "00000005", null), + Arguments.of("ELACHO", true, StringUtils.EMPTY, null), + Arguments.of("ELARICO", false, StringUtils.EMPTY, null) + ); } private static Stream byPredictive() { return Stream .of( - Arguments.of("pollich", "00000114", "POLLICH-ABERNATHY"), - Arguments.of("kilback", "00000123", "REICHERT, KILBACK AND EMARD"), - Arguments.of("darbie", "00000145", "DARBIE BLIND (MINYX)"), - Arguments.of("pietro", StringUtils.EMPTY, StringUtils.EMPTY) + Arguments.of("pollich", null, null, "00000114", "POLLICH-ABERNATHY"), + Arguments.of("pollich", 0, 2, "00000114", "POLLICH-ABERNATHY"), + Arguments.of("pollich", 4, 10, StringUtils.EMPTY, StringUtils.EMPTY), + Arguments.of("kilback", null, null, "00000123", "REICHERT, KILBACK AND EMARD"), + Arguments.of("kilback", 0, 1, "00000123", "REICHERT, KILBACK AND EMARD"), + Arguments.of("darbie", null, null, "00000145", "DARBIE BLIND (MINYX)"), + Arguments.of("darbie", 0, 4, "00000145", "DARBIE BLIND (MINYX)"), + Arguments.of("pietro", null, null, StringUtils.EMPTY, StringUtils.EMPTY), + Arguments.of("pietro", 0, 5, StringUtils.EMPTY, StringUtils.EMPTY), + Arguments.of("pietro", 4, 10, StringUtils.EMPTY, StringUtils.EMPTY) ); } diff --git a/legacy/src/test/java/ca/bc/gov/app/service/ClientSearchServiceIntegrationTest.java b/legacy/src/test/java/ca/bc/gov/app/service/ClientSearchServiceIntegrationTest.java index f4354175dd..90e9a9ca38 100644 --- a/legacy/src/test/java/ca/bc/gov/app/service/ClientSearchServiceIntegrationTest.java +++ b/legacy/src/test/java/ca/bc/gov/app/service/ClientSearchServiceIntegrationTest.java @@ -17,6 +17,7 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.PageRequest; import reactor.test.StepVerifier; import reactor.test.StepVerifier.FirstStep; @@ -101,7 +102,7 @@ void shouldFindByContact( @DisplayName("should do predictive search") @ParameterizedTest @MethodSource("byPredictive") - void shouldSearchWithPredictiveSearch( + void shouldSearchWithComplexSearch( String searchValue, String expectedClientNumber, String expectedClientName @@ -109,7 +110,7 @@ void shouldSearchWithPredictiveSearch( FirstStep test = service - .predictiveSearch(searchValue) + .complexSearch(searchValue, PageRequest.of(0, 5)) .as(StepVerifier::create); if(StringUtils.isNotBlank(expectedClientNumber)) { @@ -322,7 +323,7 @@ private static Stream emptyCases() { private static Stream byPredictive() { return Stream .of( - Arguments.of("indian canada", "00000006", "INDIAN CANADA"), + Arguments.of("pollich", "00000114", "POLLICH-ABERNATHY"), Arguments.of("kilback", "00000123", "REICHERT, KILBACK AND EMARD"), Arguments.of("darbie", "00000145", "DARBIE BLIND (MINYX)"), Arguments.of("pietro", StringUtils.EMPTY, StringUtils.EMPTY)