From b00ccc3dbbf2e170ffa65b1c3771fa8c2bfa0772 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kalle=20W=C3=A5hlin?= Date: Tue, 21 Apr 2026 14:39:50 +0200 Subject: [PATCH 1/3] Add mappings for non-vocab resources (libraries) and use them in disambiguation --- .../groovy/whelk/rest/api/SearchUtils2.java | 8 +- .../java/whelk/sru/servlet/SruServlet.java | 6 +- .../whelk/sru/servlet/XSearchServlet.java | 9 +- .../groovy/whelk/search2/Disambiguate.java | 30 +- .../groovy/whelk/search2/ObjectQuery.java | 5 +- .../whelk/search2/PredicateObjectQuery.java | 4 +- .../src/main/groovy/whelk/search2/Query.java | 16 +- .../groovy/whelk/search2/ResourceLookup.java | 373 ++++++++++++++++++ .../groovy/whelk/search2/SuggestQuery.java | 4 +- .../groovy/whelk/search2/VocabMappings.java | 328 --------------- .../groovy/whelk/search2/querytree/Link.java | 6 +- .../whelk/search2/DisambiguateSpec.groovy | 1 + .../whelk/search2/querytree/TestData.groovy | 12 +- 13 files changed, 430 insertions(+), 372 deletions(-) create mode 100644 whelk-core/src/main/groovy/whelk/search2/ResourceLookup.java delete mode 100644 whelk-core/src/main/groovy/whelk/search2/VocabMappings.java diff --git a/rest/src/main/groovy/whelk/rest/api/SearchUtils2.java b/rest/src/main/groovy/whelk/rest/api/SearchUtils2.java index eb46d7972d..a448c6f739 100644 --- a/rest/src/main/groovy/whelk/rest/api/SearchUtils2.java +++ b/rest/src/main/groovy/whelk/rest/api/SearchUtils2.java @@ -9,7 +9,7 @@ import whelk.search2.ESSettings; import whelk.search2.Query; import whelk.search2.QueryParams; -import whelk.search2.VocabMappings; +import whelk.search2.ResourceLookup; import java.io.IOException; import java.util.*; @@ -18,14 +18,14 @@ import static whelk.util.Jackson.mapper; public class SearchUtils2 { - private final VocabMappings vocabMappings; + private final ResourceLookup resourceLookup; private final Whelk whelk; private final ESSettings esSettings; SearchUtils2(Whelk whelk) { this.whelk = whelk; this.esSettings = new ESSettings(whelk); - this.vocabMappings = VocabMappings.load(whelk); + this.resourceLookup = ResourceLookup.load(whelk); } Map doSearch(Map queryParameters) throws InvalidQueryException, IOException { @@ -36,7 +36,7 @@ Map doSearch(Map queryParameters) throws Inval QueryParams queryParams = new QueryParams(queryParameters); AppParams appParams = new AppParams(getAppConfig(queryParameters), whelk.getJsonld()); - Query query = Query.init(queryParams, appParams, vocabMappings, esSettings, whelk); + Query query = Query.init(queryParams, appParams, resourceLookup, esSettings, whelk); return query.collectResults(); } diff --git a/sru/src/main/java/whelk/sru/servlet/SruServlet.java b/sru/src/main/java/whelk/sru/servlet/SruServlet.java index e08770aa75..2667e28696 100644 --- a/sru/src/main/java/whelk/sru/servlet/SruServlet.java +++ b/sru/src/main/java/whelk/sru/servlet/SruServlet.java @@ -32,13 +32,13 @@ public class SruServlet extends WhelkHttpServlet { JsonLD2MarcXMLConverter converter; XMLInputFactory xmlInputFactory = XMLInputFactory.newInstance(); - VocabMappings vocabMappings; + ResourceLookup resourceLookup; ESSettings esSettings; @Override protected void init(Whelk whelk) { converter = new JsonLD2MarcXMLConverter(whelk.getMarcFrameConverter()); - vocabMappings = VocabMappings.load(whelk); + resourceLookup = ResourceLookup.load(whelk); esSettings = new ESSettings(whelk); } @@ -74,7 +74,7 @@ public void doGet(HttpServletRequest req, HttpServletResponse res) throws IOExce paramsAsIfSearch.put("_stats", new String[] { "false" }); // don't need facets QueryParams qp = new QueryParams(paramsAsIfSearch); AppParams ap = new AppParams(new HashMap<>(), whelk.getJsonld()); - Query query = new Query(qp, ap, vocabMappings, esSettings, whelk); + Query query = new Query(qp, ap, resourceLookup, esSettings, whelk); results = query.collectResults(); } catch (InvalidQueryException | ParseCancellationException e) { logger.info("Bad query: \"" + parameters.get("query")[0] + "\" -> " + e.getMessage()); diff --git a/sru/src/main/java/whelk/sru/servlet/XSearchServlet.java b/sru/src/main/java/whelk/sru/servlet/XSearchServlet.java index 0f4e7bfb98..4c3ef59d11 100644 --- a/sru/src/main/java/whelk/sru/servlet/XSearchServlet.java +++ b/sru/src/main/java/whelk/sru/servlet/XSearchServlet.java @@ -21,7 +21,7 @@ import whelk.search2.ESSettings; import whelk.search2.Query; import whelk.search2.QueryParams; -import whelk.search2.VocabMappings; +import whelk.search2.ResourceLookup; import whelk.util.DocumentUtil; import whelk.util.FresnelUtil; import whelk.util.http.HttpTools; @@ -58,7 +58,6 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; -import java.util.TreeSet; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -145,14 +144,14 @@ public static class Errors { JsonLD2MarcXMLConverter converter; XMLInputFactory xmlInputFactory = XMLInputFactory.newInstance(); - VocabMappings vocabMappings; + ResourceLookup resourceLookup; ESSettings esSettings; Map transformers; @Override protected void init(Whelk whelk) { converter = new JsonLD2MarcXMLConverter(whelk.getMarcFrameConverter()); - vocabMappings = VocabMappings.load(whelk); + resourceLookup = ResourceLookup.load(whelk); esSettings = new ESSettings(whelk); try { @@ -226,7 +225,7 @@ public void doGet2(HttpServletRequest req, HttpServletResponse res) throws IOExc QueryParams qp = new QueryParams(paramsAsIfSearch); AppParams ap = new AppParams(new HashMap<>(), whelk.getJsonld()); - var results = new Query(qp, ap, vocabMappings, esSettings, whelk).collectResults(); + var results = new Query(qp, ap, resourceLookup, esSettings, whelk).collectResults(); @SuppressWarnings("unchecked") List> items = (List>) results.get("items"); diff --git a/whelk-core/src/main/groovy/whelk/search2/Disambiguate.java b/whelk-core/src/main/groovy/whelk/search2/Disambiguate.java index 3d0afda0f2..0519412b0f 100644 --- a/whelk-core/src/main/groovy/whelk/search2/Disambiguate.java +++ b/whelk-core/src/main/groovy/whelk/search2/Disambiguate.java @@ -10,24 +10,25 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import static whelk.JsonLd.ID_KEY; import static whelk.JsonLd.LD_KEYS; import static whelk.JsonLd.VOCAB_KEY; import static whelk.JsonLd.looksLikeIri; import static whelk.search2.Query.NONE_CATEGORY; import static whelk.search2.Query.WORK_CATEGORY; import static whelk.search2.QueryUtil.encodeUri; -import static whelk.search2.VocabMappings.expandPrefixed; +import static whelk.search2.ResourceLookup.expandPrefixed; public class Disambiguate { private final JsonLd jsonLd; - private final VocabMappings vocabMappings; + private final ResourceLookup resourceLookup; private final Map filterAliasMappings; private final List nsPrecedenceOrder; - public Disambiguate(VocabMappings vocabMappings, Collection appFilterAliases, Collection queryFilterAliases, JsonLd jsonLd) { - this.vocabMappings = vocabMappings; + public Disambiguate(ResourceLookup resourceLookup, Collection appFilterAliases, Collection queryFilterAliases, JsonLd jsonLd) { + this.resourceLookup = resourceLookup; this.filterAliasMappings = getFilterAliasMappings(appFilterAliases, queryFilterAliases); this.jsonLd = jsonLd; this.nsPrecedenceOrder = List.of("rdf", "librissearch", (String) jsonLd.context.get(VOCAB_KEY), "bibdb", "bulk", "marc"); // FIXME @@ -35,8 +36,8 @@ public Disambiguate(VocabMappings vocabMappings, Collection appFilt // For test only @PackageScope - public Disambiguate(VocabMappings vocabMappings, Collection filterAliasMappings, JsonLd jsonLd) { - this.vocabMappings = vocabMappings; + public Disambiguate(ResourceLookup resourceLookup, Collection filterAliasMappings, JsonLd jsonLd) { + this.resourceLookup = resourceLookup; this.filterAliasMappings = getFilterAliasMappings(filterAliasMappings, List.of()); this.jsonLd = jsonLd; this.nsPrecedenceOrder = List.of("rdf", "librissearch", (String) jsonLd.context.get(VOCAB_KEY), "bibdb", "bulk", "marc"); // FIXME @@ -81,7 +82,7 @@ public Selector restrictByValue(Selector selector, String value) { } private boolean isRestrictedByValue(String propertyKey) { - return vocabMappings.propertiesRestrictedByValue().containsKey(propertyKey); + return resourceLookup.vocabMappings().propertiesRestrictedByValue().containsKey(propertyKey); } private Property restrictByValue(Property property, String value) { @@ -96,7 +97,7 @@ private Property restrictByValue(Property property, String value) { } private String tryCoerce(String property, String value) { - var coercingSubPropertyKey = vocabMappings.propertiesRestrictedByValue() + var coercingSubPropertyKey = resourceLookup.vocabMappings().propertiesRestrictedByValue() .getOrDefault(property, Map.of()) .get(value); if (coercingSubPropertyKey != null) { @@ -126,7 +127,7 @@ private Selector _mapQueryKey(Token token) { private PathElement mapSingleKey(Token token) { for (String ns : nsPrecedenceOrder) { - Set mappedProperties = vocabMappings.properties() + Set mappedProperties = resourceLookup.vocabMappings().properties() .getOrDefault(token.value().toLowerCase(), Map.of()) .getOrDefault(ns, Set.of()); if (mappedProperties.size() == 1) { @@ -182,6 +183,7 @@ private Optional mapValueForProperty(Property property, String value, Tok } if (property.isType() || property.isVocabTerm()) { for (String ns : nsPrecedenceOrder) { + var vocabMappings = resourceLookup.vocabMappings(); var mappings = property.isType() ? vocabMappings.classes() : vocabMappings.enums(); Set mappedClasses = mappings.getOrDefault(value.toLowerCase(), Map.of()).getOrDefault(ns, Set.of()); if (mappedClasses.size() == 1) { @@ -202,6 +204,16 @@ private Optional mapValueForProperty(Property property, String value, Tok String expanded = expandPrefixed(value); if (looksLikeIri(expanded)) { return Optional.of(new Link(encodeUri(expanded), token)); + } else if (property.range().size() == 1){ + var range = property.range().getFirst(); + var mappedResourceDescription = resourceLookup.externalMappings().byType() + .getOrDefault(range, Map.of()) + .getOrDefault(value.toLowerCase(), Map.of()); + if (!mappedResourceDescription.isEmpty()) { + var link = new Link((String) mappedResourceDescription.get(ID_KEY), token); + link.setChip(mappedResourceDescription); + return Optional.of(link); + } } } /* diff --git a/whelk-core/src/main/groovy/whelk/search2/ObjectQuery.java b/whelk-core/src/main/groovy/whelk/search2/ObjectQuery.java index 6cede631e7..3616131d7c 100644 --- a/whelk-core/src/main/groovy/whelk/search2/ObjectQuery.java +++ b/whelk-core/src/main/groovy/whelk/search2/ObjectQuery.java @@ -2,7 +2,6 @@ import whelk.JsonLd; import whelk.Whelk; -import whelk.component.ElasticSearch; import whelk.exception.InvalidQueryException; import whelk.search2.querytree.And; import whelk.search2.querytree.Condition; @@ -40,8 +39,8 @@ public class ObjectQuery extends Query { protected final Link object; private final List curatedPredicates; - public ObjectQuery(QueryParams queryParams, AppParams appParams, VocabMappings vocabMappings, ESSettings esSettings, Whelk whelk) throws InvalidQueryException { - super(queryParams, appParams, vocabMappings, esSettings, whelk); + public ObjectQuery(QueryParams queryParams, AppParams appParams, ResourceLookup resourceLookup, ESSettings esSettings, Whelk whelk) throws InvalidQueryException { + super(queryParams, appParams, resourceLookup, esSettings, whelk); this.object = loadObject(); this.curatedPredicates = loadCuratedPredicates(); } diff --git a/whelk-core/src/main/groovy/whelk/search2/PredicateObjectQuery.java b/whelk-core/src/main/groovy/whelk/search2/PredicateObjectQuery.java index 54b5677cbb..a7a5268cc8 100644 --- a/whelk-core/src/main/groovy/whelk/search2/PredicateObjectQuery.java +++ b/whelk-core/src/main/groovy/whelk/search2/PredicateObjectQuery.java @@ -25,8 +25,8 @@ import java.util.stream.Stream; public class PredicateObjectQuery extends ObjectQuery { - public PredicateObjectQuery(QueryParams queryParams, AppParams appParams, VocabMappings vocabMappings, ESSettings esSettings, Whelk whelk) throws InvalidQueryException { - super(queryParams, appParams, vocabMappings, esSettings, whelk); + public PredicateObjectQuery(QueryParams queryParams, AppParams appParams, ResourceLookup resourceLookup, ESSettings esSettings, Whelk whelk) throws InvalidQueryException { + super(queryParams, appParams, resourceLookup, esSettings, whelk); } @Override diff --git a/whelk-core/src/main/groovy/whelk/search2/Query.java b/whelk-core/src/main/groovy/whelk/search2/Query.java index c3a049b3cf..d7be2d957b 100644 --- a/whelk-core/src/main/groovy/whelk/search2/Query.java +++ b/whelk-core/src/main/groovy/whelk/search2/Query.java @@ -3,7 +3,6 @@ import com.google.common.base.Predicates; import whelk.JsonLd; import whelk.Whelk; -import whelk.component.ElasticSearch; import whelk.exception.InvalidQueryException; import whelk.search2.querytree.And; import whelk.search2.querytree.Condition; @@ -21,7 +20,6 @@ import whelk.search2.querytree.Resource; import whelk.search2.querytree.Value; import whelk.search2.querytree.YearRange; -import whelk.util.DocumentUtil; import whelk.util.FresnelUtil; import java.util.ArrayList; @@ -93,12 +91,12 @@ public enum Connective { public Query(QueryParams queryParams, AppParams appParams, - VocabMappings vocabMappings, + ResourceLookup resourceLookup, ESSettings esSettings, Whelk whelk) throws InvalidQueryException { this.queryParams = queryParams; this.appParams = appParams; - this.disambiguate = new Disambiguate(vocabMappings, appParams.filterAliases, queryParams.aliased, whelk.getJsonld()); + this.disambiguate = new Disambiguate(resourceLookup, appParams.filterAliases, queryParams.aliased, whelk.getJsonld()); this.esSettings = esSettings; this.whelk = whelk; this.qTree = new QueryTree(queryParams.q, disambiguate); @@ -108,12 +106,12 @@ public Query(QueryParams queryParams, this.stats = new Stats(); } - public static Query init(QueryParams queryParams, AppParams appParams, VocabMappings vocabMappings, ESSettings esSettings, Whelk whelk) throws InvalidQueryException { + public static Query init(QueryParams queryParams, AppParams appParams, ResourceLookup resourceLookup, ESSettings esSettings, Whelk whelk) throws InvalidQueryException { return switch (getSearchMode(queryParams)) { - case STANDARD_SEARCH -> new Query(queryParams, appParams, vocabMappings, esSettings, whelk); - case OBJECT_SEARCH -> new ObjectQuery(queryParams, appParams, vocabMappings, esSettings, whelk); - case PREDICATE_OBJECT_SEARCH -> new PredicateObjectQuery(queryParams, appParams, vocabMappings, esSettings, whelk); - case SUGGEST -> new SuggestQuery(queryParams, appParams, vocabMappings, esSettings, whelk); + case STANDARD_SEARCH -> new Query(queryParams, appParams, resourceLookup, esSettings, whelk); + case OBJECT_SEARCH -> new ObjectQuery(queryParams, appParams, resourceLookup, esSettings, whelk); + case PREDICATE_OBJECT_SEARCH -> new PredicateObjectQuery(queryParams, appParams, resourceLookup, esSettings, whelk); + case SUGGEST -> new SuggestQuery(queryParams, appParams, resourceLookup, esSettings, whelk); }; } diff --git a/whelk-core/src/main/groovy/whelk/search2/ResourceLookup.java b/whelk-core/src/main/groovy/whelk/search2/ResourceLookup.java new file mode 100644 index 0000000000..eefb0ca872 --- /dev/null +++ b/whelk-core/src/main/groovy/whelk/search2/ResourceLookup.java @@ -0,0 +1,373 @@ +package whelk.search2; + +import whelk.Document; +import whelk.JsonLd; +import whelk.Whelk; +import whelk.search2.querytree.Link; +import whelk.search2.querytree.Property; +import whelk.search2.querytree.Term; +import whelk.search2.querytree.VocabTerm; +import whelk.util.DocumentUtil; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import java.util.function.Predicate; +import java.util.stream.Stream; + +import static whelk.JsonLd.ID_KEY; +import static whelk.JsonLd.Owl.DATATYPE_PROPERTY; +import static whelk.JsonLd.Owl.EQUIVALENT_CLASS; +import static whelk.JsonLd.Owl.EQUIVALENT_PROPERTY; +import static whelk.JsonLd.Owl.OBJECT_PROPERTY; +import static whelk.JsonLd.Rdfs.RDF_TYPE; +import static whelk.JsonLd.VOCAB_KEY; +import static whelk.JsonLd.asList; +import static whelk.search2.QueryUtil.loadThing; +import static whelk.util.DocumentUtil.getAtPath; + +public record ResourceLookup(VocabMappings vocabMappings, ExternalMappings externalMappings) { + public static ResourceLookup load(Whelk whelk) { + return new ResourceLookup(VocabMappings.load(whelk), ExternalMappings.load(whelk)); + } + + public record VocabMappings( + /* + Map>> + for example: + [ + "språk" : ["https://id.kb.se/vocab/": ["language", "associatedLanguage"]], + "bibliotek": ["librissearch": ["librissearch:itemHeldBy"]], + "format" : ["librissearch": ["librissearch:hasInstanceType"], "https://id.kb.se/vocab/": ["hasFormat", "format"]] + ] + */ + Map>> properties, + /* + Map>> + for example: + [ + "person" : ["https://id.kb.se/vocab/": ["Person"]], + "digitalresource": ["https://id.kb.se/vocab/": ["DigitalResource"]] + ] + */ + Map>> classes, + /* + Map>> + for example: + [ + "biblioteksnivå": ["marc": ["marc:MinimalLevel"]] + ] + */ + Map>> enums, + /* + Map>> + for example: + [ + "workCategory": [ + "https://id.kb.se/term/ktg/Literature": ["findCategory"], + "https://id.kb.se/term/ktg/Software" : ["findCategory"], + "https://id.kb.se/term/saogf/Poesi" : ["identifyCategory"] + ] + ] + */ + Map>> propertiesRestrictedByValue + ) { + // :category :heuristicIdentifier too broad...? + private static final Set notatingProps = Set.of("label", "prefLabel", "altLabel", "code", "librisQueryCode"); + + static VocabMappings load(Whelk whelk) { + return getMappings(whelk); + } + + private static VocabMappings getMappings(Whelk whelk) { + var jsonLd = whelk.getJsonld(); + var vocab = jsonLd.vocabIndex; + var systemVocabNs = (String) whelk.getJsonld().context.get(VOCAB_KEY); + + Map>> properties = new HashMap<>(); + Map>> classes = new HashMap<>(); + Map>> enums = new HashMap<>(); + + List coercingProperties = new ArrayList<>(); + + vocab.forEach((termKey, termDefinition) -> { + String ns = getNs(termKey, systemVocabNs); + if (isProperty(termDefinition)) { + addAllMappings(termKey, ns, properties, whelk); + if (jsonLd.getCategoryMembers("librissearch:coercing").contains(termKey)) { + coercingProperties.add(termKey); + } + } else if (isClass(termDefinition, jsonLd)) { + addAllMappings(termKey, ns, classes, whelk); + } else if (isEnum(termDefinition, jsonLd)) { + addAllMappings(termKey, ns, enums, whelk); + } + }); + + addMapping(JsonLd.TYPE_KEY, RDF_TYPE, "rdf", properties); + + return new VocabMappings(properties, classes, enums, getPropertiesRestrictedByValue(whelk, coercingProperties)); + } + + private static String getNs(String termKey, String systemVocabNs) { + return JsonLd.looksLikeIri(termKey) + ? termKey.substring(termKey.lastIndexOf("/") + 1) + : (termKey.contains(":") ? termKey.substring(0, termKey.indexOf(":")) : systemVocabNs); + } + + private static void addAllMappings(String termKey, String ns, Map>> mappings, Whelk whelk) { + addMapping(termKey, termKey, ns, mappings); + addMappings(termKey, ns, mappings, whelk.getJsonld()); + addEquivTermMappings(termKey, ns, mappings, whelk); + } + + private static void addMapping(String from, String to, String ns, Map>> mappings) { + mappings.computeIfAbsent(from.toLowerCase(), x -> new HashMap<>()) + .computeIfAbsent(ns, x -> new HashSet<>()) + .add(to); + } + + private static void addMappings(String termKey, String ns, Map>> mappings, JsonLd jsonLd) { + addMappings(termKey, ns, mappings, jsonLd, jsonLd.vocabIndex.get(termKey)); + } + + private static void addMappings(String termKey, String ns, Map>> mappings, JsonLd jsonLd, Map termDefinition) { + String termId = (String) termDefinition.get(ID_KEY); + + addMapping(termId, termKey, ns, mappings); + addMapping(toPrefixed(termId), termKey, ns, mappings); + addMapping(dropNs(termId), termKey, ns, mappings); + + for (String prop : notatingProps) { + if (termDefinition.containsKey(prop)) { + getAsList(termDefinition, List.of(prop)) + .forEach(value -> addMapping((String) value, termKey, ns, mappings)); + } + + String alias = (String) jsonLd.langContainerAlias.get(prop); + if (termDefinition.containsKey(alias)) { + for (String lang : jsonLd.locales) { + getAsList(termDefinition, List.of(alias, lang)) + .forEach(langStr -> addMapping((String) langStr, termKey, ns, mappings)); + } + } + } + } + + private static String dropNs(String termIri) { + return termIri.substring(termIri.lastIndexOf("/") + 1); + } + + private static void addEquivTermMappings(String termKey, String ns, Map>> mappings, Whelk whelk) { + var jsonLd = whelk.getJsonld(); + var vocab = jsonLd.vocabIndex; + + String mappingProperty = isProperty(vocab.get(termKey)) ? EQUIVALENT_PROPERTY : EQUIVALENT_CLASS; + + getAsList(vocab, List.of(termKey, mappingProperty)).forEach(term -> { + String equivPropIri = get(term, List.of(ID_KEY), ""); + if (!equivPropIri.isEmpty()) { + String equivPropKey = jsonLd.toTermKey(equivPropIri); + if (!vocab.containsKey(equivPropKey)) { + var thing = loadThing(equivPropIri, whelk); + if (!thing.isEmpty()) { + addMappings(termKey, ns, mappings, jsonLd, thing); + } else { + addMapping(equivPropIri, termKey, ns, mappings); + addMapping(toPrefixed(equivPropIri), termKey, ns, mappings); + } + } + } + }); + } + + private static boolean isClass(Map termDefinition, JsonLd jsonLd) { + return getTypes(termDefinition).stream().anyMatch(type -> jsonLd.isSubClassOf((String) type, "Class")); + } + + private static boolean isEnum(Map termDefinition, JsonLd jsonLd) { + return getTypes(termDefinition).stream() + .map(String.class::cast) + .flatMap(type -> Stream.concat(jsonLd.getSuperClasses(type).stream(), Stream.of(type))) + .map(jsonLd::getInRange) + .flatMap(Set::stream) + .filter(s -> isProperty(jsonLd.vocabIndex.getOrDefault(s, Map.of()))) + .anyMatch(jsonLd::isVocabTerm); + } + + private static boolean isProperty(Map termDefinition) { + return isObjectProperty(termDefinition) || isDatatypeProperty(termDefinition); + } + + public static boolean isObjectProperty(Map termDefinition) { + return getTypes(termDefinition).stream().anyMatch(OBJECT_PROPERTY::equals); + } + + private static boolean isDatatypeProperty(Map termDefinition) { + return getTypes(termDefinition).stream().anyMatch(DATATYPE_PROPERTY::equals); + } + + private static List getTypes(Map termDefinition) { + return asList(termDefinition.get(JsonLd.TYPE_KEY)); + } + + private static Map>> getPropertiesRestrictedByValue(Whelk whelk, List coercingProps) { + JsonLd ld = whelk.getJsonld(); + + Map> groupedBySuperProp = new HashMap<>(); + coercingProps.forEach(coercing -> { + String superProp = get(ld.vocabIndex, List.of(coercing, JsonLd.Rdfs.SUB_PROPERTY_OF, 0, ID_KEY), ""); + if (!superProp.isEmpty()) { + groupedBySuperProp.computeIfAbsent(ld.toTermKey(superProp), x -> new ArrayList<>()) + .add(coercing); + } + }); + + Map>> propertiesRestrictedByValue = new HashMap<>(); + + groupedBySuperProp.forEach((superProp, coercing) -> { + var types = new HashSet(); + + ld.getRange(superProp).forEach(type -> { + types.add(type); + types.addAll(ld.getSubClasses(type)); + }); + + for (String type : types) { + for (var doc : whelk.getStorage().loadAllByType(type)) { + var iri = doc.getThingIdentifiers().stream().findFirst().orElseThrow(); + for (var n : coercing) { + var propDef = ld.vocabIndex.getOrDefault(n, Map.of()); + Property.getObjectHasValueRestrictions(propDef, ld).forEach(hasValueRestriction -> { + var onProperty = hasValueRestriction.onProperty(); + var hasValue = hasValueRestriction.value(); + var path = List.of(JsonLd.GRAPH_KEY, 1, onProperty.name()); + Predicate hasMatchingValue = o -> switch (hasValue) { + case Term term -> o instanceof String s && s.equals(term.term()); + case VocabTerm vocabTerm -> o instanceof String s && s.equals(vocabTerm.key()); + case Link link -> o instanceof Map m && link.iri().equals(m.get(JsonLd.ID_KEY)); + default -> false; + }; + var matches = ((List) JsonLd.asList(DocumentUtil.getAtPath(doc.data, path, List.of()))).stream() + .anyMatch(hasMatchingValue); + if (matches) { + propertiesRestrictedByValue.computeIfAbsent(superProp, k -> new HashMap<>()) + .computeIfAbsent(iri, k -> new ArrayList<>()) + .add(n); + } + }); + } + } + } + }); + + return propertiesRestrictedByValue; + } + + @SuppressWarnings("unchecked") + private static T get(Object o, List path, T defaultTo) { + return (T) getAtPath(o, path, defaultTo); + } + + private static List getAsList(Object o, List path) { + return asList(get(o, path, null)); + } + } + + public record ExternalMappings( + /* + Map> + for example: + [ + "Library" : ["s": ["https://libris.kb.se/library/S"]], + "bibdb:Organization": ["kb": ["https://libris.kb.se/library/org/KB"]] + ] + */ + Map>> byType + ) { + static ExternalMappings load(Whelk whelk) { + return loadMappings(whelk); + } + + private static ExternalMappings loadMappings(Whelk whelk) { + Map>> mappings = new HashMap<>(); + mappings.put("Library", loadMappingsForType("Library", List.of("sigel"), whelk)); + mappings.put("bibdb:Organization", loadMappingsForType("bibdb:Organization", List.of("code"), whelk)); + return new ExternalMappings(mappings); + } + + private static Map> loadMappingsForType(String type, Collection properties, Whelk whelk) { + Map> mappings = new HashMap<>(); + whelk.loadAllByType(type) + .forEach(doc -> { + var description = doc.getThing(); + properties.forEach(p -> { + if (description.containsKey(p) && description.get(p) instanceof String s) { + var chip = QueryUtil.castToStringObjectMap(whelk.jsonld.toChip(description)); + mappings.put(s.toLowerCase(), chip); + } + }); + }); + return mappings; + } + } + + public static String toPrefixed(String iri) { + // TODO: get prefix mappings from context + Map nsToPrefix = new HashMap<>(); + nsToPrefix.put("https://id.kb.se/vocab/", "kbv:"); + nsToPrefix.put("http://id.loc.gov/ontologies/bibframe/", "bf:"); + nsToPrefix.put("http://purl.org/dc/terms/", "dc:"); + nsToPrefix.put("http://schema.org/", "sdo:"); + nsToPrefix.put("https://id.kb.se/term/sao/", "sao:"); + nsToPrefix.put("https://id.kb.se/marc/", "marc:"); + nsToPrefix.put("https://id.kb.se/term/saogf/", "saogf:"); + nsToPrefix.put("https://id.kb.se/term/barn/", "barn:"); + nsToPrefix.put("https://id.kb.se/term/barngf/", "barngf:"); + nsToPrefix.put("https://libris.kb.se/library/", "sigel:"); + nsToPrefix.put("https://id.kb.se/language/", "lang:"); + nsToPrefix.put(Document.getBASE_URI().toString(), "libris:"); + + for (String ns : nsToPrefix.keySet()) { + if (iri.startsWith(ns)) { + return iri.replace(ns, nsToPrefix.get(ns)); + } + } + + return iri; + } + + public static String expandPrefixed(String s) { + if (!s.contains(":")) { + return s; + } + // TODO: get prefix mappings from context + Map nsToPrefix = new HashMap<>(); + nsToPrefix.put("https://id.kb.se/vocab/", "kbv:"); + nsToPrefix.put("http://id.loc.gov/ontologies/bibframe/", "bf:"); + nsToPrefix.put("http://purl.org/dc/terms/", "dc:"); + nsToPrefix.put("http://schema.org/", "sdo:"); + nsToPrefix.put("https://id.kb.se/term/sao/", "sao:"); + nsToPrefix.put("https://id.kb.se/marc/", "marc:"); + nsToPrefix.put("https://id.kb.se/term/saogf/", "saogf:"); + nsToPrefix.put("https://id.kb.se/term/barn/", "barn:"); + nsToPrefix.put("https://id.kb.se/term/barngf/", "barngf:"); + nsToPrefix.put("https://libris.kb.se/library/", "sigel:"); + nsToPrefix.put("https://id.kb.se/language/", "lang:"); + nsToPrefix.put(Document.getBASE_URI().toString(), "libris:"); + + for (String ns : nsToPrefix.keySet()) { + String prefix = nsToPrefix.get(ns); + if (s.startsWith(prefix)) { + return s.replace(prefix, ns); + } + } + + return s; + } +} diff --git a/whelk-core/src/main/groovy/whelk/search2/SuggestQuery.java b/whelk-core/src/main/groovy/whelk/search2/SuggestQuery.java index 36f9234313..dfe31d3c53 100644 --- a/whelk-core/src/main/groovy/whelk/search2/SuggestQuery.java +++ b/whelk-core/src/main/groovy/whelk/search2/SuggestQuery.java @@ -59,8 +59,8 @@ static Edited empty() { private boolean propertySearch = false; - public SuggestQuery(QueryParams queryParams, AppParams appParams, VocabMappings vocabMappings, ESSettings esSettings, Whelk whelk) throws InvalidQueryException { - super(queryParams, appParams, vocabMappings, esSettings, whelk); + public SuggestQuery(QueryParams queryParams, AppParams appParams, ResourceLookup resourceLookup, ESSettings esSettings, Whelk whelk) throws InvalidQueryException { + super(queryParams, appParams, resourceLookup, esSettings, whelk); this.edited = getEdited(); this.suggestQueryTree = getSuggestQueryTree(); } diff --git a/whelk-core/src/main/groovy/whelk/search2/VocabMappings.java b/whelk-core/src/main/groovy/whelk/search2/VocabMappings.java deleted file mode 100644 index 0c43e8f027..0000000000 --- a/whelk-core/src/main/groovy/whelk/search2/VocabMappings.java +++ /dev/null @@ -1,328 +0,0 @@ -package whelk.search2; - -import whelk.Document; -import whelk.JsonLd; -import whelk.Whelk; -import whelk.search2.querytree.Link; -import whelk.search2.querytree.Property; -import whelk.search2.querytree.Term; -import whelk.search2.querytree.VocabTerm; -import whelk.util.DocumentUtil; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import java.util.function.Predicate; -import java.util.stream.Stream; - -import static whelk.JsonLd.ID_KEY; -import static whelk.JsonLd.Owl.DATATYPE_PROPERTY; -import static whelk.JsonLd.Owl.EQUIVALENT_CLASS; -import static whelk.JsonLd.Owl.EQUIVALENT_PROPERTY; -import static whelk.JsonLd.Owl.OBJECT_PROPERTY; -import static whelk.JsonLd.Rdfs.RDF_TYPE; -import static whelk.JsonLd.VOCAB_KEY; -import static whelk.JsonLd.asList; -import static whelk.search2.QueryUtil.loadThing; -import static whelk.util.DocumentUtil.getAtPath; - -public record VocabMappings( - /* - Map>> - for example: - [ - "språk" : ["https://id.kb.se/vocab/": ["language", "associatedLanguage"]], - "bibliotek": ["librissearch": ["librissearch:itemHeldBy"]], - "format" : ["librissearch": ["librissearch:hasInstanceType"], "https://id.kb.se/vocab/": ["hasFormat", "format"]] - ] - */ - Map>> properties, - /* - Map>> - for example: - [ - "person" : ["https://id.kb.se/vocab/": ["Person"]], - "digitalresource": ["https://id.kb.se/vocab/": ["DigitalResource"]] - ] - */ - Map>> classes, - /* - Map>> - for example: - [ - "biblioteksnivå": ["marc": ["marc:MinimalLevel"]] - ] - */ - Map>> enums, - /* - Map>> - for example: - [ - "workCategory": [ - "https://id.kb.se/term/ktg/Literature": ["findCategory"], - "https://id.kb.se/term/ktg/Software" : ["findCategory"], - "https://id.kb.se/term/saogf/Poesi" : ["identifyCategory"] - ] - ] - */ - Map>> propertiesRestrictedByValue -) { - // :category :heuristicIdentifier too broad...? - private static final Set notatingProps = Set.of("label", "prefLabel", "altLabel", "code", "librisQueryCode"); - - public static VocabMappings load(Whelk whelk) { - return getMappings(whelk); - } - - private static VocabMappings getMappings(Whelk whelk) { - var jsonLd = whelk.getJsonld(); - var vocab = jsonLd.vocabIndex; - var systemVocabNs = (String) whelk.getJsonld().context.get(VOCAB_KEY); - - Map>> properties = new HashMap<>(); - Map>> classes = new HashMap<>(); - Map>> enums = new HashMap<>(); - - List coercingProperties = new ArrayList<>(); - - vocab.forEach((termKey, termDefinition) -> { - String ns = getNs(termKey, systemVocabNs); - if (isProperty(termDefinition)) { - addAllMappings(termKey, ns, properties, whelk); - if (jsonLd.getCategoryMembers("librissearch:coercing").contains(termKey)) { - coercingProperties.add(termKey); - } - } else if (isClass(termDefinition, jsonLd)) { - addAllMappings(termKey, ns, classes, whelk); - } else if (isEnum(termDefinition, jsonLd)) { - addAllMappings(termKey, ns, enums, whelk); - } - }); - - addMapping(JsonLd.TYPE_KEY, RDF_TYPE, "rdf", properties); - - return new VocabMappings(properties, classes, enums, getPropertiesRestrictedByValue(whelk, coercingProperties)); - } - - private static String getNs(String termKey, String systemVocabNs) { - return JsonLd.looksLikeIri(termKey) - ? termKey.substring(termKey.lastIndexOf("/") + 1) - : (termKey.contains(":") ? termKey.substring(0, termKey.indexOf(":")) : systemVocabNs); - } - - private static void addAllMappings(String termKey, String ns, Map>> mappings, Whelk whelk) { - addMapping(termKey, termKey, ns, mappings); - addMappings(termKey, ns, mappings, whelk.getJsonld()); - addEquivTermMappings(termKey, ns, mappings, whelk); - } - - private static void addMapping(String from, String to, String ns, Map>> mappings) { - mappings.computeIfAbsent(from.toLowerCase(), x -> new HashMap<>()) - .computeIfAbsent(ns, x -> new HashSet<>()) - .add(to); - } - - private static void addMappings(String termKey, String ns, Map>> mappings, JsonLd jsonLd) { - addMappings(termKey, ns, mappings, jsonLd, jsonLd.vocabIndex.get(termKey)); - } - - private static void addMappings(String termKey, String ns, Map>> mappings, JsonLd jsonLd, Map termDefinition) { - String termId = (String) termDefinition.get(ID_KEY); - - addMapping(termId, termKey, ns, mappings); - addMapping(toPrefixed(termId), termKey, ns, mappings); - addMapping(dropNs(termId), termKey, ns, mappings); - - for (String prop : notatingProps) { - if (termDefinition.containsKey(prop)) { - getAsList(termDefinition, List.of(prop)) - .forEach(value -> addMapping((String) value, termKey, ns, mappings)); - } - - String alias = (String) jsonLd.langContainerAlias.get(prop); - if (termDefinition.containsKey(alias)) { - for (String lang : jsonLd.locales) { - getAsList(termDefinition, List.of(alias, lang)) - .forEach(langStr -> addMapping((String) langStr, termKey, ns, mappings)); - } - } - } - } - - private static String dropNs(String termIri) { - return termIri.substring(termIri.lastIndexOf("/") + 1); - } - - private static void addEquivTermMappings(String termKey, String ns, Map>> mappings, Whelk whelk) { - var jsonLd = whelk.getJsonld(); - var vocab = jsonLd.vocabIndex; - - String mappingProperty = isProperty(vocab.get(termKey)) ? EQUIVALENT_PROPERTY : EQUIVALENT_CLASS; - - getAsList(vocab, List.of(termKey, mappingProperty)).forEach(term -> { - String equivPropIri = get(term, List.of(ID_KEY), ""); - if (!equivPropIri.isEmpty()) { - String equivPropKey = jsonLd.toTermKey(equivPropIri); - if (!vocab.containsKey(equivPropKey)) { - var thing = loadThing(equivPropIri, whelk); - if (!thing.isEmpty()) { - addMappings(termKey, ns, mappings, jsonLd, thing); - } else { - addMapping(equivPropIri, termKey, ns, mappings); - addMapping(toPrefixed(equivPropIri), termKey, ns, mappings); - } - } - } - }); - } - - private static boolean isClass(Map termDefinition, JsonLd jsonLd) { - return getTypes(termDefinition).stream().anyMatch(type -> jsonLd.isSubClassOf((String) type, "Class")); - } - - private static boolean isEnum(Map termDefinition, JsonLd jsonLd) { - return getTypes(termDefinition).stream() - .map(String.class::cast) - .flatMap(type -> Stream.concat(jsonLd.getSuperClasses(type).stream(), Stream.of(type))) - .map(jsonLd::getInRange) - .flatMap(Set::stream) - .filter(s -> isProperty(jsonLd.vocabIndex.getOrDefault(s, Map.of()))) - .anyMatch(jsonLd::isVocabTerm); - } - - private static boolean isProperty(Map termDefinition) { - return isObjectProperty(termDefinition) || isDatatypeProperty(termDefinition); - } - - public static boolean isObjectProperty(Map termDefinition) { - return getTypes(termDefinition).stream().anyMatch(OBJECT_PROPERTY::equals); - } - - private static boolean isDatatypeProperty(Map termDefinition) { - return getTypes(termDefinition).stream().anyMatch(DATATYPE_PROPERTY::equals); - } - - private static List getTypes(Map termDefinition) { - return asList(termDefinition.get(JsonLd.TYPE_KEY)); - } - - private static Map>> getPropertiesRestrictedByValue(Whelk whelk, List coercingProps) { - JsonLd ld = whelk.getJsonld(); - - Map> groupedBySuperProp = new HashMap<>(); - coercingProps.forEach(coercing -> { - String superProp = get(ld.vocabIndex, List.of(coercing, JsonLd.Rdfs.SUB_PROPERTY_OF, 0, ID_KEY), ""); - if (!superProp.isEmpty()) { - groupedBySuperProp.computeIfAbsent(ld.toTermKey(superProp), x -> new ArrayList<>()) - .add(coercing); - } - }); - - Map>> propertiesRestrictedByValue = new HashMap<>(); - - groupedBySuperProp.forEach((superProp, coercing) -> { - var types = new HashSet(); - - ld.getRange(superProp).forEach(type -> { - types.add(type); - types.addAll(ld.getSubClasses(type)); - }); - - for (String type : types) { - for (var doc : whelk.getStorage().loadAllByType(type)) { - var iri = doc.getThingIdentifiers().stream().findFirst().orElseThrow(); - for (var n : coercing) { - var propDef = ld.vocabIndex.getOrDefault(n, Map.of()); - Property.getObjectHasValueRestrictions(propDef, ld).forEach(hasValueRestriction -> { - var onProperty = hasValueRestriction.onProperty(); - var hasValue = hasValueRestriction.value(); - var path = List.of(JsonLd.GRAPH_KEY, 1, onProperty.name()); - Predicate hasMatchingValue = o -> switch (hasValue) { - case Term term -> o instanceof String s && s.equals(term.term()); - case VocabTerm vocabTerm -> o instanceof String s && s.equals(vocabTerm.key()); - case Link link -> o instanceof Map m && link.iri().equals(m.get(JsonLd.ID_KEY)); - default -> false; - }; - var matches = ((List) JsonLd.asList(DocumentUtil.getAtPath(doc.data, path, List.of()))).stream() - .anyMatch(hasMatchingValue); - if (matches) { - propertiesRestrictedByValue.computeIfAbsent(superProp, k -> new HashMap<>()) - .computeIfAbsent(iri, k -> new ArrayList<>()) - .add(n); - } - }); - } - } - } - }); - - return propertiesRestrictedByValue; - } - - @SuppressWarnings("unchecked") - private static T get(Object o, List path, T defaultTo) { - return (T) getAtPath(o, path, defaultTo); - } - - private static List getAsList(Object o, List path) { - return asList(get(o, path, null)); - } - - public static String toPrefixed(String iri) { - // TODO: get prefix mappings from context - Map nsToPrefix = new HashMap<>(); - nsToPrefix.put("https://id.kb.se/vocab/", "kbv:"); - nsToPrefix.put("http://id.loc.gov/ontologies/bibframe/", "bf:"); - nsToPrefix.put("http://purl.org/dc/terms/", "dc:"); - nsToPrefix.put("http://schema.org/", "sdo:"); - nsToPrefix.put("https://id.kb.se/term/sao/", "sao:"); - nsToPrefix.put("https://id.kb.se/marc/", "marc:"); - nsToPrefix.put("https://id.kb.se/term/saogf/", "saogf:"); - nsToPrefix.put("https://id.kb.se/term/barn/", "barn:"); - nsToPrefix.put("https://id.kb.se/term/barngf/", "barngf:"); - nsToPrefix.put("https://libris.kb.se/library/", "sigel:"); - nsToPrefix.put("https://id.kb.se/language/", "lang:"); - nsToPrefix.put(Document.getBASE_URI().toString(), "libris:"); - - for (String ns : nsToPrefix.keySet()) { - if (iri.startsWith(ns)) { - return iri.replace(ns, nsToPrefix.get(ns)); - } - } - - return iri; - } - - public static String expandPrefixed(String s) { - if (!s.contains(":")) { - return s; - } - // TODO: get prefix mappings from context - Map nsToPrefix = new HashMap<>(); - nsToPrefix.put("https://id.kb.se/vocab/", "kbv:"); - nsToPrefix.put("http://id.loc.gov/ontologies/bibframe/", "bf:"); - nsToPrefix.put("http://purl.org/dc/terms/", "dc:"); - nsToPrefix.put("http://schema.org/", "sdo:"); - nsToPrefix.put("https://id.kb.se/term/sao/", "sao:"); - nsToPrefix.put("https://id.kb.se/marc/", "marc:"); - nsToPrefix.put("https://id.kb.se/term/saogf/", "saogf:"); - nsToPrefix.put("https://id.kb.se/term/barn/", "barn:"); - nsToPrefix.put("https://id.kb.se/term/barngf/", "barngf:"); - nsToPrefix.put("https://libris.kb.se/library/", "sigel:"); - nsToPrefix.put("https://id.kb.se/language/", "lang:"); - nsToPrefix.put(Document.getBASE_URI().toString(), "libris:"); - - for (String ns : nsToPrefix.keySet()) { - String prefix = nsToPrefix.get(ns); - if (s.startsWith(prefix)) { - return s.replace(prefix, ns); - } - } - - return s; - } -} diff --git a/whelk-core/src/main/groovy/whelk/search2/querytree/Link.java b/whelk-core/src/main/groovy/whelk/search2/querytree/Link.java index b88839bfda..b34adcb522 100644 --- a/whelk-core/src/main/groovy/whelk/search2/querytree/Link.java +++ b/whelk-core/src/main/groovy/whelk/search2/querytree/Link.java @@ -1,7 +1,7 @@ package whelk.search2.querytree; import whelk.search2.QueryUtil; -import whelk.search2.VocabMappings; +import whelk.search2.ResourceLookup; import java.util.LinkedHashMap; import java.util.Map; @@ -69,7 +69,7 @@ public Map description() { @Override public String queryForm() { - return token != null ? token.formatted() : QueryUtil.quote(VocabMappings.toPrefixed(iri)); + return token != null ? token.formatted() : QueryUtil.quote(ResourceLookup.toPrefixed(iri)); } @Override @@ -79,7 +79,7 @@ public String jsonForm() { @Override public String toString() { - return VocabMappings.toPrefixed(iri); + return ResourceLookup.toPrefixed(iri); } @Override diff --git a/whelk-core/src/test/groovy/whelk/search2/DisambiguateSpec.groovy b/whelk-core/src/test/groovy/whelk/search2/DisambiguateSpec.groovy index bd0408e550..c36e3e9164 100644 --- a/whelk-core/src/test/groovy/whelk/search2/DisambiguateSpec.groovy +++ b/whelk-core/src/test/groovy/whelk/search2/DisambiguateSpec.groovy @@ -80,5 +80,6 @@ class DisambiguateSpec extends Specification { 'p12' | 'xyz' | InvalidValue.forbidden('xyz') 'p12' | '19900101' | InvalidValue.forbidden('19900101') 'p12' | '1990/01/01' | InvalidValue.forbidden('1990/01/01') + 'p15' | 'XYZ' | new Link('https://libris.kb.se/XYZ') } } diff --git a/whelk-core/src/test/groovy/whelk/search2/querytree/TestData.groovy b/whelk-core/src/test/groovy/whelk/search2/querytree/TestData.groovy index 007116c172..3a068d9c72 100644 --- a/whelk-core/src/test/groovy/whelk/search2/querytree/TestData.groovy +++ b/whelk-core/src/test/groovy/whelk/search2/querytree/TestData.groovy @@ -3,7 +3,7 @@ package whelk.search2.querytree import whelk.JsonLd import whelk.search2.Disambiguate import whelk.search2.EsMappings -import whelk.search2.VocabMappings +import whelk.search2.ResourceLookup import java.util.stream.Stream @@ -72,7 +72,9 @@ class TestData { ] ] - def vocabMappings = new VocabMappings(propertyMappings, classMappings, enumMappings, propertiesRestrictedByValue) + def vocabMappings = new ResourceLookup.VocabMappings(propertyMappings, classMappings, enumMappings, propertiesRestrictedByValue) + def otherMappings = new ResourceLookup.ExternalMappings(['T5': ['XYZ': ['@id': 'https://libris.kb.se/XYZ']]]) + def resourceMappings = new ResourceLookup(vocabMappings, otherMappings) def filterAliases = [ excludeFilter, @@ -80,7 +82,7 @@ class TestData { XYFilter ] - return new Disambiguate(vocabMappings, filterAliases, getJsonLd()) + return new Disambiguate(resourceMappings, filterAliases, getJsonLd()) } static def getJsonLd() { @@ -177,7 +179,8 @@ class TestData { ], [ '@id' : 'p15', - '@type': 'ObjectProperty' + '@type': 'ObjectProperty', + 'range': ['@id': 'T5'] ], [ '@id' : 'ctxProp', @@ -250,6 +253,7 @@ class TestData { ['@id': 'T2x', '@type': 'Class', 'subClassOf': [['@id': 'T2']]], ['@id': 'T3x', '@type': 'Class', 'subClassOf': [['@id': 'T3']]], ['@id': 'T4', '@type': 'Class'], + ['@id': 'T5', '@type': 'Class'], ['@id': 'E1', '@type': 'Class'], ['@id': 'E2', '@type': 'Class'], ['@id': 'p', '@type': 'DatatypeProperty'], From a3bb73aa40b7e914bbcfd5628d611b1eec597326 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kalle=20W=C3=A5hlin?= Date: Tue, 21 Apr 2026 15:02:43 +0200 Subject: [PATCH 2/3] Avoid redundant chip loading --- whelk-core/src/main/groovy/whelk/search2/Query.java | 4 +++- whelk-core/src/main/groovy/whelk/search2/querytree/Link.java | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/whelk-core/src/main/groovy/whelk/search2/Query.java b/whelk-core/src/main/groovy/whelk/search2/Query.java index d7be2d957b..ebeddc2111 100644 --- a/whelk-core/src/main/groovy/whelk/search2/Query.java +++ b/whelk-core/src/main/groovy/whelk/search2/Query.java @@ -578,7 +578,9 @@ private void loadChips() { } private void queue(Link link) { - linkMap.computeIfAbsent(link.iri(), k -> new ArrayList<>()).add(link); + if (!link.isChipLoaded()) { + linkMap.computeIfAbsent(link.iri(), k -> new ArrayList<>()).add(link); + } } private void queue(Collection links) { diff --git a/whelk-core/src/main/groovy/whelk/search2/querytree/Link.java b/whelk-core/src/main/groovy/whelk/search2/querytree/Link.java index b34adcb522..7971e05da9 100644 --- a/whelk-core/src/main/groovy/whelk/search2/querytree/Link.java +++ b/whelk-core/src/main/groovy/whelk/search2/querytree/Link.java @@ -62,6 +62,10 @@ public Token token() { return token; } + public boolean isChipLoaded() { + return !chip.isEmpty(); + } + @Override public Map description() { return chip; From 185d2eccc9e83c42543759d13e50cfd160abfe4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kalle=20W=C3=A5hlin?= Date: Tue, 21 Apr 2026 15:39:20 +0200 Subject: [PATCH 3/3] Fix unit test --- .../src/test/groovy/whelk/search2/querytree/TestData.groovy | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/whelk-core/src/test/groovy/whelk/search2/querytree/TestData.groovy b/whelk-core/src/test/groovy/whelk/search2/querytree/TestData.groovy index 3a068d9c72..59f0b0f39c 100644 --- a/whelk-core/src/test/groovy/whelk/search2/querytree/TestData.groovy +++ b/whelk-core/src/test/groovy/whelk/search2/querytree/TestData.groovy @@ -73,8 +73,8 @@ class TestData { ] def vocabMappings = new ResourceLookup.VocabMappings(propertyMappings, classMappings, enumMappings, propertiesRestrictedByValue) - def otherMappings = new ResourceLookup.ExternalMappings(['T5': ['XYZ': ['@id': 'https://libris.kb.se/XYZ']]]) - def resourceMappings = new ResourceLookup(vocabMappings, otherMappings) + def externalMappings = new ResourceLookup.ExternalMappings(['T5': ['xyz': ['@id': 'https://libris.kb.se/XYZ']]]) + def resourceLookup = new ResourceLookup(vocabMappings, externalMappings) def filterAliases = [ excludeFilter, @@ -82,7 +82,7 @@ class TestData { XYFilter ] - return new Disambiguate(resourceMappings, filterAliases, getJsonLd()) + return new Disambiguate(resourceLookup, filterAliases, getJsonLd()) } static def getJsonLd() {