diff --git a/src/main/java/com/iemr/common/identity/controller/IdentityESController.java b/src/main/java/com/iemr/common/identity/controller/IdentityESController.java index ca64beb..60ac6de 100644 --- a/src/main/java/com/iemr/common/identity/controller/IdentityESController.java +++ b/src/main/java/com/iemr/common/identity/controller/IdentityESController.java @@ -1,6 +1,5 @@ package com.iemr.common.identity.controller; - import java.text.SimpleDateFormat; import java.util.*; @@ -32,11 +31,12 @@ public class IdentityESController { @Autowired private ElasticsearchService elasticsearchService; - @Autowired - private JwtUtil jwtUtil; + @Autowired + private JwtUtil jwtUtil; @Autowired private IdentityService idService; + /** * MAIN UNIVERSAL SEARCH ENDPOINT * Searches across all fields - name, phone, ID, etc. @@ -44,158 +44,150 @@ public class IdentityESController { * Usage: GET /beneficiary/search?query=vani * Usage: GET /beneficiary/search?query=9876543210 */ - @GetMapping("/search") - public ResponseEntity> search(@RequestParam String query, HttpServletRequest request) { + @GetMapping("/search") + public ResponseEntity> search(@RequestParam String query, HttpServletRequest request, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "100") int size) { try { String jwtToken = CookieUtil.getJwtTokenFromCookie(request); - String userId = jwtUtil.getUserIdFromToken(jwtToken); - int userID=Integer.parseInt(userId); - List> results = elasticsearchService.universalSearch(query, userID); - - Map response = new HashMap<>(); - response.put("data", results); - response.put("statusCode", 200); - response.put("errorMessage", "Success"); - response.put("status", "Success"); - - return ResponseEntity.ok(response); - + String userId = jwtUtil.getUserIdFromToken(jwtToken); + int userID = Integer.parseInt(userId); + Map results = elasticsearchService.universalSearch(query, userID, page, size); + return ResponseEntity.ok(results); + } catch (Exception e) { Map errorResponse = new HashMap<>(); errorResponse.put("data", new ArrayList<>()); errorResponse.put("statusCode", 500); errorResponse.put("errorMessage", e.getMessage()); errorResponse.put("status", "Error"); - + return ResponseEntity.status(500).body(errorResponse); } } - -/** - * NEW Elasticsearch-based advance search - */ -@Operation(summary = "Get beneficiaries by advance search using Elasticsearch") -@PostMapping(path = "/advancedSearchES", headers = "Authorization") -public ResponseEntity> advanceSearchBeneficiariesES( + /** + * NEW Elasticsearch-based advance search + */ + + @Operation(summary = "Get beneficiaries by advance search using Elasticsearch") + @PostMapping(path = "/advancedSearchES", headers = "Authorization") + public ResponseEntity> advanceSearchBeneficiariesES( @RequestBody String searchFilter, - HttpServletRequest request) { - - logger.info("IdentityESController.advanceSearchBeneficiariesES - start {}", searchFilter); - Map response = new HashMap<>(); - - try { - JsonObject searchParams = JsonParser.parseString(searchFilter).getAsJsonObject(); - logger.info("Search params = {}", searchParams); - - String firstName = getString(searchParams, "firstName"); - String lastName = getString(searchParams, "lastName"); - Integer genderId = getInteger(searchParams, "genderId"); - Date dob = getDate(searchParams, "dob"); - String fatherName = getString(searchParams, "fatherName"); - String spouseName = getString(searchParams, "spouseName"); - String phoneNumber = getString(searchParams, "phoneNumber"); - String beneficiaryId = getString(searchParams, "beneficiaryId"); - String healthId = getString(searchParams, "healthId"); - String aadharNo = getString(searchParams, "aadharNo"); - Boolean is1097 = getBoolean(searchParams, "is1097"); - - Integer stateId = getLocationInt(searchParams, "stateId"); - Integer districtId = getLocationInt(searchParams, "districtId"); - Integer blockId = getLocationInt(searchParams, "blockId"); - Integer villageId = getLocationInt(searchParams, "villageId"); - - String jwtToken = CookieUtil.getJwtTokenFromCookie(request); - Integer userID = Integer.parseInt(jwtUtil.getUserIdFromToken(jwtToken)); - - logger.info( - "ES Advance search - firstName={}, genderId={}, stateId={}, districtId={}, userId={}", - firstName, genderId, stateId, districtId, userID - ); - - Map searchResults = - idService.advancedSearchBeneficiariesES( - firstName, lastName, genderId, dob, - stateId, districtId, blockId, villageId, - fatherName, spouseName, phoneNumber, - beneficiaryId, healthId, aadharNo, - userID, null, is1097 - ); - - response.put("data", searchResults.get("data")); - response.put("count", searchResults.get("count")); - response.put("source", searchResults.get("source")); - response.put("statusCode", 200); - response.put("errorMessage", "Success"); - response.put("status", "Success"); - - return ResponseEntity.ok(response); - - } catch (Exception e) { - logger.error("Error in beneficiary ES advance search", e); - response.put("data", Collections.emptyList()); - response.put("count", 0); - response.put("source", "error"); - response.put("statusCode", 500); - response.put("errorMessage", e.getMessage()); - response.put("status", "Error"); - return ResponseEntity.status(500).body(response); - } -} - -// Helper methods to extract values from JsonObject - -private String getString(JsonObject json, String key) { - return json.has(key) && !json.get(key).isJsonNull() - ? json.get(key).getAsString() - : null; -} - -private Integer getInteger(JsonObject json, String key) { - return json.has(key) && !json.get(key).isJsonNull() - ? json.get(key).getAsInt() - : null; -} - -private Boolean getBoolean(JsonObject json, String key) { - return json.has(key) && !json.get(key).isJsonNull() - ? json.get(key).getAsBoolean() - : null; -} - -private Date getDate(JsonObject json, String key) { - if (json.has(key) && !json.get(key).isJsonNull()) { + HttpServletRequest request) { + + logger.info("IdentityESController.advanceSearchBeneficiariesES - start {}", searchFilter); + Map response = new HashMap<>(); + try { - return new SimpleDateFormat("yyyy-MM-dd") - .parse(json.get(key).getAsString()); + JsonObject searchParams = JsonParser.parseString(searchFilter).getAsJsonObject(); + logger.info("Search params = {}", searchParams); + + String firstName = getString(searchParams, "firstName"); + String lastName = getString(searchParams, "lastName"); + Integer genderId = getInteger(searchParams, "genderId"); + Date dob = getDate(searchParams, "dob"); + String fatherName = getString(searchParams, "fatherName"); + String spouseName = getString(searchParams, "spouseName"); + String phoneNumber = getString(searchParams, "phoneNumber"); + String beneficiaryId = getString(searchParams, "beneficiaryId"); + String healthId = getString(searchParams, "healthId"); + String aadharNo = getString(searchParams, "aadharNo"); + Boolean is1097 = getBoolean(searchParams, "is1097"); + + Integer stateId = getLocationInt(searchParams, "stateId"); + Integer districtId = getLocationInt(searchParams, "districtId"); + Integer blockId = getLocationInt(searchParams, "blockId"); + Integer villageId = getLocationInt(searchParams, "villageId"); + + String jwtToken = CookieUtil.getJwtTokenFromCookie(request); + Integer userID = Integer.parseInt(jwtUtil.getUserIdFromToken(jwtToken)); + + logger.info( + "ES Advance search - firstName={}, genderId={}, stateId={}, districtId={}, userId={}", + firstName, genderId, stateId, districtId, userID); + + Map searchResults = idService.advancedSearchBeneficiariesES( + firstName, lastName, genderId, dob, + stateId, districtId, blockId, villageId, + fatherName, spouseName, phoneNumber, + beneficiaryId, healthId, aadharNo, + userID, null, is1097); + + response.put("data", searchResults.get("data")); + response.put("count", searchResults.get("count")); + response.put("source", searchResults.get("source")); + response.put("statusCode", 200); + response.put("errorMessage", "Success"); + response.put("status", "Success"); + + return ResponseEntity.ok(response); + } catch (Exception e) { - logger.error("Invalid date format for {} ", key, e); + logger.error("Error in beneficiary ES advance search", e); + response.put("data", Collections.emptyList()); + response.put("count", 0); + response.put("source", "error"); + response.put("statusCode", 500); + response.put("errorMessage", e.getMessage()); + response.put("status", "Error"); + return ResponseEntity.status(500).body(response); } } - return null; -} -private Integer getLocationInt(JsonObject root, String field) { + // Helper methods to extract values from JsonObject + + private String getString(JsonObject json, String key) { + return json.has(key) && !json.get(key).isJsonNull() + ? json.get(key).getAsString() + : null; + } + + private Integer getInteger(JsonObject json, String key) { + return json.has(key) && !json.get(key).isJsonNull() + ? json.get(key).getAsInt() + : null; + } - if (root.has(field) && !root.get(field).isJsonNull()) { - return root.get(field).getAsInt(); + private Boolean getBoolean(JsonObject json, String key) { + return json.has(key) && !json.get(key).isJsonNull() + ? json.get(key).getAsBoolean() + : null; } - String[] addressKeys = { - "currentAddress", - "permanentAddress", - "emergencyAddress" - }; - - for (String addressKey : addressKeys) { - if (root.has(addressKey) && root.get(addressKey).isJsonObject()) { - JsonObject addr = root.getAsJsonObject(addressKey); - if (addr.has(field) && !addr.get(field).isJsonNull()) { - return addr.get(field).getAsInt(); + private Date getDate(JsonObject json, String key) { + if (json.has(key) && !json.get(key).isJsonNull()) { + try { + return new SimpleDateFormat("yyyy-MM-dd") + .parse(json.get(key).getAsString()); + } catch (Exception e) { + logger.error("Invalid date format for {} ", key, e); } } + return null; } - return null; -} - + + private Integer getLocationInt(JsonObject root, String field) { + + if (root.has(field) && !root.get(field).isJsonNull()) { + return root.get(field).getAsInt(); + } + + String[] addressKeys = { + "currentAddress", + "permanentAddress", + "emergencyAddress" + }; + + for (String addressKey : addressKeys) { + if (root.has(addressKey) && root.get(addressKey).isJsonObject()) { + JsonObject addr = root.getAsJsonObject(addressKey); + if (addr.has(field) && !addr.get(field).isJsonNull()) { + return addr.get(field).getAsInt(); + } + } + } + return null; + } + } \ No newline at end of file diff --git a/src/main/java/com/iemr/common/identity/dto/BeneficiariesESDTO.java b/src/main/java/com/iemr/common/identity/dto/BeneficiariesESDTO.java index b8d9253..ecd396d 100644 --- a/src/main/java/com/iemr/common/identity/dto/BeneficiariesESDTO.java +++ b/src/main/java/com/iemr/common/identity/dto/BeneficiariesESDTO.java @@ -6,6 +6,8 @@ import lombok.AllArgsConstructor; import java.util.Date; import java.util.List; + +import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; @Data @@ -13,142 +15,143 @@ @AllArgsConstructor @JsonIgnoreProperties(ignoreUnknown = true) public class BeneficiariesESDTO { - + @JsonProperty("benRegId") private Long benRegId; - + @JsonProperty("beneficiaryID") private String beneficiaryID; - + @JsonProperty("firstName") private String firstName; - + @JsonProperty("lastName") private String lastName; - + @JsonProperty("genderID") private Integer genderID; - + @JsonProperty("genderName") private String genderName; - + @JsonProperty("dOB") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", timezone = "UTC") private Date dOB; - + @JsonProperty("age") private Integer age; - + @JsonProperty("phoneNum") private String phoneNum; - + // @JsonProperty("aadharNo") // private String aadharNo; - + // @JsonProperty("govtIdentityNo") // private String govtIdentityNo; - + @JsonProperty("fatherName") private String fatherName; - + @JsonProperty("spouseName") private String spouseName; - + @JsonProperty("createdBy") private String createdBy; - + @JsonProperty("createdDate") private Date createdDate; - + @JsonProperty("lastModDate") private Long lastModDate; - + @JsonProperty("benAccountID") private Long benAccountID; - + @JsonProperty("isHIVPos") private String isHIVPos; - + @JsonProperty("healthID") private String healthID; - + @JsonProperty("abhaID") private String abhaID; - + @JsonProperty("familyID") private String familyID; - + @JsonProperty("stateID") private Integer stateID; - + @JsonProperty("stateName") private String stateName; - + @JsonProperty("stateCode") private String stateCode; - + @JsonProperty("districtID") private Integer districtID; - + @JsonProperty("districtName") private String districtName; - + @JsonProperty("blockID") private Integer blockID; - + @JsonProperty("blockName") private String blockName; - + @JsonProperty("villageID") private Integer villageID; - + @JsonProperty("villageName") private String villageName; - + @JsonProperty("districtBranchID") private Integer districtBranchID; - + @JsonProperty("districtBranchName") private String districtBranchName; - + @JsonProperty("parkingPlaceID") private Integer parkingPlaceID; - + @JsonProperty("servicePointID") private Integer servicePointID; - + @JsonProperty("servicePointName") private String servicePointName; - + @JsonProperty("pinCode") private String pinCode; - + @JsonProperty("permStateID") private Integer permStateID; - + @JsonProperty("permStateName") private String permStateName; - + @JsonProperty("permDistrictID") private Integer permDistrictID; - + @JsonProperty("permDistrictName") private String permDistrictName; - + @JsonProperty("permBlockID") private Integer permBlockID; - + @JsonProperty("permBlockName") private String permBlockName; - + @JsonProperty("permVillageID") private Integer permVillageID; - + @JsonProperty("permVillageName") private String permVillageName; - + @JsonProperty("phoneNumbers") private List phoneNumbers; - + @Data @NoArgsConstructor @AllArgsConstructor @@ -156,16 +159,16 @@ public class BeneficiariesESDTO { public static class PhoneNumberDTO { @JsonProperty("benPhMapID") private Long benPhMapID; - + @JsonProperty("phoneNo") private String phoneNo; - + @JsonProperty("parentBenRegID") private Long parentBenRegID; - + @JsonProperty("benRelationshipID") private Integer benRelationshipID; - + @JsonProperty("benRelationshipType") private String benRelationshipType; } diff --git a/src/main/java/com/iemr/common/identity/service/elasticsearch/ElasticsearchService.java b/src/main/java/com/iemr/common/identity/service/elasticsearch/ElasticsearchService.java index 67ede6e..665efbf 100644 --- a/src/main/java/com/iemr/common/identity/service/elasticsearch/ElasticsearchService.java +++ b/src/main/java/com/iemr/common/identity/service/elasticsearch/ElasticsearchService.java @@ -63,83 +63,53 @@ public class ElasticsearchService { * Universal search with score-based filtering and location ranking * Only returns records that actually match the query (not all 10000) */ -public List> universalSearch(String query, Integer userId) { + +public Map universalSearch(String query, Integer userId, int page, int size) { try { final Map userLocation = - (userId != null) ? getUserLocation(userId) : null; - + (userId != null) ? getUserLocation(userId) : null; boolean isNumeric = query.matches("\\d+"); - - // Determine minimum score threshold based on query type double minScore = isNumeric ? 1.0 : 3.0; + + // Calculate from parameter for pagination + int from = page * size; + + // Add a reasonable max limit + int maxSize = Math.min(size, 100); // Limit to 100 per page SearchResponse response = esClient.search(s -> s .index(beneficiaryIndex) + .from(from) + .size(maxSize) .query(q -> q .functionScore(fs -> fs .query(qq -> qq .bool(b -> { + if (!isNumeric) { - // Name searches with higher boost for exact matches b.should(s1 -> s1.multiMatch(mm -> mm .query(query) .fields("firstName^3", "lastName^3", "fatherName^2", "spouseName^2") .type(TextQueryType.BestFields) .fuzziness("AUTO") )); - - // Exact keyword matches get highest boost b.should(s2 -> s2.term(t -> t.field("firstName.keyword").value(query).boost(5.0f))); b.should(s3 -> s3.term(t -> t.field("lastName.keyword").value(query).boost(5.0f))); - b.should(s4 -> s4.term(t -> t.field("fatherName.keyword").value(query).boost(4.0f))); - b.should(s5 -> s5.term(t -> t.field("spouseName.keyword").value(query).boost(4.0f))); } - // ID searches - exact matches get very high boost + // ID searches b.should(s6 -> s6.term(t -> t.field("healthID").value(query).boost(10.0f))); b.should(s7 -> s7.term(t -> t.field("abhaID").value(query).boost(10.0f))); - b.should(s8 -> s8.term(t -> t.field("familyID").value(query).boost(8.0f))); - b.should(s9 -> s9.term(t -> t.field("beneficiaryID").value(query).boost(10.0f))); - b.should(s10 -> s10.term(t -> t.field("benId").value(query).boost(10.0f))); - b.should(s11 -> s11.term(t -> t.field("aadharNo").value(query).boost(9.0f))); - b.should(s12 -> s12.term(t -> t.field("govtIdentityNo").value(query).boost(8.0f))); - + b.should(s8 -> s8.term(t -> t.field("beneficiaryID").value(query).boost(10.0f))); + if (isNumeric) { - // Partial matches for numeric fields b.should(s13 -> s13.wildcard(w -> w.field("phoneNum").value("*" + query + "*").boost(3.0f))); - b.should(s14 -> s14.wildcard(w -> w.field("healthID").value("*" + query + "*").boost(2.0f))); - b.should(s15 -> s15.wildcard(w -> w.field("abhaID").value("*" + query + "*").boost(2.0f))); - b.should(s16 -> s16.wildcard(w -> w.field("familyID").value("*" + query + "*").boost(2.0f))); - b.should(s17 -> s17.wildcard(w -> w.field("beneficiaryID").value("*" + query + "*").boost(2.0f))); - b.should(s18 -> s18.wildcard(w -> w.field("benId").value("*" + query + "*").boost(2.0f))); - b.should(s19 -> s19.wildcard(w -> w.field("aadharNo").value("*" + query + "*").boost(2.0f))); - b.should(s20 -> s20.wildcard(w -> w.field("govtIdentityNo").value("*" + query + "*").boost(2.0f))); - - // Prefix matches b.should(s21 -> s21.prefix(p -> p.field("phoneNum").value(query).boost(4.0f))); - b.should(s22 -> s22.prefix(p -> p.field("healthID").value(query).boost(3.0f))); - b.should(s23 -> s23.prefix(p -> p.field("abhaID").value(query).boost(3.0f))); - b.should(s24 -> s24.prefix(p -> p.field("familyID").value(query).boost(3.0f))); - b.should(s25 -> s25.prefix(p -> p.field("beneficiaryID").value(query).boost(3.0f))); - b.should(s26 -> s26.prefix(p -> p.field("benId").value(query).boost(3.0f))); - + try { Long numericValue = Long.parseLong(query); b.should(s27 -> s27.term(t -> t.field("benRegId").value(numericValue).boost(10.0f))); - b.should(s28 -> s28.term(t -> t.field("benAccountID").value(numericValue).boost(8.0f))); - - int intValue = numericValue.intValue(); - b.should(s29 -> s29.term(t -> t.field("genderID").value(intValue).boost(2.0f))); - b.should(s30 -> s30.term(t -> t.field("age").value(intValue).boost(1.0f))); - b.should(s31 -> s31.term(t -> t.field("stateID").value(intValue).boost(1.0f))); - b.should(s32 -> s32.term(t -> t.field("districtID").value(intValue).boost(1.0f))); - b.should(s33 -> s33.term(t -> t.field("blockID").value(intValue).boost(1.0f))); - b.should(s34 -> s34.term(t -> t.field("villageID").value(intValue).boost(1.0f))); - b.should(s35 -> s35.term(t -> t.field("servicePointID").value(intValue).boost(1.0f))); - b.should(s36 -> s36.term(t -> t.field("parkingPlaceID").value(intValue).boost(1.0f))); - - logger.info("Added numeric searches for value: {}", numericValue); } catch (NumberFormatException e) { logger.warn("Failed to parse numeric value: {}", query); } @@ -149,55 +119,68 @@ public List> universalSearch(String query, Integer userId) { return b; }) ) - // Add location-based scoring if user location is available .functions(getFunctionScores(userLocation)) .scoreMode(FunctionScoreMode.Sum) .boostMode(FunctionBoostMode.Multiply) ) ) - .minScore(minScore) // KEY: Only return results above minimum score - .size(500) // Reasonable limit - returns only matches up to 500 - .sort(so -> so - .score(sc -> sc.order(SortOrder.Desc)) // Sort by relevance score - ) + .minScore(minScore) + .sort(so -> so.score(sc -> sc.order(SortOrder.Desc))) + .trackTotalHits(th -> th.enabled(true)) // Track total hits , BeneficiariesESDTO.class); - logger.info("ES returned {} hits for query: '{}' (min score: {})", - response.hits().hits().size(), query, minScore); + long totalHits = response.hits().total().value(); + + logger.info("ES returned {} hits out of {} total for query: '{}' (page: {}, size: {})", + response.hits().hits().size(), totalHits, query, page, size); - List> allResults = response.hits().hits().stream() + List> results = response.hits().hits().stream() .map(hit -> { - if (hit.source() != null) { - logger.debug("Hit score: {}, benRegId: {}, name: {} {}", - hit.score(), - hit.source().getBenRegId(), - hit.source().getFirstName(), - hit.source().getLastName()); - } Map result = mapESResultToExpectedFormat(hit.source()); if (result != null) { - result.put("_score", hit.score()); // Include score for debugging + result.put("_score", hit.score()); } return result; }) .filter(Objects::nonNull) .collect(Collectors.toList()); - if (allResults.isEmpty()) { - logger.info("No results found in ES, falling back to database"); - return searchInDatabaseDirectly(query); - } + Map paginatedResponse = new HashMap<>(); + paginatedResponse.put("data", results); + paginatedResponse.put("totalResults", totalHits); + paginatedResponse.put("currentPage", page); + paginatedResponse.put("pageSize", maxSize); + paginatedResponse.put("totalPages", (int) Math.ceil((double) totalHits / maxSize)); + paginatedResponse.put("hasMore", from + results.size() < totalHits); - logger.info("Returning {} matched results", allResults.size()); - return allResults; + return paginatedResponse; } catch (Exception e) { logger.error("ES universal search failed: {}", e.getMessage(), e); - logger.info("Fallback: Searching in MySQL database"); - return searchInDatabaseDirectly(query); + return createFallbackPaginatedResponse(query, page, size); } } +private Map createFallbackPaginatedResponse(String query, int page, int size) { + List> dbResults = searchInDatabaseDirectly(query); + int from = page * size; + int to = Math.min(from + size, dbResults.size()); + + List> pageResults = dbResults.subList( + Math.min(from, dbResults.size()), + to + ); + + Map response = new HashMap<>(); + response.put("data", pageResults); + response.put("totalResults", dbResults.size()); + response.put("currentPage", page); + response.put("pageSize", size); + response.put("totalPages", (int) Math.ceil((double) dbResults.size() / size)); + response.put("hasMore", to < dbResults.size()); + + return response; +} /** * Generate function scores for location-based ranking */ @@ -485,14 +468,6 @@ private List> searchInDatabaseForAdvanced( return Collections.emptyList(); } } - - - /** - * Overloaded method without userId (backward compatibility) - */ - public List> universalSearch(String query) { - return universalSearch(query, null); - } /** * Get user location from database @@ -615,6 +590,7 @@ private Map mapESResultToExpectedFormat(BeneficiariesESDTO esDat result.put("genderID", esData.getGenderID()); result.put("genderName", esData.getGenderName()); result.put("dob", esData.getDOB()); + result.put("dOB", esData.getDOB()); result.put("age", esData.getAge()); result.put("fatherName", esData.getFatherName() != null ? esData.getFatherName() : ""); result.put("spouseName", esData.getSpouseName() != null ? esData.getSpouseName() : ""); diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 22a9ff6..31bca02 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -78,6 +78,7 @@ jwt.refresh.expiration=604800000 # Connection pool settings elasticsearch.connection.timeout=5000 elasticsearch.socket.timeout=60000 +elasticsearch.max.result.window=20000 # Logging logging.level.com.iemr.common.identity.service.elasticsearch=INFO