From 0e631aad6750c5568341d57f29ce0bd361cb1aca Mon Sep 17 00:00:00 2001 From: Paulo Gomes da Cruz Junior Date: Thu, 3 Oct 2024 14:44:36 -0700 Subject: [PATCH 1/2] feat(FSADT1-1524): added legacy predictive search --- .../controller/ClientSearchController.java | 9 ++++ .../app/dto/PredictiveSearchResultDto.java | 35 ++++++++++++++ .../repository/ForestClientRepository.java | 46 ++++++++++++++++++ .../gov/app/service/ClientSearchService.java | 16 ++++++- ...ClientSearchControllerIntegrationTest.java | 47 +++++++++++++++++++ .../ClientSearchServiceIntegrationTest.java | 37 +++++++++++++++ 6 files changed, 189 insertions(+), 1 deletion(-) create mode 100644 legacy/src/main/java/ca/bc/gov/app/dto/PredictiveSearchResultDto.java 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 3344834c57..ae1dabc9c5 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 @@ -3,6 +3,7 @@ import ca.bc.gov.app.dto.AddressSearchDto; import ca.bc.gov.app.dto.ContactSearchDto; import ca.bc.gov.app.dto.ForestClientDto; +import ca.bc.gov.app.dto.PredictiveSearchResultDto; import ca.bc.gov.app.service.ClientSearchService; import io.micrometer.observation.annotation.Observed; import java.time.LocalDate; @@ -136,6 +137,14 @@ 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); + } + } diff --git a/legacy/src/main/java/ca/bc/gov/app/dto/PredictiveSearchResultDto.java b/legacy/src/main/java/ca/bc/gov/app/dto/PredictiveSearchResultDto.java new file mode 100644 index 0000000000..1e591cd33d --- /dev/null +++ b/legacy/src/main/java/ca/bc/gov/app/dto/PredictiveSearchResultDto.java @@ -0,0 +1,35 @@ +package ca.bc.gov.app.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.apache.commons.lang3.StringUtils; + +public record PredictiveSearchResultDto( + String clientNumber, + String clientAcronym, + String clientName, + String clientFirstName, + String doingBusinessAs, + String clientIdentification, + String clientMiddleName, + String city, + String clientType, + String statusCode, + long score +) { + + @JsonProperty("name") + public String name() { + if (StringUtils.isNotBlank(this.clientFirstName)) { + return Stream.of(this.clientFirstName, this.clientMiddleName, this.clientName) + .filter(Objects::nonNull) + .map(String::trim) + .collect(Collectors.joining(" ")); + } else { + return this.clientName; + } + } + +} 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 178d6f507d..3643fea834 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 @@ -1,7 +1,9 @@ package ca.bc.gov.app.repository; +import ca.bc.gov.app.dto.PredictiveSearchResultDto; import ca.bc.gov.app.entity.ForestClientEntity; import java.time.LocalDateTime; +import org.springframework.data.domain.Pageable; import org.springframework.data.r2dbc.repository.Query; import org.springframework.data.repository.query.Param; import org.springframework.data.repository.query.ReactiveQueryByExampleExecutor; @@ -16,6 +18,8 @@ public interface ForestClientRepository extends ReactiveCrudRepository, ReactiveSortingRepository { + Flux findBy(Pageable page); + @Query(""" SELECT * FROM FOREST_CLIENT x WHERE (UPPER(x.REGISTRY_COMPANY_TYPE_CODE) || x.CORP_REGN_NMBR) = UPPER(:registrationNumber) @@ -56,4 +60,46 @@ Flux findClientByIncorporationOrName( Mono findByClientNumber(String clientNumber); + @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, + c.client_status_code as status_code, + ( + 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 + LEFT JOIN the.CLIENT_LOCATION cl ON c.client_number = cl.client_number + WHERE + ( + c.client_number = :value + OR c.CLIENT_ACRONYM = :value + OR UTL_MATCH.JARO_WINKLER_SIMILARITY(c.client_name,:value) >= 90 + OR c.client_name LIKE '%' || :value || '%' + OR UTL_MATCH.JARO_WINKLER_SIMILARITY(c.legal_first_name,:value) >= 90 + OR UTL_MATCH.JARO_WINKLER_SIMILARITY(dba.doing_business_as_name,:value) >= 90 + 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 + ) AND + cl.CLIENT_LOCN_CODE = '00' + ORDER BY score DESC + FETCH FIRST 5 ROWS ONLY""") + Flux findByPredictiveSearch(String value); + } 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 1c1bc3214b..279505e863 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 @@ -6,6 +6,7 @@ import ca.bc.gov.app.dto.AddressSearchDto; import ca.bc.gov.app.dto.ContactSearchDto; import ca.bc.gov.app.dto.ForestClientDto; +import ca.bc.gov.app.dto.PredictiveSearchResultDto; import ca.bc.gov.app.entity.ClientDoingBusinessAsEntity; import ca.bc.gov.app.entity.ForestClientContactEntity; import ca.bc.gov.app.entity.ForestClientEntity; @@ -531,6 +532,20 @@ public Flux findByClientName(String clientName) { ); } + public Flux predictiveSearch(String value){ + log.info("Predictive search for value {}", value); + 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() + ) + ); + } + /** * This method is used to search for clients based on a given query criteria, page number, and * page size. It first creates a query based on the provided query criteria. Then, it counts the @@ -563,5 +578,4 @@ private Flux searchClientByQuery( .doOnNext(client -> log.info("Found client for query {}", queryCriteria)); } - } 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 c397572097..b0302ee169 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 @@ -276,6 +276,43 @@ void shouldSearchClientName( } + @ParameterizedTest + @MethodSource("byPredictive") + @DisplayName("Search using the predictive search") + void shouldSearchPredicatively( + String searchValue, + String expectedClientNumber, + String expectedClientName + ) { + + ResponseSpec response = + client + .get() + .uri(uriBuilder -> + uriBuilder + .path("/api/search/predictive") + .queryParam("value", Optional.ofNullable(searchValue)) + .build(new HashMap<>()) + ) + .header("Content-Type", MediaType.APPLICATION_JSON_VALUE) + .exchange(); + + if (StringUtils.isNotBlank(expectedClientNumber)) { + response + .expectStatus().isOk() + .expectBody() + .jsonPath("$[0].clientNumber").isNotEmpty() + .jsonPath("$[0].clientNumber").isEqualTo(expectedClientNumber) + .jsonPath("$[0].clientName").isNotEmpty() + .jsonPath("$[0].name").isEqualTo(expectedClientName) + .consumeWith(System.out::println); + }else{ + response.expectStatus().isOk() + .expectBody().isEmpty(); + } + + } + private static Stream byEmail() { return Stream.concat( @@ -485,4 +522,14 @@ private static Stream doingBusinessAs() { ); } + private static Stream byPredictive() { + return Stream + .of( + Arguments.of("indian canada", "00000006", "INDIAN CANADA"), + Arguments.of("kilback", "00000123", "REICHERT, KILBACK AND EMARD"), + Arguments.of("darbie", "00000145", "DARBIE BLIND"), + Arguments.of("pietro", 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 6f697812bc..9db033101d 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 @@ -6,6 +6,7 @@ import ca.bc.gov.app.dto.AddressSearchDto; import ca.bc.gov.app.dto.ContactSearchDto; import ca.bc.gov.app.dto.ForestClientDto; +import ca.bc.gov.app.dto.PredictiveSearchResultDto; import ca.bc.gov.app.exception.MissingRequiredParameterException; import ca.bc.gov.app.extensions.AbstractTestContainerIntegrationTest; import java.util.List; @@ -97,6 +98,31 @@ void shouldFindByContact( } + @DisplayName("should do predictive search") + @ParameterizedTest + @MethodSource("byPredictive") + void shouldSearchWithPredictiveSearch( + String searchValue, + String expectedClientNumber, + String expectedClientName + ) { + + FirstStep test = + service + .predictiveSearch(searchValue) + .as(StepVerifier::create); + + if(StringUtils.isNotBlank(expectedClientNumber)) { + test + .assertNext(dto -> { + assertNotNull(dto); + assertEquals(expectedClientNumber, dto.clientNumber()); + assertEquals(expectedClientName, dto.name()); + }); + } + test.verifyComplete(); + } + private void verifyTestData( List expectedList, Class exception, @@ -293,4 +319,15 @@ private static Stream emptyCases() { ); } + private static Stream byPredictive() { + return Stream + .of( + Arguments.of("indian canada", "00000006", "INDIAN CANADA"), + Arguments.of("kilback", "00000123", "REICHERT, KILBACK AND EMARD"), + Arguments.of("darbie", "00000145", "DARBIE BLIND"), + Arguments.of("pietro", StringUtils.EMPTY, StringUtils.EMPTY) + ); + } + + } \ No newline at end of file From 624a50b16fcc1cde5b264a8f2bc765b46989378d Mon Sep 17 00:00:00 2001 From: Paulo Gomes da Cruz Junior Date: Thu, 3 Oct 2024 15:14:03 -0700 Subject: [PATCH 2/2] test: fixing tests --- .../app/controller/ClientSearchControllerIntegrationTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 b0302ee169..1a05541894 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 @@ -308,7 +308,8 @@ void shouldSearchPredicatively( .consumeWith(System.out::println); }else{ response.expectStatus().isOk() - .expectBody().isEmpty(); + .expectBody() + .consumeWith(System.out::println).json("[]"); } }