diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/terminology/TerminologyService.java b/src/main/java/de/numcodex/feasibility_gui_backend/terminology/TerminologyService.java index 2c32cd25..b54f4e9a 100644 --- a/src/main/java/de/numcodex/feasibility_gui_backend/terminology/TerminologyService.java +++ b/src/main/java/de/numcodex/feasibility_gui_backend/terminology/TerminologyService.java @@ -1,11 +1,13 @@ package de.numcodex.feasibility_gui_backend.terminology; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import de.numcodex.feasibility_gui_backend.terminology.api.CategoryEntry; import de.numcodex.feasibility_gui_backend.terminology.api.CriteriaProfileData; import de.numcodex.feasibility_gui_backend.terminology.api.TerminologyEntry; import de.numcodex.feasibility_gui_backend.terminology.persistence.*; +import lombok.NonNull; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; @@ -31,6 +33,9 @@ public class TerminologyService { private String uiProfilePath; + @NonNull + private ObjectMapper jsonUtil; + @Value("${app.ontologyOrder}") private List sortedCategories; private Map terminologyEntries = new HashMap<>(); @@ -42,7 +47,8 @@ public TerminologyService(@Value("${app.ontologyFolder}") String uiProfilePath, UiProfileRepository uiProfileRepository, TermCodeRepository termCodeRepository, ContextualizedTermCodeRepository contextualizedTermCodeRepository, - MappingRepository mappingRepository) throws IOException { + MappingRepository mappingRepository, + ObjectMapper jsonUtil) throws IOException { this.uiProfilePath = uiProfilePath; readInTerminologyEntries(); generateTerminologyEntriesWithoutDirectChildren(); @@ -51,6 +57,7 @@ public TerminologyService(@Value("${app.ontologyFolder}") String uiProfilePath, this.termCodeRepository = termCodeRepository; this.contextualizedTermCodeRepository = contextualizedTermCodeRepository; this.mappingRepository = mappingRepository; + this.jsonUtil = jsonUtil; } private void readInTerminologyEntries() throws IOException { @@ -204,41 +211,41 @@ public List getIntersection(String criteriaSetUrl, List contextT return contextualizedTermCodeRepository.filterByCriteriaSetUrl(criteriaSetUrl, contextTermCodeHashList); } - public List getCriteriaProfileData(List criterionIds) { + public List getCriteriaProfileData(List criteriaIds) { List results = new ArrayList<>(); - for (String id : criterionIds) { - CriteriaProfileData cse = new CriteriaProfileData(); + for (String id : criteriaIds) { + CriteriaProfileData criteriaProfileData = new CriteriaProfileData(); TermCode tc = termCodeRepository.findTermCodeByContextualizedTermcodeHash(id).orElse(null); Context c = termCodeRepository.findContextByContextualizedTermcodeHash(id).orElse(null); - cse.setId(id); + criteriaProfileData.setId(id); try { - cse.setUiProfile(getUiProfile(id)); - } catch (UiProfileNotFoundException e) { - cse.setUiProfile(null); + de.numcodex.feasibility_gui_backend.terminology.api.UiProfile uip = jsonUtil.readValue(getUiProfile(id), de.numcodex.feasibility_gui_backend.terminology.api.UiProfile.class); + criteriaProfileData.setUiProfile(uip); + } catch (UiProfileNotFoundException | JsonProcessingException e) { + criteriaProfileData.setUiProfile(null); } if (c != null) { - cse.setContext(de.numcodex.feasibility_gui_backend.common.api.TermCode.builder() + criteriaProfileData.setContext(de.numcodex.feasibility_gui_backend.common.api.TermCode.builder() .code(c.getCode()) .display(c.getDisplay()) .system(c.getSystem()) .version(c.getVersion()) .build()); } else { - cse.setContext(null); + criteriaProfileData.setContext(null); } if (tc != null) { - cse.setTermCode(de.numcodex.feasibility_gui_backend.common.api.TermCode.builder() + criteriaProfileData.setTermCode(de.numcodex.feasibility_gui_backend.common.api.TermCode.builder() .code(tc.getCode()) .display(tc.getDisplay()) .system(tc.getSystem()) .version(tc.getVersion()) .build()); } else { - cse.setTermCode(null); + criteriaProfileData.setTermCode(null); } - - results.add(cse); + results.add(criteriaProfileData); } return results; diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/terminology/api/CriteriaProfileData.java b/src/main/java/de/numcodex/feasibility_gui_backend/terminology/api/CriteriaProfileData.java index 72a60ed0..6848ee71 100644 --- a/src/main/java/de/numcodex/feasibility_gui_backend/terminology/api/CriteriaProfileData.java +++ b/src/main/java/de/numcodex/feasibility_gui_backend/terminology/api/CriteriaProfileData.java @@ -19,5 +19,5 @@ public class CriteriaProfileData { @EqualsAndHashCode.Include private TermCode termCode ; @JsonProperty("uiProfile") - private String uiProfile; + private UiProfile uiProfile; } diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/terminology/api/EsSearchResultEntry.java b/src/main/java/de/numcodex/feasibility_gui_backend/terminology/api/EsSearchResultEntry.java index af88ce27..e042199d 100644 --- a/src/main/java/de/numcodex/feasibility_gui_backend/terminology/api/EsSearchResultEntry.java +++ b/src/main/java/de/numcodex/feasibility_gui_backend/terminology/api/EsSearchResultEntry.java @@ -14,6 +14,7 @@ public class EsSearchResultEntry { private String terminology; private String termcode; private String kdsModule; + private boolean selectable; public static EsSearchResultEntry of(OntologyListItemDocument ontologyListItemDocument) { return EsSearchResultEntry.builder() @@ -24,6 +25,7 @@ public static EsSearchResultEntry of(OntologyListItemDocument ontologyListItemDo .terminology(ontologyListItemDocument.getTerminology()) .termcode(ontologyListItemDocument.getTermcode()) .kdsModule(ontologyListItemDocument.getKdsModule()) + .selectable(ontologyListItemDocument.isSelectable()) .build(); } } diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/terminology/api/UiProfile.java b/src/main/java/de/numcodex/feasibility_gui_backend/terminology/api/UiProfile.java new file mode 100644 index 00000000..922fb25a --- /dev/null +++ b/src/main/java/de/numcodex/feasibility_gui_backend/terminology/api/UiProfile.java @@ -0,0 +1,15 @@ +package de.numcodex.feasibility_gui_backend.terminology.api; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Data; + +import java.util.List; + +@Data +@JsonInclude(JsonInclude.Include.NON_NULL) +public class UiProfile { + private String name; + private boolean timeRestrictionAllowed; + private String valueDefinition; + private List attributeDefinitions; +} 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 f3c041d5..d3e2524c 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 @@ -11,6 +11,8 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.elasticsearch.client.elc.ElasticsearchAggregations; @@ -19,14 +21,15 @@ import org.springframework.data.elasticsearch.core.SearchHits; import org.springframework.data.elasticsearch.core.query.Criteria; import org.springframework.data.elasticsearch.core.query.CriteriaQuery; +import org.springframework.data.util.Pair; import org.springframework.lang.Nullable; import org.springframework.stereotype.Service; -import java.util.ArrayList; -import java.util.List; +import java.util.*; @Service @Slf4j +@ConditionalOnExpression("${app.elastic.enabled}") public class TerminologyEsService { private ElasticsearchOperations operations; @@ -44,8 +47,9 @@ public TerminologyEsService(ElasticsearchOperations operations, OntologyItemEsRe this.ontologyListItemEsRepository = ontologyListItemEsRepository; } - public OntologyItemDocument getOntologyItemByHash(String hash) { - return ontologyItemEsRepository.findById(hash).orElseThrow(OntologyItemNotFoundException::new); + public EsSearchResultEntry getSearchResultEntryByHash(String hash) { + var ontologyItem = ontologyListItemEsRepository.findById(hash).orElseThrow(OntologyItemNotFoundException::new); + return EsSearchResultEntry.of(ontologyItem); } public List getAvailableFilters() { @@ -85,6 +89,9 @@ public OntologySearchResult performOntologySearchWithRepo(String keyword, .build(); } + /* + I know this is kinda stupid, but it works for now (availability has to be included though). + */ public EsSearchResult performOntologySearchWithRepoAndPaging(String keyword, @Nullable List context, @Nullable List kdsModule, @@ -93,8 +100,61 @@ public EsSearchResult performOntologySearchWithRepoAndPaging(String keyword, @Nullable int pageSize, @Nullable int page) { -// var searchHitPage = ontologyListItemEsRepository.findByNameContainingIgnoreCaseOrTermcodeContainingIgnoreCase(keyword, keyword, PageRequest.of(page, pageSize)); - var searchHitPage = ontologyListItemEsRepository.findByNameOrTermcodeMultiMatch(keyword, PageRequest.of(page, pageSize)); + + List>> filterList = new ArrayList<>(); + if (context != null) { + filterList.add(Pair.of("context.code", context)); + } + if (kdsModule != null) { + filterList.add(Pair.of("kds_module", kdsModule)); + } + if (terminology != null) { + filterList.add(Pair.of("terminology", terminology)); + } + + Page searchHitPage; + + switch (filterList.size()) { + case 0 -> { + searchHitPage = ontologyListItemEsRepository + .findByNameOrTermcodeMultiMatch0Filters(keyword, + PageRequest.of(page, pageSize)); + } + case 1 -> {searchHitPage = ontologyListItemEsRepository + .findByNameOrTermcodeMultiMatch1Filter(keyword, + filterList.get(0).getFirst(), + filterList.get(0).getSecond(), + PageRequest.of(page, pageSize)); + } + case 2 -> {searchHitPage = ontologyListItemEsRepository + .findByNameOrTermcodeMultiMatch2Filters(keyword, + filterList.get(0).getFirst(), + filterList.get(0).getSecond(), + filterList.get(1).getFirst(), + filterList.get(1).getSecond(), + PageRequest.of(page, pageSize)); + } + case 3 -> {searchHitPage = ontologyListItemEsRepository + .findByNameOrTermcodeMultiMatch3Filters(keyword, + filterList.get(0).getFirst(), + filterList.get(0).getSecond(), + filterList.get(1).getFirst(), + filterList.get(1).getSecond(), + filterList.get(2).getFirst(), + filterList.get(2).getSecond(), + PageRequest.of(page, pageSize)); + } + default -> {searchHitPage = ontologyListItemEsRepository + .findByNameOrTermcodeMultiMatch3Filters(keyword, + filterList.get(0).getFirst(), + filterList.get(0).getSecond(), + filterList.get(1).getFirst(), + filterList.get(1).getSecond(), + filterList.get(2).getFirst(), + filterList.get(2).getSecond(), + PageRequest.of(page, pageSize)); + } + } List ontologyItems = new ArrayList<>(); searchHitPage.getContent().forEach(hit -> ontologyItems.add(EsSearchResultEntry.of(hit))); @@ -219,4 +279,14 @@ private NativeQuery buildNativeQuery(String keyword, .build(); return query; } + + public OntologyItemRelationsDocument getOntologyItemRelationsByHash(String hash) { + var ontologyItem = ontologyItemEsRepository.findById(hash).orElseThrow(OntologyItemNotFoundException::new); + return OntologyItemRelationsDocument.builder() + .translations(ontologyItem.getTranslations()) + .parents(ontologyItem.getParents()) + .children(ontologyItem.getChildren()) + .relatedTerms(ontologyItem.getRelatedTerms()) + .build(); + } } diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/terminology/es/model/OntologyItemDocument.java b/src/main/java/de/numcodex/feasibility_gui_backend/terminology/es/model/OntologyItemDocument.java index 82a9d5ad..4864668b 100644 --- a/src/main/java/de/numcodex/feasibility_gui_backend/terminology/es/model/OntologyItemDocument.java +++ b/src/main/java/de/numcodex/feasibility_gui_backend/terminology/es/model/OntologyItemDocument.java @@ -1,5 +1,6 @@ package de.numcodex.feasibility_gui_backend.terminology.es.model; +import de.numcodex.feasibility_gui_backend.common.api.TermCode; import jakarta.persistence.Id; import lombok.Builder; import lombok.Data; @@ -17,7 +18,7 @@ public class OntologyItemDocument { private @Id String id; private String name; private int availability; - private String context; + private TermCode context; private String terminology; private String termcode; @Field(name = "kds_module") private String kdsModule; diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/terminology/es/model/OntologyItemRelationsDocument.java b/src/main/java/de/numcodex/feasibility_gui_backend/terminology/es/model/OntologyItemRelationsDocument.java new file mode 100644 index 00000000..5b1c89b6 --- /dev/null +++ b/src/main/java/de/numcodex/feasibility_gui_backend/terminology/es/model/OntologyItemRelationsDocument.java @@ -0,0 +1,24 @@ +package de.numcodex.feasibility_gui_backend.terminology.es.model; + +import jakarta.persistence.Id; +import lombok.Builder; +import lombok.Data; +import org.springframework.data.elasticsearch.annotations.Document; +import org.springframework.data.elasticsearch.annotations.Field; +import org.springframework.data.elasticsearch.annotations.FieldType; + +import java.util.Collection; + +@Data +@Builder +@Document(indexName = "ontology") +public class OntologyItemRelationsDocument { + @Field(type = FieldType.Nested, includeInParent = true, name = "translations") + private Collection translations; + @Field(type = FieldType.Nested, includeInParent = true, name = "parents") + private Collection parents; + @Field(type = FieldType.Nested, includeInParent = true, name = "children") + private Collection children; + @Field(type = FieldType.Nested, includeInParent = true, name = "related_terms") + private Collection relatedTerms; +} diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/terminology/es/model/OntologyListItemDocument.java b/src/main/java/de/numcodex/feasibility_gui_backend/terminology/es/model/OntologyListItemDocument.java index df8e3ce9..7a56adcc 100644 --- a/src/main/java/de/numcodex/feasibility_gui_backend/terminology/es/model/OntologyListItemDocument.java +++ b/src/main/java/de/numcodex/feasibility_gui_backend/terminology/es/model/OntologyListItemDocument.java @@ -20,4 +20,5 @@ public class OntologyListItemDocument { private String termcode; @Field(name = "kds_module") private String kdsModule; + private boolean selectable; } diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/terminology/es/model/Relative.java b/src/main/java/de/numcodex/feasibility_gui_backend/terminology/es/model/Relative.java index 559d5778..eea4a89b 100644 --- a/src/main/java/de/numcodex/feasibility_gui_backend/terminology/es/model/Relative.java +++ b/src/main/java/de/numcodex/feasibility_gui_backend/terminology/es/model/Relative.java @@ -2,11 +2,13 @@ import lombok.Builder; import lombok.Data; +import org.springframework.data.elasticsearch.annotations.Field; @Data @Builder public class Relative { private String name; + @Field(name = "contextualized_termcode_hash") private String contextualizedTermcodeHash; } diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/terminology/es/repository/OntologyListItemEsRepository.java b/src/main/java/de/numcodex/feasibility_gui_backend/terminology/es/repository/OntologyListItemEsRepository.java index ef6c0163..85e03b12 100644 --- a/src/main/java/de/numcodex/feasibility_gui_backend/terminology/es/repository/OntologyListItemEsRepository.java +++ b/src/main/java/de/numcodex/feasibility_gui_backend/terminology/es/repository/OntologyListItemEsRepository.java @@ -8,12 +8,129 @@ import org.springframework.data.elasticsearch.core.SearchHits; import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; +import java.util.List; + @ConditionalOnExpression("${app.elastic.enabled}") public interface OntologyListItemEsRepository extends ElasticsearchRepository { SearchHits findByNameContainingIgnoreCaseOrTermcodeContainingIgnoreCase(String name, String termcode); - Page findByNameContainingIgnoreCaseOrTermcodeContainingIgnoreCase(String name, String termcode, Pageable pageable); + @Query(""" + { + "bool": { + "must": [ + { + "multi_match": { + "query": "?0", + "fields": [ + "name", + "termcode^2" + ] + } + } + ] + } + } + """ + ) + Page findByNameOrTermcodeMultiMatch0Filters(String searchterm, + Pageable pageable); + + @Query(""" + { + "bool": { + "must": [ + { + "multi_match": { + "query": "?0", + "fields": [ + "name", + "termcode^2" + ] + } + } + ], + "filter": { + "bool" : { + "must" : [ + {"terms" : { "?1": ?2 } } + ] + } + } + } + } + """ + ) + Page findByNameOrTermcodeMultiMatch1Filter(String searchterm, + String filterKey, + List filterValues, + Pageable pageable); + + @Query(""" + { + "bool": { + "must": [ + { + "multi_match": { + "query": "?0", + "fields": [ + "name", + "termcode^2" + ] + } + } + ], + "filter": { + "bool" : { + "must" : [ + {"terms" : { "?1": ?2 } }, + {"terms" : { "?3": ?4 } } + ] + } + } + } + } + """ + ) + Page findByNameOrTermcodeMultiMatch2Filters(String searchterm, + String filterKey1, + List filterValues1, + String filterKey2, + List filterValues2, + Pageable pageable); - @Query("{\"multi_match\":{\"query\":\"?0\",\"fields\":[ \"name\",\"termcode^2\"]}}") - Page findByNameOrTermcodeMultiMatch(String searchterm, Pageable pageable); + @Query(""" + { + "bool": { + "must": [ + { + "multi_match": { + "query": "?0", + "fields": [ + "name", + "termcode^2" + ] + } + } + ], + "filter": { + "bool" : { + "must" : [ + {"terms" : { "?1": ?2 } }, + {"terms" : { "?3": ?4 } }, + {"terms" : { "?5": ?6 } } + ] + } + } + } + } + """ + ) + Page findByNameOrTermcodeMultiMatch3Filters(String searchterm, + String filterKey1, + List filterValues1, + String filterKey2, + List filterValues2, + String filterKey3, + List filterValues3, + Pageable pageable); } \ No newline at end of file diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/terminology/v3/TerminologyEsController.java b/src/main/java/de/numcodex/feasibility_gui_backend/terminology/v3/TerminologyEsController.java index bcccff52..61b6f064 100644 --- a/src/main/java/de/numcodex/feasibility_gui_backend/terminology/v3/TerminologyEsController.java +++ b/src/main/java/de/numcodex/feasibility_gui_backend/terminology/v3/TerminologyEsController.java @@ -1,6 +1,7 @@ package de.numcodex.feasibility_gui_backend.terminology.v3; import de.numcodex.feasibility_gui_backend.terminology.api.EsSearchResult; +import de.numcodex.feasibility_gui_backend.terminology.api.EsSearchResultEntry; import de.numcodex.feasibility_gui_backend.terminology.es.TerminologyEsService; import de.numcodex.feasibility_gui_backend.terminology.es.model.*; import org.springframework.beans.factory.annotation.Autowired; @@ -10,7 +11,7 @@ import java.util.List; @RestController -@RequestMapping("api/v3/terminology/search") +@RequestMapping("") @ConditionalOnExpression("${app.elastic.enabled}") @CrossOrigin public class TerminologyEsController { @@ -22,7 +23,12 @@ public TerminologyEsController(TerminologyEsService terminologyEsService) { this.terminologyEsService = terminologyEsService; } - @GetMapping("") + @GetMapping("api/v3/terminology/search/filter") + public List getAvailableFilters() { + return terminologyEsService.getAvailableFilters(); + } + + @GetMapping("api/v3/terminology/entry/search") public EsSearchResult searchOntologyItemsCriteriaQuery(@RequestParam("searchterm") String keyword, @RequestParam(value = "context", required = false) List context, @RequestParam(value = "kdsModule", required = false) List kdsModule, @@ -35,13 +41,13 @@ public EsSearchResult searchOntologyItemsCriteriaQuery(@RequestParam("searchterm .performOntologySearchWithRepoAndPaging(keyword, context, kdsModule, terminology, availability, pageSize, page); } - @GetMapping("/filter") - public List getAvailableFilters() { - return terminologyEsService.getAvailableFilters(); + @GetMapping("api/v3/terminology/entry/{hash}/relations") + public OntologyItemRelationsDocument getOntologyItemRelationsByHash(@PathVariable("hash") String hash) { + return terminologyEsService.getOntologyItemRelationsByHash(hash); } - @GetMapping("/{hash}") - public OntologyItemDocument getOntologyItemByHash(@PathVariable("hash") String hash) { - return terminologyEsService.getOntologyItemByHash(hash); + @GetMapping("api/v3/terminology/entry/{hash}") + public EsSearchResultEntry getOntologyItemByHash(@PathVariable("hash") String hash) { + return terminologyEsService.getSearchResultEntryByHash(hash); } } diff --git a/src/test/java/de/numcodex/feasibility_gui_backend/terminology/TerminologyServiceTest.java b/src/test/java/de/numcodex/feasibility_gui_backend/terminology/TerminologyServiceTest.java index 28c737ae..2b424c3e 100644 --- a/src/test/java/de/numcodex/feasibility_gui_backend/terminology/TerminologyServiceTest.java +++ b/src/test/java/de/numcodex/feasibility_gui_backend/terminology/TerminologyServiceTest.java @@ -1,5 +1,6 @@ package de.numcodex.feasibility_gui_backend.terminology; +import com.fasterxml.jackson.databind.ObjectMapper; import de.numcodex.feasibility_gui_backend.terminology.api.CategoryEntry; import de.numcodex.feasibility_gui_backend.terminology.persistence.*; import org.junit.jupiter.api.BeforeEach; @@ -50,8 +51,11 @@ class TerminologyServiceTest { @Mock private MappingRepository mappingRepository; + @Mock + private ObjectMapper jsonUtil; + private TerminologyService createTerminologyService(String uiProfilePath) throws IOException { - return new TerminologyService(uiProfilePath, uiProfileRepository, termCodeRepository, contextualizedTermCodeRepository, mappingRepository); + return new TerminologyService(uiProfilePath, uiProfileRepository, termCodeRepository, contextualizedTermCodeRepository, mappingRepository, jsonUtil); } @BeforeEach