diff --git a/.github/integration-test/ontology/dse/profile_tree.json b/.github/integration-test/ontology/dse/profile_tree.json index 718c85e4..0011dc10 100644 --- a/.github/integration-test/ontology/dse/profile_tree.json +++ b/.github/integration-test/ontology/dse/profile_tree.json @@ -1,63 +1,135 @@ { - "name": "Root", - "module": "no-module", - "url": "no-url", - "children": [ + "name": "Root", + "module": "no-module", + "url": "no-url", + "children": [ + { + "id": "e5dbef02-a690-4f5a-b1b9-081302aff04d", + "name": "modul-medikation", + "display": { + "original": "Medikation", + "translations": [ + { + "language": "de-DE", + "value": "Medikation" + }, + { + "language": "en-US", + "value": "Medication" + } + ] + }, + "url": "modul-medikation", + "module": "modul-medikation", + "selectable": false, + "leaf": false, + "children": [ { - "id": "de5c2903-00aa-4d64-92e5-2161d56e3daf", - "children": [ - { - "id": "69f4718f-1767-4517-90f8-6c079f627eed", - "name": "MII_PR_Medikation_MedicationStatement", - "display": "MII PR Medikation MedicationStatement", - "module": "modul-medikation", - "url": "https://www.medizininformatik-initiative.de/fhir/core/modul-medikation/StructureDefinition/MedicationStatement", - "leaf": true, - "selectable": true - }, - { - "id": "83b4d6be-b3e1-451a-adf2-c307477e7b97", - "name": "MII_PR_Medikation_Medikationsliste", - "display": "MII PR Medikation Medikationsliste", - "module": "modul-medikation", - "url": "https://www.medizininformatik-initiative.de/fhir/core/modul-medikation/StructureDefinition/medikationsliste", - "leaf": true, - "selectable": true - }, - { - "id": "fd375ed9-b3cd-414f-8a56-c70e10bb9609", - "name": "MII_PR_Medikation_MedicationRequest", - "display": "MII PR Medikation MedicationRequest", - "module": "modul-medikation", - "url": "https://www.medizininformatik-initiative.de/fhir/core/modul-medikation/StructureDefinition/MedicationRequest", - "leaf": true, - "selectable": true - }, - { - "id": "323a3ad1-1c10-45f5-848a-2dc5fb100c47", - "name": "MII_PR_Medikation_Medication", - "display": "MII PR Medikation Medication", - "module": "modul-medikation", - "url": "https://www.medizininformatik-initiative.de/fhir/core/modul-medikation/StructureDefinition/Medication", - "leaf": true, - "selectable": true - }, - { - "id": "81ed0f03-1d77-49c9-8391-d4c25702652c", - "name": "MII_PR_Medikation_MedicationAdministration", - "display": "MII PR Medikation MedicationAdministration", - "module": "modul-medikation", - "url": "https://www.medizininformatik-initiative.de/fhir/core/modul-medikation/StructureDefinition/MedicationAdministration", - "leaf": true, - "selectable": true - } - ], - "name": "modul-medikation", - "display": "Medikation", - "url": "https://www.medizininformatik-initiative.de/fhir/core/modul-medikation/StructureDefinition/MedicationStatement", - "module": "modul-medikation", - "selectable": true, - "leaf": false + "id": "5486577b-430b-4a42-a3c8-28c6d6eaf4e8", + "name": "MII_PR_Medikation_MedicationStatement", + "display": { + "original": "MII PR Medikation MedicationStatement", + "translations": [ + { + "language": "de-DE", + "value": "" + }, + { + "language": "en-US", + "value": "MII PR Medikation MedicationStatement" + } + ] + }, + "module": "modul-medikation", + "url": "https://www.medizininformatik-initiative.de/fhir/core/modul-medikation/StructureDefinition/MedicationStatement", + "leaf": true, + "selectable": true + }, + { + "id": "491759f1-8afc-4a35-9a35-34642211f02a", + "name": "MII_PR_Medikation_Medikationsliste", + "display": { + "original": "MII PR Medikation Medikationsliste", + "translations": [ + { + "language": "de-DE", + "value": "" + }, + { + "language": "en-US", + "value": "MII PR Medikation Medicationlist" + } + ] + }, + "module": "modul-medikation", + "url": "https://www.medizininformatik-initiative.de/fhir/core/modul-medikation/StructureDefinition/medikationsliste", + "leaf": true, + "selectable": true + }, + { + "id": "9d1fe081-3964-4951-b743-716d1b60d779", + "name": "MII_PR_Medikation_MedicationRequest", + "display": { + "original": "MII PR Medikation MedicationRequest", + "translations": [ + { + "language": "de-DE", + "value": "" + }, + { + "language": "en-US", + "value": "MII PR Medikation MedicationRequest" + } + ] + }, + "module": "modul-medikation", + "url": "https://www.medizininformatik-initiative.de/fhir/core/modul-medikation/StructureDefinition/MedicationRequest", + "leaf": true, + "selectable": true + }, + { + "id": "1026f31f-78ec-4e20-9b40-c5b02b857bc3", + "name": "MII_PR_Medikation_Medication", + "display": { + "original": "MII PR Medikation Medication", + "translations": [ + { + "language": "de-DE", + "value": "" + }, + { + "language": "en-US", + "value": "MII PR Medikation Medication" + } + ] + }, + "module": "modul-medikation", + "url": "https://www.medizininformatik-initiative.de/fhir/core/modul-medikation/StructureDefinition/Medication", + "leaf": true, + "selectable": true + }, + { + "id": "d8affe77-892f-45b5-8747-b5ca5400d1be", + "name": "MII_PR_Medikation_MedicationAdministration", + "display": { + "original": "MII PR Medikation MedicationAdministration", + "translations": [ + { + "language": "de-DE", + "value": "" + }, + { + "language": "en-US", + "value": "MII PR Medikation MedicationAdministration" + } + ] + }, + "module": "modul-medikation", + "url": "https://www.medizininformatik-initiative.de/fhir/core/modul-medikation/StructureDefinition/MedicationAdministration", + "leaf": true, + "selectable": true } - ] + ] + } + ] } \ No newline at end of file diff --git a/.gitignore b/.gitignore index 9a7ec987..d146abe4 100644 --- a/.gitignore +++ b/.gitignore @@ -56,4 +56,6 @@ googleCodeStyle.xml result.log ontology +update-availability/* +!update-availability/README.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 32f2ca2a..7379ee0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,21 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +## [UNRELEASED] - yyyy-mm-dd + +### Added +### Changed +### Deprecated +### Removed +### Fixed +### Security + +## [6.0.0-alpha.3] - 2024-10-21 + +### Changed +- Allow empty search +- Change structure of dse profile and details for new translation structure + ## [6.0.0-alpha.2] - 2024-09-04 ### Added diff --git a/docker-compose.yml b/docker-compose.yml index 8468c98f..7fada739 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -139,7 +139,7 @@ services: condition: service_healthy environment: ELASTIC_HOST: http://dataportal-elastic:9200 - ELASTIC_GIT_TAG: v3.0.0-test.2 + ELASTIC_GIT_TAG: v3.0.0-alpha ELASTIC_FILEPATH: https://github.com/medizininformatik-initiative/fhir-ontology-generator/raw/TAGPLACEHOLDER/example/fdpg-ontology/ ELASTIC_FILENAME: elastic.zip # if set to false, existing indices are not overridden. diff --git a/elastic-update-availability.sh b/elastic-update-availability.sh new file mode 100755 index 00000000..5c8bfdcc --- /dev/null +++ b/elastic-update-availability.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +set -e + +BASE_DIR="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 || exit 1 ; pwd -P )" +ES_BASE_URL=${ES_BASE_URL:-http://localhost:9200} +ES_INDEX=${ES_INDEX:-ontology} + +FILES=("$BASE_DIR"/update-availability/*) +for availUpdateBundle in "${FILES[@]}"; do + if [[ $(basename "$availUpdateBundle") == *.json ]]; then + echo "Sending Availability Update bundle $availUpdateBundle ..." + response=$(curl --write-out "%{http_code}" -s --output /dev/null -XPOST -H 'Content-Type: application/json' --data-binary @"$availUpdateBundle" "$ES_BASE_URL/$ES_INDEX/_bulk") + echo "$response" + else + echo "Skipping $availUpdateBundle (not a .json file)" + fi +done diff --git a/pom.xml b/pom.xml index fa82226f..d4ad56e1 100644 --- a/pom.xml +++ b/pom.xml @@ -6,13 +6,13 @@ org.springframework.boot spring-boot-starter-parent - 3.3.3 + 3.3.4 de.medizininformatik-initiative DataportalBackend - 6.0.0-alpha.2 + 6.0.0-alpha.3 Dataportal Backend Backend of the Dataportal @@ -27,7 +27,7 @@ 17 4.10.0 4.10.0 - v3.0.0-test.2 + v3.0.0-alpha @@ -49,17 +49,17 @@ io.undertow undertow-core - 2.3.16.Final + 2.3.17.Final io.undertow undertow-servlet - 2.3.16.Final + 2.3.17.Final io.undertow undertow-websockets-jsr - 2.3.16.Final + 2.3.17.Final @@ -141,7 +141,7 @@ org.springdoc springdoc-openapi-starter-webmvc-ui - 2.0.4 + 2.6.0 @@ -198,6 +198,12 @@ 6.6.2 + + ca.uhn.hapi.fhir + org.hl7.fhir.utilities + 6.3.26 + + com.bucket4j bucket4j-core @@ -237,7 +243,7 @@ de.medizininformatik-initiative sq2cql - 0.3.0 + v0.5.0-alpha.1 diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/config/WebSecurityConfig.java b/src/main/java/de/numcodex/feasibility_gui_backend/config/WebSecurityConfig.java index 6c0896d9..1f6982de 100644 --- a/src/main/java/de/numcodex/feasibility_gui_backend/config/WebSecurityConfig.java +++ b/src/main/java/de/numcodex/feasibility_gui_backend/config/WebSecurityConfig.java @@ -51,7 +51,7 @@ public class WebSecurityConfig { public static final String PATH_DSE = "/dse"; public static final String PATH_CODEABLE_CONCEPT = "/codeable-concept"; public static final String PATH_SWAGGER_UI = "/swagger-ui/**"; - public static final String PATH_SWAGGER_CONFIG = "/v4/api-docs/**"; + public static final String PATH_SWAGGER_CONFIG = "/v3/api-docs/**"; @Value("${app.keycloakAllowedRole}") private String keycloakAllowedRole; diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/dse/api/DisplayEntry.java b/src/main/java/de/numcodex/feasibility_gui_backend/dse/api/DisplayEntry.java new file mode 100644 index 00000000..2fae710f --- /dev/null +++ b/src/main/java/de/numcodex/feasibility_gui_backend/dse/api/DisplayEntry.java @@ -0,0 +1,15 @@ +package de.numcodex.feasibility_gui_backend.dse.api; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; + +import java.util.List; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@Builder +public record DisplayEntry( + @JsonProperty String original, + @JsonProperty List translations + ) { +} diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/dse/api/DseProfile.java b/src/main/java/de/numcodex/feasibility_gui_backend/dse/api/DseProfile.java index 649b0234..0ff50e78 100644 --- a/src/main/java/de/numcodex/feasibility_gui_backend/dse/api/DseProfile.java +++ b/src/main/java/de/numcodex/feasibility_gui_backend/dse/api/DseProfile.java @@ -10,7 +10,7 @@ @Builder public record DseProfile( @JsonProperty String url, - @JsonProperty String display, + @JsonProperty DisplayEntry display, @JsonProperty List fields, @JsonProperty List filters, @JsonProperty String errorCode, diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/dse/api/DseProfileTreeNode.java b/src/main/java/de/numcodex/feasibility_gui_backend/dse/api/DseProfileTreeNode.java index 93c954a5..4e8f9192 100644 --- a/src/main/java/de/numcodex/feasibility_gui_backend/dse/api/DseProfileTreeNode.java +++ b/src/main/java/de/numcodex/feasibility_gui_backend/dse/api/DseProfileTreeNode.java @@ -12,7 +12,7 @@ public record DseProfileTreeNode( @JsonProperty String id, @JsonProperty List children, @JsonProperty String name, - @JsonProperty String display, + @JsonProperty DisplayEntry display, @JsonProperty String module, @JsonProperty String url, @JsonProperty boolean leaf, diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/dse/api/Field.java b/src/main/java/de/numcodex/feasibility_gui_backend/dse/api/Field.java index 4ca0b3b4..a9312877 100644 --- a/src/main/java/de/numcodex/feasibility_gui_backend/dse/api/Field.java +++ b/src/main/java/de/numcodex/feasibility_gui_backend/dse/api/Field.java @@ -10,8 +10,8 @@ @Builder public record Field( @JsonProperty String id, - @JsonProperty String display, - @JsonProperty String name, + @JsonProperty DisplayEntry display, + @JsonProperty DisplayEntry description, @JsonProperty String type, @JsonProperty List children ) { diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/dse/api/LocalizedValue.java b/src/main/java/de/numcodex/feasibility_gui_backend/dse/api/LocalizedValue.java new file mode 100644 index 00000000..e2edf722 --- /dev/null +++ b/src/main/java/de/numcodex/feasibility_gui_backend/dse/api/LocalizedValue.java @@ -0,0 +1,13 @@ +package de.numcodex.feasibility_gui_backend.dse.api; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@Builder +public record LocalizedValue( + @JsonProperty String language, + @JsonProperty String value +) { +} diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/query/QueryHandlerService.java b/src/main/java/de/numcodex/feasibility_gui_backend/query/QueryHandlerService.java index 802573ac..2bfccdec 100644 --- a/src/main/java/de/numcodex/feasibility_gui_backend/query/QueryHandlerService.java +++ b/src/main/java/de/numcodex/feasibility_gui_backend/query/QueryHandlerService.java @@ -203,6 +203,7 @@ private Query convertQueryToApi(de.numcodex.feasibility_gui_backend.query.persis .content(jsonUtil.readValue(in.getQueryContent().getQueryContent(), StructuredQuery.class)) .label(savedQuery.get().getLabel()) .comment(savedQuery.get().getComment()) + .totalNumberOfPatients(savedQuery.get().getResultSize()) .build(); } else { return Query.builder() diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/query/api/Query.java b/src/main/java/de/numcodex/feasibility_gui_backend/query/api/Query.java index afd5e2a2..853859ab 100644 --- a/src/main/java/de/numcodex/feasibility_gui_backend/query/api/Query.java +++ b/src/main/java/de/numcodex/feasibility_gui_backend/query/api/Query.java @@ -11,7 +11,8 @@ public record Query( @JsonProperty long id, @JsonProperty StructuredQuery content, @JsonProperty String label, - @JsonProperty String comment + @JsonProperty String comment, + @JsonProperty long totalNumberOfPatients ) { } diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/query/translation/QueryTranslatorSpringConfig.java b/src/main/java/de/numcodex/feasibility_gui_backend/query/translation/QueryTranslatorSpringConfig.java index de1d42dd..6f246e41 100644 --- a/src/main/java/de/numcodex/feasibility_gui_backend/query/translation/QueryTranslatorSpringConfig.java +++ b/src/main/java/de/numcodex/feasibility_gui_backend/query/translation/QueryTranslatorSpringConfig.java @@ -5,7 +5,8 @@ import de.numcodex.sq2cql.Translator; import de.numcodex.sq2cql.model.Mapping; import de.numcodex.sq2cql.model.MappingContext; -import de.numcodex.sq2cql.model.TermCodeNode; +import de.numcodex.sq2cql.model.MappingTreeBase; +import de.numcodex.sq2cql.model.MappingTreeModuleRoot; import org.springframework.beans.factory.annotation.BeanFactoryAnnotationUtils; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; @@ -18,6 +19,7 @@ import java.io.File; import java.io.IOException; +import java.util.Arrays; import java.util.HashMap; import java.util.Map; import java.util.Objects; @@ -78,35 +80,47 @@ QueryTranslationComponent createQueryTranslationService(@Qualifier("json") Query @Bean Translator createCqlTranslator(@Qualifier("translation") ObjectMapper jsonUtil) throws IOException { var mappings = jsonUtil.readValue(new File(mappingsFile), Mapping[].class); - var conceptTree = jsonUtil.readValue(new File(conceptTreeFile), TermCodeNode.class); + var mappingTreeBase = new MappingTreeBase(Arrays.stream(jsonUtil.readValue(new File(conceptTreeFile), MappingTreeModuleRoot[].class)).toList()); + return Translator.of(MappingContext.of( Stream.of(mappings) .collect(Collectors.toMap(Mapping::key, Function.identity(), (a, b) -> a)), - conceptTree, - Map.ofEntries(entry("http://fhir.de/CodeSystem/bfarm/icd-10-gm", "icd10"), - entry("mii.abide", "abide"), - entry("http://fhir.de/CodeSystem/bfarm/ops", "ops"), - entry("http://dicom.nema.org/resources/ontology/DCM", "dcm"), - entry("https://www.medizininformatik-initiative.de/fhir/core/modul-person/CodeSystem/Vitalstatus", "vitalstatus"), - entry("http://loinc.org", "loinc"), - entry("https://fhir.bbmri.de/CodeSystem/SampleMaterialType", "sample"), - entry("http://fhir.de/CodeSystem/bfarm/atc", "atc"), - entry("http://snomed.info/sct", "snomed"), - entry("http://terminology.hl7.org/CodeSystem/condition-ver-status", "cvs"), - entry("http://hl7.org/fhir/administrative-gender", "gender"), - entry("urn:oid:1.2.276.0.76.5.409", "urn409"), - entry( - "https://www.netzwerk-universitaetsmedizin.de/fhir/CodeSystem/ecrf-parameter-codes", - "numecrf"), - entry("urn:iso:std:iso:3166", "iso3166"), - entry("https://www.netzwerk-universitaetsmedizin.de/fhir/CodeSystem/frailty-score", - "frailtyscore"), - entry("http://terminology.hl7.org/CodeSystem/consentcategorycodes", "consentcategory"), - entry("urn:oid:2.16.840.1.113883.3.1937.777.24.5.3", "consent"), - entry("http://hl7.org/fhir/sid/icd-o-3", "icdo3"), - entry("http://hl7.org/fhir/consent-provision-type", "provisiontype")))); + mappingTreeBase, + Map.ofEntries(entry("http://fhir.de/CodeSystem/bfarm/icd-10-gm", "icd10"), + entry("mii.abide", "abide"), + entry("http://fhir.de/CodeSystem/bfarm/ops", "ops"), + entry("http://dicom.nema.org/resources/ontology/DCM", "dcm"), + entry("https://www.medizininformatik-initiative.de/fhir/core/modul-person/CodeSystem/Vitalstatus", "vitalstatus"), + entry("http://loinc.org", "loinc"), + entry("https://fhir.bbmri.de/CodeSystem/SampleMaterialType", "sample"), + entry("http://fhir.de/CodeSystem/bfarm/atc", "atc"), + entry("http://snomed.info/sct", "snomed"), + entry("http://terminology.hl7.org/CodeSystem/condition-ver-status", "cvs"), + entry("http://hl7.org/fhir/administrative-gender", "gender"), + entry("urn:oid:1.2.276.0.76.5.409", "urn409"), + entry( + "https://www.netzwerk-universitaetsmedizin.de/fhir/CodeSystem/ecrf-parameter-codes", + "numecrf"), + entry("urn:iso:std:iso:3166", "iso3166"), + entry("https://www.netzwerk-universitaetsmedizin.de/fhir/CodeSystem/frailty-score", + "frailtyscore"), + entry("http://terminology.hl7.org/CodeSystem/consentcategorycodes", "consentcategory"), + entry("urn:oid:2.16.840.1.113883.3.1937.777.24.5.3", "consent"), + entry("http://hl7.org/fhir/sid/icd-o-3", "icdo3"), + entry("fdpg.mii.cds", "fdpgmiicds"), + entry("http://fhir.de/CodeSystem/bfarm/alpha-id", "alphaid"), + entry("urn:iso:std:iso:11073:10101", "ISO11073"), + entry("http://terminology.hl7.org/CodeSystem/icd-o-3", "icdo3"), + entry("http://fhir.de/CodeSystem/dkgev/Fachabteilungsschluessel", "fachabteilungsschluessel"), + entry("http://terminology.hl7.org/CodeSystem/v3-ActCode", "v3actcode"), + entry("http://fhir.de/CodeSystem/dkgev/Fachabteilungsschluessel-erweitert", "fachabteilungsschluesselerweitert"), + entry("http://fhir.de/CodeSystem/kontaktart-de", "kontaktart"), + entry("http://hl7.org/fhir/sid/icd-10", "sidicd10"), + entry("http://fhir.de/CodeSystem/Kontaktebene", "kontaktebene"), + entry("http://hl7.org/fhir/consent-provision-type", "provisiontype")))); } + @Qualifier("cql") @Lazy @Bean diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/query/v4/QueryHandlerRestController.java b/src/main/java/de/numcodex/feasibility_gui_backend/query/v4/QueryHandlerRestController.java index b84a1e37..055d9bbc 100644 --- a/src/main/java/de/numcodex/feasibility_gui_backend/query/v4/QueryHandlerRestController.java +++ b/src/main/java/de/numcodex/feasibility_gui_backend/query/v4/QueryHandlerRestController.java @@ -309,6 +309,7 @@ public ResponseEntity getQuery(@PathVariable("id") Long queryId, .content(structuredQueryValidation.annotateStructuredQuery(query.content(), skipValidation)) .label(query.label()) .comment(query.comment()) + .totalNumberOfPatients(query.totalNumberOfPatients()) .build(); return new ResponseEntity<>(annotatedQuery, HttpStatus.OK); } diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/terminology/es/CodeableConceptService.java b/src/main/java/de/numcodex/feasibility_gui_backend/terminology/es/CodeableConceptService.java index f0dbd403..a4d97f63 100644 --- a/src/main/java/de/numcodex/feasibility_gui_backend/terminology/es/CodeableConceptService.java +++ b/src/main/java/de/numcodex/feasibility_gui_backend/terminology/es/CodeableConceptService.java @@ -78,15 +78,24 @@ private SearchHits findByCodeOrDisplay(String keyword, }); } - var mmQuery = new MultiMatchQuery.Builder() - .query(keyword) - .fields(List.of("termcode.display", "termcode.code^2")) - .build(); - - var boolQuery = new BoolQuery.Builder() - .must(List.of(mmQuery._toQuery())) - .filter(filterTerms.isEmpty() ? List.of() : filterTerms) - .build(); + BoolQuery boolQuery; + + if (keyword.isEmpty()) { + boolQuery = new BoolQuery.Builder() + .filter(filterTerms.isEmpty() ? List.of() : filterTerms) + .build(); + + } else { + var mmQuery = new MultiMatchQuery.Builder() + .query(keyword) + .fields(List.of("termcode.display", "termcode.code^2")) + .build(); + + boolQuery = new BoolQuery.Builder() + .must(List.of(mmQuery._toQuery())) + .filter(filterTerms.isEmpty() ? List.of() : filterTerms) + .build(); + } var query = new NativeQueryBuilder() .withQuery(boolQuery._toQuery()) diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/terminology/es/TerminologyEsService.java b/src/main/java/de/numcodex/feasibility_gui_backend/terminology/es/TerminologyEsService.java index 49aef356..6c54ee55 100644 --- a/src/main/java/de/numcodex/feasibility_gui_backend/terminology/es/TerminologyEsService.java +++ b/src/main/java/de/numcodex/feasibility_gui_backend/terminology/es/TerminologyEsService.java @@ -1,6 +1,8 @@ package de.numcodex.feasibility_gui_backend.terminology.es; import co.elastic.clients.elasticsearch._types.FieldValue; +import co.elastic.clients.elasticsearch._types.InlineScript; +import co.elastic.clients.elasticsearch._types.Script; import co.elastic.clients.elasticsearch._types.aggregations.Aggregation; import co.elastic.clients.elasticsearch._types.aggregations.StringTermsBucket; import co.elastic.clients.elasticsearch._types.query_dsl.*; @@ -139,22 +141,55 @@ private SearchHits findByNameOrTermcode(String keyword }); } - var mmQuery = new MultiMatchQuery.Builder() - .query(keyword) - .fields(List.of("name", "termcode^2")) + BoolQuery boolQuery; + + if (keyword.isEmpty()) { + boolQuery = new BoolQuery.Builder() + .filter(filterTerms.isEmpty() ? List.of() : filterTerms) + .build(); + } else { + var mmQuery = new MultiMatchQuery.Builder() + .query(keyword) + .fields(List.of("name", "termcode^2")) + .build(); + + boolQuery = new BoolQuery.Builder() + .must(List.of(mmQuery._toQuery())) + .filter(filterTerms.isEmpty() ? List.of() : filterTerms) + .build(); + } + + var innerQuery = new NativeQueryBuilder() + .withQuery(boolQuery._toQuery()) + .withPageable(pageRequest) .build(); - var boolQuery = new BoolQuery.Builder() - .must(List.of(mmQuery._toQuery())) - .filter(filterTerms.isEmpty() ? List.of() : filterTerms) + var inlineScript = new InlineScript.Builder() + .source("doc['availability'].value == 0 ? _score : _score + 100") .build(); - var query = new NativeQueryBuilder() - .withQuery(boolQuery._toQuery()) + var availabilityScoreScript = new Script.Builder() + .inline(inlineScript) + .build(); + + var function = FunctionScoreBuilders.scriptScore() + .script(availabilityScoreScript) + .build(); + + var functionList = List.of(function._toFunctionScore()); + + var functionScoreQuery = new FunctionScoreQuery.Builder() + .query(innerQuery.getQuery()) + .functions(functionList) + .boostMode(FunctionBoostMode.Replace) + .build(); + + var finalQuery = new NativeQueryBuilder() + .withQuery(functionScoreQuery._toQuery()) .withPageable(pageRequest) .build(); - return operations.search(query, OntologyListItemDocument.class); + return operations.search(finalQuery, OntologyListItemDocument.class); } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 560a4550..5a5f62f4 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -28,8 +28,10 @@ spring: springdoc: swagger-ui: - url: /v4/api-docs/swagger.yaml + disable-swagger-default-url: true tryItOutEnabled: false + path: /swagger-ui + url: /v3/api-docs/swagger.yaml management: endpoints: enabled-by-default: false @@ -39,7 +41,6 @@ management: health: elasticsearch: enabled: ${ELASTIC_SEARCH_ENABLED:true} - app: dseProfileTreeFile: ${DSE_PROFILE_TREE_FILE:ontology/dse/profile_tree.json} terminologySystemsFile: ${TERMINOLOGY_SYSTEMS_FILE:ontology/terminology_systems.json} diff --git a/src/main/resources/static/v4/api-docs/swagger.yaml b/src/main/resources/static/v3/api-docs/swagger.yaml similarity index 100% rename from src/main/resources/static/v4/api-docs/swagger.yaml rename to src/main/resources/static/v3/api-docs/swagger.yaml diff --git a/src/test/java/de/numcodex/feasibility_gui_backend/dse/DseServiceTest.java b/src/test/java/de/numcodex/feasibility_gui_backend/dse/DseServiceTest.java index c357147b..a23cf6b1 100644 --- a/src/test/java/de/numcodex/feasibility_gui_backend/dse/DseServiceTest.java +++ b/src/test/java/de/numcodex/feasibility_gui_backend/dse/DseServiceTest.java @@ -3,7 +3,9 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException; +import de.numcodex.feasibility_gui_backend.dse.api.DisplayEntry; import de.numcodex.feasibility_gui_backend.dse.api.DseProfileTreeNode; +import de.numcodex.feasibility_gui_backend.dse.api.LocalizedValue; import de.numcodex.feasibility_gui_backend.dse.persistence.DseProfile; import de.numcodex.feasibility_gui_backend.dse.persistence.DseProfileRepository; import org.junit.jupiter.api.BeforeEach; @@ -129,9 +131,23 @@ private de.numcodex.feasibility_gui_backend.dse.api.DseProfile createDummyDsePro return de.numcodex.feasibility_gui_backend.dse.api.DseProfile.builder() .url("http://example.com") - .display("some-display") + .display(createDummyDisplayEntry()) .fields(List.of()) .filters(List.of()) .build(); } + + private DisplayEntry createDummyDisplayEntry() { + return DisplayEntry.builder() + .original("some-display") + .translations(List.of(createDummyTranslation())) + .build(); + } + + private LocalizedValue createDummyTranslation() { + return LocalizedValue.builder() + .language("en") + .value("display value") + .build(); + } } \ No newline at end of file diff --git a/src/test/java/de/numcodex/feasibility_gui_backend/dse/v4/DseRestControllerIT.java b/src/test/java/de/numcodex/feasibility_gui_backend/dse/v4/DseRestControllerIT.java index cc9a2669..f3932f74 100644 --- a/src/test/java/de/numcodex/feasibility_gui_backend/dse/v4/DseRestControllerIT.java +++ b/src/test/java/de/numcodex/feasibility_gui_backend/dse/v4/DseRestControllerIT.java @@ -2,8 +2,10 @@ import com.fasterxml.jackson.databind.ObjectMapper; import de.numcodex.feasibility_gui_backend.dse.DseService; +import de.numcodex.feasibility_gui_backend.dse.api.DisplayEntry; import de.numcodex.feasibility_gui_backend.dse.api.DseProfile; import de.numcodex.feasibility_gui_backend.dse.api.DseProfileTreeNode; +import de.numcodex.feasibility_gui_backend.dse.api.LocalizedValue; import de.numcodex.feasibility_gui_backend.query.ratelimiting.RateLimitingInterceptor; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; @@ -77,7 +79,7 @@ void testGetProfileData_succeedsWith200OnFoundProfile() throws Exception { mockMvc.perform(get(URI.create(PATH_API + PATH_DSE + "/profile-data")).param("ids", "https://www.medizininformatik-initiative.de/fhir/core/modul-labor/StructureDefinition/ObservationLab,foobar").with(csrf())) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) - .andExpect(jsonPath("$[0].display").value("some-display")); + .andExpect(jsonPath("$[0].display.original").value("some-display")); } @Test @@ -95,9 +97,23 @@ private DseProfile createDummyDseProfileEntry() { return DseProfile.builder() .url("http://example.com") - .display("some-display") + .display(createDummyDisplayEntry()) .fields(List.of()) .filters(List.of()) .build(); } + + private DisplayEntry createDummyDisplayEntry() { + return DisplayEntry.builder() + .original("some-display") + .translations(List.of(createDummyTranslation())) + .build(); + } + + private LocalizedValue createDummyTranslation() { + return LocalizedValue.builder() + .language("en") + .value("display value") + .build(); + } } \ No newline at end of file diff --git a/src/test/java/de/numcodex/feasibility_gui_backend/terminology/es/CodeableConceptServiceIT.java b/src/test/java/de/numcodex/feasibility_gui_backend/terminology/es/CodeableConceptServiceIT.java index 9871a23f..f09a2683 100644 --- a/src/test/java/de/numcodex/feasibility_gui_backend/terminology/es/CodeableConceptServiceIT.java +++ b/src/test/java/de/numcodex/feasibility_gui_backend/terminology/es/CodeableConceptServiceIT.java @@ -96,6 +96,14 @@ void testPerformCodeableConceptSearchWithRepoAndPaging_findsNone() { assertThat(page.getTotalHits()).isZero(); } + @Test + void testPerformCodeableConceptSearchWithRepoAndPaging_findsAllWithNoKeyword() { + var page = assertDoesNotThrow (() -> codeableConceptService.performCodeableConceptSearchWithRepoAndPaging("", List.of(), 20, 0)); + + assertNotNull(page); + assertThat(page.getTotalHits()).isEqualTo(3L); + } + @Test void testPerformCodeableConceptSearchWithRepoAndPaging_findsTwoOrOneDependingOnFilter() { var pageNoFilter = assertDoesNotThrow (() -> codeableConceptService.performCodeableConceptSearchWithRepoAndPaging("ba", List.of(), 20, 0)); diff --git a/src/test/java/de/numcodex/feasibility_gui_backend/terminology/es/CodeableConceptServiceTest.java b/src/test/java/de/numcodex/feasibility_gui_backend/terminology/es/CodeableConceptServiceTest.java index 2f1c0952..0a8b6bc0 100644 --- a/src/test/java/de/numcodex/feasibility_gui_backend/terminology/es/CodeableConceptServiceTest.java +++ b/src/test/java/de/numcodex/feasibility_gui_backend/terminology/es/CodeableConceptServiceTest.java @@ -94,6 +94,17 @@ void testPerformCodeableConceptSearchWithRepoAndPaging_succeedsWithEmptyResult() assertThat(result.getTotalHits()).isZero(); } + @Test + void testPerformCodeableConceptSearchWithRepoAndPaging_succeedsWithEmptyKeyword() { + SearchHits dummySearchHitsPage = createDummySearchHitsPage(5); + doReturn(dummySearchHitsPage).when(operations).search(any(NativeQuery.class), any(Class.class)); + + var result = assertDoesNotThrow(() -> codeableConceptService.performCodeableConceptSearchWithRepoAndPaging("", List.of(), 20, 0)); + + assertNotNull(result); + assertThat(result.getTotalHits()).isNotZero(); + } + @Test void testGetSearchResultEntryByCode_succeeds() { CodeableConceptDocument dummyCodeableConceptDocument = createDummyCodeableConceptDocument("1"); diff --git a/src/test/java/de/numcodex/feasibility_gui_backend/terminology/es/TerminologyEsServiceIT.java b/src/test/java/de/numcodex/feasibility_gui_backend/terminology/es/TerminologyEsServiceIT.java index 1ea58307..142a12fd 100644 --- a/src/test/java/de/numcodex/feasibility_gui_backend/terminology/es/TerminologyEsServiceIT.java +++ b/src/test/java/de/numcodex/feasibility_gui_backend/terminology/es/TerminologyEsServiceIT.java @@ -107,6 +107,13 @@ void testPerformOntologySearchWithPaging_zeroResults() { assertThat(page.totalHits()).isZero(); } + @Test + void testPerformOntologySearchWithPaging_findsAllWithNoKeyword() { + var page = terminologyEsService.performOntologySearchWithPaging("", null, null, null, null, false, 20, 0); + assertThat(page).isNotNull(); + assertThat(page.totalHits()).isEqualTo(4L); + } + @Test void testPerformOntologySearchWithPaging_oneResult() { var page = terminologyEsService.performOntologySearchWithPaging("Hauttransplan", null, null, null, null, false, 20, 0); diff --git a/src/test/java/de/numcodex/feasibility_gui_backend/terminology/es/TerminologyEsServiceTest.java b/src/test/java/de/numcodex/feasibility_gui_backend/terminology/es/TerminologyEsServiceTest.java index f80e5e82..b71ed0ec 100644 --- a/src/test/java/de/numcodex/feasibility_gui_backend/terminology/es/TerminologyEsServiceTest.java +++ b/src/test/java/de/numcodex/feasibility_gui_backend/terminology/es/TerminologyEsServiceTest.java @@ -150,6 +150,23 @@ void testPerformOntologySearchWithPaging(List criteriaSets, List assertThat(searchResult.results()).containsExactlyInAnyOrderElementsOf(dummySearchHitsPage.getSearchHits().stream().map(sh -> EsSearchResultEntry.of(sh.getContent())).toList()); } + @ParameterizedTest + @MethodSource("generateArgumentsForTestPerformOntologySearchWithPaging") + void testPerformOntologySearchWithPagingEmptyKeyword(List criteriaSets, List context, List kdsModule, List terminology, Boolean availability, Integer pageSize, Integer page) { + int totalHits = new Random().nextInt(100); + SearchHits dummySearchHitsPage = createDummySearchHitsPage(totalHits); + doReturn(dummySearchHitsPage).when(operations).search(any(NativeQuery.class), any(Class.class)); + + var searchResult = assertDoesNotThrow( + () -> terminologyEsService.performOntologySearchWithPaging( + "", criteriaSets, context, kdsModule, terminology, availability, pageSize, page) + ); + + assertThat(searchResult.totalHits()).isEqualTo(totalHits); + assertThat(searchResult.results().size()).isEqualTo(dummySearchHitsPage.getTotalHits()); + assertThat(searchResult.results()).containsExactlyInAnyOrderElementsOf(dummySearchHitsPage.getSearchHits().stream().map(sh -> EsSearchResultEntry.of(sh.getContent())).toList()); + } + @Test void testPerformOntologySearchWithRepoAndPaging_throwsOnInvalidPageSize() { assertThrows(IllegalArgumentException.class, () -> terminologyEsService.performOntologySearchWithPaging( diff --git a/src/test/resources/ontology/dse/profile_tree.json b/src/test/resources/ontology/dse/profile_tree.json index 718c85e4..83c1b732 100644 --- a/src/test/resources/ontology/dse/profile_tree.json +++ b/src/test/resources/ontology/dse/profile_tree.json @@ -7,45 +7,105 @@ "id": "de5c2903-00aa-4d64-92e5-2161d56e3daf", "children": [ { - "id": "69f4718f-1767-4517-90f8-6c079f627eed", + "id": "28913b3f-3f9d-473b-a38f-2e63f7f45ea3", "name": "MII_PR_Medikation_MedicationStatement", - "display": "MII PR Medikation MedicationStatement", + "display": { + "original": "MII PR Medikation MedicationStatement", + "translations": [ + { + "language": "de-DE", + "value": "" + }, + { + "language": "en-US", + "value": "MII PR Medikation MedicationStatement" + } + ] + }, "module": "modul-medikation", "url": "https://www.medizininformatik-initiative.de/fhir/core/modul-medikation/StructureDefinition/MedicationStatement", "leaf": true, "selectable": true }, { - "id": "83b4d6be-b3e1-451a-adf2-c307477e7b97", + "id": "d90ed770-c7ec-4b00-8032-539444b31040", "name": "MII_PR_Medikation_Medikationsliste", - "display": "MII PR Medikation Medikationsliste", + "display": { + "original": "MII PR Medikation Medikationsliste", + "translations": [ + { + "language": "de-DE", + "value": "" + }, + { + "language": "en-US", + "value": "MII PR Medikation Medicationlist" + } + ] + }, "module": "modul-medikation", "url": "https://www.medizininformatik-initiative.de/fhir/core/modul-medikation/StructureDefinition/medikationsliste", "leaf": true, "selectable": true }, { - "id": "fd375ed9-b3cd-414f-8a56-c70e10bb9609", + "id": "b149b0f7-368e-4529-ae6a-11cf9ce46fd2", "name": "MII_PR_Medikation_MedicationRequest", - "display": "MII PR Medikation MedicationRequest", + "display": { + "original": "MII PR Medikation MedicationRequest", + "translations": [ + { + "language": "de-DE", + "value": "" + }, + { + "language": "en-US", + "value": "MII PR Medikation MedicationRequest" + } + ] + }, "module": "modul-medikation", "url": "https://www.medizininformatik-initiative.de/fhir/core/modul-medikation/StructureDefinition/MedicationRequest", "leaf": true, "selectable": true }, { - "id": "323a3ad1-1c10-45f5-848a-2dc5fb100c47", + "id": "265efd74-ec95-4a9d-ba1d-a67d346f6e16", "name": "MII_PR_Medikation_Medication", - "display": "MII PR Medikation Medication", + "display": { + "original": "MII PR Medikation Medication", + "translations": [ + { + "language": "de-DE", + "value": "" + }, + { + "language": "en-US", + "value": "MII PR Medikation Medication" + } + ] + }, "module": "modul-medikation", "url": "https://www.medizininformatik-initiative.de/fhir/core/modul-medikation/StructureDefinition/Medication", "leaf": true, "selectable": true }, { - "id": "81ed0f03-1d77-49c9-8391-d4c25702652c", + "id": "47609129-402a-4daf-9652-dd2c2fcc533d", "name": "MII_PR_Medikation_MedicationAdministration", - "display": "MII PR Medikation MedicationAdministration", + "display": { + "original": "MII PR Medikation MedicationAdministration", + "translations": [ + { + "language": "de-DE", + "value": "" + }, + { + "language": "en-US", + "value": "MII PR Medikation MedicationAdministration" + } + ] + }, "module": "modul-medikation", "url": "https://www.medizininformatik-initiative.de/fhir/core/modul-medikation/StructureDefinition/MedicationAdministration", "leaf": true, @@ -53,7 +113,19 @@ } ], "name": "modul-medikation", - "display": "Medikation", + "display": { + "original": "Medikation", + "translations": [ + { + "language": "de-DE", + "value": "Medikation" + }, + { + "language": "en-US", + "value": "Medication" + } + ] + }, "url": "https://www.medizininformatik-initiative.de/fhir/core/modul-medikation/StructureDefinition/MedicationStatement", "module": "modul-medikation", "selectable": true, diff --git a/update-availability/README.md b/update-availability/README.md new file mode 100644 index 00000000..426451ee --- /dev/null +++ b/update-availability/README.md @@ -0,0 +1,31 @@ +# Folder to add availabilty update files + + +## Example file structure for update .json files + +Note that elastic search accepts this file structure of newline delimited json input even though the document as a whole is not a valid json. +The files must be terminated by a newline. + +``` +{"update": {"_id": "3c94e2d6-2e27-35b2-aeb7-90a476efd099"}} +{"doc": {"availability": 1000}} +{"update": {"_id": "017ec73f-65f9-3f09-99ac-d27ce04a3875"}} +{"doc": {"availability": 0}} +{"update": {"_id": "cba79ec5-7a8b-33d4-9457-9747c7b4035a"}} +{"doc": {"availability": 0}} +{"update": {"_id": "0b39df9d-91bf-35a6-bd5b-9d5e21d611d2"}} +{"doc": {"availability": 0}} +{"update": {"_id": "fad536d0-cb09-342c-af41-311822a79f0e"}} +{"doc": {"availability": 0}} +{"update": {"_id": "ad09df7a-e33d-3910-a581-7b9b365117da"}} +{"doc": {"availability": 100}} +{"update": {"_id": "8550edea-8e1a-3a3b-bfa5-f6a5ca759b39"}} +{"doc": {"availability": 0}} +{"update": {"_id": "8c9885ce-9813-3057-8281-4d0fe8f27d2a"}} +{"doc": {"availability": 0}} +{"update": {"_id": "b7d623d8-14d9-3b02-a719-0f6b81cda017"}} +{"doc": {"availability": 100}} +{"update": {"_id": "69b733e2-99eb-3a12-bcd1-8e7b16c750ae"}} +{"doc": {"availability": 10000}} + +``` \ No newline at end of file