From 0c3b09a32ea73077c8d8c50ce72cc63aa9c97f61 Mon Sep 17 00:00:00 2001
From: Vanitha S <116701245+vanitha1822@users.noreply.github.com>
Date: Thu, 8 Jan 2026 09:27:11 +0530
Subject: [PATCH 01/12] Elasticsearch implementation for Beneficiary Search
(#123)
* fix: ES Implementation-mapping, indexing and async records
* fix: add service for ES Search
* fix: search implementation
* fix: add additional fields as per the requirement
* fix: comment extra fields
* fix: rename the files, remove commented code
* fix: update pom.xml
* fix: revert advancesearch
* fix: add properties
* fix: coderabbit comments
* fix: remove comment code
* fix: accept numeric values for search
* fix: update the env variable
* fix: advance search functionality
* fix: update the advance search ES functionality
* fix: sync and fetch benid
* fix: size limit issue
* fix: improve response time
* fix: updated the end point to advancedSearchES
---
pom.xml | 22 +-
src/main/environment/common_ci.properties | 10 +
src/main/environment/common_docker.properties | 10 +
.../environment/common_example.properties | 11 +
.../common/identity/ScheduledSyncJob.java | 70 +
.../identity/config/ElasticsearchConfig.java | 100 +
.../config/ElasticsearchSyncConfig.java | 56 +
.../controller/IdentityESController.java | 201 +
.../ElasticsearchSyncController.java | 430 ++
.../elasticsearch/BeneficiaryDocument.java | 138 +
.../elasticsearch/ElasticsearchSyncJob.java | 95 +
.../com/iemr/common/identity/domain/User.java | 2 +-
.../identity/dto/BeneficiariesESDTO.java | 172 +
.../identity/mapper/BeneficiaryESMapper.java | 136 +
.../common/identity/repo/BenAddressRepo.java | 69 +-
.../common/identity/repo/BenDetailRepo.java | 212 +
.../common/identity/repo/BenMappingRepo.java | 100 +
.../repo/elasticsearch/SyncJobRepo.java | 43 +
.../identity/service/IdentityService.java | 3715 +++++++++--------
.../elasticsearch/BeneficiaryDataService.java | 140 +
.../BeneficiaryDocumentDataService.java | 191 +
.../BeneficiaryElasticsearchIndexService.java | 346 ++
.../BeneficiaryElasticsearchIndexUpdater.java | 107 +
.../BeneficiaryTransactionHelper.java | 67 +
.../ElasticsearchIndexingService.java | 127 +
.../elasticsearch/ElasticsearchService.java | 948 +++++
.../ElasticsearchSyncService.java | 484 +++
.../service/elasticsearch/SyncJobService.java | 145 +
.../common/identity/utils/CookieUtil.java | 37 +-
.../identity/utils/JwtAuthenticationUtil.java | 2 +-
.../utils/JwtUserIdValidationFilter.java | 1 +
src/main/resources/application.properties | 60 +-
32 files changed, 6489 insertions(+), 1758 deletions(-)
create mode 100644 src/main/java/com/iemr/common/identity/ScheduledSyncJob.java
create mode 100644 src/main/java/com/iemr/common/identity/config/ElasticsearchConfig.java
create mode 100644 src/main/java/com/iemr/common/identity/config/ElasticsearchSyncConfig.java
create mode 100644 src/main/java/com/iemr/common/identity/controller/IdentityESController.java
create mode 100644 src/main/java/com/iemr/common/identity/controller/elasticsearch/ElasticsearchSyncController.java
create mode 100644 src/main/java/com/iemr/common/identity/data/elasticsearch/BeneficiaryDocument.java
create mode 100644 src/main/java/com/iemr/common/identity/data/elasticsearch/ElasticsearchSyncJob.java
create mode 100644 src/main/java/com/iemr/common/identity/dto/BeneficiariesESDTO.java
create mode 100644 src/main/java/com/iemr/common/identity/mapper/BeneficiaryESMapper.java
create mode 100644 src/main/java/com/iemr/common/identity/repo/elasticsearch/SyncJobRepo.java
create mode 100644 src/main/java/com/iemr/common/identity/service/elasticsearch/BeneficiaryDataService.java
create mode 100644 src/main/java/com/iemr/common/identity/service/elasticsearch/BeneficiaryDocumentDataService.java
create mode 100644 src/main/java/com/iemr/common/identity/service/elasticsearch/BeneficiaryElasticsearchIndexService.java
create mode 100644 src/main/java/com/iemr/common/identity/service/elasticsearch/BeneficiaryElasticsearchIndexUpdater.java
create mode 100644 src/main/java/com/iemr/common/identity/service/elasticsearch/BeneficiaryTransactionHelper.java
create mode 100644 src/main/java/com/iemr/common/identity/service/elasticsearch/ElasticsearchIndexingService.java
create mode 100644 src/main/java/com/iemr/common/identity/service/elasticsearch/ElasticsearchService.java
create mode 100644 src/main/java/com/iemr/common/identity/service/elasticsearch/ElasticsearchSyncService.java
create mode 100644 src/main/java/com/iemr/common/identity/service/elasticsearch/SyncJobService.java
diff --git a/pom.xml b/pom.xml
index 84eb1596..27341f1e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
4.0.0
com.iemr.common.identity
identity-api
- 3.6.0
+ 3.6.1
war
@@ -73,6 +73,26 @@
1.3.2
+ org.springframework.boot
+ spring-boot-starter-data-elasticsearch
+
+
+ co.elastic.clients
+ elasticsearch-java
+ 8.11.0
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-elasticsearch
+
+
+ co.elastic.clients
+ elasticsearch-java
+ 8.11.0
+
+
+
org.springframework.boot
spring-boot-devtools
runtime
diff --git a/src/main/environment/common_ci.properties b/src/main/environment/common_ci.properties
index 75ad8759..98736180 100644
--- a/src/main/environment/common_ci.properties
+++ b/src/main/environment/common_ci.properties
@@ -23,3 +23,13 @@ fhir-url=@env.FHIR_API@
spring.redis.host=@env.REDIS_HOST@
cors.allowed-origins=@env.CORS_ALLOWED_ORIGINS@
+
+# Elasticsearch Configuration
+elasticsearch.host=@env.ELASTICSEARCH_HOST@
+elasticsearch.port=@env.ELASTICSEARCH_PORT@
+elasticsearch.username=@env.ELASTICSEARCH_USERNAME@
+elasticsearch.password=@env.ELASTICSEARCH_PASSWORD@
+elasticsearch.index.beneficiary=@env.ELASTICSEARCH_INDEX_BENEFICIARY@
+
+# Enable/Disable ES (for gradual rollout)
+elasticsearch.enabled=@env.ELASTICSEARCH_ENABLED@
diff --git a/src/main/environment/common_docker.properties b/src/main/environment/common_docker.properties
index 90942c28..8f66c5b0 100644
--- a/src/main/environment/common_docker.properties
+++ b/src/main/environment/common_docker.properties
@@ -23,3 +23,13 @@ fhir-url=${FHIR_API}
spring.redis.host=${REDIS_HOST}
cors.allowed-origins=${CORS_ALLOWED_ORIGINS}
+
+# Elasticsearch Configuration
+elasticsearch.host=${ELASTICSEARCH_HOST}
+elasticsearch.port=${ELASTICSEARCH_PORT}
+elasticsearch.username=${ELASTICSEARCH_USERNAME}
+elasticsearch.password=${ELASTICSEARCH_PASSWORD}
+elasticsearch.index.beneficiary=${ELASTICSEARCH_INDEX_BENEFICIARY}
+
+# Enable/Disable ES (for gradual rollout)
+elasticsearch.enabled=${ELASTICSEARCH_ENABLED}
\ No newline at end of file
diff --git a/src/main/environment/common_example.properties b/src/main/environment/common_example.properties
index ad15db00..45018aa6 100644
--- a/src/main/environment/common_example.properties
+++ b/src/main/environment/common_example.properties
@@ -18,3 +18,14 @@ fhir-url=http://localhost:8093/
# Redis Config
spring.redis.host=localhost
cors.allowed-origins=http://localhost:*
+
+# Elasticsearch Configuration
+elasticsearch.host=localhost
+elasticsearch.port=9200
+elasticsearch.username=elastic
+elasticsearch.password=piramalES
+elasticsearch.index.beneficiary=beneficiary_index
+
+# Enable/Disable ES (for gradual rollout)
+elasticsearch.enabled=true
+
diff --git a/src/main/java/com/iemr/common/identity/ScheduledSyncJob.java b/src/main/java/com/iemr/common/identity/ScheduledSyncJob.java
new file mode 100644
index 00000000..568d072c
--- /dev/null
+++ b/src/main/java/com/iemr/common/identity/ScheduledSyncJob.java
@@ -0,0 +1,70 @@
+package com.iemr.common.identity;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import com.iemr.common.identity.data.elasticsearch.ElasticsearchSyncJob;
+import com.iemr.common.identity.service.elasticsearch.SyncJobService;
+
+/**
+ * Scheduled jobs for Elasticsearch sync
+ *
+ * To enable scheduled sync, set: elasticsearch.sync.scheduled.enabled=true in
+ * application.properties
+ */
+@Component
+public class ScheduledSyncJob {
+
+ private static final Logger logger = LoggerFactory.getLogger(ScheduledSyncJob.class);
+
+ @Autowired
+ private SyncJobService syncJobService;
+
+ @Value("${elasticsearch.sync.scheduled.enabled:true}")
+ private boolean scheduledSyncEnabled;
+
+ /**
+ * Run full sync every day at 2 AM Cron: second, minute, hour, day, month,
+ * weekday
+ */
+ @Scheduled(cron = "${elasticsearch.sync.scheduled.cron:0 0 2 * * ?}")
+ public void scheduledFullSync() {
+ if (!scheduledSyncEnabled) {
+ logger.debug("Scheduled sync is disabled");
+ return;
+ }
+
+ logger.info("Starting scheduled full sync job");
+ try {
+ // Check if there's already a sync running
+ if (syncJobService.isFullSyncRunning()) {
+ logger.warn("Full sync already running. Skipping scheduled sync.");
+ return;
+ }
+
+ // Start async sync
+ ElasticsearchSyncJob job = syncJobService.startFullSyncJob("SCHEDULER");
+ logger.info("Scheduled sync job started: jobId={}", job.getJobId());
+
+ } catch (Exception e) {
+ logger.error("Error starting scheduled sync: {}", e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Clean up old completed jobs (keep last 30 days) Runs every Sunday at 3 AM
+ */
+ @Scheduled(cron = "0 0 3 * * SUN")
+ public void cleanupOldJobs() {
+ if (!scheduledSyncEnabled) {
+ return;
+ }
+
+ logger.info("Running cleanup of old sync jobs...");
+
+ }
+}
diff --git a/src/main/java/com/iemr/common/identity/config/ElasticsearchConfig.java b/src/main/java/com/iemr/common/identity/config/ElasticsearchConfig.java
new file mode 100644
index 00000000..a908b646
--- /dev/null
+++ b/src/main/java/com/iemr/common/identity/config/ElasticsearchConfig.java
@@ -0,0 +1,100 @@
+package com.iemr.common.identity.config;
+
+import java.io.IOException;
+
+import co.elastic.clients.elasticsearch.ElasticsearchClient;
+import co.elastic.clients.json.jackson.JacksonJsonpMapper;
+import co.elastic.clients.transport.ElasticsearchTransport;
+import co.elastic.clients.transport.rest_client.RestClientTransport;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.http.HttpHost;
+import org.apache.http.auth.AuthScope;
+import org.apache.http.auth.UsernamePasswordCredentials;
+import org.apache.http.impl.client.BasicCredentialsProvider;
+import org.elasticsearch.client.RestClient;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class ElasticsearchConfig {
+
+ private static final Logger logger = LoggerFactory.getLogger(ElasticsearchConfig.class);
+
+ @Value("${elasticsearch.host}")
+ private String esHost;
+
+ @Value("${elasticsearch.port}")
+ private int esPort;
+
+ @Value("${elasticsearch.username}")
+ private String esUsername;
+
+ @Value("${elasticsearch.password}")
+ private String esPassword;
+
+ @Value("${elasticsearch.index.beneficiary}")
+ private String indexName;
+
+ @Bean
+ public ElasticsearchClient elasticsearchClient() {
+ BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider();
+ credentialsProvider.setCredentials(
+ AuthScope.ANY,
+ new UsernamePasswordCredentials(esUsername, esPassword)
+ );
+
+ RestClient restClient = RestClient.builder(
+ new HttpHost(esHost, esPort, "http")
+ ).setHttpClientConfigCallback(httpClientBuilder ->
+ httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider)
+ ).build();
+
+ ElasticsearchTransport transport = new RestClientTransport(
+ restClient,
+ new JacksonJsonpMapper()
+ );
+
+ return new ElasticsearchClient(transport);
+ }
+
+ @Bean
+ public Boolean createIndexMapping(ElasticsearchClient client) throws IOException {
+
+ // Check if index exists
+ boolean exists = client.indices().exists(e -> e.index(indexName)).value();
+
+ if (!exists) {
+ client.indices().create(c -> c
+ .index(indexName)
+ .mappings(m -> m
+ .properties("beneficiaryRegID", p -> p.keyword(k -> k))
+ .properties("firstName", p -> p.text(t -> t
+ .fields("keyword", f -> f.keyword(k -> k))
+ .analyzer("standard")
+ ))
+ .properties("lastName", p -> p.text(t -> t
+ .fields("keyword", f -> f.keyword(k -> k))
+ .analyzer("standard")
+ ))
+ .properties("phoneNum", p -> p.keyword(k -> k))
+ .properties("fatherName", p -> p.text(t -> t.analyzer("standard")))
+ .properties("spouseName", p -> p.text(t -> t.analyzer("standard")))
+ .properties("aadharNo", p -> p.keyword(k -> k))
+ .properties("govtIdentityNo", p -> p.keyword(k -> k))
+ )
+ .settings(s -> s
+ .numberOfShards("3")
+ .numberOfReplicas("1")
+ .refreshInterval(t -> t.time("1s"))
+ )
+ );
+
+ logger.info("Created Elasticsearch index with proper mappings");
+ }
+ return true;
+
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/iemr/common/identity/config/ElasticsearchSyncConfig.java b/src/main/java/com/iemr/common/identity/config/ElasticsearchSyncConfig.java
new file mode 100644
index 00000000..bb72f91a
--- /dev/null
+++ b/src/main/java/com/iemr/common/identity/config/ElasticsearchSyncConfig.java
@@ -0,0 +1,56 @@
+package com.iemr.common.identity.config;
+
+import java.util.concurrent.Executor;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.scheduling.annotation.EnableAsync;
+import org.springframework.scheduling.annotation.EnableScheduling;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+
+/**
+ * Configuration for async processing and scheduling
+ */
+@Configuration
+@EnableAsync
+@EnableScheduling
+public class ElasticsearchSyncConfig {
+
+ /**
+ * Thread pool for Elasticsearch sync operations
+ * Configured for long-running background jobs
+ */
+ @Bean(name = "elasticsearchSyncExecutor")
+ public Executor elasticsearchSyncExecutor() {
+ ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
+
+ // Only 1-2 sync jobs should run at a time to avoid overwhelming DB/ES
+ executor.setCorePoolSize(5);
+ executor.setMaxPoolSize(10);
+ executor.setQueueCapacity(100);
+ executor.setThreadNamePrefix("es-sync-");
+ executor.setKeepAliveSeconds(60);
+
+ // Handle rejected tasks
+ executor.setRejectedExecutionHandler((r, executor1) -> {
+ throw new RuntimeException("Elasticsearch sync queue is full. Please wait for current job to complete.");
+ });
+
+ executor.initialize();
+ return executor;
+ }
+
+ /**
+ * General purpose async executor
+ */
+ @Bean(name = "taskExecutor")
+ public Executor taskExecutor() {
+ ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
+ executor.setCorePoolSize(5);
+ executor.setMaxPoolSize(10);
+ executor.setQueueCapacity(100);
+ executor.setThreadNamePrefix("async-");
+ executor.initialize();
+ return executor;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/iemr/common/identity/controller/IdentityESController.java b/src/main/java/com/iemr/common/identity/controller/IdentityESController.java
new file mode 100644
index 00000000..ca64bebd
--- /dev/null
+++ b/src/main/java/com/iemr/common/identity/controller/IdentityESController.java
@@ -0,0 +1,201 @@
+package com.iemr.common.identity.controller;
+
+
+import java.text.SimpleDateFormat;
+import java.util.*;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import com.iemr.common.identity.service.IdentityService;
+import com.iemr.common.identity.service.elasticsearch.ElasticsearchService;
+import com.iemr.common.identity.utils.CookieUtil;
+import com.iemr.common.identity.utils.JwtUtil;
+
+import io.swagger.v3.oas.annotations.Operation;
+import jakarta.servlet.http.HttpServletRequest;
+
+/**
+ * Elasticsearch-enabled Beneficiary Search Controller
+ * All search endpoints with ES support
+ */
+@RestController
+@RequestMapping("/beneficiary")
+public class IdentityESController {
+
+ private static final Logger logger = LoggerFactory.getLogger(IdentityESController.class);
+
+ @Autowired
+ private ElasticsearchService elasticsearchService;
+
+ @Autowired
+ private JwtUtil jwtUtil;
+
+ @Autowired
+ private IdentityService idService;
+ /**
+ * MAIN UNIVERSAL SEARCH ENDPOINT
+ * Searches across all fields - name, phone, ID, etc.
+ *
+ * Usage: GET /beneficiary/search?query=vani
+ * Usage: GET /beneficiary/search?query=9876543210
+ */
+ @GetMapping("/search")
+ public ResponseEntity