Repository: incubator-rya Updated Branches: refs/heads/master 85caccf41 -> 6ce0b00b4
RYA-298, RYA-299 Domain/range inference. Closes #197. Project: http://git-wip-us.apache.org/repos/asf/incubator-rya/repo Commit: http://git-wip-us.apache.org/repos/asf/incubator-rya/commit/6ce0b00b Tree: http://git-wip-us.apache.org/repos/asf/incubator-rya/tree/6ce0b00b Diff: http://git-wip-us.apache.org/repos/asf/incubator-rya/diff/6ce0b00b Branch: refs/heads/master Commit: 6ce0b00b4e6f7038ac83019ea20d387afbf1e7ec Parents: 85caccf Author: Jesse Hatfield <jesse.hatfi...@parsons.com> Authored: Fri Aug 4 13:05:45 2017 -0400 Committer: Caleb Meier <caleb.me...@parsons.com> Committed: Fri Aug 18 07:52:48 2017 -0700 ---------------------------------------------------------------------- .../RdfCloudTripleStoreConnection.java | 2 + .../inference/DomainRangeVisitor.java | 114 +++++++++ .../inference/InferenceEngine.java | 230 ++++++++++++++++++- .../inference/DomainRangeVisitorTest.java | 140 +++++++++++ .../inference/InferenceEngineTest.java | 60 +++++ .../rdftriplestore/inference/InferenceIT.java | 48 +++- 6 files changed, 582 insertions(+), 12 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/incubator-rya/blob/6ce0b00b/sail/src/main/java/org/apache/rya/rdftriplestore/RdfCloudTripleStoreConnection.java ---------------------------------------------------------------------- diff --git a/sail/src/main/java/org/apache/rya/rdftriplestore/RdfCloudTripleStoreConnection.java b/sail/src/main/java/org/apache/rya/rdftriplestore/RdfCloudTripleStoreConnection.java index 6784c90..483d4ee 100644 --- a/sail/src/main/java/org/apache/rya/rdftriplestore/RdfCloudTripleStoreConnection.java +++ b/sail/src/main/java/org/apache/rya/rdftriplestore/RdfCloudTripleStoreConnection.java @@ -50,6 +50,7 @@ import org.apache.rya.rdftriplestore.evaluation.QueryJoinSelectOptimizer; import org.apache.rya.rdftriplestore.evaluation.RdfCloudTripleStoreEvaluationStatistics; import org.apache.rya.rdftriplestore.evaluation.RdfCloudTripleStoreSelectivityEvaluationStatistics; import org.apache.rya.rdftriplestore.evaluation.SeparateFilterJoinsVisitor; +import org.apache.rya.rdftriplestore.inference.DomainRangeVisitor; import org.apache.rya.rdftriplestore.inference.HasValueVisitor; import org.apache.rya.rdftriplestore.inference.InferenceEngine; import org.apache.rya.rdftriplestore.inference.InverseOfVisitor; @@ -349,6 +350,7 @@ public class RdfCloudTripleStoreConnection extends SailConnectionBase { && this.inferenceEngine != null ) { try { + tupleExpr.visit(new DomainRangeVisitor(queryConf, inferenceEngine)); tupleExpr.visit(new HasValueVisitor(queryConf, inferenceEngine)); tupleExpr.visit(new PropertyChainVisitor(queryConf, inferenceEngine)); tupleExpr.visit(new TransitivePropertyVisitor(queryConf, inferenceEngine)); http://git-wip-us.apache.org/repos/asf/incubator-rya/blob/6ce0b00b/sail/src/main/java/org/apache/rya/rdftriplestore/inference/DomainRangeVisitor.java ---------------------------------------------------------------------- diff --git a/sail/src/main/java/org/apache/rya/rdftriplestore/inference/DomainRangeVisitor.java b/sail/src/main/java/org/apache/rya/rdftriplestore/inference/DomainRangeVisitor.java new file mode 100644 index 0000000..f97e396 --- /dev/null +++ b/sail/src/main/java/org/apache/rya/rdftriplestore/inference/DomainRangeVisitor.java @@ -0,0 +1,114 @@ +package org.apache.rya.rdftriplestore.inference; +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import java.util.Set; +import java.util.UUID; + +import org.apache.rya.api.RdfCloudTripleStoreConfiguration; +import org.apache.rya.api.utils.NullableStatementImpl; +import org.apache.rya.rdftriplestore.utils.FixedStatementPattern; +import org.openrdf.model.URI; +import org.openrdf.model.vocabulary.RDF; +import org.openrdf.model.vocabulary.RDFS; +import org.openrdf.query.algebra.StatementPattern; +import org.openrdf.query.algebra.TupleExpr; +import org.openrdf.query.algebra.Var; + +/** + * Expands the query tree to account for any relevant domain and range information known to the + * {@link InferenceEngine}. + * + * Given a triple <:s :p :o>, if :p has an rdfs:domain of :D and rdfs:range of :R, the semantics of + * domain and range imply that :s has type :D and that :o has type :R. + * + * Only operates on {@link StatementPattern} nodes whose form is <?subject rdfs:type :DefinedClass>. + * If the class is the domain of any predicate, as reported by the {@link InferenceEngine}, a + * subtree is constructed to infer the type based on the domain. If the class is the range of any + * predicate, a subtree is similarly constructed to infer the type based on the range. If one or + * both apply, then the original node is replaced with the union of the new subtree(s) and the + * original statement pattern. + * + * If there are multiple ways to infer the type for one resource, the resulting query tree will + * match all of them and produce multiple solutions for that resource. + */ +public class DomainRangeVisitor extends AbstractInferVisitor { + /** + * Creates a new {@link DomainRangeVisitor}, which is enabled by default. + * @param conf The {@link RdfCloudTripleStoreConfiguration}. + * @param inferenceEngine The InferenceEngine containing the relevant ontology. + */ + public DomainRangeVisitor(RdfCloudTripleStoreConfiguration conf, InferenceEngine inferenceEngine) { + super(conf, inferenceEngine); + include = true; + } + + /** + * Checks whether this statement pattern might be inferred using domain and/or range knowledge, + * and, if so, replaces the statement pattern with a union of itself and any possible + * derivations. + */ + @Override + protected void meetSP(StatementPattern node) throws Exception { + final Var subjVar = node.getSubjectVar(); + final Var predVar = node.getPredicateVar(); + final Var objVar = node.getObjectVar(); + final Var contextVar = node.getContextVar(); + // Only applies to statement patterns that query for members of a defined type. + if (predVar != null && RDF.TYPE.equals(predVar.getValue()) + && objVar != null && objVar.getValue() instanceof URI) { + final URI inferredType = (URI) objVar.getValue(); + // Preserve the original node so explicit type assertions are still matched: + TupleExpr currentNode = node.clone(); + // If there are any properties with this type as domain, check for appropriate triples: + final Set<URI> domainProperties = inferenceEngine.getPropertiesWithDomain(inferredType); + if (!domainProperties.isEmpty()) { + Var domainPredVar = new Var("p-" + UUID.randomUUID()); + Var domainObjVar = new Var("o-" + UUID.randomUUID()); + domainObjVar.setAnonymous(true); + Var domainVar = new Var(RDFS.DOMAIN.stringValue(), RDFS.DOMAIN); + StatementPattern domainSP = new DoNotExpandSP(subjVar, domainPredVar, domainObjVar, contextVar); + // Enumerate predicates having this type as domain + FixedStatementPattern domainFSP = new FixedStatementPattern(domainPredVar, domainVar, objVar); + for (URI property : domainProperties) { + domainFSP.statements.add(new NullableStatementImpl(property, RDFS.DOMAIN, inferredType)); + } + // For each such predicate, any triple <subjVar predicate _:any> implies the type + currentNode = new InferUnion(currentNode, new InferJoin(domainFSP, domainSP)); + } + // If there are any properties with this type as range, check for appropriate triples: + final Set<URI> rangeProperties = inferenceEngine.getPropertiesWithRange(inferredType); + if (!rangeProperties.isEmpty()) { + Var rangePredVar = new Var("p-" + UUID.randomUUID()); + Var rangeSubjVar = new Var("s-" + UUID.randomUUID()); + rangeSubjVar.setAnonymous(true); + Var rangeVar = new Var(RDFS.RANGE.stringValue(), RDFS.RANGE); + StatementPattern rangeSP = new DoNotExpandSP(rangeSubjVar, rangePredVar, subjVar, contextVar); + // Enumerate predicates having this type as range + FixedStatementPattern rangeFSP = new FixedStatementPattern(rangePredVar, rangeVar, objVar); + for (URI property : rangeProperties) { + rangeFSP.statements.add(new NullableStatementImpl(property, RDFS.RANGE, inferredType)); + } + // For each such predicate, any triple <_:any predicate subjVar> implies the type + currentNode = new InferUnion(currentNode, new InferJoin(rangeFSP, rangeSP)); + } + node.replaceWith(currentNode); + } + } +} http://git-wip-us.apache.org/repos/asf/incubator-rya/blob/6ce0b00b/sail/src/main/java/org/apache/rya/rdftriplestore/inference/InferenceEngine.java ---------------------------------------------------------------------- diff --git a/sail/src/main/java/org/apache/rya/rdftriplestore/inference/InferenceEngine.java b/sail/src/main/java/org/apache/rya/rdftriplestore/inference/InferenceEngine.java index d0ff51b..43f00e0 100644 --- a/sail/src/main/java/org/apache/rya/rdftriplestore/inference/InferenceEngine.java +++ b/sail/src/main/java/org/apache/rya/rdftriplestore/inference/InferenceEngine.java @@ -28,9 +28,11 @@ import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.Stack; import java.util.Timer; import java.util.TimerTask; import java.util.TreeMap; +import java.util.concurrent.ConcurrentHashMap; import org.openrdf.model.Resource; import org.openrdf.model.Statement; @@ -73,6 +75,8 @@ public class InferenceEngine { private Set<URI> transitivePropertySet; private Map<Resource, Map<URI, Value>> hasValueByType; private Map<URI, Map<Resource, Value>> hasValueByProperty; + private Map<URI, Set<URI>> domainByType; + private Map<URI, Set<URI>> rangeByType; private RyaDAO ryaDAO; private RdfCloudTripleStoreConfiguration conf; @@ -371,6 +375,8 @@ public class InferenceEngine { } } + refreshDomainRange(); + refreshPropertyRestrictions(); } catch (QueryEvaluationException e) { @@ -411,6 +417,164 @@ public class InferenceEngine { } } + /** + * Queries domain and range information, then populates the inference engine with direct + * domain/range relations and any that can be inferred from the subclass graph, subproperty + * graph, and inverse property map. Should be called after that class and property information + * has been refreshed. + * + * Computes indirect domain/range: + * - If p1 has domain c, and p2 is a subproperty of p1, then p2 also has domain c. + * - If p1 has range c, and p2 is a subproperty of p1, then p2 also has range c. + * - If p1 has domain c, and p2 is the inverse of p1, then p2 has range c. + * - If p1 has range c, and p2 is the inverse of p1, then p2 has domain c. + * - If p has domain c1, and c1 is a subclass of c2, then p also has domain c2. + * - If p has range c1, and c1 is a subclass of c2, then p also has range c2. + * @throws QueryEvaluationException + */ + private void refreshDomainRange() throws QueryEvaluationException { + Map<URI, Set<URI>> domainByTypePartial = new ConcurrentHashMap<>(); + Map<URI, Set<URI>> rangeByTypePartial = new ConcurrentHashMap<>(); + // First, populate domain and range based on direct domain/range triples. + CloseableIteration<Statement, QueryEvaluationException> iter = RyaDAOHelper.query(ryaDAO, null, RDFS.DOMAIN, null, conf); + try { + while (iter.hasNext()) { + Statement st = iter.next(); + Resource property = st.getSubject(); + Value domainType = st.getObject(); + if (domainType instanceof URI && property instanceof URI) { + if (!domainByTypePartial.containsKey(domainType)) { + domainByTypePartial.put((URI) domainType, new HashSet<>()); + } + domainByTypePartial.get(domainType).add((URI) property); + } + } + } finally { + if (iter != null) { + iter.close(); + } + } + iter = RyaDAOHelper.query(ryaDAO, null, RDFS.RANGE, null, conf); + try { + while (iter.hasNext()) { + Statement st = iter.next(); + Resource property = st.getSubject(); + Value rangeType = st.getObject(); + if (rangeType instanceof URI && property instanceof URI) { + if (!rangeByTypePartial.containsKey(rangeType)) { + rangeByTypePartial.put((URI) rangeType, new HashSet<>()); + } + rangeByTypePartial.get(rangeType).add((URI) property); + } + } + } finally { + if (iter != null) { + iter.close(); + } + } + // Then combine with the subclass/subproperty graphs and the inverse property map to compute + // the closure of domain and range per class. + Set<URI> domainRangeTypeSet = new HashSet<>(domainByTypePartial.keySet()); + domainRangeTypeSet.addAll(rangeByTypePartial.keySet()); + // Extend to subproperties: make sure that using a more specific form of a property + // still triggers its domain/range inferences. + // Mirror for inverse properties: make sure that using the inverse form of a property + // triggers the inverse domain/range inferences. + // These two rules can recursively trigger one another. + for (URI domainRangeType : domainRangeTypeSet) { + Set<URI> propertiesWithDomain = domainByTypePartial.getOrDefault(domainRangeType, new HashSet<>()); + Set<URI> propertiesWithRange = rangeByTypePartial.getOrDefault(domainRangeType, new HashSet<>()); + // Since findParents will traverse the subproperty graph and find all indirect + // subproperties, the subproperty rule does not need to trigger itself directly. + // And since no more than one inverseOf relationship is stored for any property, the + // inverse property rule does not need to trigger itself directly. However, each rule + // can trigger the other, so keep track of how the inferred domains/ranges were + // discovered so we can apply only those rules that might yield new information. + Stack<URI> domainViaSuperProperty = new Stack<>(); + Stack<URI> rangeViaSuperProperty = new Stack<>(); + Stack<URI> domainViaInverseProperty = new Stack<>(); + Stack<URI> rangeViaInverseProperty = new Stack<>(); + // Start with the direct domain/range assertions, which can trigger any rule. + domainViaSuperProperty.addAll(propertiesWithDomain); + domainViaInverseProperty.addAll(propertiesWithDomain); + rangeViaSuperProperty.addAll(propertiesWithRange); + rangeViaInverseProperty.addAll(propertiesWithRange); + // Repeatedly infer domain/range from subproperties/inverse properties until no new + // information can be generated. + while (!(domainViaSuperProperty.isEmpty() && rangeViaSuperProperty.isEmpty() + && domainViaInverseProperty.isEmpty() && rangeViaInverseProperty.isEmpty())) { + // For a type c and property p, if c is a domain of p, then c is the range of any + // inverse of p. Would be redundant for properties discovered via inverseOf. + while (!domainViaSuperProperty.isEmpty()) { + URI property = domainViaSuperProperty.pop(); + URI inverseProperty = findInverseOf(property); + if (inverseProperty != null && propertiesWithRange.add(inverseProperty)) { + rangeViaInverseProperty.push(inverseProperty); + } + } + // For a type c and property p, if c is a range of p, then c is the domain of any + // inverse of p. Would be redundant for properties discovered via inverseOf. + while (!rangeViaSuperProperty.isEmpty()) { + URI property = rangeViaSuperProperty.pop(); + URI inverseProperty = findInverseOf(property); + if (inverseProperty != null && propertiesWithDomain.add(inverseProperty)) { + domainViaInverseProperty.push(inverseProperty); + } + } + // For a type c and property p, if c is a domain of p, then c is also a domain of + // p's subproperties. Would be redundant for properties discovered via this rule. + while (!domainViaInverseProperty.isEmpty()) { + URI property = domainViaInverseProperty.pop(); + Set<URI> subProperties = findParents(subPropertyOfGraph, property); + subProperties.removeAll(propertiesWithDomain); + propertiesWithDomain.addAll(subProperties); + domainViaSuperProperty.addAll(subProperties); + } + // For a type c and property p, if c is a range of p, then c is also a range of + // p's subproperties. Would be redundant for properties discovered via this rule. + while (!rangeViaInverseProperty.isEmpty()) { + URI property = rangeViaInverseProperty.pop(); + Set<URI> subProperties = findParents(subPropertyOfGraph, property); + subProperties.removeAll(propertiesWithRange); + propertiesWithRange.addAll(subProperties); + rangeViaSuperProperty.addAll(subProperties); + } + } + if (!propertiesWithDomain.isEmpty()) { + domainByTypePartial.put(domainRangeType, propertiesWithDomain); + } + if (!propertiesWithRange.isEmpty()) { + rangeByTypePartial.put(domainRangeType, propertiesWithRange); + } + } + // Once all properties have been found for each domain/range class, extend to superclasses: + // make sure that the consequent of a domain/range inference goes on to apply any more + // general classes as well. + for (URI subtype : domainRangeTypeSet) { + Set<URI> supertypes = findChildren(subClassOfGraph, subtype); + Set<URI> propertiesWithDomain = domainByTypePartial.getOrDefault(subtype, new HashSet<>()); + Set<URI> propertiesWithRange = rangeByTypePartial.getOrDefault(subtype, new HashSet<>()); + for (URI supertype : supertypes) { + // For a property p and its domain c: all of c's superclasses are also domains of p. + if (!propertiesWithDomain.isEmpty() && !domainByTypePartial.containsKey(supertype)) { + domainByTypePartial.put(supertype, new HashSet<>()); + } + for (URI property : propertiesWithDomain) { + domainByTypePartial.get(supertype).add(property); + } + // For a property p and its range c: all of c's superclasses are also ranges of p. + if (!propertiesWithRange.isEmpty() && !rangeByTypePartial.containsKey(supertype)) { + rangeByTypePartial.put(supertype, new HashSet<>()); + } + for (URI property : propertiesWithRange) { + rangeByTypePartial.get(supertype).add(property); + } + } + } + domainByType = domainByTypePartial; + rangeByType = rangeByTypePartial; + } + private void refreshPropertyRestrictions() throws QueryEvaluationException { // Get a set of all property restrictions of any type CloseableIteration<Statement, QueryEvaluationException> iter = RyaDAOHelper.query(ryaDAO, null, OWL.ONPROPERTY, null, conf); @@ -482,27 +646,35 @@ public class InferenceEngine { } public Set<URI> findParents(Graph graph, URI vertexId) { - Set<URI> parents = new HashSet<>(); + return findConnected(graph, vertexId, Direction.IN); + } + + public Set<URI> findChildren(Graph graph, URI vertexId) { + return findConnected(graph, vertexId, Direction.OUT); + } + + private Set<URI> findConnected(Graph graph, URI vertexId, Direction traversal) { + Set<URI> connected = new HashSet<>(); if (graph == null) { - return parents; + return connected; } Vertex v = getVertex(graph, vertexId); if (v == null) { - return parents; + return connected; } - addParents(v, parents); - return parents; + addConnected(v, connected, traversal); + return connected; } - private static void addParents(Vertex v, Set<URI> parents) { - v.edges(Direction.IN).forEachRemaining(edge -> { - Vertex ov = edge.vertices(Direction.OUT).next(); + private static void addConnected(Vertex v, Set<URI> connected, Direction traversal) { + v.edges(traversal).forEachRemaining(edge -> { + Vertex ov = edge.vertices(traversal.opposite()).next(); Object o = ov.property(URI_PROP).value(); if (o != null && o instanceof URI) { - boolean contains = parents.contains(o); + boolean contains = connected.contains(o); if (!contains) { - parents.add((URI) o); - addParents(ov, parents); + connected.add((URI) o); + addConnected(ov, connected, traversal); } } }); @@ -739,4 +911,40 @@ public class InferenceEngine { } return implications; } + + /** + * For a given type, get all properties which have that type as a domain. That type can be + * inferred for any resource which is a subject of any triple involving one of these properties. + * Accounts for class and property hierarchy, for example that a subproperty implicitly has its + * superproperty's domain, as well as inverse properties, where a property's range is its + * inverse property's domain. + * @param domainType The type to check against the known domains + * @return The set of properties with domain of that type, meaning that any triple whose + * predicate belongs to that set implies that the triple's subject belongs to the type. + */ + public Set<URI> getPropertiesWithDomain(URI domainType) { + Set<URI> properties = new HashSet<>(); + if (domainByType.containsKey(domainType)) { + properties.addAll(domainByType.get(domainType)); + } + return properties; + } + + /** + * For a given type, get all properties which have that type as a range. That type can be + * inferred for any resource which is an object of any triple involving one of these properties. + * Accounts for class and property hierarchy, for example that a subproperty implicitly has its + * superproperty's range, as well as inverse properties, where a property's domain is its + * inverse property's range. + * @param rangeType The type to check against the known ranges + * @return The set of properties with range of that type, meaning that any triple whose + * predicate belongs to that set implies that the triple's object belongs to the type. + */ + public Set<URI> getPropertiesWithRange(URI rangeType) { + Set<URI> properties = new HashSet<>(); + if (rangeByType.containsKey(rangeType)) { + properties.addAll(rangeByType.get(rangeType)); + } + return properties; + } } http://git-wip-us.apache.org/repos/asf/incubator-rya/blob/6ce0b00b/sail/src/test/java/org/apache/rya/rdftriplestore/inference/DomainRangeVisitorTest.java ---------------------------------------------------------------------- diff --git a/sail/src/test/java/org/apache/rya/rdftriplestore/inference/DomainRangeVisitorTest.java b/sail/src/test/java/org/apache/rya/rdftriplestore/inference/DomainRangeVisitorTest.java new file mode 100644 index 0000000..40f9098 --- /dev/null +++ b/sail/src/test/java/org/apache/rya/rdftriplestore/inference/DomainRangeVisitorTest.java @@ -0,0 +1,140 @@ +package org.apache.rya.rdftriplestore.inference; +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.HashSet; +import java.util.Set; +import java.util.Stack; + +import org.apache.rya.accumulo.AccumuloRdfConfiguration; +import org.apache.rya.rdftriplestore.utils.FixedStatementPattern; +import org.junit.Assert; +import org.junit.Test; +import org.openrdf.model.Resource; +import org.openrdf.model.Statement; +import org.openrdf.model.URI; +import org.openrdf.model.ValueFactory; +import org.openrdf.model.impl.ValueFactoryImpl; +import org.openrdf.model.vocabulary.RDF; +import org.openrdf.model.vocabulary.RDFS; +import org.openrdf.query.algebra.Join; +import org.openrdf.query.algebra.Projection; +import org.openrdf.query.algebra.ProjectionElem; +import org.openrdf.query.algebra.ProjectionElemList; +import org.openrdf.query.algebra.StatementPattern; +import org.openrdf.query.algebra.TupleExpr; +import org.openrdf.query.algebra.Union; +import org.openrdf.query.algebra.Var; + +public class DomainRangeVisitorTest { + private static final AccumuloRdfConfiguration conf = new AccumuloRdfConfiguration(); + private static final ValueFactory vf = new ValueFactoryImpl(); + private static final URI person = vf.createURI("lubm:Person"); + private static final URI advisor = vf.createURI("lubm:advisor"); + private static final URI takesCourse = vf.createURI("lubm:takesCourse"); + + @Test + public void testRewriteTypePattern() throws Exception { + final InferenceEngine inferenceEngine = mock(InferenceEngine.class); + final Set<URI> domainPredicates = new HashSet<>(); + final Set<URI> rangePredicates = new HashSet<>(); + domainPredicates.add(advisor); + domainPredicates.add(takesCourse); + rangePredicates.add(advisor); + when(inferenceEngine.getPropertiesWithDomain(person)).thenReturn(domainPredicates); + when(inferenceEngine.getPropertiesWithRange(person)).thenReturn(rangePredicates); + final Var subjVar = new Var("s"); + final StatementPattern originalSP = new StatementPattern(subjVar, new Var("p", RDF.TYPE), new Var("o", person)); + final Projection query = new Projection(originalSP, new ProjectionElemList(new ProjectionElem("s", "subject"))); + query.visit(new DomainRangeVisitor(conf, inferenceEngine)); + // Resulting tree should consist of Unions of: + // 1. The original StatementPattern + // 2. A join checking for domain inference + // 3. A join checking for range inference + boolean containsOriginal = false; + boolean containsDomain = false; + boolean containsRange = false; + final Stack<TupleExpr> nodes = new Stack<>(); + nodes.push(query.getArg()); + while (!nodes.isEmpty()) { + final TupleExpr currentNode = nodes.pop(); + if (currentNode instanceof Union) { + nodes.push(((Union) currentNode).getLeftArg()); + nodes.push(((Union) currentNode).getRightArg()); + } + else if (currentNode instanceof StatementPattern) { + Assert.assertFalse(containsOriginal); + Assert.assertEquals(originalSP, currentNode); + containsOriginal = true; + } + else if (currentNode instanceof Join) { + final TupleExpr left = ((Join) currentNode).getLeftArg(); + final TupleExpr right = ((Join) currentNode).getRightArg(); + Assert.assertTrue("Left-hand side should enumerate domain/range predicates", + left instanceof FixedStatementPattern); + Assert.assertTrue("Right-hand side should be a non-expandable SP matching triples satisfying domain/range", + right instanceof DoNotExpandSP); + final FixedStatementPattern fsp = (FixedStatementPattern) left; + final StatementPattern sp = (StatementPattern) right; + // fsp should be <predicate var, domain/range, original type var> + boolean isDomain = RDFS.DOMAIN.equals(fsp.getPredicateVar().getValue()); + boolean isRange = RDFS.RANGE.equals(fsp.getPredicateVar().getValue()); + Assert.assertTrue(isDomain || isRange); + Assert.assertEquals(originalSP.getObjectVar(), fsp.getObjectVar()); + // sp should have same predicate var + Assert.assertEquals(fsp.getSubjectVar(), sp.getPredicateVar()); + // collect predicates that are enumerated in the FixedStatementPattern + final Set<Resource> queryPredicates = new HashSet<>(); + for (Statement statement : fsp.statements) { + queryPredicates.add(statement.getSubject()); + } + if (isDomain) { + Assert.assertFalse(containsDomain); + // sp should be <original subject var, predicate var, unbound object var> for domain + Assert.assertEquals(originalSP.getSubjectVar(), sp.getSubjectVar()); + Assert.assertFalse(sp.getObjectVar().hasValue()); + // verify predicates + Assert.assertEquals(2, fsp.statements.size()); + Assert.assertEquals(domainPredicates, queryPredicates); + Assert.assertTrue(queryPredicates.contains(advisor)); + Assert.assertTrue(queryPredicates.contains(takesCourse)); + containsDomain = true; + } + else { + Assert.assertFalse(containsRange); + // sp should be <unbound subject var, predicate var, original subject var> for range + Assert.assertFalse(sp.getSubjectVar().hasValue()); + Assert.assertEquals(originalSP.getSubjectVar(), sp.getObjectVar()); + // verify predicates + Assert.assertEquals(1, fsp.statements.size()); + Assert.assertEquals(rangePredicates, queryPredicates); + Assert.assertTrue(queryPredicates.contains(advisor)); + containsRange = true; + } + } + else { + Assert.fail("Expected nested Unions of Joins and StatementPatterns, found: " + currentNode.toString()); + } + } + Assert.assertTrue(containsOriginal && containsDomain && containsRange); + } +} http://git-wip-us.apache.org/repos/asf/incubator-rya/blob/6ce0b00b/sail/src/test/java/org/apache/rya/rdftriplestore/inference/InferenceEngineTest.java ---------------------------------------------------------------------- diff --git a/sail/src/test/java/org/apache/rya/rdftriplestore/inference/InferenceEngineTest.java b/sail/src/test/java/org/apache/rya/rdftriplestore/inference/InferenceEngineTest.java index 58aeb88..31ea3e8 100644 --- a/sail/src/test/java/org/apache/rya/rdftriplestore/inference/InferenceEngineTest.java +++ b/sail/src/test/java/org/apache/rya/rdftriplestore/inference/InferenceEngineTest.java @@ -165,6 +165,66 @@ public class InferenceEngineTest extends TestCase { } @Test + public void testDomainRange() throws Exception { + String insert = "INSERT DATA { GRAPH <http://updated/test> {\n" + + " <urn:p1> rdfs:subPropertyOf <urn:p2> . \n" + + " <urn:p2> rdfs:subPropertyOf <urn:p3> . \n" + + " <urn:q1> rdfs:subPropertyOf <urn:q2> . \n" + + " <urn:q2> rdfs:subPropertyOf <urn:q3> . \n" + + " <urn:i1> rdfs:subPropertyOf <urn:i2> . \n" + + " <urn:i2> rdfs:subPropertyOf <urn:i3> . \n" + + " <urn:j1> rdfs:subPropertyOf <urn:j2> . \n" + + " <urn:j2> rdfs:subPropertyOf <urn:j3> . \n" + + " <urn:p2> owl:inverseOf <urn:i2> . \n" + + " <urn:i1> owl:inverseOf <urn:q2> . \n" + + " <urn:q1> owl:inverseOf <urn:j2> . \n" + + " <urn:D1> rdfs:subClassOf <urn:D2> . \n" + + " <urn:D2> rdfs:subClassOf <urn:D3> . \n" + + " <urn:R1> rdfs:subClassOf <urn:R2> . \n" + + " <urn:R2> rdfs:subClassOf <urn:R3> . \n" + + " <urn:p2> rdfs:domain <urn:D2> . \n" + + " <urn:p2> rdfs:range <urn:R2> . \n" + + "}}"; + conn.prepareUpdate(QueryLanguage.SPARQL, insert).execute(); + inferenceEngine.refreshGraph(); + Set<URI> hasDomainD1 = inferenceEngine.getPropertiesWithDomain(vf.createURI("urn:D1")); + Set<URI> hasDomainD2 = inferenceEngine.getPropertiesWithDomain(vf.createURI("urn:D2")); + Set<URI> hasDomainD3 = inferenceEngine.getPropertiesWithDomain(vf.createURI("urn:D3")); + Set<URI> hasRangeD1 = inferenceEngine.getPropertiesWithRange(vf.createURI("urn:D1")); + Set<URI> hasRangeD2 = inferenceEngine.getPropertiesWithRange(vf.createURI("urn:D2")); + Set<URI> hasRangeD3 = inferenceEngine.getPropertiesWithRange(vf.createURI("urn:D3")); + Set<URI> hasDomainR1 = inferenceEngine.getPropertiesWithDomain(vf.createURI("urn:R1")); + Set<URI> hasDomainR2 = inferenceEngine.getPropertiesWithDomain(vf.createURI("urn:R2")); + Set<URI> hasDomainR3 = inferenceEngine.getPropertiesWithDomain(vf.createURI("urn:R3")); + Set<URI> hasRangeR1 = inferenceEngine.getPropertiesWithRange(vf.createURI("urn:R1")); + Set<URI> hasRangeR2 = inferenceEngine.getPropertiesWithRange(vf.createURI("urn:R2")); + Set<URI> hasRangeR3 = inferenceEngine.getPropertiesWithRange(vf.createURI("urn:R3")); + Set<URI> empty = new HashSet<>(); + Set<URI> expectedForward = new HashSet<>(); + expectedForward.add(vf.createURI("urn:p2")); + expectedForward.add(vf.createURI("urn:p1")); + expectedForward.add(vf.createURI("urn:q2")); + expectedForward.add(vf.createURI("urn:q1")); + Set<URI> expectedInverse = new HashSet<>(); + expectedInverse.add(vf.createURI("urn:i1")); + expectedInverse.add(vf.createURI("urn:i2")); + expectedInverse.add(vf.createURI("urn:j1")); + expectedInverse.add(vf.createURI("urn:j2")); + Assert.assertEquals(empty, hasDomainD1); + Assert.assertEquals(empty, hasRangeD1); + Assert.assertEquals(empty, hasDomainR1); + Assert.assertEquals(empty, hasRangeR1); + Assert.assertEquals(expectedForward, hasDomainD2); + Assert.assertEquals(expectedInverse, hasRangeD2); + Assert.assertEquals(expectedInverse, hasDomainR2); + Assert.assertEquals(expectedForward, hasRangeR2); + Assert.assertEquals(expectedForward, hasDomainD3); + Assert.assertEquals(expectedInverse, hasRangeD3); + Assert.assertEquals(expectedInverse, hasDomainR3); + Assert.assertEquals(expectedForward, hasRangeR3); + } + + @Test public void testHasValueGivenProperty() throws Exception { String insert = "INSERT DATA { GRAPH <http://updated/test> {\n" + " <urn:Biped> owl:onProperty <urn:walksUsingLegs> . \n" http://git-wip-us.apache.org/repos/asf/incubator-rya/blob/6ce0b00b/sail/src/test/java/org/apache/rya/rdftriplestore/inference/InferenceIT.java ---------------------------------------------------------------------- diff --git a/sail/src/test/java/org/apache/rya/rdftriplestore/inference/InferenceIT.java b/sail/src/test/java/org/apache/rya/rdftriplestore/inference/InferenceIT.java index 6824751..c43c9e0 100644 --- a/sail/src/test/java/org/apache/rya/rdftriplestore/inference/InferenceIT.java +++ b/sail/src/test/java/org/apache/rya/rdftriplestore/inference/InferenceIT.java @@ -92,7 +92,9 @@ public class InferenceIT extends TestCase { solutions.add(arg0); } @Override - public void startQueryResult(List<String> arg0) throws TupleQueryResultHandlerException { } + public void startQueryResult(List<String> arg0) throws TupleQueryResultHandlerException { + solutions.clear(); + } }; } @@ -142,6 +144,50 @@ public class InferenceIT extends TestCase { } @Test + public void testDomainRangeQuery() throws Exception { + final String ontology = "PREFIX lubm: <http://swat.lehigh.edu/onto/univ-bench.owl#>\n" + + "INSERT DATA {\n" + + " lubm:advisor rdfs:domain lubm:Person ;\n" + + " rdfs:range lubm:Professor ;" + + " owl:inverseOf lubm:advisee .\n" + + " lubm:teachesCourse rdfs:domain lubm:Professor ;\n" + + " rdfs:range lubm:Course ." + + " lubm:takesCourse rdfs:domain lubm:Student ;\n" + + " rdfs:range lubm:Course ." + + " lubm:FullProfessor rdfs:subClassOf lubm:Professor .\n" + + " lubm:Professor rdfs:subClassOf lubm:Faculty .\n" + + " lubm:Faculty rdfs:subClassOf lubm:Person .\n" + + " lubm:Student rdfs:subClassOf lubm:Person .\n" + + "}"; + final String instances = "PREFIX lubm: <http://swat.lehigh.edu/onto/univ-bench.owl#>\n" + + "INSERT DATA {\n" + + " <urn:Professor1> a lubm:Professor .\n" + + " <urn:Student1> a lubm:Student .\n" + + " <urn:Student2> lubm:advisor <urn:Professor2> .\n" + + " <urn:Student3> lubm:advisor <urn:Professor2> .\n" + + " <urn:Professor3> lubm:advisee <urn:Student4> .\n" + + " <urn:Professor4> lubm:teachesCourse <urn:CS100> .\n" + + " <urn:Student1> lubm:takesCourse <urn:CS100> .\n" + + "}"; + final String query = "SELECT ?x { ?x a <http://swat.lehigh.edu/onto/univ-bench.owl#Faculty> }"; + conn.prepareUpdate(QueryLanguage.SPARQL, ontology).execute(); + inferenceEngine.refreshGraph(); + conn.prepareUpdate(QueryLanguage.SPARQL, instances).execute(); + conn.prepareTupleQuery(QueryLanguage.SPARQL, query).evaluate(resultHandler); + Set<Value> expected = new HashSet<>(); + expected.add(vf.createURI("urn:Professor1")); + expected.add(vf.createURI("urn:Professor2")); + expected.add(vf.createURI("urn:Professor3")); + expected.add(vf.createURI("urn:Professor4")); + Set<Value> returned = new HashSet<>(); + for (BindingSet bs : solutions) { + returned.add(bs.getBinding("x").getValue()); + } + Assert.assertEquals(expected, returned); + Assert.assertEquals(5, solutions.size()); + } + + @Test public void testHasValueTypeQuery() throws Exception { final String ontology = "INSERT DATA { GRAPH <http://updated/test> {\n" + " <urn:Biped> owl:onProperty <urn:walksOnLegs> ; owl:hasValue \"2\"^^<xsd:integer> . \n"