feature: Reconciliation Jobs
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
using Elastic.Clients.Elasticsearch;
|
||||
using Elastic.Clients.Elasticsearch.IndexManagement;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
public class ElasticIndexProvisioner : IHostedService
|
||||
{
|
||||
private readonly ElasticsearchClient _elastic;
|
||||
private readonly ElasticsearchOptions _options;
|
||||
private readonly ILogger<ElasticIndexProvisioner> _logger;
|
||||
|
||||
public ElasticIndexProvisioner(
|
||||
ElasticsearchClient elastic,
|
||||
IOptions<ElasticsearchOptions> options,
|
||||
ILogger<ElasticIndexProvisioner> logger)
|
||||
{
|
||||
_elastic = elastic;
|
||||
_options = options.Value;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await EnsureIndexAsync<PatientEncounterDocument>(
|
||||
_options.Indices.PatientEncounters, BuildPatientEncountersMapping());
|
||||
await EnsureIndexAsync<ObservationDocument>(
|
||||
_options.Indices.Observations, BuildObservationsMapping());
|
||||
await EnsureIndexAsync<ClinicalAlertDocument>(
|
||||
_options.Indices.ClinicalAlerts, BuildClinicalAlertsMapping());
|
||||
}
|
||||
|
||||
private async Task EnsureIndexAsync<T>(
|
||||
string indexName,
|
||||
Action<CreateIndexRequestDescriptor<T>> configure) where T : class
|
||||
{
|
||||
var exists = await _elastic.Indices.ExistsAsync(indexName);
|
||||
if (exists.Exists)
|
||||
{
|
||||
_logger.LogInformation("Elasticsearch index '{Index}' already exists — skipping", indexName);
|
||||
return;
|
||||
}
|
||||
|
||||
var resp = await _elastic.Indices.CreateAsync<T>(indexName, configure);
|
||||
if (!resp.IsValidResponse)
|
||||
throw new InvalidOperationException(
|
||||
$"Failed to create Elasticsearch index '{indexName}': {resp.DebugInformation}");
|
||||
|
||||
_logger.LogInformation("Created Elasticsearch index '{Index}'", indexName);
|
||||
}
|
||||
|
||||
private Action<CreateIndexRequestDescriptor<PatientEncounterDocument>> BuildPatientEncountersMapping() =>
|
||||
d => d.Mappings(m => m.Properties(p => p
|
||||
.Keyword(k => k.EncounterId)
|
||||
.Keyword(k => k.PatientId)
|
||||
.Keyword(k => k.Mrn)
|
||||
// text for full-text search + keyword sub-field for exact sort/filter
|
||||
.Text(t => t.PatientName, tf => tf
|
||||
.Fields(f => f.Keyword(k => k.PatientName)))
|
||||
.Keyword(k => k.Department)
|
||||
.Keyword(k => k.Status)
|
||||
.Keyword(k => k.AttendingPhysician)
|
||||
.Date(d => d.AdmittedAt)
|
||||
.IntegerNumber(i => i.OpenAlertCount)
|
||||
.Date(d => d.LastObservationAt!)
|
||||
));
|
||||
|
||||
private Action<CreateIndexRequestDescriptor<ObservationDocument>> BuildObservationsMapping() =>
|
||||
d => d.Mappings(m => m.Properties(p => p
|
||||
.Keyword(k => k.ObservationId)
|
||||
.Keyword(k => k.EncounterId)
|
||||
.Keyword(k => k.PatientId)
|
||||
.Keyword(k => k.Mrn)
|
||||
.Keyword(k => k.ObservationCode)
|
||||
// float: observations are decimal values like 97.3, 2.5, 118.0
|
||||
// auto-mapped 'long' would truncate fractional parts silently
|
||||
.FloatNumber(f => f.Value)
|
||||
.Keyword(k => k.Unit)
|
||||
.Keyword(k => k.Source)
|
||||
.Date(d => d.RecordedAt)
|
||||
));
|
||||
|
||||
private Action<CreateIndexRequestDescriptor<ClinicalAlertDocument>> BuildClinicalAlertsMapping() =>
|
||||
d => d.Mappings(m => m.Properties(p => p
|
||||
.Keyword(k => k.AlertId)
|
||||
.Keyword(k => k.EncounterId)
|
||||
.Keyword(k => k.PatientId)
|
||||
.Keyword(k => k.Department)
|
||||
.Keyword(k => k.AlertType)
|
||||
.Keyword(k => k.Severity)
|
||||
.Keyword(k => k.Status)
|
||||
.Date(d => d.TriggeredAt)
|
||||
));
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
}
|
||||
Reference in New Issue
Block a user