diff --git a/whelk-core/src/main/groovy/whelk/search2/Disambiguate.java b/whelk-core/src/main/groovy/whelk/search2/Disambiguate.java index 7b60839ed4..3d0afda0f2 100644 --- a/whelk-core/src/main/groovy/whelk/search2/Disambiguate.java +++ b/whelk-core/src/main/groovy/whelk/search2/Disambiguate.java @@ -3,7 +3,6 @@ import groovy.transform.PackageScope; import whelk.JsonLd; import whelk.search2.querytree.*; -import whelk.util.Restrictions; import java.time.format.DateTimeParseException; import java.util.*; @@ -14,6 +13,8 @@ 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; @@ -69,10 +70,10 @@ public Selector restrictByValue(Selector selector, String value) { return switch (selector) { case Property p -> restrictByValue(p, value); case Path path -> { - var narrowed = restrictByValue(path.last(), value); + var coerced = restrictByValue(path.last(), value); List newPath = new ArrayList<>(path.path()); newPath.removeLast(); - newPath.addAll(narrowed.path()); + newPath.addAll(coerced.path()); yield new Path(newPath, path.token()); } case Key k -> k; @@ -84,22 +85,22 @@ private boolean isRestrictedByValue(String propertyKey) { } private Property restrictByValue(Property property, String value) { - var narrowed = tryNarrow(property.name(), value); - if (narrowed != null) { - return new Property.NarrowedRestrictedProperty(property, narrowed, jsonLd); + var coercing = tryCoerce(property.name(), value); + if (coercing != null) { + return new Property.CoercingSubProperty(property, coercing, jsonLd); + } else if (property.name().equals(WORK_CATEGORY)) { + // FIXME: Don't hardcode + return new Property.CoercingSubProperty(property, NONE_CATEGORY, jsonLd); } return property; } - private String tryNarrow(String property, String value) { - var narrowedByValue = vocabMappings.propertiesRestrictedByValue() + private String tryCoerce(String property, String value) { + var coercingSubPropertyKey = vocabMappings.propertiesRestrictedByValue() .getOrDefault(property, Map.of()) - .get(expandPrefixed(value)); - if (narrowedByValue != null) { - return narrowedByValue.getFirst(); - } else if (property.equals(Restrictions.CATEGORY)) { - // FIXME: Don't hardcode - return Restrictions.NONE_CATEGORY; + .get(value); + if (coercingSubPropertyKey != null) { + return coercingSubPropertyKey.getFirst(); } return null; } diff --git a/whelk-core/src/main/groovy/whelk/search2/ObjectQuery.java b/whelk-core/src/main/groovy/whelk/search2/ObjectQuery.java index acabee68ce..6cede631e7 100644 --- a/whelk-core/src/main/groovy/whelk/search2/ObjectQuery.java +++ b/whelk-core/src/main/groovy/whelk/search2/ObjectQuery.java @@ -91,7 +91,7 @@ protected EsQuery doGetEsQuery() { } List subjectTypes = Stream.concat(givenSubjectTypes.stream(), inferredSubjectTypes.stream()).toList(); - var aggQuery = getEsAggQuery(subjectTypes); + var aggQuery = new LinkedHashMap<>(getEsAggQuery(subjectTypes)); aggQuery.putAll(getPAggQuery(predicateToSubjectTypes)); esQueryDsl.put("aggs", aggQuery); diff --git a/whelk-core/src/main/groovy/whelk/search2/Query.java b/whelk-core/src/main/groovy/whelk/search2/Query.java index 65154fc154..c3a049b3cf 100644 --- a/whelk-core/src/main/groovy/whelk/search2/Query.java +++ b/whelk-core/src/main/groovy/whelk/search2/Query.java @@ -23,7 +23,6 @@ import whelk.search2.querytree.YearRange; import whelk.util.DocumentUtil; import whelk.util.FresnelUtil; -import whelk.util.Restrictions; import java.util.ArrayList; import java.util.Collection; @@ -71,6 +70,12 @@ public class Query { private ReducedQueryTree fullQueryTree; + static final String WORK_CATEGORY = "librissearch:workCategory"; + + private static final String FIND_CATEGORY = "librissearch:findCategory"; + private static final String IDENTIFY_CATEGORY = "librissearch:identifyCategory"; + static final String NONE_CATEGORY = "librissearch:noneCategory"; + public enum SearchMode { SUGGEST, STANDARD_SEARCH, @@ -450,14 +455,13 @@ private static void addSliceToAggQuery(Map query, Property property = slice.getProperty(); - if (!slice.getShowIf().isEmpty()) { + if (!slice.getShowIf().isEmpty() + && ctx.selectedFacets.isInactive(FIND_CATEGORY) + && ctx.selectedFacets.isInactive(IDENTIFY_CATEGORY) + && ctx.selectedFacets.isInactive(NONE_CATEGORY)) { // Enable @none facet if find/identify/@none in query // TODO don't hardcode this if we decide it is what we want - if (ctx.selectedFacets().getSelected(Restrictions.FIND_CATEGORY).isEmpty() - && ctx.selectedFacets().getSelected(Restrictions.IDENTIFY_CATEGORY).isEmpty() - && ctx.selectedFacets().getSelected(Restrictions.NONE_CATEGORY).isEmpty()) { - return; - } + return; } if (property.isRestrictedSubProperty() && !property.hasIndexKey()) { @@ -465,7 +469,7 @@ private static void addSliceToAggQuery(Map query, throw new RuntimeException("Can't handle combined fields in aggs query"); } - property.getAltSelectors(ctx.jsonLd, ctx.rdfSubjectTypes).stream() + property.getAltSelectors(ctx.jsonLd, ctx.rdfSubjectTypes, false).stream() .map(s -> s.withPrependedMetaProperty(ctx.jsonLd)) .forEach(selector -> { String field = selector.esField(); @@ -494,8 +498,8 @@ private static void addSliceToAggQuery(Map query, m.remove(slice.subSlice().propertyKey()); } // TODO don't hardcode this if we decide it is what we want - if (Restrictions.FIND_CATEGORY.equals(pKey) || Restrictions.IDENTIFY_CATEGORY.equals(pKey)) { - m.remove(Restrictions.NONE_CATEGORY); + if (FIND_CATEGORY.equals(pKey) || IDENTIFY_CATEGORY.equals(pKey)) { + m.remove(NONE_CATEGORY); } //if ("_categoryByCollection.@none".equals(pKey)) { // m.remove("_categoryByCollection.find"); @@ -509,7 +513,7 @@ private static void addSliceToAggQuery(Map query, query.put(field, filterWrap(aggs, property.name(), filter)); }); } - + private static Map buildCoreAqqQuery(String field, AppParams.Slice slice, AggContext ctx) { return buildCoreAqqQuery(field, slice, ctx, false); } @@ -667,14 +671,16 @@ public List> getObservations(AppParams.Slice slice, Value pa var property = slice.getProperty(); String propertyKey = slice.propertyKey(); - List> observations = new ArrayList<>(); - - Connective connective = selectedFacets.getConnective(propertyKey); - QueryTree qt = selectedFacets.isRangeFilter(propertyKey) + QueryTree qt = slice.isRange() ? qTree.removeAll(selectedFacets.getRangeSelected(propertyKey)) : qTree; + List selected = selectedFacets.getSelected(propertyKey); + Connective connective = selectedFacets.inferConnective(propertyKey).orElse(slice.defaultConnective()); + + List> observations = new ArrayList<>(); + this.buckets.entrySet() .stream() // TODO only do this for nested aggs of the same property etc etc @@ -697,7 +703,7 @@ public List> getObservations(AppParams.Slice slice, Value pa // TODO boolean isSelected = selectedValue != null && !selectedValue.isEmpty() ? selectedValue.stream().anyMatch(n -> n instanceof Condition c2 && c2.value() instanceof Link l && v instanceof Link l2 && l.iri().equals(l2.iri())) - : selectedFacets.isSelected(c, propertyKey); + : selected.contains(c); Consumer addObservation = alteredTree -> { Map observation = new LinkedHashMap<>(); @@ -731,9 +737,8 @@ public List> getObservations(AppParams.Slice slice, Value pa //List selected = selectedValue != null ? selectedValue : Collections.emptyList(); //addObservation.accept(qt.remove(selected).add(pv)); Predicate f = (Node n) -> n instanceof Condition c2 - && c2.selector().path().getLast() instanceof Property p - && "category".equals(p.queryKey()) - && p.isRestrictedSubProperty(); + && c2.selector() instanceof Property p + && p instanceof Property.CoercingSubProperty coercing && coercing.getSuperProperty().name().equals(WORK_CATEGORY); var qt2 = qt.removeAll(qt.findTopNodesByCondition(n -> f.test(n) || n instanceof Or or && or.children().stream().anyMatch(f))); if (selectedValue == null || !selectedValue.contains(c)) { @@ -744,7 +749,6 @@ public List> getObservations(AppParams.Slice slice, Value pa return; } - var selected = selectedFacets.getSelected(propertyKey); if (isSelected) { selected.stream() .filter(c::equals) @@ -810,14 +814,14 @@ public Map getSliceByDimension(List slices, Sel /* // Move @none to under selected find/identify // TODO don't hardcode this if we decide it is what we want - var none = s.remove(Restrictions.NONE_CATEGORY); + var none = s.remove(NONE_CATEGORY); if (none != null) { - var find = s.get(Restrictions.FIND_CATEGORY); + var find = s.get(FIND_CATEGORY); if (find != null) { DocumentUtil.traverse(find, (value, path) -> { - if (value instanceof Map m && m.containsKey("_selected") && m.get("_selected").equals(true) && !path.contains(Restrictions.NONE_CATEGORY)) { + if (value instanceof Map m && m.containsKey("_selected") && m.get("_selected").equals(true) && !path.contains(NONE_CATEGORY)) { var newV = new HashMap<>(m); - ((Map) newV.computeIfAbsent("sliceByDimension", k -> new HashMap<>())).put(Restrictions.NONE_CATEGORY, none); + ((Map) newV.computeIfAbsent("sliceByDimension", k -> new HashMap<>())).put(NONE_CATEGORY, none); return new DocumentUtil.Replace(newV); } return DocumentUtil.NOP; @@ -865,13 +869,13 @@ private Map getSliceByDimension(List slices, Se var sliceNode = new LinkedHashMap<>(); var observations = sliceResult.getObservations(slice, parentValue, mySelectedValue, selectedFacets); if (!observations.isEmpty() || parentValue != null) { - if (selectedFacets.isRangeFilter(propertyKey)) { + if (slice.isRange()) { sliceNode.put("search", getRangeTemplate(property)); } sliceNode.put("dimension", property.name()); sliceNode.put("observation", observations); sliceNode.put("maxItems", slice.size()); - sliceNode.put("_connective", selectedFacets.getConnective(propertyKey).name()); + sliceNode.put("_connective", selectedFacets.inferConnective(propertyKey).orElse(slice.defaultConnective())); result.put(property.name(), sliceNode); } }); diff --git a/whelk-core/src/main/groovy/whelk/search2/SelectedFacets.java b/whelk-core/src/main/groovy/whelk/search2/SelectedFacets.java index 155d59d3ae..61be704e94 100644 --- a/whelk-core/src/main/groovy/whelk/search2/SelectedFacets.java +++ b/whelk-core/src/main/groovy/whelk/search2/SelectedFacets.java @@ -6,83 +6,83 @@ import whelk.search2.querytree.Or; import whelk.search2.querytree.QueryTree; import whelk.search2.querytree.YearRange; -import whelk.util.Restrictions; -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.Optional; import java.util.Set; import java.util.function.Predicate; public class SelectedFacets { private final Map> selectedByPropertyKey = new HashMap<>(); - private final Map propertyKeyToConnective = new HashMap<>(); - private final Set rangeProps = new HashSet<>(); + private final Map> multiSelectedByPropertyKey = new HashMap<>(); + private final Map> radioSelectedByPropertyKey = new HashMap<>(); + + private final Set selectable = new HashSet<>(); // TODO: don't hardcode private final Set radioProps = Set.of( - Restrictions.FIND_CATEGORY, - Restrictions.IDENTIFY_CATEGORY + "librissearch:findCategory", + "librissearch:identifyCategory" ); public SelectedFacets(QueryTree queryTree, List sliceList) { - //FIXME - for (String radioProp : radioProps) { - selectedByPropertyKey.put(radioProp, new ArrayList<>()); - } - init(queryTree, sliceList); } public boolean isSelectable(String propertyKey) { - return selectedByPropertyKey.containsKey(propertyKey); + return selectable.contains(propertyKey); } - public boolean isMultiOrRadio(String propertyKey) { - return isMultiSelectable(propertyKey) || isRadioButton(propertyKey); + public boolean isInactive(String propertyKey) { + return getSelected(propertyKey).isEmpty(); } - public boolean isRadioButton(String propertyKey) { - return isSelectable(propertyKey) && radioProps.contains(propertyKey); + public boolean isMultiOrRadio(String propertyKey) { + return isOrSelected(propertyKey) || anyRadioSelected(); } - private boolean isMultiSelectable(String propertyKey) { - return isSelectable(propertyKey) && Query.Connective.OR.equals(propertyKeyToConnective.get(propertyKey)); + public boolean isRadioButton(String propertyKey) { + return radioProps.contains(propertyKey); } public List getSelected(String propertyKey) { - return isSelectable(propertyKey) ? selectedByPropertyKey.get(propertyKey) : List.of(); - } - - public boolean isSelected(Condition condition, String propertyKey) { - return isSelectable(propertyKey) && selectedByPropertyKey.get(propertyKey).contains(condition); + if (isAndSelected(propertyKey)) { + return getAndSelected(propertyKey); + } + if (isOrSelected(propertyKey)) { + return getOrSelected(propertyKey); + } + if (isRadioSelected(propertyKey)) { + return getRadioSelected(propertyKey); + } + return List.of(); } - public Map> getAllMultiOrRadioSelected() { - Map> result = new HashMap<>(); - selectedByPropertyKey.forEach((pKey, selected) -> { - if (isMultiOrRadio(pKey) && !getSelected(pKey).isEmpty()) { - result.put(pKey, getSelected(pKey)); - } - }); - return result; + public Optional inferConnective(String propertyKey) { + if (isAndSelected(propertyKey)) { + return Optional.of(Query.Connective.AND); + } + if (isOrSelected(propertyKey)) { + return Optional.of(Query.Connective.OR); + } + return Optional.empty(); } - public List getRangeSelected(String propertyKey) { + List getRangeSelected(String propertyKey) { return getSelected(propertyKey).stream() .filter(c -> c.operator().isRange() || c.value() instanceof YearRange) .toList(); } - public Query.Connective getConnective(String propertyKey) { - return propertyKeyToConnective.getOrDefault(propertyKey, Query.Connective.AND); - } - - public boolean isRangeFilter(String propertyKey) { - return rangeProps.contains(propertyKey); + public Map> getAllMultiOrRadioSelected() { + Map> result = new HashMap<>(); + result.putAll(multiSelectedByPropertyKey); + result.putAll(radioSelectedByPropertyKey); + return result; } public static QueryTree buildMultiSelectedTree(Collection> multiSelected) { @@ -98,54 +98,73 @@ public static QueryTree buildMultiSelectedTree(Collection isProperty = n -> n instanceof Condition c && c.selector().equals(property); - Predicate hasEqualsOp = n -> ((Condition) n).operator().equals(Operator.EQUALS); - Predicate isPropertyEquals = n -> isProperty.test(n) && hasEqualsOp.test(n); + private boolean anyRadioSelected() { + return !radioSelectedByPropertyKey.isEmpty(); + } - List allNodesWithProperty = queryTree.allDescendants().filter(isProperty).map(Condition.class::cast).toList(); + private List getAndSelected(String propertyKey) { + return selectedByPropertyKey.get(propertyKey); + } - if (slice.subSlice() != null) { - addSlice(slice.subSlice(), queryTree); - } + private List getOrSelected(String propertyKey) { + return multiSelectedByPropertyKey.get(propertyKey); + } - if (allNodesWithProperty.isEmpty()) { - selectedByPropertyKey.put(pKey, List.of()); - propertyKeyToConnective.put(pKey, slice.defaultConnective()); - return; - } + private List getRadioSelected(String propertyKey) { + return radioSelectedByPropertyKey.get(propertyKey); + } + + private void addSlice(AppParams.Slice slice, QueryTree queryTree) { + if (slice.subSlice() != null) { + addSlice(slice.subSlice(), queryTree); + } + + Predicate isProperty = n -> n instanceof Condition c && slice.getProperty().equals(c.selector()); - List selected = queryTree.getTopNodes().stream() - .filter(slice.isRange() ? isProperty : isPropertyEquals) - .map(Condition.class::cast) - .toList(); - - List> multiSelected = queryTree.getTopNodesOfType(Or.class).stream() - .map(Or::children) - .filter(orChildren -> orChildren.stream().allMatch(isPropertyEquals)) - .map(selectedConditions -> selectedConditions.stream().map(Condition.class::cast).toList()) - .toList(); - - if (selected.isEmpty() && multiSelected.size() == 1) { - var selectedConditions = multiSelected.getFirst(); - if (selectedConditions.equals(allNodesWithProperty)) { - selectedByPropertyKey.put(pKey, selectedConditions); - propertyKeyToConnective.put(pKey, Query.Connective.OR); + List selected = queryTree.getTopNodes().stream() + .filter(isProperty) + .map(Condition.class::cast) + .toList(); + + List> multiSelected = queryTree.getTopNodesOfType(Or.class).stream() + .map(Or::children) + .filter(orChildren -> orChildren.stream().allMatch(isProperty)) + .map(selectedConditions -> selectedConditions.stream().map(Condition.class::cast).toList()) + .toList(); + + String pKey = slice.getProperty().name(); + + if (selected.isEmpty() && multiSelected.size() == 1 && !isRadioButton(pKey)) { + multiSelectedByPropertyKey.put(pKey, multiSelected.getFirst()); + } else if (selected.size() == 1 && multiSelected.isEmpty()) { + if (isRadioButton(pKey)) { + radioSelectedByPropertyKey.put(pKey, selected); + } else { + switch (slice.defaultConnective()) { + case AND -> selectedByPropertyKey.put(pKey, selected); + case OR -> multiSelectedByPropertyKey.put(pKey, selected); } - } else if (multiSelected.isEmpty() && selected.equals(allNodesWithProperty)) { - selectedByPropertyKey.put(pKey, selected); - propertyKeyToConnective.put(pKey, selected.size() == 1 ? slice.defaultConnective() : Query.Connective.AND); } + } else if (selected.size() > 1 && multiSelected.isEmpty()) { + selectedByPropertyKey.put(pKey, selected); + } else if (!selected.isEmpty() || !multiSelected.isEmpty()) { + // Can't be mirrored in facets + return; } + + selectable.add(pKey); } private void init(QueryTree queryTree, List sliceList) { diff --git a/whelk-core/src/main/groovy/whelk/search2/SuggestQuery.java b/whelk-core/src/main/groovy/whelk/search2/SuggestQuery.java index 08f7f6e3cc..36f9234313 100644 --- a/whelk-core/src/main/groovy/whelk/search2/SuggestQuery.java +++ b/whelk-core/src/main/groovy/whelk/search2/SuggestQuery.java @@ -44,8 +44,8 @@ public class SuggestQuery extends Query { put("Subject", List.of("subject")); put("Language", List.of("language", "originalLanguage")); put("BibliographicAgent", List.of("contributor", "subject")); - put("WorkCategory", List.of("category")); - put("InstanceCategory", List.of("hasInstanceCategory")); + put("WorkCategory", List.of("workCategory")); + put("InstanceCategory", List.of("instanceCategory")); }}; private record Edited(Node node, Token token) { diff --git a/whelk-core/src/main/groovy/whelk/search2/VocabMappings.java b/whelk-core/src/main/groovy/whelk/search2/VocabMappings.java index 581ea29c8a..0c43e8f027 100644 --- a/whelk-core/src/main/groovy/whelk/search2/VocabMappings.java +++ b/whelk-core/src/main/groovy/whelk/search2/VocabMappings.java @@ -8,7 +8,6 @@ import whelk.search2.querytree.Term; import whelk.search2.querytree.VocabTerm; import whelk.util.DocumentUtil; -import whelk.util.Restrictions; import java.util.ArrayList; import java.util.HashMap; @@ -63,7 +62,7 @@ public record VocabMappings( Map>> for example: [ - "category": [ + "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"] @@ -88,10 +87,15 @@ private static VocabMappings getMappings(Whelk whelk) { 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)) { @@ -101,7 +105,7 @@ private static VocabMappings getMappings(Whelk whelk) { addMapping(JsonLd.TYPE_KEY, RDF_TYPE, "rdf", properties); - return new VocabMappings(properties, classes, enums, getPropertiesRestrictedByValue(whelk)); + return new VocabMappings(properties, classes, enums, getPropertiesRestrictedByValue(whelk, coercingProperties)); } private static String getNs(String termKey, String systemVocabNs) { @@ -206,17 +210,21 @@ private static List getTypes(Map termDefinition) { return asList(termDefinition.get(JsonLd.TYPE_KEY)); } - private static Map>> getPropertiesRestrictedByValue(Whelk whelk) { + private static Map>> getPropertiesRestrictedByValue(Whelk whelk, List coercingProps) { JsonLd ld = whelk.getJsonld(); Map> groupedBySuperProp = new HashMap<>(); - Restrictions.NARROWS.forEach((narrowerProp, superProp) -> - groupedBySuperProp.computeIfAbsent(superProp, x -> new ArrayList<>()) - .add(narrowerProp)); + 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, narrowerProps) -> { + groupedBySuperProp.forEach((superProp, coercing) -> { var types = new HashSet(); ld.getRange(superProp).forEach(type -> { @@ -227,10 +235,10 @@ private static Map>> getPropertiesRestrictedByV for (String type : types) { for (var doc : whelk.getStorage().loadAllByType(type)) { var iri = doc.getThingIdentifiers().stream().findFirst().orElseThrow(); - for (var n : narrowerProps) { + for (var n : coercing) { var propDef = ld.vocabIndex.getOrDefault(n, Map.of()); Property.getObjectHasValueRestrictions(propDef, ld).forEach(hasValueRestriction -> { - var onProperty = hasValueRestriction.property(); + var onProperty = hasValueRestriction.onProperty(); var hasValue = hasValueRestriction.value(); var path = List.of(JsonLd.GRAPH_KEY, 1, onProperty.name()); Predicate hasMatchingValue = o -> switch (hasValue) { diff --git a/whelk-core/src/main/groovy/whelk/search2/querytree/Condition.java b/whelk-core/src/main/groovy/whelk/search2/querytree/Condition.java index 9b741167e1..ed27cf2d3a 100644 --- a/whelk-core/src/main/groovy/whelk/search2/querytree/Condition.java +++ b/whelk-core/src/main/groovy/whelk/search2/querytree/Condition.java @@ -144,7 +144,7 @@ public Type asTypeNode() { } private ExpandedNode expandWithAltSelectors(JsonLd jsonLd, Collection rdfSubjectTypes) { - List withAltSelectors = selector.getAltSelectors(jsonLd, rdfSubjectTypes).stream() + List withAltSelectors = selector.getAltSelectors(jsonLd, rdfSubjectTypes, true).stream() .map(s -> s.withPrependedMetaProperty(jsonLd)) .map(this::withSelector) .map(s -> s._expand(jsonLd)) @@ -169,12 +169,9 @@ private List getPrefilledFields(List path) { for (PathElement pe : path) { currentPath.add(pe); if (pe instanceof Property p && p.isRestrictedSubProperty() && !p.hasIndexKey()) { - for (Restrictions.OnProperty r : p.objectOnPropertyRestrictions()) { - // Support only HasValue restriction for now - if (r instanceof Restrictions.HasValue(Property property, Value v)) { - var restrictedPath = new Path(Stream.concat(currentPath.stream(), property.path().stream()).toList()); - prefilledFields.add(new Condition(restrictedPath, EQUALS, v)); - } + for (Restrictions.HasValue r : p.objectOnPropertyRestrictions()) { + var restrictedPath = new Path(Stream.concat(currentPath.stream(), r.onProperty().path().stream()).toList()); + prefilledFields.add(new Condition(restrictedPath, EQUALS, r.value())); } } } diff --git a/whelk-core/src/main/groovy/whelk/search2/querytree/Key.java b/whelk-core/src/main/groovy/whelk/search2/querytree/Key.java index 045ec5f7f2..61b08f5662 100644 --- a/whelk-core/src/main/groovy/whelk/search2/querytree/Key.java +++ b/whelk-core/src/main/groovy/whelk/search2/querytree/Key.java @@ -31,7 +31,7 @@ public List path() { } @Override - public List getAltSelectors(JsonLd jsonLd, Collection rdfSubjectTypes) { + public List getAltSelectors(JsonLd jsonLd, Collection rdfSubjectTypes, boolean allowIncompatible) { return List.of(this); } diff --git a/whelk-core/src/main/groovy/whelk/search2/querytree/Path.java b/whelk-core/src/main/groovy/whelk/search2/querytree/Path.java index 77a97b1301..ee0ddde34d 100644 --- a/whelk-core/src/main/groovy/whelk/search2/querytree/Path.java +++ b/whelk-core/src/main/groovy/whelk/search2/querytree/Path.java @@ -52,10 +52,16 @@ public List path() { } @Override - public List getAltSelectors(JsonLd jsonLd, Collection rdfSubjectTypes) { - return getAltPaths(path(), jsonLd, rdfSubjectTypes).stream() - .map(l -> l.size() > 1 ? new Path(l) : l.getFirst()) - .toList(); + public List getAltSelectors(JsonLd jsonLd, Collection rdfSubjectTypes, boolean allowIncompatible) { + List altSelectors = new ArrayList<>(); + getAltPaths(path(), jsonLd, rdfSubjectTypes, allowIncompatible).forEach(l -> { + if (l.size() == 1) { + altSelectors.add(l.getFirst()); + } else if (l.size() > 1) { + altSelectors.add(new Path(l)); + } + }); + return altSelectors; } @Override @@ -70,28 +76,15 @@ public Selector withPrependedMetaProperty(JsonLd jsonLd) { return new Path(newPath); } - private List> getAltPaths(List tail, JsonLd jsonLd, Collection rdfSubjectTypes) { + private List> getAltPaths(List tail, JsonLd jsonLd, Collection rdfSubjectTypes, boolean allowIncompatible) { if (tail.isEmpty()) { return List.of(List.of()); } var next = tail.getFirst(); var newTail = tail.subList(1, tail.size()); - var nextAltSelectors = next.getAltSelectors(jsonLd, rdfSubjectTypes); - if (nextAltSelectors.isEmpty()) { - // Indicates that an integral relation has been canceled out by its reverse - // For example, when instanceOf is prepended to hasInstance.x - // the integral property is dropped and only the tail (x) is kept - return getAltPaths(newTail, jsonLd, List.of()); - } + var nextAltSelectors = next.getAltSelectors(jsonLd, rdfSubjectTypes, allowIncompatible); return nextAltSelectors.stream() - .flatMap(s -> getAltPaths(newTail, jsonLd, next.range()).stream() - .filter(altPath -> - // Avoid creating alternative paths caused by inverse integral round-trips. - // For example, if the original path is hasInstance.x, do not - // generate the alternative path x via instanceOf.hasInstance.x. - !(s instanceof Property p1 - && !altPath.isEmpty() && altPath.getFirst() instanceof Property p2 - && p1.isInverseOf(p2))) + .flatMap(s -> getAltPaths(newTail, jsonLd, next.range(), allowIncompatible).stream() .map(altPath -> Stream.concat(s.path().stream(), altPath.stream()))) .map(Stream::toList) .toList(); diff --git a/whelk-core/src/main/groovy/whelk/search2/querytree/Property.java b/whelk-core/src/main/groovy/whelk/search2/querytree/Property.java index ec1b3bb868..d023300f9e 100644 --- a/whelk-core/src/main/groovy/whelk/search2/querytree/Property.java +++ b/whelk-core/src/main/groovy/whelk/search2/querytree/Property.java @@ -48,7 +48,7 @@ public non-sealed class Property extends PathElement { protected boolean isLdSetContainer; protected Property superProperty; - protected List objectOnPropertyRestrictions; + protected List objectOnPropertyRestrictions; private static final String LIBRIS_SEARCH_NS = "librissearch:"; @@ -102,8 +102,8 @@ public static Property buildProperty(String propertyKey, JsonLd jsonLd, Key.Reco if (isShorthand(propDef)) { return new ShorthandProperty(propertyKey, jsonLd, queryKey); } - if (Restrictions.isNarrowingProperty(propertyKey)) { - return new NarrowedRestrictedProperty(propertyKey, jsonLd, queryKey); + if (isCoercing(propDef)) { + return new CoercingSubProperty(propertyKey, jsonLd, queryKey); } if (RDF_TYPE.equals(propertyKey)) { return new RdfType(jsonLd, queryKey); @@ -119,7 +119,9 @@ public static Property buildProperty(String propertyKey, JsonLd jsonLd, Key.Reco @Override public String queryKey() { - return queryKey != null ? queryKey.queryKey() : name; + return queryKey != null + ? queryKey.queryKey() + : (name.startsWith(LIBRIS_SEARCH_NS) ? name.replace(LIBRIS_SEARCH_NS, "") : name); // FIXME } @Override @@ -135,8 +137,8 @@ public List path() { } @Override - public List getAltSelectors(JsonLd jsonLd, Collection rdfSubjectTypes) { - return _getAltSelectors(jsonLd, rdfSubjectTypes); + public List getAltSelectors(JsonLd jsonLd, Collection rdfSubjectTypes, boolean allowIncompatible) { + return _getAltSelectors(jsonLd, rdfSubjectTypes, allowIncompatible); } @Override @@ -153,7 +155,7 @@ public boolean isValid() { @Override public boolean isType() { - return isRdfType(); + return isRdfType() || TYPE_KEY.equals(indexKey); } @Override @@ -236,14 +238,12 @@ public boolean hasIndexKey() { return indexKey != null; } - public List objectOnPropertyRestrictions() { + public List objectOnPropertyRestrictions() { return objectOnPropertyRestrictions != null ? objectOnPropertyRestrictions : List.of(); } - protected List getObjectHasValueRestrictions(JsonLd jsonLd) { - return getObjectHasValueRestrictions(definition, jsonLd).stream() - .map(Restrictions.OnProperty.class::cast) - .toList(); + protected List getObjectHasValueRestrictions(JsonLd jsonLd) { + return getObjectHasValueRestrictions(definition, jsonLd); } public static List getObjectHasValueRestrictions(Map definition, JsonLd jsonLd) { @@ -305,7 +305,7 @@ public int hashCode() { return Objects.hash(toString()); } - private List _getAltSelectors(JsonLd jsonLd, Collection rdfSubjectTypes) { + private List _getAltSelectors(JsonLd jsonLd, Collection rdfSubjectTypes, boolean allowIncompatible) { if (rdfSubjectTypes.isEmpty() || isPlatformTerm() || isRdfType()) { return List.of(this); } @@ -322,18 +322,14 @@ private List _getAltSelectors(JsonLd jsonLd, Collection rdfSub List> altPaths = integralRelations.stream() .filter(followIntegralRelation) - .map(ir -> { - var altPath = new ArrayList<>(path()); - if (ir.isInverseOf(altPath.getFirst())) { - altPath.removeFirst(); - } else { - altPath.addFirst(ir); - } - return altPath; - }) + .map(ir -> Stream.concat(Stream.of(ir), path().stream()).toList()) .collect(Collectors.toList()); - if (altPaths.isEmpty() || isRecordProperty || rdfSubjectTypes.stream().anyMatch(t -> this.mayAppearOnType(t, jsonLd))) { + if (isRecordProperty || rdfSubjectTypes.stream().anyMatch(t -> this.mayAppearOnType(t, jsonLd))) { + altPaths.add(List.of(this)); + } + + if (altPaths.isEmpty() && allowIncompatible) { altPaths.add(List.of(this)); } @@ -384,6 +380,11 @@ private static boolean isComposite(Map definition) { return isCategory("https://id.kb.se/ns/librissearch/composite", definition); } + private static boolean isCoercing(Map definition) { + // FIXME: don't hardcode + return isCategory("https://id.kb.se/ns/librissearch/coercing", definition); + } + private static boolean isShorthand(Map definition) { // FIXME: don't hardcode return isCategory("https://id.kb.se/vocab/shorthand", definition); @@ -462,17 +463,21 @@ public Meta(JsonLd jsonLd, Key.RecognizedKey key) { } } - public static final class NarrowedRestrictedProperty extends Property { - public NarrowedRestrictedProperty(Property superProperty, String subPropertyKey, JsonLd jsonLd) { + public static final class CoercingSubProperty extends Property { + public CoercingSubProperty(Property superProperty, String subPropertyKey, JsonLd jsonLd) { super(subPropertyKey, jsonLd); this.superProperty = superProperty; } - public NarrowedRestrictedProperty(String subPropertyKey, JsonLd jsonLd, Key.RecognizedKey queryKey) { + public CoercingSubProperty(String subPropertyKey, JsonLd jsonLd, Key.RecognizedKey queryKey) { super(subPropertyKey, jsonLd, queryKey); this.superProperty = getSuperProperty(jsonLd); } + public Property getSuperProperty() { + return superProperty; + } + @Override public String queryKey() { return superProperty.queryKey(); @@ -483,6 +488,7 @@ public Map definition() { return superProperty.definition(); } + @Override public String esField() { if (hasIndexKey()) { return indexKey; @@ -494,11 +500,6 @@ public String esField() { public boolean isRestrictedSubProperty() { return true; } - - @Override - public boolean equals(Object obj) { - return super.equals(obj) || superProperty.equals(obj); - } } private static final class AnonymousProperty extends Property { @@ -529,7 +530,7 @@ public String esField() { } @Override - protected List getObjectHasValueRestrictions(JsonLd jsonLd) { + protected List getObjectHasValueRestrictions(JsonLd jsonLd) { var range = getUnambiguousRange(jsonLd); if (range != null) { /* @@ -575,9 +576,9 @@ public CompositeProperty(String name, JsonLd jsonLd, Key.RecognizedKey key) { } @Override - public List getAltSelectors(JsonLd jsonLd, Collection rdfSubjectTypes) { + public List getAltSelectors(JsonLd jsonLd, Collection rdfSubjectTypes, boolean allowIncompatible) { return getComponents(jsonLd).stream() - .flatMap(s -> s.getAltSelectors(jsonLd, rdfSubjectTypes).stream()) + .flatMap(s -> s.getAltSelectors(jsonLd, rdfSubjectTypes, allowIncompatible).stream()) .toList(); } @@ -627,8 +628,8 @@ public List path() { } @Override - public List getAltSelectors(JsonLd jsonLd, Collection rdfSubjectTypes) { - return new Path(propertyChain).getAltSelectors(jsonLd, rdfSubjectTypes); + public List getAltSelectors(JsonLd jsonLd, Collection rdfSubjectTypes, boolean allowIncompatible) { + return new Path(propertyChain).getAltSelectors(jsonLd, rdfSubjectTypes, allowIncompatible); } @Override diff --git a/whelk-core/src/main/groovy/whelk/search2/querytree/QueryTreeBuilder.java b/whelk-core/src/main/groovy/whelk/search2/querytree/QueryTreeBuilder.java index e884cc1962..5ef4817000 100644 --- a/whelk-core/src/main/groovy/whelk/search2/querytree/QueryTreeBuilder.java +++ b/whelk-core/src/main/groovy/whelk/search2/querytree/QueryTreeBuilder.java @@ -135,10 +135,10 @@ private static Node buildFromCode(Ast.Code c, Disambiguate disambiguate, Selecto private static Condition buildCondition(Selector selector, Operator operator, Ast.Leaf leaf, Disambiguate disambiguate) { Token token = getToken(leaf.value()); - if (disambiguate.isRestrictedByValue(selector)) { - selector = disambiguate.restrictByValue(selector, token.value()); - } Value value = disambiguate.mapValueForSelector(selector, token).orElse(new FreeText(token)); + if (value instanceof Resource r && disambiguate.isRestrictedByValue(selector)) { + selector = disambiguate.restrictByValue(selector, r.jsonForm()); + } Condition condition = new Condition(selector, operator, value); return condition.isTypeNode() ? condition.asTypeNode() : condition; } diff --git a/whelk-core/src/main/groovy/whelk/search2/querytree/Selector.java b/whelk-core/src/main/groovy/whelk/search2/querytree/Selector.java index aaa582eefb..e18fc9ba02 100644 --- a/whelk-core/src/main/groovy/whelk/search2/querytree/Selector.java +++ b/whelk-core/src/main/groovy/whelk/search2/querytree/Selector.java @@ -15,7 +15,7 @@ public sealed interface Selector permits Path, PathElement { List path(); - List getAltSelectors(JsonLd jsonLd, Collection rdfSubjectTypes); + List getAltSelectors(JsonLd jsonLd, Collection rdfSubjectTypes, boolean allowIncompatible); Selector withPrependedMetaProperty(JsonLd jsonLd); boolean isValid(); diff --git a/whelk-core/src/main/groovy/whelk/util/Restrictions.java b/whelk-core/src/main/groovy/whelk/util/Restrictions.java index 8e44e611ee..2555b168cc 100644 --- a/whelk-core/src/main/groovy/whelk/util/Restrictions.java +++ b/whelk-core/src/main/groovy/whelk/util/Restrictions.java @@ -3,32 +3,10 @@ import whelk.search2.querytree.Property; import whelk.search2.querytree.Value; -import java.util.Collection; -import java.util.Map; - - public class Restrictions { - public static String CATEGORY = "category"; - public static String FIND_CATEGORY = "librissearch:findCategory"; - public static String IDENTIFY_CATEGORY = "librissearch:identifyCategory"; - public static String NONE_CATEGORY = "librissearch:noneCategory"; - - public sealed interface OnProperty permits HasNoneOfValues, HasValue { - } - - public record HasValue(Property property, Value value) implements OnProperty { - } - - public record HasNoneOfValues(Property property, Collection values) implements OnProperty { + public sealed interface OnProperty permits HasValue { } - public static boolean isNarrowingProperty(String propertyName) { - return NARROWS.containsKey(propertyName); + public record HasValue(Property onProperty, Value value) implements OnProperty { } - - public static final Map NARROWS = Map.of( - FIND_CATEGORY, CATEGORY, - IDENTIFY_CATEGORY, CATEGORY, - NONE_CATEGORY, CATEGORY - ); } diff --git a/whelk-core/src/test/groovy/whelk/search2/QuerySpec.groovy b/whelk-core/src/test/groovy/whelk/search2/QuerySpec.groovy index e926b93068..b67b0a6b54 100644 --- a/whelk-core/src/test/groovy/whelk/search2/QuerySpec.groovy +++ b/whelk-core/src/test/groovy/whelk/search2/QuerySpec.groovy @@ -24,7 +24,7 @@ class QuerySpec extends Specification { "sliceList": [ ["dimensionChain": ["findCategory"], "slice": ["dimensionChain": ["identifyCategory"]]], ["dimensionChain": ["noneCategory"], "itemLimit": 100, "connective": "OR", "showIf": ["category"]], - ["dimensionChain": ["hasInstanceCategory"], "itemLimit": 100] + ["dimensionChain": ["instanceCategory"], "itemLimit": 100] ] ] ] @@ -242,53 +242,53 @@ class QuerySpec extends Specification { expect: aggQuery == [ "_categoryByCollection.find.@id" : [ + "filter" : [ + "match_all" : [:] + ], "aggs" : [ "librissearch:findCategory" : [ "terms" : [ + "size" : 10, + "field" : "_categoryByCollection.find.@id", "order" : [ "_count" : "desc" - ], - "field" : "_categoryByCollection.find.@id", - "size" : 10 + ] ], "aggs" : [ "_categoryByCollection.identify.@id" : [ + "filter" : [ + "match_all" : [:] + ], "aggs" : [ "librissearch:identifyCategory" : [ "terms" : [ + "size" : 10, + "field" : "_categoryByCollection.identify.@id", "order" : [ "_count" : "desc" - ], - "field" : "_categoryByCollection.identify.@id", - "size" : 10 + ] ] ] - ], - "filter" : [ - "match_all": [:] ] ] ] ] - ], - "filter" : [ - "match_all": [:] ] ], - "@reverse.instanceOf.category.@id" : [ + "@reverse.instanceOf._categoryByCollection.@none.@id" : [ + "filter" : [ + "match_all" : [:] + ], "aggs" : [ - "hasInstanceCategory" : [ + "librissearch:instanceCategory" : [ "terms" : [ + "size" : 100, + "field" : "@reverse.instanceOf._categoryByCollection.@none.@id", "order" : [ "_count" : "desc" - ], - "field" : "@reverse.instanceOf.category.@id", - "size" : 100 + ] ] ] - ], - "filter" : [ - "match_all": [:] ] ] ] @@ -296,8 +296,8 @@ class QuerySpec extends Specification { def "build agg query for categories 2"() { given: - SelectedFacets selectedFacets = new SelectedFacets(new QueryTree('category:"https://id.kb.se/term/ktg/X"', disambiguate), appParams2.sliceList) - Map aggQuery = Query.buildAggQuery(appParams2.sliceList, jsonLd, [], esSettings, selectedFacets) + SelectedFacets selectedFacets = new SelectedFacets(new QueryTree('workCategory:"https://id.kb.se/term/ktg/X"', disambiguate), appParams2.sliceList) + Map aggQuery = Query.buildAggQuery(appParams2.sliceList, jsonLd, ['T2'], esSettings, selectedFacets) expect: aggQuery == [ @@ -357,15 +357,15 @@ class QuerySpec extends Specification { ] ] ], - "@reverse.instanceOf.category.@id" : [ + "@reverse.instanceOf._categoryByCollection.@none.@id" : [ "aggs" : [ - "hasInstanceCategory" : [ + "librissearch:instanceCategory" : [ "terms" : [ "order" : [ "_count" : "desc" ], "size" : 100, - "field" : "@reverse.instanceOf.category.@id" + "field" : "@reverse.instanceOf._categoryByCollection.@none.@id" ] ] ], @@ -381,4 +381,92 @@ class QuerySpec extends Specification { ] ] } + + def "build agg query for categories 3"() { + given: + SelectedFacets selectedFacets = new SelectedFacets(new QueryTree('workCategory:"https://id.kb.se/term/ktg/Y"', disambiguate), appParams2.sliceList) + Map aggQuery = Query.buildAggQuery(appParams2.sliceList, jsonLd, ['T1'], esSettings, selectedFacets) + + expect: + aggQuery == [ + "instanceOf._categoryByCollection.find.@id" : [ + "aggs" : [ + "librissearch:findCategory" : [ + "terms" : [ + "size" : 10, + "field" : "instanceOf._categoryByCollection.find.@id", + "order" : [ + "_count" : "desc" + ] + ], + "aggs" : [ + "instanceOf._categoryByCollection.identify.@id" : [ + "aggs" : [ + "librissearch:identifyCategory" : [ + "terms" : [ + "size" : 10, + "field" : "instanceOf._categoryByCollection.identify.@id", + "order" : [ + "_count" : "desc" + ] + ] + ] + ], + "filter" : [ + "match_all" : [:] + ] + ] + ] + ] + ], + "filter" : [ + "match_all" : [:] + ] + ], + "instanceOf._categoryByCollection.@none.@id" : [ + "aggs" : [ + "librissearch:noneCategory" : [ + "terms" : [ + "size" : 100, + "field" : "instanceOf._categoryByCollection.@none.@id", + "order" : [ + "_count" : "desc" + ] + ] + ] + ], + "filter" : [ + "bool" : [ + "filter" : [ + "term" : [ + "instanceOf._categoryByCollection.identify.@id" : "https://id.kb.se/term/ktg/Y" + ] + ] + ] + ] + ], + "_categoryByCollection.@none.@id" : [ + "aggs" : [ + "librissearch:instanceCategory" : [ + "terms" : [ + "size" : 100, + "field" : "_categoryByCollection.@none.@id", + "order" : [ + "_count" : "desc" + ] + ] + ] + ], + "filter" : [ + "bool" : [ + "filter" : [ + "term" : [ + "instanceOf._categoryByCollection.identify.@id" : "https://id.kb.se/term/ktg/Y" + ] + ] + ] + ] + ] + ] + } } diff --git a/whelk-core/src/test/groovy/whelk/search2/querytree/ConditionSpec.groovy b/whelk-core/src/test/groovy/whelk/search2/querytree/ConditionSpec.groovy index dd6701b46c..102d196613 100644 --- a/whelk-core/src/test/groovy/whelk/search2/querytree/ConditionSpec.groovy +++ b/whelk-core/src/test/groovy/whelk/search2/querytree/ConditionSpec.groovy @@ -100,10 +100,8 @@ class ConditionSpec extends Specification { "p1:v1" | ["T1"] | "instanceOf.p1:v1 OR p1:v1" "p1:v1" | ["T2"] | "hasInstance.p1:v1 OR p1:v1" "p1:v1" | ["T3"] | "p1:v1" - "hasInstance.p7:v7" | ["T1"] | "p7:v7" "hasInstance.p7:v7" | ["T2"] | "hasInstance.p7:v7" "instanceOf.p8:v8" | ["T1"] | "instanceOf.p8:v8" - "instanceOf.p8:v8" | ["T2"] | "p8:v8" "p5:x" | [] | "meta.p5:x" "meta.p5:x" | [] | "meta.p5:x" "bibliography:x" | ["T1"] | "instanceOf.meta.bibliography:x OR meta.bibliography:x" diff --git a/whelk-core/src/test/groovy/whelk/search2/querytree/EsQueryTreeSpec.groovy b/whelk-core/src/test/groovy/whelk/search2/querytree/EsQueryTreeSpec.groovy index 59b53393ec..5aea057a09 100644 --- a/whelk-core/src/test/groovy/whelk/search2/querytree/EsQueryTreeSpec.groovy +++ b/whelk-core/src/test/groovy/whelk/search2/querytree/EsQueryTreeSpec.groovy @@ -123,7 +123,7 @@ class EsQueryTreeSpec extends Specification { def "category ES query"() { given: - def q = 'type:T1x category:"https://id.kb.se/term/ktg/Y" category:("https://id.kb.se/term/ktg/A" OR "https://id.kb.se/term/ktg/B")' + def q = 'type:T2x workCategory:"https://id.kb.se/term/ktg/Y" workCategory:("https://id.kb.se/term/ktg/A" OR "https://id.kb.se/term/ktg/B")' QueryTree qt = new QueryTree(q, disambiguate) def appConfig = [ "statistics": [ @@ -143,7 +143,7 @@ class EsQueryTreeSpec extends Specification { "bool": [ "filter": [ "term": [ - "@type": "T1x" + "@type": "T2x" ] ] ] @@ -169,8 +169,8 @@ class EsQueryTreeSpec extends Specification { ] ] ]] - ] - ], [ + ]], + [ "bool": [ "filter": [ "term": [ diff --git a/whelk-core/src/test/groovy/whelk/search2/querytree/QueryTreeSpec.groovy b/whelk-core/src/test/groovy/whelk/search2/querytree/QueryTreeSpec.groovy index 8f97574eda..885d503253 100644 --- a/whelk-core/src/test/groovy/whelk/search2/querytree/QueryTreeSpec.groovy +++ b/whelk-core/src/test/groovy/whelk/search2/querytree/QueryTreeSpec.groovy @@ -69,9 +69,16 @@ class QueryTreeSpec extends Specification { "(x OR y) p1:x" | "(x OR y) p1:x" "x OR y" | "x OR y" "(x OR y) z" | "(x OR y) z" - "findcategory:\"https://id.kb.se/term/ktg/X\"" | "category:\"https://id.kb.se/term/ktg/X\"" - "identifycategory:\"https://id.kb.se/term/ktg/Y\"" | "category:\"https://id.kb.se/term/ktg/Y\"" - "nonecategory:\"https://id.kb.se/term/ktg/Z\"" | "category:\"https://id.kb.se/term/ktg/Z\"" + "findcategory:\"https://id.kb.se/term/ktg/X\"" | "workCategory:\"https://id.kb.se/term/ktg/X\"" + "identifycategory:\"https://id.kb.se/term/ktg/Y\"" | "workCategory:\"https://id.kb.se/term/ktg/Y\"" + "workcategory:\"https://id.kb.se/term/ktg/X\"" | "workcategory:\"https://id.kb.se/term/ktg/X\"" + "workcategory:\"https://id.kb.se/term/ktg/Y\"" | "workcategory:\"https://id.kb.se/term/ktg/Y\"" + "workcategory:\"https://id.kb.se/term/ktg/Z\"" | "workcategory:\"https://id.kb.se/term/ktg/Z\"" + "workcategory:Y" | "workcategory:Y" + "workcategory:(X Y)" | "workcategory:(X Y)" + "instancecategory:\"https://id.kb.se/term/ktg/Z\"" | "instancecategory:\"https://id.kb.se/term/ktg/Z\"" + "instancecategory:X" | "instancecategory:X" + "instancecategory:(X Y)" | "instancecategory:(X Y)" "category:\"https://id.kb.se/term/ktg/X\"" | "category:\"https://id.kb.se/term/ktg/X\"" "category:\"https://id.kb.se/term/ktg/Y\"" | "category:\"https://id.kb.se/term/ktg/Y\"" "category:\"https://id.kb.se/term/ktg/Z\"" | "category:\"https://id.kb.se/term/ktg/Z\"" diff --git a/whelk-core/src/test/groovy/whelk/search2/querytree/SelectorSpec.groovy b/whelk-core/src/test/groovy/whelk/search2/querytree/SelectorSpec.groovy index 61c8cd3672..af9465a711 100644 --- a/whelk-core/src/test/groovy/whelk/search2/querytree/SelectorSpec.groovy +++ b/whelk-core/src/test/groovy/whelk/search2/querytree/SelectorSpec.groovy @@ -27,7 +27,7 @@ class SelectorSpec extends Specification { Selector p = ((Condition) QueryTreeBuilder.buildTree("$_p:v", disambiguate)).selector() expect: - p.getAltSelectors(jsonLd, types).collect { it.path().collect { it.toString() }.join(".") } == result + p.getAltSelectors(jsonLd, types, true).collect { it.path().collect { it.toString() }.join(".") } == result where: _p | types | result @@ -49,9 +49,7 @@ class SelectorSpec extends Specification { "p9" | ["T1", "T2"] | ["p9"] "p9" | ["T3"] | ["p9"] "hasInstance.p7" | ["T2"] | ["hasInstance.p7"] - "instanceOf.p8" | ["T2"] | ["p8"] "type" | ["T2"] | ["rdf:type"] - "hasInstance.p7" | ["T1"] | ["p7"] "instanceOf.p8" | ["T1"] | ["instanceOf.p8"] "type" | ["T1"] | ["rdf:type"] "p7.p14" | ["T2"] | ["hasInstance.p7.hasComponent.p14", "hasInstance.p7.p14"] 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 e3132a735c..007116c172 100644 --- a/whelk-core/src/test/groovy/whelk/search2/querytree/TestData.groovy +++ b/whelk-core/src/test/groovy/whelk/search2/querytree/TestData.groovy @@ -38,7 +38,8 @@ class TestData { 'p' : ['p', 'p1'] as Set, 'plabel' : ['p2', 'p3'] as Set, 'pp' : ['p3', 'p4'] as Set, - 'category' : ['category'] as Set, + 'workcategory' : ['librissearch:workCategory'] as Set, + 'instancecategory': ['librissearch:instanceCategory'] as Set, 'findcategory' : ['librissearch:findCategory'] as Set, 'identifycategory': ['librissearch:identifyCategory'] as Set, 'nonecategory' : ['librissearch:noneCategory'] as Set, @@ -65,7 +66,7 @@ class TestData { Stream.of(propertyMappings, classMappings, enumMappings).forEach(insertNamespace) def propertiesRestrictedByValue = [ - 'category': [ + 'librissearch:workCategory': [ 'https://id.kb.se/term/ktg/X': ['librissearch:findCategory'], 'https://id.kb.se/term/ktg/Y': ['librissearch:identifyCategory'] ] @@ -207,38 +208,36 @@ class TestData { 'range' : ['@id': 'T4'] ], [ - '@id' : 'category', - '@type': 'ObjectProperty' + '@id' : 'librissearch:workCategory', + 'category' : ['@id': "https://id.kb.se/ns/librissearch/composite"], + '@type' : 'ObjectProperty', + 'domain' : ['@id': 'T2'] ], [ - '@id' : 'hasInstanceCategory', - 'category' : ['@id': "https://id.kb.se/vocab/shorthand"], - 'domain' : ['@id': 'T2'], - '@type' : 'ObjectProperty', - 'propertyChainAxiom': [['@list': [ - ['@id': 'hasInstance'], - ['@id': 'category'] - ]]] + '@id' : 'librissearch:instanceCategory', + '@type' : 'ObjectProperty', + 'domain' : ['@id': 'T1'], + 'ls:indexKey': '_categoryByCollection.@none' ], [ '@id' : 'librissearch:findCategory', '@type' : 'ObjectProperty', - 'subPropertyOf': [['@id': 'category']], - 'domain' : ['@id': 'T2'], + 'subPropertyOf': [['@id': 'librissearch:workCategory']], + 'category' : ['@id': "https://id.kb.se/ns/librissearch/coercing"], 'ls:indexKey' : '_categoryByCollection.find' ], [ '@id' : 'librissearch:identifyCategory', '@type' : 'ObjectProperty', - 'subPropertyOf': [['@id': 'category']], - 'domain' : ['@id': 'T2'], + 'subPropertyOf': [['@id': 'librissearch:workCategory']], + 'category' : ['@id': "https://id.kb.se/ns/librissearch/coercing"], 'ls:indexKey' : '_categoryByCollection.identify' ], [ '@id' : 'librissearch:noneCategory', '@type' : 'ObjectProperty', - 'subPropertyOf': [['@id': 'category']], - 'domain' : ['@id': 'T2'], + 'subPropertyOf': [['@id': 'librissearch:workCategory']], + 'category' : ['@id': "https://id.kb.se/ns/librissearch/coercing"], 'ls:indexKey' : '_categoryByCollection.@none' ], ['@id': 'textQuery', '@type': 'DatatypeProperty'], @@ -278,16 +277,19 @@ class TestData { static def getEsMappings() { def mappings = [ 'properties': [ - 'p3' : ['type': 'nested'], - '@reverse.instanceOf.p3' : ['type': 'nested'], - 'p15' : ['type': 'nested', "include_in_parent": true], - '@type' : ['type': 'keyword'], - 'p2' : ['type': 'keyword'], - 'p3.p4.@id' : ['type': 'keyword'], - '_categoryByCollection.find.@id' : ['type': 'keyword'], - '_categoryByCollection.identify.@id': ['type': 'keyword'], - '_categoryByCollection.@none.@id' : ['type': 'keyword'], - '@reverse.instanceOf.category.@id' : ['type': 'keyword'] + 'p3' : ['type': 'nested'], + '@reverse.instanceOf.p3' : ['type': 'nested'], + 'p15' : ['type': 'nested', "include_in_parent": true], + '@type' : ['type': 'keyword'], + 'p2' : ['type': 'keyword'], + 'p3.p4.@id' : ['type': 'keyword'], + '_categoryByCollection.find.@id' : ['type': 'keyword'], + '_categoryByCollection.identify.@id' : ['type': 'keyword'], + '_categoryByCollection.@none.@id' : ['type': 'keyword'], + 'instanceOf._categoryByCollection.find.@id' : ['type': 'keyword'], + 'instanceOf._categoryByCollection.identify.@id' : ['type': 'keyword'], + 'instanceOf._categoryByCollection.@none.@id' : ['type': 'keyword'], + '@reverse.instanceOf._categoryByCollection.@none.@id': ['type': 'keyword'] ] ] // TODO