feature: NEWS2 Composite Scoring Engine

This commit is contained in:
voltsrage
2026-06-18 17:39:31 +08:00
parent c3fbc20ddc
commit e6f7989298
31 changed files with 3118 additions and 45 deletions
@@ -58,8 +58,10 @@ public class ElasticIndexProvisioner : IHostedService
.Keyword(k => k.Department)
.Keyword(k => k.Status)
.Keyword(k => k.AttendingPhysician)
.Keyword(k => k.RoomBed)
.Text(t => t.AdmissionReason)
.Keyword(k => k.RoomBed!)
.Text(t => t.AdmissionReason!)
.IntegerNumber(i => i.News2Score!)
.Keyword(k => k.News2RiskLevel!)
.Date(d => d.AdmittedAt)
.IntegerNumber(i => i.OpenAlertCount)
.Date(d => d.LastObservationAt!)
@@ -201,8 +201,10 @@ public class EsIndexerService : BackgroundService
private async Task HandleAlertGeneratedAsync(string payload, CancellationToken ct)
{
var evt = JsonSerializer.Deserialize<AlertGeneratedEvent>(payload, EventJsonOptions)!;
using var payloadDoc = JsonDocument.Parse(payload);
var root = payloadDoc.RootElement;
var doc = new ClinicalAlertDocument
var alertDoc = new ClinicalAlertDocument
{
AlertId = evt.AlertId.ToString(),
EncounterId = evt.EncounterId.ToString(),
@@ -215,29 +217,47 @@ public class EsIndexerService : BackgroundService
};
var indexResp = await _elastic.IndexAsync(
doc,
i => i.Index(_esOptions.Indices.ClinicalAlerts).Id(doc.AlertId),
alertDoc,
i => i.Index(_esOptions.Indices.ClinicalAlerts).Id(alertDoc.AlertId),
ct);
if (!indexResp.IsValidResponse)
throw new InvalidOperationException(
$"ES index failed for alert {evt.AlertId}: {indexResp.DebugInformation}");
// Increment openAlertCount on the parent encounter document
// Increment openAlertCount on the parent encounter document.
// NEWS2 alerts also carry news2Score/news2RiskLevel in the Kafka payload;
// stamp those on patient_encounters so ward dashboards can filter by acuity.
var scriptLines = new List<string> { "ctx._source.openAlertCount += 1" };
Dictionary<string, object>? scriptParams = null;
if (root.TryGetProperty("news2Score", out var scoreElem) &&
root.TryGetProperty("news2RiskLevel", out var riskElem))
{
scriptLines.Add("ctx._source.news2Score = params.score");
scriptLines.Add("ctx._source.news2RiskLevel = params.riskLevel");
scriptParams = new Dictionary<string, object>
{
["score"] = scoreElem.GetInt32(),
["riskLevel"] = riskElem.GetString()!
};
}
var updateResp = await _elastic.UpdateAsync<PatientEncounterDocument, object>(
_esOptions.Indices.PatientEncounters,
evt.EncounterId.ToString(),
u => u
.Script(new Script(new InlineScript
{
Source = "ctx._source.openAlertCount += 1",
Language = ScriptLanguage.Painless
Source = string.Join(";\n", scriptLines),
Language = ScriptLanguage.Painless,
Params = scriptParams
}))
.RetryOnConflict(3),
ct);
if (!updateResp.IsValidResponse && updateResp.Result != Result.NotFound)
_logger.LogWarning(
"Could not increment openAlertCount for encounter {Id}", evt.EncounterId);
"Could not update patient_encounters for alert on encounter {Id}", evt.EncounterId);
}
}