From 211617216bc91687aaeaba8db4185e57e3801fa3 Mon Sep 17 00:00:00 2001 From: "David W. Lotts" Date: Thu, 1 Mar 2018 17:29:59 -0500 Subject: [PATCH 1/7] WIP indexes, temporal mostly. --- .../rya.manual/src/site/markdown/indexing.md | 179 ++++++++++++++++++ 1 file changed, 179 insertions(+) create mode 100644 extras/rya.manual/src/site/markdown/indexing.md diff --git a/extras/rya.manual/src/site/markdown/indexing.md b/extras/rya.manual/src/site/markdown/indexing.md new file mode 100644 index 000000000..98c573339 --- /dev/null +++ b/extras/rya.manual/src/site/markdown/indexing.md @@ -0,0 +1,179 @@ + +# Indexes + +This section covers the optional indexes implemented in Apache Accumulo and MongoDB. It covers how they are enabled, queried from an end user perspective, and the underlying architecture from a programmers perspective. + +## Overview of indexing + +The non-optional core indexes are SPO, POS, OSP. These quickly find statements where any combination of one, two, or three of the subject, predicate, and object are known. ALso, since each of these core indexes contain the entire statement and context graph for all statements, each index acts as the repository for the entire RDF store. + +Apache Rya has a variety of optional indexes that can be enabled. + +The optional indexes mostly focus on specific datatypes in the object, like temporal, geo, and text. The object is the only part of an RDF statement that may be a literal data, for example, strings, dates and numbers. +Other indexes find groups of related statements as in the Geo-temporal, smarturi, and entity index. + +The following is a list of the index projects. All are under the extras folder since they are optional: rya/extras/ + +indexing -- the following are grouped as one project: + + +``` +entity +freetext +temporal +org/apache/rya/indexing/entity + +org/apache/rya/indexing/smarturi +org/apache/rya/indexing/statement + +rya.forwardchain +rya.geoindexing/geo.common +rya.geoindexing/geo.geomesa +rya.geoindexing/geo.geowave +rya.geoindexing/geo.mongo +rya.giraph + +shell +``` +### Enabling Indexing +Each section will describe the how to enable its index. There are two install methods for Rya that impact how indexes are configured: +- legacy - on the fly configuration +- installer - persist configuraton in Rya instance Details + + +#### Legacy - on the fly configuration + +The legacy method is not recommended for new Rya implementations. It loads configuration data in memory from an XML file or setter methods. Most Rya features will detect missing persistent storage components and lazily create them. This includes the core indexes and any optional indexes that are enabled in the configuration. For example if one starts by connecting to new Accumulo installation with no tables, Rya will create the SPO, POS and OSP tables when they are needed. This is not the recommended because the configuration of the Rya application code can get out of sync with the backing database causing serialization errors and incomplete indexes. For example, if one run of Rya uses and maintains a Geo index, and a second run connects to the same backing store, and disables Geo indexing, the index will be missing any new statement insertions made in the second run. + +#### Installer - persisted RyaDetails + +The installer method uses persistent storage to keep the configuration consistent. Rya looks for a store: "RyaDetails" and overwrites the in-memory configuration. RyaDetails is created using the install API, or using the administrative shell commands when Rya is first created. Indexes can be created only at this time. In contrast with the legacy method, all indexes enabled in the backing database will be maintained regardless of how a Rya application is configured. See the following classes for details: + + - [RyaClientExample.java](https://github.com/apache/incubator-rya/blob/master/extras/indexingExample/src/main/java/RyaClientExample.java Github) + - org.apache.rya.api.instance.RyaDetails, + - org.apache.rya.api.client.accumulo.AccumuloInstall + - org.apache.rya.api.client.mongo.MongoInstall + +### 1. Example Code: indexingExample + +The [indexingExample](https://github.com/apache/incubator-rya/blob/master/extras/indexingExample Github) project contains complete code examples of the indexes using the Rya API directly, not the web-based interface. +The following are found in: extras/indexingExample/src/main/java + + RyaClientExample.java -- install and manage a Rya instance. + MongoRyaDirectExample.java -- Several indexes for MongoDB + RyaDirectExample.java -- several indexes for Accumulo + EntityDirectExample.java -- Entity index + InferenceExamples.java -- Inferencing + ProspectorExample.java -- Prospector usage + StatementMetadataExample.java -- reified query with StatementMetadata + + +### 2. Index: Temporal +The temporal index quickly locates dates and times using a list of temporal relations. + +#### Enable and Options +Temporal indexing is enabled in the installer configuration builder by setting +method `setUseAccumuloTemporalIndex()` to true. For example: +```java +AccumuloIndexingConfiguration.builder().setUseAccumuloTemporalIndex(true); +``` +When using Rya with the shell command: install-with-parameters, or in legacy (not recommended) set the configuration key: +```java +ConfigUtils.USE_TEMPORAL = "sc.use_temporal" +``` +to true, or method: +```java +ConfigUtils.getUseTemporal(conf); +``` +##### Option: TEMPORAL_PREDICATES_LIST +To Limit the indexing of inserted statements particular predicates, set the following configuration key to a list of predicate URI's. +```java + ConfigUtils.TEMPORAL_PREDICATES_LIST = "sc.temporal.predicates" +``` +This key is not supported in the installer but can be used if the client specifies it per session. + +#### Usage +Instants in the statement's object are xsd:date or xsd:datetime using any of the standard ways of formatting and some that are not so standard. (See OpenRdf's Literal.calendarValue() and org.joda.time.DateTime.parse()) The type is ignored as long as it is a RDF literal. +For example instances: +// Note that these are the same datetime instant but from different time +// zones. +// This is an arbitrary zone, BRST=Brazil, better if not local. + 2014-12-31T23:59:59-02:00 + 2015-01-01T01:59:59Z + 2014-12-31T20:59:59-05:00 + +Intervals are expressed as [beginDatetime,endDatetime] where the two are instance expressions formated as instances. For example: + + [2016-12-31T20:59:59-05:00,2016-12-31T21:00:00-05:00] + +The following relations are supported. The first column is the FILTER operation URI, the second a list of applicable temporal literals used as parameters in the `FILTER` expression. +``` +PREFIX tempo: +tempo:equals InstantEqualsInstant +tempo:before InstantBeforeInstant InstantBeforeInterval +tempo:after InstantAfterInstant InstantAfterInterval +tempo:within InstantInsideInterval +TODO InstantHasBeginningInterval +TODO InstantHasEndInterval +``` +In SPARQL, these are used in a `FILTER` clause in the `WHERE` for example: +```SPARQL +PREFIX tempo: + ... +WHERE { + ... +FILTER( tempo:after(?time, '2001-01-01T01:01:03-08:00') ) . +} +``` +#### Architecture Accumulo +Temporal under Accumulo is maintained in a single table. Each statement that is indexed has four entries in the temporal table: O, SPO, SO and PO. Where O is the object that is always a datetime, S is subject, and P is predicate. +Row Keys are in these two forms. Brackets denotes optional: [x] denotes x is optional: + 1. constraintPrefix datetime + 2. datetime 0x/00 uniquesuffix + where + constraintPrefix = 0x/00 hash([subject][predicate]) + uniquesuffix = some bytes to make it unique, like hash(statement). + +The graph/context is handled as in the core indexes, using the +Index lookup is a matter of hashing zero, one or two of subject and predicate, concatinating the datetime object, + +Injest will store a statement in the index if it meets the criterion: Object should be a literal, and one of the list validPredicates from the configuration. If the validPredicates list is empty, accept all predicates. +If it does not meet the criteria, it is silently ignored. +It will Log a warning if the object is not parse-able. +It attempts to parse with OpenRdf's Literal.calendarValue() . +If that fails, tries: org.joda.time.DateTime.parse() . + +### 3. Index: Entity +#### Enable and Options +##### Option: ??? +#### Usage +#### Architecture + +### documentIndex + +The DocumentIndexIntersectingIterator facilitates document-partitioned indexing. It involves grouping a set of documents together and indexing those documents into a single row of an Accumulo table. This allows a tablet server to perform boolean AND operations on terms in the index. This uses [Document-Partitioned Indexing in Accumulo.](https://accumulo.apache.org/1.6/accumulo_user_manual.html#_document_partitioned_indexing "Accumulo user manual") + +### Index: +#### Enable and Options +##### Option: ??? +#### Usage +#### Architecture From eeb0d489a85ffe221669d85c9ab41fc498e0b74c Mon Sep 17 00:00:00 2001 From: "David W. Lotts" Date: Tue, 6 Mar 2018 14:52:16 -0500 Subject: [PATCH 2/7] index page, TODO complete free text index. --- .../rya.manual/src/site/markdown/indexing.md | 191 +++++++++++++++--- 1 file changed, 168 insertions(+), 23 deletions(-) diff --git a/extras/rya.manual/src/site/markdown/indexing.md b/extras/rya.manual/src/site/markdown/indexing.md index 98c573339..349d75949 100644 --- a/extras/rya.manual/src/site/markdown/indexing.md +++ b/extras/rya.manual/src/site/markdown/indexing.md @@ -88,7 +88,7 @@ The following are found in: extras/indexingExample/src/main/java ### 2. Index: Temporal -The temporal index quickly locates dates and times using a list of temporal relations. +The temporal index quickly locates dates and times (instants) or ranges of contiguous instants (intervals) using an expression using temporal relations. #### Enable and Options Temporal indexing is enabled in the installer configuration builder by setting @@ -98,7 +98,7 @@ AccumuloIndexingConfiguration.builder().setUseAccumuloTemporalIndex(true); ``` When using Rya with the shell command: install-with-parameters, or in legacy (not recommended) set the configuration key: ```java -ConfigUtils.USE_TEMPORAL = "sc.use_temporal" +ConfigurationFields.USE_TEMPORAL = "sc.use_temporal" ``` to true, or method: ```java @@ -111,21 +111,22 @@ To Limit the indexing of inserted statements particular predicates, set the foll ``` This key is not supported in the installer but can be used if the client specifies it per session. -#### Usage +#### Temporal Index Usage Instants in the statement's object are xsd:date or xsd:datetime using any of the standard ways of formatting and some that are not so standard. (See OpenRdf's Literal.calendarValue() and org.joda.time.DateTime.parse()) The type is ignored as long as it is a RDF literal. -For example instances: -// Note that these are the same datetime instant but from different time -// zones. -// This is an arbitrary zone, BRST=Brazil, better if not local. +For example temporal instant literals: + 2014-12-31T23:59:59-02:00 2015-01-01T01:59:59Z 2014-12-31T20:59:59-05:00 + 2014-12-31 + +Note that the first three are the same date-time instant but from different time zones. The index normalizes into time zone zero. -Intervals are expressed as [beginDatetime,endDatetime] where the two are instance expressions formated as instances. For example: +Intervals are expressed as [beginDatetime,endDatetime] where the two are temporal instant expressions formated as above. For example, here is a one second interval, happy new year!: - [2016-12-31T20:59:59-05:00,2016-12-31T21:00:00-05:00] + [2017-12-31T20:59:59-05:00,2017-12-31T21:00:00-05:00] -The following relations are supported. The first column is the FILTER operation URI, the second a list of applicable temporal literals used as parameters in the `FILTER` expression. +The following relations are supported. The first column is the `FILTER` operation URI, the second a list of applicable temporal literals used as parameters in the `FILTER` expression. ``` PREFIX tempo: tempo:equals InstantEqualsInstant @@ -144,7 +145,7 @@ WHERE { FILTER( tempo:after(?time, '2001-01-01T01:01:03-08:00') ) . } ``` -#### Architecture Accumulo +#### Temporal Architecture Accumulo Temporal under Accumulo is maintained in a single table. Each statement that is indexed has four entries in the temporal table: O, SPO, SO and PO. Where O is the object that is always a datetime, S is subject, and P is predicate. Row Keys are in these two forms. Brackets denotes optional: [x] denotes x is optional: 1. constraintPrefix datetime @@ -154,7 +155,7 @@ Row Keys are in these two forms. Brackets denotes optional: [x] denotes x is opt uniquesuffix = some bytes to make it unique, like hash(statement). The graph/context is handled as in the core indexes, using the -Index lookup is a matter of hashing zero, one or two of subject and predicate, concatinating the datetime object, +Index lookup is a matter of hashing zero, one or two of subject and predicate, and appending the datetime object, followed by a uniqueness suffix if the subject and predicate are omitted. Injest will store a statement in the index if it meets the criterion: Object should be a literal, and one of the list validPredicates from the configuration. If the validPredicates list is empty, accept all predicates. If it does not meet the criteria, it is silently ignored. @@ -162,18 +163,162 @@ It will Log a warning if the object is not parse-able. It attempts to parse with OpenRdf's Literal.calendarValue() . If that fails, tries: org.joda.time.DateTime.parse() . +#### Temporal Architecture MongoDB +MongoDB uses its native indexing on the core collection. + ### 3. Index: Entity -#### Enable and Options -##### Option: ??? -#### Usage -#### Architecture +Improves queries on **Entities**, also known as "star" queries, using the Accumulo's DocumentIndexIntersectingIterator. -### documentIndex -The DocumentIndexIntersectingIterator facilitates document-partitioned indexing. It involves grouping a set of documents together and indexing those documents into a single row of an Accumulo table. This allows a tablet server to perform boolean AND operations on terms in the index. This uses [Document-Partitioned Indexing in Accumulo.](https://accumulo.apache.org/1.6/accumulo_user_manual.html#_document_partitioned_indexing "Accumulo user manual") +#### Entity Enable and Options +Entity indexing is enabled in the installer configuration builder by setting +method `setUseAccumuloEntityIndex()` to true. For example: +```java +AccumuloIndexingConfiguration.builder().setUseAccumuloEntityIndex(true); +``` +When using Rya with the shell command: install-with-parameters, or in legacy (not recommended) set the configuration key: +```java +ConfigurationFields.USE_ENTITY = "sc.use_entity" +``` +to true. Test with method: -### Index: -#### Enable and Options -##### Option: ??? -#### Usage -#### Architecture +```java +ConfigUtils.getUseEntity(conf); +``` +##### Entity Options + +There are no options for this index. + +#### Entity Usage + +This section covers two aspects of Entity indexing: + - The entity model API that allows containing an entity and it's properties, and + - entity centric queries. + +#### Entity API + +An **Entity** is a named concept that has at least one defined structure +and multiple values that fit within each of those structures. A structure is +defined by a Type. A value that fits within that Type is a Property. + +For example, suppose we want to represent a type of icecream as an Entity. +First we must define what properties an icecream entity may have: +``` + Type ID: + Properties: + + + +``` +Now we can represent our icecream whose brand is "Awesome Icecream" and whose +flavor is "Chocolate", but has no ingredients or nutritional information, as +an Entity by doing the following: + +```java + final Entity entity = Entity.builder() + .setSubject(new RyaURI("urn:GTIN-14/00012345600012")) + .setExplicitType(new RyaURI("urn:icecream")) + .setProperty(new RyaURI("urn:icecream"), new Property(new RyaURI("urn:brand"), new RyaType(XMLSchema.STRING, "Awesome Icecream"))) + .setProperty(new RyaURI("urn:icecream"), new Property(new RyaURI("urn:flavor"), new RyaType(XMLSchema.STRING, "Chocolate"))) + .build(); +``` + +The two types of Entities that may be created are **implicit** and **explicit**. +An implicit Entity is one who has at least one Property that matches +the Type, but nothing has explicitly indicated it is of that Type. +Once that type is added to the entity, it is an **explicitly typed Entity**. + +#### Entity Centric Queries + +This is an example of a query that will be optimized by the Entity index: + +```SQL +# Who does Carol work with, that is friends with Alice and carpools with Bob? +Select ?e +Where { + :carol :worksWith ?e + ?e :friends :Alice + ?e :carPools :Bob +} +# Shorthand form: :e1 :p1 ?e. ?e :p2 :v1; :p3 :v2. +``` +Queries forms that benefit are: + - Properties for an Entity (See the example above) + ?e :p1 :e1. :e1 :p2 ?v1; :p3 ?v2. + + - Entity with Properties + :e1 :p1 ?e. ?e :p2 :v1; :p3 :v2. + + - “Friends of Friends” + :e1 :p1 ?e. ?e :p2 ?v; :p3 ?v2. + +Another form of query is not improved by the index: + + - Unknown Intersection + :e1 :p1 ?e. ?e ?p2 :v; ?p3 :v2. + + +#### Entity Architecture + +Accumulo's `DocumentIndexIntersectingIterator` facilitates document-partitioned indexing. The document in this case are the Entity's properties. It involves grouping (entity) a set of documents (properties) together and indexing those documents into a single row of an Accumulo table. This allows a tablet server to perform boolean `AND` operations on terms in the index. For more information see the Accumulo documentation [Document-Partitioned Indexing in Accumulo.](https://accumulo.apache.org/1.6/accumulo_user_manual.html#_document_partitioned_indexing "Accumulo user manual") + +A PDF presentation exists on the Rya wiki: [Entity Centric Indexing in Rya](https://cwiki.apache.org/confluence/display/RYA/Rya+Office+Hours) + +### Index: Free Text +This index allows searching the words in stored RDF objects that contain text. + +#### Free Text Enable and Options +FreeText indexing is enabled in the installer configuration builder by setting +method `setUseAccumuloFreeTextIndex()` to true. For example: + +```java +AccumuloIndexingConfiguration.builder().setUseAccumuloFreetextIndex(true) +``` + +When using Rya with the shell command: install-with-parameters, or in legacy mode (not recommended) set the configuration key: + +```java +ConfigurationFields.USE_FREETEXT = "sc.use_freetext" +``` + +to true. Test with method: + +```java +ConfigUtils.getUseFreeText(conf); +``` +##### FreeText Options + +FreeText indexing two optional arguments. + +###### Free Text Option: FREETEXT_PREDICATES_LIST +To Limit the indexing of inserted statements particular predicates, set the following configuration key to a list of predicate URI's. + + ConfigUtils.FREETEXT_PREDICATES_LIST = "sc.freetext.predicates" + +This key is not supported in the installer but can be used if the client specifies it per session. + +It defaults to empty, which will match all predicates. + +###### Free Text Option: FREE_TEXT_QUERY_TERM_LIMIT +The maximum number of terms allowed per query. +If a query contains more than this number, this IO error will be thrown. +``` +Query contains too many terms. Term limit: 999. Term Count: 999 +``` +It defaults to 100 terms. + +#### Free Text Usage +TODO ???? +#### Free Text Architecture +TODO ???? + +=============template============== +### Index: ???? +This index allows optimized searching ????. + +#### ???? Enable and Options +ConfigurationFields.USE_FREETEXT = "sc.use_freetext" +setUseAccumuloFreetextIndex(true) +##### ???? Option: ??? +#### ???? Usage +#### ???? Architecture From 457110af4f94c2235656ad19f8bf5ec4934f975e Mon Sep 17 00:00:00 2001 From: "David W. Lotts" Date: Tue, 13 Mar 2018 14:25:41 -0400 Subject: [PATCH 3/7] manual [WIP] Resoning half done, todo Geo. --- .../src/site/markdown/indexReasoning.md | 1958 +++++++++++++++++ .../rya.manual/src/site/markdown/indexing.md | 160 +- 2 files changed, 2105 insertions(+), 13 deletions(-) create mode 100644 extras/rya.manual/src/site/markdown/indexReasoning.md diff --git a/extras/rya.manual/src/site/markdown/indexReasoning.md b/extras/rya.manual/src/site/markdown/indexReasoning.md new file mode 100644 index 000000000..7c633bf87 --- /dev/null +++ b/extras/rya.manual/src/site/markdown/indexReasoning.md @@ -0,0 +1,1958 @@ +**Reasoning** + +# Overview + +Rya provides two independent reasoning capabilities for different tasks: + +1. A backward-chaining inference engine: Reasoning rules are applied at + query time through the recognition of particular Resource + Description Framework (RDF)/OWL (Web Ontology Language) semantics + and expansion of queries to include additional inferred assertions + +2. A forward-chaining reasoner: A MapReduce based tool that reasons + over an entire data set as a batch process to detect inconsistencies + for individual nodes using a subset of OWL semantics + +Within this document, both of these capabilities will be detailed. +Specifics of the implementation will be highlighted, and the usage of +both capabilities will be described. + +# **Backward-Chaining Inference Engine** + +Apache Rya currently provides simple backward-chaining inferencing via +query rewriting. The supported list of inferred relationships includes: + + +- rdfs:subClassOf +- rdfs:subPropertyOf +- rdfs:domain +- rdfs:range +- owl:equivalentClass +- owl:equivalentProperty +- owl:inverseOf +- owl:SymmetricProperty +- owl:TransitiveProperty +- owl:ReflexiveProperty +- owl:sameAs +- owl:unionOf +- owl:intersectionOf +- owl:oneOf +- owl:someValuesFrom +- owl:allValuesFrom +- owl:hasValue +- owl:hasSelf +- owl:propertyChainAxiom + +## Implementation + +Backward-chaining inference is implemented in two phases: + +1. The inference engine periodically queries for the supported terms + and stores the relevant schema in memory + +2. At query time, the stored schema is used to rewrite queries such + that they will return correct results under the semantic + implications of those RDFS and OWL terms. + +For example, the semantics of rdfs:subClassOf state that when A is a +subclass of B, any instance of A is also an instance of B. To handle +this, the inference engine periodically queries the data for +rdfs:subClassOf connections, and builds a graph of subclass +relationships including the connection between A and B. Then, when a +query asks for individuals with type B, the query is internally +translated to include any individuals with type A as well, because these +individuals are also implicitly members of B. + +There are several limitations to the Rya implementation of backwards +chaining inference: + +1. **Applies to constant types and properties**. Most of the supported + ontology terms relate to properties and types, and allow the + derivation of either type membership or triples with specific + properties. Given a rich ontology, each type may have several + possible derivations using the inference rules. When members of that + type are queried for specifically, all possible derivations are + considered. However, when the query includes type as a variable, + applying the inference rules would require checking every derivation + for every known type, which would increase complexity dramatically. + Therefore, queries for the type of an instance are only rewritten + when the type is a constant, not a variable. That is, statement + patterns of the form “`?x rdf:type :Class`” are rewritten according to + inference rules, but statement patterns of the form “`?x rdf:type + ?type`” are not. A similar restriction is applied to properties: + statement patterns of the form “`:Individual :prop ?y`” will be + rewritten to account for `subproperties` and inverse properties of + `:prop`, but statement patterns of the form “`:Individual ?p ?y`” will + not be. + +2. **Limited recursive rewriting**. In principle, multiple inference + rules could be composed to form a long chain of derivations for an + original query node. However, this chain could be arbitrarily long + and lead to highly inefficient queries. In general, any portion of a + query which is rewritten to apply some inference rule will not in + turn be rewritten again to apply another rule. Where possible, + alternative ways to derive the same result will be considered, but + compound derivations will not. For example, if type A can be derived + from `owl:someValuesFrom` semantics, or type A can be derived from + `owl:allValuesFrom` semantics, a query for individuals of type A will + match either case. However, if the `owl:someValuesFrom` case requires + a neighboring individual to also belong to type A, then the final + query will check for the neighbor's type directly, and not apply the + inference rules a second time within the same case. However, where + possible, all rules consider type hierarchy, so neighbors belonging + to subclasses of A will still be matched. + +3. **No nondeterministic inferences**. Some inference rules can be used + to derive that one of several facts are true or that some arbitrary + node exists with a certain characteristic. Full OWL reasoners take + these implications into account, and continue reasoning to determine + which of the options is correct or whether the arbitrary node + matches any specific node or should be considered a new node. Rya's + backward-chaining inference does not consider such implications. For + example,` owl:someValuesFrom `semantics allow an ontology to state + that type A is the set of all individuals with at least one edge of + property p to some individual of type B. This yields two + implications: any individual with an edge with predicate p to a + member of B is implicitly a member of A; and any individual stated + to belong to A has some edge with predicate p to an unknown member + of B. The former is handled whenever a query asks for members of A, + but the latter is ignored, because the inference engine has no way + of applying the fact that an edge exists to an unknown node. + (Because of the open world assumption, neither the edge nor the + other node need even exist in the known data.) For similar reasons, + when X is the union of Y and Z, members of both Y and Z are inferred + to be members of X, but members of X may belong to Y, Z, or both, + and the disjunction is not used in query rewriting. + +4. **Transitivity limitations and performance**. If a query includes a + statement pattern invoking a transitive property, then a chain of + any number of triples might be used to derive that statement + pattern. This chaining is conducted at query execution time, which + can lead to slow queries. To avoid chaining in both directions, + transitivity inference semantics will only be applied to statement + patterns where either the subject or object is constant, and the + other is a variable. + +5. **Schema size and consistency**. All query rewriting relies on + ontology information returned by the inference engine process. This + process runs periodically, querying the data for supported schema + triples and keeping schema information in memory. This means that + changes to the schema will only be applied to queries once the + inference engine refreshes its internal representation. This design + also assumes that the schema itself can be stored in memory (but + makes no assumptions about the amount of instance data). + +6. **Query complexity**. In general, backward-chaining increases query + complexity by expanding queries to include alternative ways to + derive simple facts, often requiring joins of multiple related + facts. A single statement pattern checking for a specific type can + be replaced by a union of several statement patterns and joins, + depending on the complexity of the ontology with respect to that + type. The other limitations imposed seek to alleviate this, but some + increase in complexity is inevitable. + +## Usage + +The inference engine is a scheduled job that runs periodically, at a +default interval of every 5 minutes. It queries the relationships in the +store and constructs the internal ontology representation necessary to +answer supported inferencing questions. + +This also means that if you load a model into the store, it could take +up to 5 minutes for the inferred relationships to be available. + +### Configuration + +Inferencing is disabled by default, but can be enabled by setting the +configuration property “`query.infer`” to "`true`", or by calling the +`setInfer` method of `RdfCloudTripleStoreConfiguration`. + +Individual query rewriting rules can be enabled or disabled in +isolation. By default, if inferencing is enabled, all rules are applied. +For each query rewriting step, there exists a configuration property and +corresponding method in `RdfCloudTripleStoreConfiguration` to enable or +disable that step. For example, to disable the expansion of queries “`?x +rdf:type :Class`” according to subclass logic, set the property +“`infer.include.subclassof`” to “`false`”, or call +`setInferSubClassOf(false)` on the configuration object. Note that there +is not always a one-to-one mapping between ontology terms and the query +rewriting rules. For example, subclass rewriting expands queries for +type statements according to subclass relationships which may have been +extracted from `rdfs:subClassOf`, `owl:equivalentClass`, `owl:unionOf`, and +`owl:intersectionOf` statements, so disabling this step will disable all +such inferences. On the other hand, the internal subclass relationships +are still maintained and may be used by other steps, meaning that if +subclass rewriting is disabled but domain and range rewriting is +enabled, the `rdfs:domain` rules will still propagate to subclasses. + +#### Explicit Setup + +Set up the RdfCloudTripleStore with the correct Data Access Object +(DAO). We add an InferenceEngine as well to the store. If this is not +added, then no inferencing will be done on the queries: +``` +//setup +Connector connector = new ZooKeeperInstance("instance", +"zoo1,zoo2,zoo3").getConnector("user", "password"); +final RdfCloudTripleStore store = new RdfCloudTripleStore(); +AccumuloRyaDAO crdfdao = new AccumuloRyaDAO(); +crdfdao.setConnector(connector); + +AccumuloRdfConfiguration conf = new AccumuloRdfConfiguration(); +conf.setTablePrefix("rts_"); +conf.setDisplayQueryPlan(true); +crdfdao.setConf(conf); +store.setRdfDao(crdfdao); + +ProspectorServiceEvalStatsDAO evalDao = new +ProspectorServiceEvalStatsDAO(connector, conf); +evalDao.init(); +store.setRdfEvalStatsDAO(evalDao); + +InferenceEngine inferenceEngine = new InferenceEngine(); +inferenceEngine.setRdfDao(crdfdao); +inferenceEngine.setConf(conf); +store.setInferenceEngine(inferenceEngine); + +Repository myRepository = new RyaSailRepository(store); +myRepository.initialize(); +RepositoryConnection conn = myRepository.getConnection(); + +//query code goes here + +//close +conn.close(); +myRepository.shutDown(); +``` +### Samples + +We will go through some quick samples on loading inferred relationships, +seeing and diagnosing the query plan, and checking the data. More +examples of individual inference rules can be found in the +indexingExample package. + +#### rdfs:subClassOf + +First, load the following `subClassOf` relationship: “`UndergraduateStudent +subclassof Student subclassof Person`”. Then load three triples defining +“`UgradA rdf:type UndergraduateStudent, StudentB rdf:type Student, +PersonC rdf:type Person`”. +``` +conn.add(new StatementImpl(vf.createURI(litdupsNS, +"UndergraduateStudent"), + +RDFS.SUBCLASSOF, vf.createURI(litdupsNS, "Student"))); + +conn.add(new StatementImpl(vf.createURI(litdupsNS, "Student"), +RDFS.SUBCLASSOF, + +vf.createURI(litdupsNS, "Person"))); + +conn.add(new StatementImpl(vf.createURI(litdupsNS, "UgradA"), RDF.TYPE, + +vf.createURI(litdupsNS, "UndergraduateStudent"))); + +conn.add(new StatementImpl(vf.createURI(litdupsNS, "StudentB"), +RDF.TYPE, + +vf.createURI(litdupsNS, "Student"))); + +conn.add(new StatementImpl(vf.createURI(litdupsNS, "PersonC"), RDF.TYPE, + +vf.createURI(litdupsNS, "Person"))); + +conn.commit(); +``` +Once the model is committed, it may take up to the scheduled refresh +interval (5 minutes by default) for the inferred relationships to be +ready. (Note that the inference engine can be manually prompted to +refresh its schema by calling the refreshGraph method on the +InferenceEngine object, and the refresh interval can be set with +setRefreshGraphSchedule. Periodic refresh can be enabled or disabled +altogether with the setSchedule method.) + +Now run the following query: +``` +PREFIX rdfs: +PREFIX rdf: +PREFIX lit: +select * where {?s rdf:type lit:Person.} +``` +This should return the following results: +``` +[s=urn:test:litdups#StudentB] +[s=urn:test:litdups#PersonC] +[s=urn:test:litdups#UgradA] +``` +After the query is automatically rewritten according to inference rules, +the query plan is: +``` +QueryRoot +Projection +ProjectionElemList +ProjectionElem "s" +Join +FixedStatementPattern +Var (name=79f261ee-e930-4af1-bc09-e637cc0affef) +Var (name=c-79f261ee-e930-4af1-bc09-e637cc0affef, +value=http://www.w3.org/2000/01/rdf-schema#subClassOf) +Var (name=-const-2, value=urn:test:litdups#Person, anonymous) +DoNotExpandSP +Var (name=s) +Var (name=-const-1, +value=http://www.w3.org/1999/02/22-rdf-syntax-ns#type, +anonymous) +Var (name=79f261ee-e930-4af1-bc09-e637cc0affef) +``` + +First, the inference engine is asked which types are subclasses of +Person. The inference engine traverses its graph of subclass +relationships to find that both Student and UndergraduateStudent are +subclasses of Person. These are enumerated by the FixedStatementPattern +and bound to a newly created variable, then this information is joined +with the statement pattern “(?s rdf:type ?inf)” where ?inf is that newly +created variable (the results from the inference engine). + +#### rdfs:subPropertyOf + +rdfs:subPropertyOf defines that a property can be an instance of another +property. For example, a “gradDegreeFrom subPropertyOf degreeFrom”. + +Also, owl:equivalentProperty can be thought of as specialized +subPropertyOf relationship where if “propA equivalentProperty propB” +then that means that “propA subPropertyOf propB AND propB subPropertyOf +propA”. + +Sample code: + +conn.add(new StatementImpl(vf.createURI(litdupsNS, +"undergradDegreeFrom"), + +RDFS.SUBPROPERTYOF, vf.createURI(litdupsNS, "degreeFrom"))); + +conn.add(new StatementImpl(vf.createURI(litdupsNS, "gradDegreeFrom"), + +RDFS.SUBPROPERTYOF, vf.createURI(litdupsNS, "degreeFrom"))); + +conn.add(new StatementImpl(vf.createURI(litdupsNS, "degreeFrom"), +RDFS.SUBPROPERTYOF, + +vf.createURI(litdupsNS, "memberOf"))); + +conn.add(new StatementImpl(vf.createURI(litdupsNS, "memberOf"), +RDFS.SUBPROPERTYOF, + +vf.createURI(litdupsNS, "associatedWith"))); + +conn.add(new StatementImpl(vf.createURI(litdupsNS, "UgradA"), +vf.createURI(litdupsNS, + +"undergradDegreeFrom"), vf.createURI(litdupsNS, "Harvard"))); + +conn.add(new StatementImpl(vf.createURI(litdupsNS, "GradB"), +vf.createURI(litdupsNS, + +"gradDegreeFrom"), vf.createURI(litdupsNS, "Yale"))); + +conn.add(new StatementImpl(vf.createURI(litdupsNS, "ProfessorC"), + +vf.createURI(litdupsNS, "memberOf"), vf.createURI(litdupsNS, +"Harvard"))); + +conn.commit(); + +With query: + +PREFIX rdfs: + +PREFIX rdf: + +PREFIX lit: + +select * where {?s lit:memberOf lit:Harvard.} + +Will return results: + +[s=urn:test:litdups#UgradA] + +[s=urn:test:litdups#ProfessorC] + +Since UgradA has undergraduateDegreeFrom Harvard and ProfessorC is +memberOf Harvard. + +This is very similar to the subClassOf relationship above: the inference +engine provides the set of properties which have subPropertyOf +relationships to memberOf (including indirectly, as in +undergraduateDegreeFrom, because subPropertyOf is transitive), and the +second part of the Join checks for triples with those properties and +with object \"\`Harvard\`\". + +Query plan: + +QueryRoot + +Projection + +ProjectionElemList + +ProjectionElem "s" + +Join + +FixedStatementPattern + +Var (name=0bad69f3-4769-4293-8318-e828b23dc52a) + +Var (name=c-0bad69f3-4769-4293-8318-e828b23dc52a, + +value=http://www.w3.org/2000/01/rdf-schema#subPropertyOf) + +Var (name=-const-1, value=urn:test:litdups#memberOf, anonymous) + +DoNotExpandSP + +Var (name=s) + +Var (name=0bad69f3-4769-4293-8318-e828b23dc52a) + +Var (name=-const-2, value=urn:test:litdups#Harvard, anonymous) + +#### owl:inverseOf + +InverseOf defines a property that is an inverse relation of another +property. For example, a student who has a degreeFrom a University also +means that the University hasAlumnus student. + +Code: + +conn.add(new StatementImpl(vf.createURI(litdupsNS, "degreeFrom"), +OWL.INVERSEOF, + +vf.createURI(litdupsNS, "hasAlumnus"))); + +conn.add(new StatementImpl(vf.createURI(litdupsNS, "UgradA"), +vf.createURI(litdupsNS, + +"degreeFrom"), vf.createURI(litdupsNS, "Harvard"))); + +conn.add(new StatementImpl(vf.createURI(litdupsNS, "GradB"), +vf.createURI(litdupsNS, + +"degreeFrom"), vf.createURI(litdupsNS, "Harvard"))); + +conn.add(new StatementImpl(vf.createURI(litdupsNS, "Harvard"), +vf.createURI(litdupsNS, + +"hasAlumnus"), vf.createURI(litdupsNS, "AlumC"))); + +conn.commit(); + +Query: + +PREFIX rdfs: + +PREFIX rdf: + +PREFIX lit: + +select * where {lit:Harvard lit:hasAlumnus ?s.} + +Result: + +[s=urn:test:litdups#AlumC] + +[s=urn:test:litdups#GradB] + +[s=urn:test:litdups#UgradA] + +The query planner will expand the statement pattern “Harvard hasAlumnus +?s” to a Union between “Harvard hasAlumnus ?s” and “?s degreeFrom +Harvard”. + +As a caveat, it is important to note that in general Union queries do +not have the best performance, so having a property that has both an +inverseOf and subPropertyOf, could cause a query plan that might take a +long time to execute, depending on how the query planner orders the +joins. + +Query plan: + +QueryRoot + +Projection + +ProjectionElemList + +ProjectionElem "s" + +InferUnion + +StatementPattern + +Var (name=-const-1, value=urn:test:litdups#Harvard, anonymous) + +Var (name=-const-2, value=urn:test:litdups#hasAlumnus, anonymous) + +Var (name=s) + +StatementPattern + +Var (name=s) + +Var (name=-const-2, value=urn:test:litdups#degreeFrom) + +Var (name=-const-1, value=urn:test:litdups#Harvard, anonymous) + +#### SymmetricProperty + +SymmetricProperty defines a relationship where, for example, if Bob is a +friendOf Jeff, then Jeff is a friendOf Bob. + +Code: + +conn.add(new StatementImpl(vf.createURI(litdupsNS, "friendOf"), +RDF.TYPE, + +OWL.SYMMETRICPROPERTY)); + +conn.add(new StatementImpl(vf.createURI(litdupsNS, "Bob"), +vf.createURI(litdupsNS, + +"friendOf"), vf.createURI(litdupsNS, "Jeff"))); + +conn.add(new StatementImpl(vf.createURI(litdupsNS, "James"), +vf.createURI(litdupsNS, + +"friendOf"), vf.createURI(litdupsNS, "Jeff"))); + +conn.commit(); + +Query: + +PREFIX rdfs: + +PREFIX rdf: + +PREFIX lit: + +select * where {?s lit:friendOf lit:Bob.} + +Results: + +[s=urn:test:litdups#Jeff] + +The query planner will recognize that `friendOf` is a +SymmetricProperty and devise a Union to find the specified relationship +and inverse. + +Query plan: + +QueryRoot + +Projection + +ProjectionElemList + +ProjectionElem "s" + +InferUnion + +StatementPattern + +Var (name=s) + +Var (name=-const-1, value=urn:test:litdups#friendOf, anonymous) + +Var (name=-const-2, value=urn:test:litdups#Bob, anonymous) + +StatementPattern + +Var (name=-const-2, value=urn:test:litdups#Bob, anonymous) + +Var (name=-const-1, value=urn:test:litdups#friendOf, anonymous) + +Var (name=s) + +#### owl:TransitiveProperty + +TransitiveProperty provides a transitive relationship between resources. +For example, if Queens is subRegionOf NYC and NYC is subRegionOf NY, +then Queens is transitively a subRegionOf NY. + +Code: + +conn.add(new StatementImpl(vf.createURI(litdupsNS, "subRegionOf"), +RDF.TYPE, + +OWL.TRANSITIVEPROPERTY)); + +conn.add(new StatementImpl(vf.createURI(litdupsNS, "Queens"), +vf.createURI(litdupsNS, + +"subRegionOf"), vf.createURI(litdupsNS, "NYC"))); + +conn.add(new StatementImpl(vf.createURI(litdupsNS, "NYC"), +vf.createURI(litdupsNS, + +"subRegionOf"), vf.createURI(litdupsNS, "NY"))); + +conn.add(new StatementImpl(vf.createURI(litdupsNS, "NY"), +vf.createURI(litdupsNS, + +"subRegionOf"), vf.createURI(litdupsNS, "US"))); + +conn.add(new StatementImpl(vf.createURI(litdupsNS, "US"), +vf.createURI(litdupsNS, + +"subRegionOf"), vf.createURI(litdupsNS, "NorthAmerica"))); + +conn.add(new StatementImpl(vf.createURI(litdupsNS, "NorthAmerica"), + +vf.createURI(litdupsNS, "subRegionOf"), vf.createURI(litdupsNS, +"World"))); + +conn.commit(); + +Query: + +PREFIX rdfs: + +PREFIX rdf: + +PREFIX lit: + +select * where {?s lit:subRegionOf lit:NorthAmerica.} + +Results: + +[s=urn:test:litdups#Queens] + +[s=urn:test:litdups#NYC] + +[s=urn:test:litdups#NY] + +[s=urn:test:litdups#US] + +The TransitiveProperty relationship works by running recursive queries +till all the results are returned. + +It is important to note that certain TransitiveProperty relationships +will not work: + + - Open ended property: “?s subRegionOf ?o” (At least one of the + properties must be filled or will be filled as the query gets + answered.) + + - Closed property: “Queens subRegionOf NY” (At least one of the + properties must be empty.) + +Query plan: + +QueryRoot + +Projection + +ProjectionElemList + +ProjectionElem "s" + +TransitivePropertySP + +Var (name=s) + +Var (name=-const-1, value=urn:test:litdups#subRegionOf, anonymous) + +Var (name=-const-2, value=urn:test:litdups#NorthAmerica, anonymous) + +# **Forward-Chaining Reasoner** + +The Rya Forward Chaining Reasoner is implemented as a forward-chaining +reasoner consisting of MapReduce jobs. Mappers partition triples by +neighborhood (subject and object nodes) and each reducer receives a +stream of triples in a particular node's neighborhood, applies inference +rules as appropriate, and generates new triples and inconsistencies. +This process repeats until all possible inferences have been made. + +The prototype tool current supports inferences over a subset of the +if/then rules that make up the OWL RL profile, the OWL 2 profile +intended for scalable rule-based reasoning. + +## Rules + +These are the OWL RL (Rule Language) rules currently implemented. Those +rules not implemented include any rule involving owl:sameAs, rules +inferring and checking datatypes, any rule operating on a list +construct, and the few other rules requiring a complex join of instance +triples that do not all share any single node. + +### Schema Rules + +These rules only involve schema/TBox rules and are applied outside the +forward chaining step. + + - **scm-cls**: Every class is its own subclass and equivalent class, + and is a subclass of owl:Thing. + + - **scm-eqc1**: If two classes are equivalent, they are also + subclasses of each other. + + - **scm-eqc2**: If two classes are each other's subclasses, they are + also equivalent classes. + + - **scm-op**: Every object property is its own subproperty and + equivalent property. + + - **scm-dp**: Every datatype property is its own subproperty and + equivalent property. + + - **scm-eqp1**: If two properties are equivalent, they are also + subproperties of each other. + + - **scm-eqp2**: If two properties are each other's subproperties, they + are also equivalent properties. + + - **scm-spo**: subPropertyOf is transitive. + + - **scm-dom1**: A property with domain c also has as domain any of c's + superclasses. + + - **scm-dom2**: A subproperty inherits its superproperties' domains. + + - **scm-rng1**: A property with range c also has as range any of c's + superclasses. + + - **scm-rng2**: A subproperty inherits its superproperties' ranges. + + - **scm-svf1**: A property restriction c1 is a subclass of another c2 + if they are both someValuesFrom restrictions on the same property + and c1's target class is a subclass of c2's target class. + + - **scm-avf1**: A property restriction c1 is a subclass of another c2 + if they are both allValuesFrom restrictions on the same property and + c1's target class is a subclass of c2's target class. + + - **scm-hv**: A property restriction c1 is a subclass of another c2 if + they are both hasValue restrictions with the same value where c1's + target property is a subproperty of c2's target property. + + - **scm-svf2**: A property restriction c1 is a subclass of another c2 + if they are both someValuesFrom restrictions on the same class where + c1's target property is a subproperty of c2's target property. + + - **scm-avf2**: A property restriction c1 is a subclass of another c2 + if they are both allValuesFrom restrictions on the same class where + c2's target property is a subproperty of c1's target property. + +### Single-instance-triple Rules + +These rules are applied by the local reasoner immediately upon reading +in the appropriate instance triple. + + - **prp-dom**: Infer subject's type from predicate's domain. + + - **prp-rng**: Infer object's type from predicate's range. + + - **prp-irp**: If **?p** is irreflexive, **?x ?p ?x** is inconsistent. + + - **prp-symp**: If **?p** is symmetric, **?x ?p ?y** implies **?y ?p + ?x** . + + - **prp-spo1**: If **?p1** has superproperty **p2** , **?x ?p1 ?y** + implies **?x ?p2 ?**y . + + - **prp-inv1**: If **?p1** and **?p2** are inverse properties, **?x + ?p1 ?y** implies **?y ?p1 ?x** . + + - **prp-inv2**: If **?p1** and **?p2** are inverse properties, **?x + ?p2 ?y** implies **?y ?p1 ?x** . + + - **cls-svf2**: If **?x** is a someValuesFrom restriction on property + **?p** with respect to owl:Thing , **?u ?p ?v** implies **?u + rdf:type ?x** for any **?u** , **?v** . + + - **cls-hv1**: If **?x** is a hasValue restriction on property **?p** + with value **?v** , **?u rdf:type ?x** implies **?u ?p ?v** for any + **?u** . + + - **cls-hv2**: If **?x** is a hasValue restriction on property **?p** + with value **?v** , **?u ?p ?v** implies **?u rdf:type ?x** for any + **?u** . + + - **cls-nothing2**: Anything having type **owl:Nothing** is an + inconsistency. + + - **cax-sco**: Whenever a type is seen, inherit any supertypes. + +### Join Rules + +These rules are applied by the local reasoner once all the necessary +triples are seen. This requires that the reasoner hold some triples in +memory in case the appropriate matching triple is received or inferred +later during the same phase. Since the reducer assumes incoming triples +are received before outgoing triples, only the former need to be held in +memory. + + - **prp-asyp**: If **?p** is asymmetric, **?x ?p ?y** and **?y ?p ?x** + is inconsistent. + + - **prp-trp**: If **?p** is transitive, **?x ?p ?y** and **?y ?p ?z** + implies **?x ?p ?z**. + + - **prp-pdw**: If properties **?p** and **?q** are disjoint, **?x ?p + ?y** and **?x ?q ?y** is inconsistent + + - **cax-dw**: If classes **c1** and **c2** are disjoint, having both + types is inconsistent. + + - **cls-com**: If classes **c1** and **c2** are complementary, having + both types is inconsistent. + + - **cls-svf1**: If **?x** is a someValuesFrom restriction on property + **?p** with respect to type **?y**, **?u ?p ?v** and **?v rdf:type + ?y** imply **?u rdf:type ?x**. + + - **cls-avf**: If **?x** is an allValuesFrom restriction on property + **?p** with respect to type **?y**, **?u** **?p ?v** and **?u + rdf:type ?x** imply **?v rdf:type ?y**. + + - **cls-maxc1**: **?x ?p ?y** is inconsistent if **?p** has maximum + cardinality 0. + + - **cls-maxqc2**: **?x ?p ?y** is inconsistent if **?p** has maximum + qualified cardinality 0 with respect to **owl:Thing**. + +## Usage + +The complete forward-chaining inference task is performed by the class +**mvm.rya.reasoning.mr.ReasoningDriver**. This will run a series of +MapReduce jobs on the input until no further inferences can be made. +Input must be configured using properties given in a Hadoop +configuration file and/or through command line parameters. Optional +parameters control the output generated. + +### Configuration + +The following properties are all required to connect to Accumulo: + + - **ac.instance** (instance name) + + - **ac.zk** (Zookeepers) + + - **ac.username** + + - **ac.pwd** + + - **rdf.tablePrefix** (e.g. **rya_**: the dataset to perform + inference on) + +File input is not generally recommended, but is implemented for testing +purposes. To read from a file instead of an Accumulo table, specify +either **input** to use a file/directory on Hadoop Distributed File +System (HDFS), or C) **reasoning.inputLocal** to use a local +file/directory (it will be uploaded to HDFS). This requires that the +data can be loaded entirely in memory, and does not correctly handle +bnodes in instance data. Each individual MapReduce job parses the file +again, yielding new identifiers. This means redundant derivations +involving bnodes are not detected, potentially causing a loop. Accumulo +input is the most reliable option and should be preferred when possible. + +The following properties are optional: + + - **reasoning.workingDir**: An HDFS path to which all intermediate + files and outputs will be written (defaults to **tmp/reasoning**) + + - **reasoning.debug**: Keep intermediate sequence files and print + detailed information about individual input output records to output + files starting with **debug** (defaults to false) + + - **reasoning.output**: Run the OutputTool job to collect inferred + triples and inconsistencies in respective output files (defaults to + true, set to false if the content of the inferred data is not + important) + + - **reasoning.stats**: Print a table of detailed metrics to standard + out instead of the default information about the run (defaults to + false). + + - **reasoning.step**: Used internally to keep track of which iteration + the reasoning engine is on. Can be set manually to resume a previous + execution (defaults to 0, meaning reasoning starts from the + beginning -- set to the previously completed step to resume). + + - **rdf.format**: When using file input, use this to specify the RDF + serialization format (defaults to RDF/XML) + +Useful MapReduce properties include (Hadoop 2 versions): + + - **mapreduce.{map/reduce}.log.level**: Set to DEBUG to print some + summary information to the logs. Generates some diagnostic + information less detailed than the **reasoning.debug** option. + + - **mapreduce.reduce.memory.mb**: Increasing memory available to the + reducer may be necessary for very large datasets, particularly if + complex joins require the reasoner to cache many triples. + + - **mapreduce.reduce.shuffle.merge.percent**: The percentage of memory + used above which the reducer will merge different groups to disk. + May need to be lowered if reducers run out of heap space during the + shuffle. + + - **mapreduce.input.fileinputformat.split.{minsize/maxsize}**: Because + the reasoner can create many potentially small intermediate files, + it will combine small input files into larger splits (using + CombineSequenceFileInputFormat) in an effort to keep the number of + required mappers in check (each split of the data gets its own map + task). These parameters can specify the potential range of the + split. If not given, the minimum defaults to **dfs.blocksize** and + the maximum to **dfs.blocksize** x 8. + +### Example + +Given the following **conf.xml** (assuming a running Accumulo instance +with these parameters): + +> **** +> +> **** +> +> **** +> +> **ac.instance** +> +> **dev** +> +> **** +> +> **** +> +> **ac.zk** +> +> **localhost:2181** +> +> **** +> +> **** +> +> **ac.username** +> +> **root** +> +> **** +> +> **** +> +> **ac.pwd** +> +> **root** +> +> **** +> +> **** +> +> **reasoning.workingDir** +> +> **example_output** +> +> **** +> +> **** + +The following command will run the reasoner on the triples found in Rya +table **rya_spo** and produce file output in HDFS under +**example_output/final**: + +> **hadoop jar target/rya.reasoning-3.2.10-SNAPSHOT-shaded.jar +> mvm.rya.reasoning.mr.ReasoningDriver -conf conf.xml +> -Drdf.tablePrefix=rya_** + +### Output + +Summary statistics will be printed to standard out, including the number +of instance triples inferred and the number of inconsistencies found. By +default, this information will be printed at each iteration's +completion. If **reasoning.stats** is set to true, detailed statistics +will be printed at the end instead. + +The inferences themselves will be written to files under +**${reasoning.workingDir}/final**, unless output was disabled. Triples +are written as raw N-Triples, while inconsistencies are printed with +their derivations. Triples and inconsistencies will be written to +different files; each file will only exist if at least one of the +corresponding fact types was generated. + +If debug was set to true, data files and additional info for each step +**i** will be written to **${reasoning.workingDir}/step-${i}a** (output +of the forward chaining reasoner for that step) and +**${reasoning.workingDir}/step-${i}** (output of the duplicate +elimination job for that step). + +Some additional information can be found in the Hadoop logs if the log +level was set to DEBUG (with the **mapreduce.{map/reduce}.log.level** +properties). + +### Conformance Testing + +A separate tool can run conformance tests specified according to the OWL +test ontology. This tool loads appropriate tests from an input file +(tests stated to apply to OWL RL and the RDF-based semantics), +instantiates a Mini Accumulo Cluster, and runs each test. Each test +contains a premise ontology that is used as input, and output is +evaluated based on the type of test: inconsistency tests require that an +inconsistency is found, entailment tests require that a conclusion +ontology is entailed, etc. The tool considers a conclusion to be +entailed if each triple in the conclusion is either explicitly produced +by the reasoner, contained in the schema (since the current +implementation is mainly concerned with consistency detection, it may +correctly derive a schema fact but will not manifest it as a triple), or +trivially implied (e.g. parsing a conclusion specified in XML using the +**** tag will result in a triple **[bnode] rdf:type +owl:Ontology** , which the reasoner would not explicitly generate). Once +all tests are run, either success or failure is reported for each. If a +test has multiple types, e.g. both consistency and entailment, success +requires passing all appropriate checks. + +The command expects two arguments: the input file containing the tests, +and a path to use for a temporary working directory for Mini Accumulo. +Input format can be specified with **rdf.format**; defaults to RDF/XML. +HDFS is still used for intermediate storage and reasoner output +(**reasoning.workingDir**). + +Example: + +``` +hadoop jar rya.reasoning-3.2.10-SNAPSHOT-shaded.jar \ + mvm.rya.reasoning.mr.ConformanceTest -conf conf.xml \ + owl-test/profile-RL.rdf temp +``` +## Design + +### Background + +Reasoning over full OWL 2 is generally N2EXPTIME-complete, with +reasoning over the RDF-based semantics in particular being undecidable +(see here for computational properties of different subsets). In +practice, full OWL reasoners exist, but have limited scalability. + +Most traditional reasoners use tableau algorithms, which maintain a set +of logical assertions as a tree, and repeatedly reduce them to entail +new assertions. Each time a reduction requires a nondeterministic choice +-- e.g. A or B -- this corresponds to a branch in the tree. When a +reduction results in an atomic assertion that would create a +contradiction, the current branch is inconsistent, and the search for +alternative reductions backtracks and chooses a new path. When all paths +are eliminated, the original set of facts is necessarily inconsistent. + +In the interests of scalability and distributed computation, we choose +to use a simpler rule-based approach. The OWL RL profile was explicitly +designed to support scalable if/then rules, so this is a natural choice. +RL allows almost all of the OWL modeling vocabulary but its semantics +are limited specifically so that nondeterministic inferences are never +necessary. For example, no RL rule will imply that an individual must +belong to one of two different classes. One might assert that c1 is a +subclass of (c2 union c3), but this does not entail any inferences under +RL. (However, if (c1 union c2) is a subclass of c3, the RL rules do +entail that both c1 and c2 are subclasses of c3. The union construct can +have implications in RL, but only deterministic ones.) + +Our approach is similar to that of WebPie[1] (Web-scale Parallel +Inference Engine), with one notable difference being that we apply all +rules on instance data in the same MapReduce job, using one standard +partitioning scheme (by single node: a triple is partitioned based on +its subject or object). (WebPie, by contrast, uses finer partitioning +schemes tailored for specific sets of rules.) + +Reasoning strategies can generally be classified as forward-chaining or +backward-chaining (with some hybrid approaches). Forward-chaining +systems generate the closure of the ontology: apply the inference rules +over the whole knowledgebase, "materialize" any derived triples, and +repeat until no new information can be inferred. Backward-chaining +systems begin with a query and recursively apply rules that might result +in the satisfaction of that query, inferring triples dynamically. Our +current implementation is a forward-chaining batch job, suitable to +offline inconsistency checking over the full dataset. + +### Overall Architecture + +Consistency checking is implemented as a series of MapReduce jobs which +repeatedly pass over the set of known triples (starting with an Accumulo +table) to generate new triples and check for inconsistencies, and add +any new triples to the set of known triples for the next iteration, +until there are no new triples or inconsistencies generated. + +Reasoning is distributed according to Uniform Resource Identifier (URI): +Mappers read in triples from Accumulo and from previous iterations, and +partition triples according to their subject and object. Reducers +perform reasoning around one URI at a time, receiving as input only +triples involving that URI. This enables us to use MapReduce, but +requires making several passes through the data in cases where +information needs to be propagated several steps through the graph (for +example, with transitivity). + +Many of the RL rules can be applied this way, if we can additionally +assume that each reducer has access to the complete schema, or TBox. For +example, if **:c1** and **:c2** are known to be disjoint classes, a +reasoner responsible for processing triples involving node **:x** can +detect an inconsistency if it receives both **:x rdf:type :c** and **:x +rdf:type :y** . But this only works if the disjoint class relationship +was passed to that reducer, and if we process the input in parallel, the +mapper that sees **:c1 owl:disjointWith :c2** has no way of predicting +which reducers will need this information. Therefore, we do an initial +pass through the data to aggregate all semantically meaningful +information about classes and properties (the schema), and ensure that +this is sent to every reasoner. This assumes that the schema/TBox is +very small relative to the instance data/ABox, and can be held in memory +by each node. + +There may be many ways to derive the same triple, and there may be +cycles that could lead to circular derivations. An additional job is +used to prune newly generated triples that are identical to previously +known triples. + +Together, this yields the following MapReduce jobs and workflow: + +1. **SchemaFilter** + + A. Map: Reads triples from Accumulo, outputs those containing schema + information. + + B. Reduce: Collects all schema triples in one reducer, feeds them into + a Schema object, performs some reasoning on these triples only, then + serializes the schema and outputs to a file. + +2. Repeat: + + A. **ForwardChain** + + 1) Setup: Distribute Schema object to every mapper and reducer. + + 2) Map: Reads triples from Accumulo and any previous steps. For a given triple, use the schema to determine whether local reasoners for the subject and/or object might be able to use that triple. If so, output the triple with the subject and/or object as a key. (For example, if the triple is **:s :p :o** and the schema has a domain for **:p** , output **<:s, (:s :p :o)>** so the reducer for **:s** can apply the domain rule. If the triple has a range as well, also output **<:o, (:s :p :o)> )**. + + 3) Shuffle/Sort: pairs are grouped based on the key (node) and sorted based on how the node relates to the triple: incoming edges precede outgoing edges. (For example, if both **(:a :b :c)** and **(:c :d :e)** are relevant to the reducer for **:c** , then **<:c, (:a :b :c)>** will be received before **<:c, (:c :d :e)>** .) This allows joins to use less memory than they would otherwise need. + + 4) Reduce: Receives a series of triples involving a single URI. Creates + a LocalReasoner object for that URI, which combines those triples + with the schema knowledge to apply inference rules. Outputs newly + generated triples and/or inconsistencies to files. + + B. **DuplicateElimination** + + 1) Map: Reads triples from Accumulo and derived triples from previous + steps. Separates the triple itself from its derivation tree (if it's + a derived triple), and outputs the triple itself as key and the + derivation as value. + + 2) Reduce: Receives a triple as key, with one or more derivations. If + all of the derivations come from the latest reasoning step, combine + the triple with its simplest derivation and output exactly once. + Otherwise, output nothing, since the triple must have either been + derived already or present in the initial input. + +3. **OutputTool** + + A. Map: Reads generated triples and inconsistencies, and sends them to + one reducer. + + B. Reduce: Outputs the triples to one file (as simple N-Triples) and + inconsistencies to another (with their derivation trees). + +### Reasoning Logic + +#### Schema Extraction + +The SchemaFilter MapReduce job maps all schema-relevant triples to one +reducer, which constructs a Schema object incorporating the schema +relationships: first by sending the triples to the schema object one by +one, and then by applying schema inference rules to add inferred schema +information. +``` +SchemaFilter: # MapReduce job + Map: + for triple in input: + if isSchemaTriple(triple): + output(null, triple) + Reduce(null, triples): # Single reducer + schema <- new Schema() + for triple in triples: + schema.processTriple(triple) + schema.computeClosure() + output(schema) to file +``` +Where: +``` +isSchemaTriple(triple): +(s p o) <- triple +if (p is rdf:type AND o in { owl:TransitiveProperty, + owl:IrreflexiveProperty, owl:SymmetricProperty, ... }) + OR (p in { rdfs:subClassOf, rdfs:subPropertyOf, owl:inverseOf, + owl:disjointWith, ... }): + return true +else: + return false +Schema.processTriple(s, p, o): + (s p o) <- triple + + # If this is a schema-relevant type assignment + if p is rdf:type: + if o in { owl:TransitiveProperty, owl:IrreflexiveProperty, ... }: + # then s is a property with that characteristic + +properties[s] <- initialize property s if necessary + +if o == owl:TransitiveProperty: + +properties[s].transitive <- true + +... # Repeat for each characteristic + +# If this connects two schema constructs + +else if p in { rdfs:subClassOf, owl:disjointWith, owl:onProperty, ... +}: + +initialize s if necessary (class or property, depending on p) + +initialize o if necessary (class or property, depending on p) + +# Connect s to o according to p: + +switch(p): + +case rdfs:subClassOf: classes[s].superclasses.add(classes[o]) + +case rdfs:subPropertyOf: +properties[s].superproperties.add(properties[o]) + +case rdfs:domain: properties[s].domain.add(classes[o]) + +case rdfs:range: properties[s].range.add(classes[o]) + +# Rules scm-eqc1, scm-eqc2: equivalent class is equivalent to mutual +subClassOf: + +case owl:equivalentClass: + +classes[s].superclasses.add(classes[o]) + +classes[o].superclasses.add(classes[s]) + +# Rules scm-eqp1, scm-eqp2: equivalent property is equivalent to +mutual subPropertyOf: + +case owl:equivalentProperty: + +properties[s].superproperties.add(properties[o]) + +properties[o].superproperties.add(properties[s]) + +# inverse, disjoint, complement relations are all symmetric: + +case owl:inverseOf: + +properties[s].inverseproperties.add(properties[o]) + +properties[o].inverseproperties.add(properties[s]) + +case owl:disjointWith: + +classes[s].disjointclasses.add(o) + +classes[o].disjointclasses.add(s) + +case owl:complementOf: + +classes[s].complementaryclasses.add(o) + +classes[o].complementaryclasses.add(s) + +case owl:propertyDisjointWith: + +properties[s].disjointproperties.add(properties[o]) + +properties[o].disjointproperties.add(properties[s]) + +case owl:onProperty: + +classes[s].onProperty.add(properties[o]) + +# Link properties back to their property restrictions + +properties[o].addRestriction.(properties[s]) + +case owl:someValuesFrom: classes[s].someValuesFrom.add(o) + +case owl:allValuesFrom: classes[s].allValuesFrom.add(o) + +case owl:hasValue: classes[s].hasValue.add(o) + +case owl:maxCardinality: classes[s].maxCardinality <- (int) o + +case owl:maxQualifiedCardinality: classes[s].maxQualifiedCardinality +<- (int) o + +case owl:onClass: classes[s].onClass.add(classes[o]) + +computeSchemaClosure(): + +# Rule scm-spo states that owl:subPropertyOf relationships are +transitive. + +# For each property, do a BFS to find all its superproperties. + +for p in properties: + +frontier <- { sp | sp in p.superproperties } + +while frontier is not empty: + +ancestors <- UNION{ f.superproperties | f in frontier } + +p.superproperties <- p.superproperties UNION ancestors + +frontier <- UNION{ a.superproperties | a in ancestors } - +p.superproperties + +# Rules scm-hv, scm-svf2, and scm-avf2 use sub/superproperty +information to + +# infer sub/superclass relations between property restrictions: + +for c1, c2 in classes: + +# Rules apply if c1 is a restriction on a subproperty and c2 is a +restriction on a superproperty. + +if c1 is property restriction AND c2 is property restriction AND +c1.onProperty.superproperties contains c2.onProperty: + +if v exists such that c1.hasValue(v) and c2.hasValue(v): + +c1.superclasses.add(c2) + +else if c3 exists such that c1.someValuesFrom(c3) and +c2.someValuesFrom(c3): + +c1.superclasses.add(c2) + +if c3 exists such that c1.allValuesFrom(c3) and +c2.allValuesFrom(c3): + +c2.superclasses.add(c1) + +# Repeatedly apply rules that operate on the subclass graph until +complete: + +do: + +# scm-sco: subclass relationship is transitive + +compute owl:subClassOf hierarchy # same BFS algorithm as +owl:subPropertyOf + +# scm-svf1, scm-avf1: restrictions on subclasses + +for p in properties: + +for r1, r2 in property restrictions on p: + +if c1, c2 exist such that c1.superclasses contains c2: + +AND ( (r1.someValuesFrom(c1) AND r2.someValuesFrom(c2)) + +OR (r1.someValuesFrom(c1) AND r2.someValuesFrom(c2)) ): + +r1.superclasses.add(r2) + +until no new information generated + +# Once all subclass and subproperty connections have been made, use +them to + +# apply domain and range inheritance (scm-dom1, scm-dom2, scm-rng1, +scm-rng2): + +for p in properties: + +for parent in p.superproperties: + +p.domain <- p.domain UNION parent.domain + +p.range <- p.range UNION parent.range + +for c in p.domain: + +p.domain <- p.domain UNION c.superclasses + +for c in p.range: + +p.range <- p.range UNION c.superclasses +``` +#### Forward Chaining Instance Reasoning + +The ForwardChain MapReduce job scans through both the input triples and +any triples that have been derived in any previous iterations, maps +triples according to their subject or object (depending on which might +be able to use them for reasoning: can be both or neither, resulting in +0-2 map outputs per triple), runs local reasoners around individual +resources, and saves all generated triples and inconsistencies to an +intermediate output directory. + +Main ForwardChain logic: + +**ForwardChain(i): # MapReduce job, iteration i** + +**Setup: distribute schema file to each node** + +**Map(triples): # from input or previous job** + +**schema <- load from file** + +**for fact in facts:** + +**(s p o) <- fact.triple** + +**relevance <- getRelevance(fact.triple, schema)** + +**if relevance.relevantToSubject == true:** + +**output(s, fact)** + +**if relevance.relevantToObject == true:** + +**output(o, fact)** + +**Group (node, fact) according to node;** + +**Sort such that incoming edges (with respect to node) come first:** + +**(c, (a b c)) < (c, (c d e))** + +**Reduce(node, facts): # from map output** + +**schema <- load from file** + +**reasoner <- new LocalReasoner(node, i)** + +**for fact in facts:** + +**reasoner.processFact(fact)** + +**output(node.collectOutput)** + +Where a fact consists of a triple, iteration generated (if any), and +sources (if any), and where: + +**getRelevance(triple, schema):** + +**# Determine relevance according to rules and their implementations in +the reasoner** + +**(s p o) <- triple** + +**if p is rdf:type:** + +**# If this triple is a schema-relevant type assignment** + +**if o in schema.classes:** + +**relevantToSubject <- true** + +**if p in schema.properties:** + +**if schema.properties[p].domain is not empty:** + +**relevantToSubject = true** + +**if schema.properties[p].range is not empty:** + +**relevantToObject = true** + +**if schema.properties[p].isTransitive:** + +**relevantToSubject = true** + +**relevantToObject = true** + +**... # Check for all implemented rules** + +Some examples of LocalReasoner rule application: + +**processFact(fact):** + +**(s p o) <- fact.triple** + +**newFacts = {}** + +**if reasoner.node == o:** + +**# Limit recursion on self-referential edges** + +**unless s==o AND fact.iteration == reasoner.currentIteration:** + +**newFacts <- newFacts UNION processIncomingEdge(fact)** + +**if reasoner.node == s:** + +**if p is rdf:type:** + +**newFacts <- newFacts UNION processType(fact)** + +**else:** + +**newFacts <- newFacts UNION processOutgoingEdge(fact)** + +**results = {}** + +**for newFact in newFacts:** + +**results <- results UNION processFact(newFact)** + +**return results** + +**processIncomingEdge(fact):** + +**(s p o) <- fact.triple** + +**# Range rule** + +**for c in schema.properties[p].range:** + +**generate fact ((reasoner.node rdf:type c), source:fact, +iteration:reasoner.currentIteration)** + +**# Inverse property rule** + +**for p2 in schema.properties[p].inverseOf:** + +**generate fact ((reasoner.node p2 s), source:fact, +iteration:reasoner.currentIteration)** + +**# Symmetric property rule** + +**if schema.properties[p].isSymmetric:** + +**generate fact ((reasoner.node p s), source:fact, +iteration:reasoner.currentIteration)** + +**# Irreflexive property rule** + +**if schema.properties[p].isIrreflexive:** + +**if s == o:** + +**generate inconsistency (source:fact, +iteration:reasoner.currentIteration)** + +**# Transitive property rule (part 1/2)** + +**if schema.properties[p].isTransitive:** + +**if fact.distance() >= 2^(reasoner.currentIteration-1): # smart +transitive closure** + +**reasoner.transitiveEdges.put(p, fact)** + +**... # Apply each supported rule applying to incoming edges** + +**processOutgoingEdge(fact):** + +**(s p o) <- fact.triple** + +**# Domain rule** + +**for c in schema.properties[p].domain:** + +**generate fact ((reasoner.node rdf:type c), source:fact, +iteration:reasoner.currentIteration)** + +**# Transitive property rule (part 2/2)** + +**if schema.properties[p].isTransitive:** + +**for leftFact in reasoner.transitiveEdges[p]:** + +**(s0 p node) <- leftFact** + +**generate fact ((s0 p o), source:{fact, leftFact}, +iteration:reasoner.currentIteration)** + +**... # Apply each supported rule applying to outgoing edges** + +**processType(fact):** + +**(reasoner.node rdf:type c) <- fact.triple** + +**reasoner.typeFacts.put(c, fact)** + +**# Disjoint class rule** + +**for c2 in schema.classes[c].disjointclasses:** + +**if reasoner.typeFacts contains c2:** + +**generate inconsistency (source:{fact, reasoner.typeFacts[c]}, +iteration:reasoner.currentIteration)** + +**# Subclass rule** + +**for parent in schema.classes[c].superclasses:** + +**processType(fact((reasoner.node rdf:type parent), source:fact, +iteration:reasoner.currentIteration))** + +**... # Apply each supported rule applying to types** + +### Classes + +The main reasoning logic is located in **mvm.rya.reasoning**, while +MapReduce tools and utilities for interacting with Accumulo are located +in **mvm.rya.reasoning.mr**. Reasoning logic makes use of RDF constructs +in the **org.openrdf.model** API, in particular: Statement, URI, +Resource, and Value. + +#### mvm.rya.reasoning + + - **OWL2**: In general, the Sesame/OpenRDF[2] Application + Programming Interface (API) is used to represent RDF constructs and + refer to the RDF, RDFS (Resource Description Framework Schema), and + OWL vocabularies. However, the API only covers OWL 1 constructs. The + OWL2 class contains static URIs for new OWL 2 vocabulary resources: + owl:IrreflexiveProperty, owl:propertyDisjointWith, etc. + + - **OwlRule**: Enum containing the RL Rules implemented. + + - **Schema**: Schema represents the complete schema/TBox/RBox as a set + of Java objects. A schema consists of properties, classes, and their + relationships. The Schema object maps URIs to instances of + OwlProperty and Resources to instances of OwlClass. + +> The schema is built one triple at a time: the Schema takes in a single +> triple, determines what kind of schema information it represents, and +> instantiates and/or connects class and/or property objects +> accordingly. Some RL rules are applied during this process. An example +> of this is the rule that an equivalentClass relationship is equivalent +> to mutual subClassOf relationships. The remainder of the RL schema +> rules (those that require traversing multiple levels of the schema +> graph) must be applied explicitly. Invoke the closure() method after +> all triples are incorporated to fully apply these rules (e.g., the +> rules that infer a property's domain based on the domains of its +> superproperties and/or the superclasses of its domains). +> +> Once the schema has been constructed, it can be used in reasoning. The +> Schema object serves as a repository for OwlClass and OwlProperty +> objects, accessed by their URIs (or Resources), which in turn contain +> detailed schema information about themselves. +> +> The Schema class also provides static method isSchemaTriple for +> checking whether a triple contains schema-relevant information at all. + + - An **OwlProperty** or **OwlClass** represents a property or a class, + respectively. Each object holds a reference to the RDF entity that + identifies it (using to the openrdf api): a URI for each + OwlProperty, and a Resource for each class (because a class is more + general, it can be a URI or a bnode). + +> Both maintain connections to other schema constructs, according to the +> relevant schema connections that can be made for each type. In +> general, these correspond to outgoing edges with the appropriate +> schema triple. Internally, they are represented as sets of those +> entities they are connected to, one set for each type of relationship. +> For example, each OwlClass contains a set of its superclasses. These +> are accessed via getter methods that return sets of URIs or Resources, +> as appropriate. Both also apply some RL schema rules. Some rules are +> applied by the getter methods (for example, every class being its own +> subclass), while others must be explicitly invoked by the Schema (for +> example, determining that one property restriction is a subclass of +> another based on the relationships between their corresponding +> properties and classes). +> +> OwlProperty also includes boolean fields corresponding to whether the +> property is transitive, symmetric, asymmetric, etc. (All default to +> false.) An OwlProperty also contains a set of references to any +> property restrictions that apply to it. +> +> In addition to common connections to other classes like superclasses +> and disjoint classes, OwlClass instances can also represent property +> restriction information. This is represented in the same way as the +> other schema connections: a set of classes corresponding to any +> allValuesFrom triples, a set of classes corresponding to any +> someValuesFrom triples, etc. Typically, only one such connection +> should exist (plus one onProperty relationship to specify what +> property this restriction applies to), but this representation is used +> for the sake of generality. In principle, an RDF graph could contain +> two such triples, though the semantic implications are not +> well-defined. + + - **Fact** and **Derivation**: A Fact represents a piece of + information, typically a triple, that either was contained in the + input graph or was inferred by some rule. An inferred fact contains + a Derivation explaining how it was inferred. A Derivation represents + an application of a rule. It contains the OwlRule used, the set of + Facts that caused it to apply, the Resource corresponding to the + reasoner that made the inference, and the iteration number + corresponding to when the inference was made. Together, they can + represent an arbitrarily complex derivation tree. Both classes + contain some methods for interacting with this tree. + + Local reasoners operate on Facts and Derivations so that they can + make use a triple's sources as well as its contents. MapReduce tools + use Fact and Derivation for their inputs and outputs, so both + implement WritableComparable. Derivations are compared based on + their rules and direct sources. Facts are compared based only on + their direct contents (i.e. their triples), unless neither have + contents, in which case they are compared by their Derivations. + + - **AbstractReasoner**: Base class for local reasoners. The + AbstractReasoner does not contain the semantics of any reasoning + rule, but contains methods for collecting and returning derived + triples and inconsistencies, represented as Fact and Derivation + objects respectively. Every reasoner is centered around one specific + node in the graph. + + - **LocalReasoner**: Main reasoning class for miscellaneous RL + instance rules. A LocalReasoner processes triples one at a time, + collecting any new facts it generates. The LocalReasoner is + concerned with reasoning around one main node, and can use triples + only if they represent edges to or from that node. Not all edges are + useful: static methods are provided to determine whether a triple is + relevant for its subject's reasoner, its object's reasoner, both, or + neither. Relevance is determined based on the schema and based on + how the reasoner applies rules to its incoming and outgoing edges + (incoming and outgoing edges can trigger different rules). An + arbitrary triple may not be relevant to anything if the schema + contains no information about the property. If that property has a + range, it will be considered relevant to the object (i.e. the + reasoner for the object will be able to apply the range rule). If + the property is transitive, it will be relevant to both subject and + object. + +> When the reasoner receives a triple, it determines the direction of +> the edge, checks it against the appropriate set of RL rules, applies +> those rules if necessary, and recursively processes any facts newly +> generated. The LocalReasoner will perform as much inference as it can +> in its neighborhood, but many triples may ultimately need to be passed +> to reasoners for other nodes in order to produce all implied +> information. +> +> Most RL rules are handled in the LocalReasoner object itself, but it +> also contains a TypeReasoner specifically responsible for those rules +> involving the central node's types. Unlike other facts, the type facts +> aren't collected automatically and must be explicitly gathered with a +> call to collectTypes. (Collecting type assignments one at a time would +> lead to a great deal of redundancy.) + + - **TypeReasoner**: Conducts reasoning having to do with a single + node's types. Several rules involve types, so they are all handled + in the same place. The TypeReasoner contains a set of types for its + central node, which allows it to make sure the same type is not + repeatedly generated and output during one iteration. This is in + contrast to the way generic triples are handled, because not all + information can be cached this way, and because it is particularly + common to be able to derive the same type many times; e.g. the + domain rule will generate the same type assignment for every + outgoing edge with the associated predicate. + +> Also has the ability to hold information that should be asserted if +> and only if the node turns out to be a particular type, and return +> that information if/when that happens. This is particularly useful for +> property restriction rules. + +#### mvm.rya.reasoning.mr + +Contains MapReduce tools and utilities for interacting with HDFS, +Accumulo, and Rya tables in Accumulo. + +In between jobs, different types of intermediate data can be saved to +different files using MultipleOutputs: relevant intermediate triples +(Fact objects); other instance triples (Facts that were generated but +which could not be used to apply any rules at this point, based on the +Schema); schema triples (derived Fact objects which contain schema +information -- this should not generally happen, but if it does, it will +trigger a regeneration of the global Schema); and inconsistencies +(Derivation objects). + +At each iteration, ForwardChain will output generated information to its +output directory, and then DuplicateElimination will output a subset of +that information to its output directory. Each job uses as input one +directory from each previous iteration: the subset with duplicates +eliminated, if found, or the original set of generated information +otherwise. + +The individual MapReduce jobs each have different mappers for different +kinds of input: Accumulo (main source of initial input, takes in +**** ), RDF file (alternative initial +input source, takes in **** ), and +HDFS sequence file (intermediate output/input, takes in **** or **** ). + + - **ReasoningDriver**: Main driver class for reasoning application. + Executes high-level algorithm by running **SchemaFilter**, + **ForwardChain**, **DuplicateElimination**, and **OutputTool**. + + - **MRReasoningUtils**: Defines configuration parameters and contains + static methods for configuring input and output (using those + parameters) and for passing information between jobs (using those + parameters). + + - **ResourceWritable**: WritableComparable wrapper for + org.openrdf.model.Resource, so it can be used as a key/value in + MapReduce tasks. Also contains an integer field to enable arbitrary + secondary sort. Provides static classes **PrimaryComparator** to use + the Resource alone, and **SecondaryComparator** to use resource + followed by key. + + - **SchemaWritable**: Writable subclass of Schema, allowing the schema + to be directly output by a MapReduce job. + + - **ConformanceTest**: Reads and executes OWL conformance tests read + from an RDF file, and reports the results. + + - **AbstractReasoningTool**: Base class for individual MapReduce jobs. + Contains methods for configuring different kinds of input and + output, loading the schema, defining and interacting with counters + (e.g. number of instance triples derived during a job), and loading + and distributing schema information to all nodes. + + - **SchemaFilter**: First MapReduce job to run. Runs with only one + reducer. + + - Mappers call Schema.isSchemaTriple on each input triple, and + output those triples found to contain schema information. One + mapper is defined for each input source: **SchemaTableMapper**, + **SchemaRdfMapper**, and **SchemaFileMapper**. + + - The reducer **SchemaFilterReducer** receives all schema-relevant + triples, feeds them one at a time into a Schema object, invokes + Schema.closure() to do final schema reasoning, and writes that + one Schema object to file output. + + - Mapper inputs: **** , **** , **** + + - Mapper output/reducer input: ** ** + + - Reducer output: **** + + - **ForwardChain:** MapReduce job responsible for actual + forward-chaining reasoning. + + - Mappers call LocalReasoner.relevantFact to determine whether the + triple is relevant to the subject and/or object. Outputs the + Fact as the value and the subject and/or object as the key, + depending on the relevance. (For each triple, the mapper can + produce zero, one, or two outputs.) Defined by generic class + **ForwardChainMapper** and its subclasses for different inputs: + **TableMapper**, **RdfMapper**, and **FileMapper**. + + - Reducers receive a node, instantiate a LocalReasoner for that + node, and feed a stream of facts/edges involving that node to + the reasoner. After the reasoner processes a triple (potentially + involving recursively processing derived triples), the reducer + collects any newly generated facts or inconsistencies and + outputs them. After reading all triples, the reducer calls + LocalReasoner.getTypes to get any more new facts involving + types. Defined by **ReasoningReducer**. + + - Mapper inputs: **** , **** , **** + + - Mapper output/reducer input: ** ** + + - Reducer outputs: **** , **** + + - **DuplicateElimination**: MapReduce job responsible for eliminating + redundant information. This includes redundant inconsistencies, so + unlike ForwardChain, this job needs to take inconsistencies + (Derivations) as input. + + - Map phase (generic class **DuplicateEliminationMapper** with + subclasses for different inputs): Output Facts and their + Derivations. For input triples (**DuplicateTableMapper** and + **DuplicateRdfMapper**), wrap the triple in a Fact and output an + empty Derivation. For intermediate triples + (**DuplicateFileMapper**), split the Fact and Derivation, + sending the Fact with no Derivation as the key, and the + Derivation itself as the value. For inconsistencies + (**InconsistencyMapper**), wrap the Derivation in an empty Fact + (a Fact with no triple), use that as a key, and use the + Derivation itself as the value. + + - Reducers (**DuplicateEliminationReducer**) receive a Fact and a + sequence of possible Derivations. If any of the Derivations are + empty, it's an input fact and shouldn't be output at all + (because it is already known). Otherwise, output the Fact plus + the earliest Derivation received, so it isn't reported to be + newer than it is. If the Fact is empty, the logic is identical + except that the output should be the Derivation alone, as an + inconsistency. + + - Mapper inputs: **** , **** , <**Fact, NullWritable>** , + **** + + - Mapper output/reducer input: ** ** + + - Reducer outputs: **** , **** + + - **OutputTool**: MapReduce job that collects output from all previous + iterations in one place. + + - **FactMapper** and **InconsistencyMapper** read information from + the intermediate files and output a String representation as the + value and the type of information (triple or inconsistency) as + the key. + + - **OutputReducer** receives two groups: one for triples and one + for inconsistencies, along with a sequence of String + representations for each. The reducer then writes those String + representations to two different output files. + + - Mapper inputs: **** , **** + + - Mapper output/reducer input: ** ** + + - Reducer output: **** + + - **RunStatistics**: Simple tool to collect statistics from each job + executed during the run. Uses Hadoop counters to get the number of + input/output triples, number of mappers and reducers, time spent by + each, memory, input and output records, etc. + +## Current Limitations + +The Rya backward chaining algorithm currently only performs naïve +inference in that results are only deterministic when a single inference +rule is fired. Interactions of inference rules are not currently +efficiently supported. The MapReduce based inference tool is also +limited in that it has only been integrated with Accumulo-backed Rya +since it relies on a cloud infrastructure. + +## Future Research + +Future research in Rya’s reasoning work could focus on the following +objectives: + +1. Updates to reasoning to support more complex inference: Currently, + Rya’s inference capability is limited in that it only supports + deterministic evaluation of single inference rules. Multiple rules + interacting is not currently supported efficiently. More work could + be done to better support interactions among inference rules in an + efficient manner. + +2. Better integration with other query planning optimizations: + Currently, Rya’s inference capability results in inefficient + queries. Backwards chaining inference expands queries to be more + complex, often including several unions over disparate clauses. + These queries as written are extremely inefficient. Additionally, + query planning currently does not attempt to optimize inferred + queries. Integrating query planning strategies with inference would + likely greatly improve performance for expanded queries. + + + +1. + +2. diff --git a/extras/rya.manual/src/site/markdown/indexing.md b/extras/rya.manual/src/site/markdown/indexing.md index 349d75949..3faf98a13 100644 --- a/extras/rya.manual/src/site/markdown/indexing.md +++ b/extras/rya.manual/src/site/markdown/indexing.md @@ -40,17 +40,14 @@ indexing -- the following are grouped as one project: entity freetext temporal -org/apache/rya/indexing/entity - -org/apache/rya/indexing/smarturi -org/apache/rya/indexing/statement - -rya.forwardchain +yes rya.forwardchain +Yes rya.geoindexing/geo.common rya.geoindexing/geo.geomesa rya.geoindexing/geo.geowave rya.geoindexing/geo.mongo -rya.giraph + +No rya.giraph shell ``` @@ -62,7 +59,7 @@ Each section will describe the how to enable its index. There are two install m #### Legacy - on the fly configuration -The legacy method is not recommended for new Rya implementations. It loads configuration data in memory from an XML file or setter methods. Most Rya features will detect missing persistent storage components and lazily create them. This includes the core indexes and any optional indexes that are enabled in the configuration. For example if one starts by connecting to new Accumulo installation with no tables, Rya will create the SPO, POS and OSP tables when they are needed. This is not the recommended because the configuration of the Rya application code can get out of sync with the backing database causing serialization errors and incomplete indexes. For example, if one run of Rya uses and maintains a Geo index, and a second run connects to the same backing store, and disables Geo indexing, the index will be missing any new statement insertions made in the second run. +The legacy method is not recommended for new Rya installations. It relies on the Rya driver code loading a consistent set of configuration data in memory from an XML file or setter methods. Most Rya features will detect missing persistent storage components and lazily create them. This includes the core indexes and any optional indexes that are enabled in the configuration. For example if one starts by connecting to new Accumulo installation with no tables, Rya will create the SPO, POS and OSP tables when they are needed. This is not the recommended because the configuration of the Rya application code can get out of sync with the backing database causing serialization errors and incomplete indexes. For example, if one run of Rya uses and maintains a Geo index, and a second run connects to the same backing store, and disables Geo indexing, the index will be missing any new statement insertions made in the second run. #### Installer - persisted RyaDetails @@ -195,7 +192,7 @@ This section covers two aspects of Entity indexing: - The entity model API that allows containing an entity and it's properties, and - entity centric queries. -#### Entity API +##### Entity API An **Entity** is a named concept that has at least one defined structure and multiple values that fit within each of those structures. A structure is @@ -265,7 +262,7 @@ Accumulo's `DocumentIndexIntersectingIterator` facilitates document-partitioned A PDF presentation exists on the Rya wiki: [Entity Centric Indexing in Rya](https://cwiki.apache.org/confluence/display/RYA/Rya+Office+Hours) ### Index: Free Text -This index allows searching the words in stored RDF objects that contain text. +This index allows searching the words (terms) in stored RDF objects that contain text. #### Free Text Enable and Options FreeText indexing is enabled in the installer configuration builder by setting @@ -301,16 +298,153 @@ It defaults to empty, which will match all predicates. ###### Free Text Option: FREE_TEXT_QUERY_TERM_LIMIT The maximum number of terms allowed per query. -If a query contains more than this number, this IO error will be thrown. +If a query contains more than this number, this IO error will be thrown: ``` Query contains too many terms. Term limit: 999. Term Count: 999 ``` It defaults to 100 terms. #### Free Text Usage -TODO ???? + + +*This section comes from the now defunct web site **opensahara.com** via the web archive: https://web.archive.org/web/20160303214007/https://dev.opensahara.com/projects/useekm/wiki/IndexingSail +* + +Full text search (FTS) is done via the FILTER function `fts:text` . For example: + +```SQL + PREFIX fts: + SELECT DISTINCT ?result WHERE { + ?result rdf:label ?label. + FILTER(fts:text(?label, "keyword")) + } +``` + +The first argument to this function is the variable that will be filtered by an FTS. The second argument the FTS filter, and the optional third argument the full text search configuration that should be used. If you pass an FTS configuration (such as simple, dutch, english, etc.), it should match the configuration that was used for indexing the statements. The FTS filter currently supports: + +Operator | Description +---------|------------- +& | AND operator: search for multiple words that must occur, for example: `florence & machine & band`. This operator is the default, and can therefore be omitted. The following search is exactly the same: `florence machine band` +¦ | OR operator: search for word varitions (only one word must occur), for example: `florence` ¦ `machine` ¦ `band` +* | PREFIX search, for example: `floren*` + +These operators can be combined, as in: + + floren* & band | singer + +Use parentheses to change the order in which operators are evaluated, as in: + + floren* & (band | singer) + #### Free Text Architecture -TODO ???? + +##### Accumulo Free Text Architecture + +The `AccumuloFreeTextIndexer` stores and queries "free text" data from statements into tables in Accumulo. Specifically, this class +stores data into two different Accumulo Tables. This is the document table (default name: triplestore_text) and the terms +table (default name: triplestore_terms). + +The document table stores the document (i.e. a triple statement), document properties, and the terms within the document. This is the +main table used for processing a text search by using document partitioned indexing. See Accumulo class `IntersectingIterator`. + +For each document, the document table will store the following information: + +Row (partition) | Column Family | Column Qualifier | Value +----------------|----------------|------------------|-------- +shardID | d\x00 | documentHash | Document +shardID | s\x00Subject | documentHash | (empty) +shardID | p\x00Predicate | documentHash | (empty) +shardID | o\x00Object | documentHash | (empty) +shardID | c\x00Context | documentHash | (empty) +shardID | t\x00token | documentHash | (empty) + + +Note: documentHash is a sha256 Hash of the Document's Content + +The terms table is used for expanding wildcard search terms. For each token in the document table, the table will store the following +information: + + +Row (partition) | CF/CQ/Value +----------------- | ----------- +l\x00token | (empty) +r\x00Reversetoken | (empty) + + +There are two prefixes in the table, "token list" (keys with an "l" prefix) and "reverse token list" (keys with a "r" prefix). This table +is uses the "token list" to expand foo* into terms like food, foot, and football. This table uses the "reverse token list" to expand \*ar +into car, bar, and far. + +Example: Given these three statements as inputs: + +``` + rdfs:label "paul smith"@en + rdfs:label "steven anthony miller"@en + rdfs:label "steve miller"@en +``` + +Here's what the tables would look like: (Note: the hashes aren't real, the rows are not sorted, and the partition ids will vary.) + +Triplestore_text + +Row (partition) | Column Family | Column Qualifier | Value +---------------|---------------------------------|------------------|---------- +000000 | d\x00 | 08b3d233a | uri:graph1x00uri:paul\x00rdfs:label\x00"paul smith"@en +000000 | s\x00uri:paul | 08b3d233a | (empty) +000000 | p\x00rdfs:label | 08b3d233a | (empty) +000000 | o\x00"paul smith"@en | 08b3d233a | (empty) +000000 | c\x00uri:graph1 | 08b3d233a | (empty) +000000 | t\x00paul | 08b3d233a | (empty) +000000 | t\x00smith | 08b3d233a | (empty) +000000 | d\x00 | 3a575534b | uri:graph1x00uri:steve\x00rdfs:label\x00"steven anthony miller"@en +000000 | s\x00uri:steve | 3a575534b | (empty) +000000 | p\x00rdfs:label | 3a575534b | (empty) +000000 | o\x00"steven anthony miller"@en | 3a575534b | (empty) +000000 | c\x00uri:graph1 | 3a575534b | (empty) +000000 | t\x00steven | 3a575534b | (empty) +000000 | t\x00anthony | 3a575534b | (empty) +000000 | t\x00miller | 3a575534b | (empty) +000001 | d\x00 | 7bf670d06 | uri:graph1x00uri:steve\x00rdfs:label\x00"steve miller"@en +000001 | s\x00uri:steve | 7bf670d06 | (empty) +000001 | p\x00rdfs:label | 7bf670d06 | (empty) +000001 | o\x00"steve miller"@en | 7bf670d06 | (empty) +000001 | c\x00uri:graph1 | 7bf670d06 | (empty) +000001 | t\x00steve | 7bf670d06 | (empty) +000001 | t\x00miller | 7bf670d06 | (empty) + +triplestore_terms + + +Row (partition) | CF/CQ/Value +------------------|------------ +l\x00paul | (empty) +l\x00smith | (empty) +l\x00steven | (empty) +l\x00anthony | (empty) +l\x00miller | (empty) +l\x00steve | (empty) +r\x00luap | (empty) +r\x00htims | (empty) +r\x00nevets | (empty) +r\x00ynohtna | (empty) +r\x00rellim | (empty) +r\x00evets | (empty) + +The query interface is based on the useekm library which use capability provided by Lucene/Elasticsearch + +##### MongoDB Free Text Architecture +MongoDB backed Apache Rya implements free text indexing by creating a new collection whose name ends with "freetext". Any statements ingested whose predicate is in the configured predicates are also inserted in this freetext collection, with the addition of the field "text". The "text" field is assigned the statement's object. A native MongoDB Text Index is created for the "text" field, as [described in the MongoDB documentation](https://docs.mongodb.com/manual/core/index-text/#create-text-index). + +For queries, the search filter is described as the `$text` operator's [``$search` parameter in the MongoDB documentation ](https://docs.mongodb.com/manual/reference/operator/query/text/#op._S_text) + +In contrast with the Accumulo backed FreeText filter, the MongoDB implementation treats spaces as `OR` operations. If the search string is a space-delimited string, the filter performs a logical `OR` for each term and returns documents that contains any of the terms. The `phrase` approximates the `AND`: put each of the AND'ed terms inside there own double quotes, for example ``"toast" "bread"``. [You can find examples.](https://stackoverflow.com/questions/23985464/how-to-and-and-not-in-mongodb-text-search/) + +The search expression: "\"ssl certificate\" authority key" + +searches for the phrase "ssl certificate" and ("authority" or "key" or "ssl" or "certificate" ). =============template============== ### Index: ???? From f07a574613c08d41998a9391b4033460547115f5 Mon Sep 17 00:00:00 2001 From: "David W. Lotts" Date: Thu, 22 Mar 2018 16:29:22 -0400 Subject: [PATCH 4/7] Manual update, todo break indexes into separate files. Add Geo docs. --- .../src/site/markdown/indexReasoning.md | 1517 ++++++----------- .../rya.manual/src/site/markdown/indexing.md | 43 +- 2 files changed, 578 insertions(+), 982 deletions(-) diff --git a/extras/rya.manual/src/site/markdown/indexReasoning.md b/extras/rya.manual/src/site/markdown/indexReasoning.md index 7c633bf87..4b9281dc2 100644 --- a/extras/rya.manual/src/site/markdown/indexReasoning.md +++ b/extras/rya.manual/src/site/markdown/indexReasoning.md @@ -1,4 +1,4 @@ -**Reasoning** +org.apache.rya**Reasoning** # Overview @@ -324,59 +324,36 @@ then that means that “propA subPropertyOf propB AND propB subPropertyOf propA”. Sample code: - -conn.add(new StatementImpl(vf.createURI(litdupsNS, -"undergradDegreeFrom"), - -RDFS.SUBPROPERTYOF, vf.createURI(litdupsNS, "degreeFrom"))); - -conn.add(new StatementImpl(vf.createURI(litdupsNS, "gradDegreeFrom"), - -RDFS.SUBPROPERTYOF, vf.createURI(litdupsNS, "degreeFrom"))); - -conn.add(new StatementImpl(vf.createURI(litdupsNS, "degreeFrom"), -RDFS.SUBPROPERTYOF, - -vf.createURI(litdupsNS, "memberOf"))); - -conn.add(new StatementImpl(vf.createURI(litdupsNS, "memberOf"), -RDFS.SUBPROPERTYOF, - -vf.createURI(litdupsNS, "associatedWith"))); - -conn.add(new StatementImpl(vf.createURI(litdupsNS, "UgradA"), -vf.createURI(litdupsNS, - -"undergradDegreeFrom"), vf.createURI(litdupsNS, "Harvard"))); - -conn.add(new StatementImpl(vf.createURI(litdupsNS, "GradB"), -vf.createURI(litdupsNS, - -"gradDegreeFrom"), vf.createURI(litdupsNS, "Yale"))); - -conn.add(new StatementImpl(vf.createURI(litdupsNS, "ProfessorC"), - -vf.createURI(litdupsNS, "memberOf"), vf.createURI(litdupsNS, -"Harvard"))); - -conn.commit(); - +``` + conn.add(new StatementImpl(vf.createURI(litdupsNS,"undergradDegreeFrom"), + RDFS.SUBPROPERTYOF, vf.createURI(litdupsNS, "degreeFrom"))); + conn.add(new StatementImpl(vf.createURI(litdupsNS, "gradDegreeFrom"), + RDFS.SUBPROPERTYOF, vf.createURI(litdupsNS, "degreeFrom"))); + conn.add(new StatementImpl(vf.createURI(litdupsNS, "degreeFrom"), + RDFS.SUBPROPERTYOF, vf.createURI(litdupsNS, "memberOf"))); + conn.add(new StatementImpl(vf.createURI(litdupsNS, "memberOf"), + RDFS.SUBPROPERTYOF, vf.createURI(litdupsNS, "associatedWith"))); + conn.add(new StatementImpl(vf.createURI(litdupsNS, "UgradA"), + vf.createURI(litdupsNS, "undergradDegreeFrom"), + vf.createURI(litdupsNS, "Harvard"))); + conn.add(new StatementImpl(vf.createURI(litdupsNS, "GradB"), + vf.createURI(litdupsNS, "gradDegreeFrom"), vf.createURI(litdupsNS, "Yale"))); + conn.add(new StatementImpl(vf.createURI(litdupsNS, "ProfessorC"), + vf.createURI(litdupsNS, "memberOf"), vf.createURI(litdupsNS, "Harvard"))); + conn.commit(); +``` With query: - -PREFIX rdfs: - -PREFIX rdf: - -PREFIX lit: - -select * where {?s lit:memberOf lit:Harvard.} - +``` + PREFIX rdfs: + PREFIX rdf: + PREFIX lit: + select * where {?s lit:memberOf lit:Harvard.} +``` Will return results: - -[s=urn:test:litdups#UgradA] - -[s=urn:test:litdups#ProfessorC] - +``` + [s=urn:test:litdups#UgradA] + [s=urn:test:litdups#ProfessorC] +``` Since UgradA has undergraduateDegreeFrom Harvard and ProfessorC is memberOf Harvard. @@ -388,35 +365,22 @@ second part of the Join checks for triples with those properties and with object \"\`Harvard\`\". Query plan: - +``` QueryRoot - -Projection - -ProjectionElemList - -ProjectionElem "s" - -Join - -FixedStatementPattern - -Var (name=0bad69f3-4769-4293-8318-e828b23dc52a) - -Var (name=c-0bad69f3-4769-4293-8318-e828b23dc52a, - -value=http://www.w3.org/2000/01/rdf-schema#subPropertyOf) - -Var (name=-const-1, value=urn:test:litdups#memberOf, anonymous) - -DoNotExpandSP - -Var (name=s) - -Var (name=0bad69f3-4769-4293-8318-e828b23dc52a) - -Var (name=-const-2, value=urn:test:litdups#Harvard, anonymous) - + Projection + ProjectionElemList + ProjectionElem "s" + Join + FixedStatementPattern + Var (name=0bad69f3-4769-4293-8318-e828b23dc52a) + Var (name=c-0bad69f3-4769-4293-8318-e828b23dc52a, + value=http://www.w3.org/2000/01/rdf-schema#subPropertyOf) + Var (name=-const-1, value=urn:test:litdups#memberOf, anonymous) + DoNotExpandSP + Var (name=s) + Var (name=0bad69f3-4769-4293-8318-e828b23dc52a) + Var (name=-const-2, value=urn:test:litdups#Harvard, anonymous) +``` #### owl:inverseOf InverseOf defines a property that is an inverse relation of another @@ -424,155 +388,102 @@ property. For example, a student who has a degreeFrom a University also means that the University hasAlumnus student. Code: - -conn.add(new StatementImpl(vf.createURI(litdupsNS, "degreeFrom"), -OWL.INVERSEOF, - -vf.createURI(litdupsNS, "hasAlumnus"))); - -conn.add(new StatementImpl(vf.createURI(litdupsNS, "UgradA"), -vf.createURI(litdupsNS, - -"degreeFrom"), vf.createURI(litdupsNS, "Harvard"))); - -conn.add(new StatementImpl(vf.createURI(litdupsNS, "GradB"), -vf.createURI(litdupsNS, - -"degreeFrom"), vf.createURI(litdupsNS, "Harvard"))); - -conn.add(new StatementImpl(vf.createURI(litdupsNS, "Harvard"), -vf.createURI(litdupsNS, - -"hasAlumnus"), vf.createURI(litdupsNS, "AlumC"))); - +``` +conn.add(new StatementImpl(vf.createURI(litdupsNS, "degreeFrom"), OWL.INVERSEOF, + vf.createURI(litdupsNS, "hasAlumnus"))); +conn.add(new StatementImpl(vf.createURI(litdupsNS, "UgradA"), vf.createURI(litdupsNS, + "degreeFrom"), vf.createURI(litdupsNS, "Harvard"))); +conn.add(new StatementImpl(vf.createURI(litdupsNS, "GradB"), vf.createURI(litdupsNS, + "degreeFrom"), vf.createURI(litdupsNS, "Harvard"))); +conn.add(new StatementImpl(vf.createURI(litdupsNS, "Harvard"), vf.createURI(litdupsNS, + "hasAlumnus"), vf.createURI(litdupsNS, "AlumC"))); conn.commit(); - +``` Query: - -PREFIX rdfs: - -PREFIX rdf: - -PREFIX lit: - -select * where {lit:Harvard lit:hasAlumnus ?s.} - +``` + PREFIX rdfs: + PREFIX rdf: + PREFIX lit: + select * where {lit:Harvard lit:hasAlumnus ?s.} +``` Result: - -[s=urn:test:litdups#AlumC] - -[s=urn:test:litdups#GradB] - -[s=urn:test:litdups#UgradA] - -The query planner will expand the statement pattern “Harvard hasAlumnus -?s” to a Union between “Harvard hasAlumnus ?s” and “?s degreeFrom -Harvard”. +``` + [s=urn:test:litdups#AlumC] + [s=urn:test:litdups#GradB] + [s=urn:test:litdups#UgradA] +``` +The query planner will expand the statement pattern “`Harvard hasAlumnus +?s`” to a Union between “`Harvard hasAlumnus ?s`” and “`?s degreeFrom +Harvard`”. As a caveat, it is important to note that in general Union queries do not have the best performance, so having a property that has both an -inverseOf and subPropertyOf, could cause a query plan that might take a +`inverseOf` and `subPropertyOf`, could cause a query plan that might take a long time to execute, depending on how the query planner orders the joins. Query plan: - +``` QueryRoot - -Projection - -ProjectionElemList - -ProjectionElem "s" - -InferUnion - -StatementPattern - -Var (name=-const-1, value=urn:test:litdups#Harvard, anonymous) - -Var (name=-const-2, value=urn:test:litdups#hasAlumnus, anonymous) - -Var (name=s) - -StatementPattern - -Var (name=s) - -Var (name=-const-2, value=urn:test:litdups#degreeFrom) - -Var (name=-const-1, value=urn:test:litdups#Harvard, anonymous) - + Projection + ProjectionElemList + ProjectionElem "s" + InferUnion + StatementPattern + Var (name=-const-1, value=urn:test:litdups#Harvard, anonymous) + Var (name=-const-2, value=urn:test:litdups#hasAlumnus, anonymous) + Var (name=s) + StatementPattern + Var (name=s) + Var (name=-const-2, value=urn:test:litdups#degreeFrom) + Var (name=-const-1, value=urn:test:litdups#Harvard, anonymous) +``` #### SymmetricProperty SymmetricProperty defines a relationship where, for example, if Bob is a friendOf Jeff, then Jeff is a friendOf Bob. Code: - -conn.add(new StatementImpl(vf.createURI(litdupsNS, "friendOf"), -RDF.TYPE, - -OWL.SYMMETRICPROPERTY)); - -conn.add(new StatementImpl(vf.createURI(litdupsNS, "Bob"), -vf.createURI(litdupsNS, - -"friendOf"), vf.createURI(litdupsNS, "Jeff"))); - -conn.add(new StatementImpl(vf.createURI(litdupsNS, "James"), -vf.createURI(litdupsNS, - -"friendOf"), vf.createURI(litdupsNS, "Jeff"))); - +``` +conn.add(new StatementImpl(vf.createURI(litdupsNS, "friendOf"), RDF.TYPE, + OWL.SYMMETRICPROPERTY)); +conn.add(new StatementImpl(vf.createURI(litdupsNS, "Bob"), vf.createURI(litdupsNS, + "friendOf"), vf.createURI(litdupsNS, "Jeff"))); +conn.add(new StatementImpl(vf.createURI(litdupsNS, "James"), vf.createURI(litdupsNS, + "friendOf"), vf.createURI(litdupsNS, "Jeff"))); conn.commit(); - +``` Query: - +``` PREFIX rdfs: - PREFIX rdf: - PREFIX lit: - select * where {?s lit:friendOf lit:Bob.} - +``` Results: - +``` [s=urn:test:litdups#Jeff] - +``` The query planner will recognize that `friendOf` is a SymmetricProperty and devise a Union to find the specified relationship and inverse. Query plan: - +``` QueryRoot - -Projection - -ProjectionElemList - -ProjectionElem "s" - -InferUnion - -StatementPattern - -Var (name=s) - -Var (name=-const-1, value=urn:test:litdups#friendOf, anonymous) - -Var (name=-const-2, value=urn:test:litdups#Bob, anonymous) - -StatementPattern - -Var (name=-const-2, value=urn:test:litdups#Bob, anonymous) - -Var (name=-const-1, value=urn:test:litdups#friendOf, anonymous) - -Var (name=s) - + Projection + ProjectionElemList + ProjectionElem "s" + InferUnion + StatementPattern + Var (name=s) + Var (name=-const-1, value=urn:test:litdups#friendOf, anonymous) + Var (name=-const-2, value=urn:test:litdups#Bob, anonymous) + StatementPattern + Var (name=-const-2, value=urn:test:litdups#Bob, anonymous) + Var (name=-const-1, value=urn:test:litdups#friendOf, anonymous) + Var (name=s) +``` #### owl:TransitiveProperty TransitiveProperty provides a transitive relationship between resources. @@ -580,90 +491,57 @@ For example, if Queens is subRegionOf NYC and NYC is subRegionOf NY, then Queens is transitively a subRegionOf NY. Code: - -conn.add(new StatementImpl(vf.createURI(litdupsNS, "subRegionOf"), -RDF.TYPE, - -OWL.TRANSITIVEPROPERTY)); - -conn.add(new StatementImpl(vf.createURI(litdupsNS, "Queens"), -vf.createURI(litdupsNS, - -"subRegionOf"), vf.createURI(litdupsNS, "NYC"))); - -conn.add(new StatementImpl(vf.createURI(litdupsNS, "NYC"), -vf.createURI(litdupsNS, - -"subRegionOf"), vf.createURI(litdupsNS, "NY"))); - -conn.add(new StatementImpl(vf.createURI(litdupsNS, "NY"), -vf.createURI(litdupsNS, - -"subRegionOf"), vf.createURI(litdupsNS, "US"))); - -conn.add(new StatementImpl(vf.createURI(litdupsNS, "US"), -vf.createURI(litdupsNS, - -"subRegionOf"), vf.createURI(litdupsNS, "NorthAmerica"))); - -conn.add(new StatementImpl(vf.createURI(litdupsNS, "NorthAmerica"), - -vf.createURI(litdupsNS, "subRegionOf"), vf.createURI(litdupsNS, -"World"))); - -conn.commit(); - +``` +QueryRoot + Projection + ProjectionElemList + ProjectionElem "s" + InferUnion + StatementPattern + Var (name=s) + Var (name=-const-1, value=urn:test:litdups#friendOf, anonymous) + Var (name=-const-2, value=urn:test:litdups#Bob, anonymous) + StatementPattern + Var (name=-const-2, value=urn:test:litdups#Bob, anonymous) + Var (name=-const-1, value=urn:test:litdups#friendOf, anonymous) + Var (name=s) +``` Query: - -PREFIX rdfs: - -PREFIX rdf: - -PREFIX lit: - -select * where {?s lit:subRegionOf lit:NorthAmerica.} - +```SQL + PREFIX rdfs: + PREFIX rdf: + PREFIX lit: + select * where {?s lit:subRegionOf lit:NorthAmerica.} +``` Results: - -[s=urn:test:litdups#Queens] - -[s=urn:test:litdups#NYC] - -[s=urn:test:litdups#NY] - -[s=urn:test:litdups#US] - -The TransitiveProperty relationship works by running recursive queries +``` + [s=urn:test:litdups#Queens] + [s=urn:test:litdups#NYC] + [s=urn:test:litdups#NY] + [s=urn:test:litdups#US] +``` +The `TransitiveProperty` relationship works by running recursive queries till all the results are returned. -It is important to note that certain TransitiveProperty relationships +It is important to note that certain `TransitiveProperty` relationships will not work: - - Open ended property: “?s subRegionOf ?o” (At least one of the + - Open ended property: “`?s subRegionOf ?o`” (At least one of the properties must be filled or will be filled as the query gets answered.) - - Closed property: “Queens subRegionOf NY” (At least one of the + - Closed property: “`Queens subRegionOf NY`” (At least one of the properties must be empty.) - -Query plan: - +``` QueryRoot - -Projection - -ProjectionElemList - -ProjectionElem "s" - -TransitivePropertySP - -Var (name=s) - -Var (name=-const-1, value=urn:test:litdups#subRegionOf, anonymous) - -Var (name=-const-2, value=urn:test:litdups#NorthAmerica, anonymous) - + Projection + ProjectionElemList + ProjectionElem "s" + TransitivePropertySP + Var (name=s) + Var (name=-const-1, value=urn:test:litdups#subRegionOf, anonymous) + Var (name=-const-2, value=urn:test:litdups#NorthAmerica, anonymous) +``` # **Forward-Chaining Reasoner** The Rya Forward Chaining Reasoner is implemented as a forward-chaining @@ -680,7 +558,7 @@ intended for scalable rule-based reasoning. ## Rules These are the OWL RL (Rule Language) rules currently implemented. Those -rules not implemented include any rule involving owl:sameAs, rules +rules not implemented include any rule involving `owl:sameAs`, rules inferring and checking datatypes, any rule operating on a list construct, and the few other rules requiring a complex join of instance triples that do not all share any single node. @@ -692,53 +570,37 @@ forward chaining step. - **scm-cls**: Every class is its own subclass and equivalent class, and is a subclass of owl:Thing. - - **scm-eqc1**: If two classes are equivalent, they are also subclasses of each other. - - **scm-eqc2**: If two classes are each other's subclasses, they are also equivalent classes. - - **scm-op**: Every object property is its own subproperty and equivalent property. - - **scm-dp**: Every datatype property is its own subproperty and equivalent property. - - **scm-eqp1**: If two properties are equivalent, they are also subproperties of each other. - - **scm-eqp2**: If two properties are each other's subproperties, they are also equivalent properties. - - **scm-spo**: subPropertyOf is transitive. - - **scm-dom1**: A property with domain c also has as domain any of c's superclasses. - - **scm-dom2**: A subproperty inherits its superproperties' domains. - - **scm-rng1**: A property with range c also has as range any of c's superclasses. - - **scm-rng2**: A subproperty inherits its superproperties' ranges. - - **scm-svf1**: A property restriction c1 is a subclass of another c2 if they are both someValuesFrom restrictions on the same property and c1's target class is a subclass of c2's target class. - - **scm-avf1**: A property restriction c1 is a subclass of another c2 if they are both allValuesFrom restrictions on the same property and c1's target class is a subclass of c2's target class. - - **scm-hv**: A property restriction c1 is a subclass of another c2 if they are both hasValue restrictions with the same value where c1's target property is a subproperty of c2's target property. - - **scm-svf2**: A property restriction c1 is a subclass of another c2 if they are both someValuesFrom restrictions on the same class where c1's target property is a subproperty of c2's target property. - - **scm-avf2**: A property restriction c1 is a subclass of another c2 if they are both allValuesFrom restrictions on the same class where c2's target property is a subproperty of c1's target property. @@ -749,38 +611,16 @@ These rules are applied by the local reasoner immediately upon reading in the appropriate instance triple. - **prp-dom**: Infer subject's type from predicate's domain. - - **prp-rng**: Infer object's type from predicate's range. - - - **prp-irp**: If **?p** is irreflexive, **?x ?p ?x** is inconsistent. - - - **prp-symp**: If **?p** is symmetric, **?x ?p ?y** implies **?y ?p - ?x** . - - - **prp-spo1**: If **?p1** has superproperty **p2** , **?x ?p1 ?y** - implies **?x ?p2 ?**y . - - - **prp-inv1**: If **?p1** and **?p2** are inverse properties, **?x - ?p1 ?y** implies **?y ?p1 ?x** . - - - **prp-inv2**: If **?p1** and **?p2** are inverse properties, **?x - ?p2 ?y** implies **?y ?p1 ?x** . - - - **cls-svf2**: If **?x** is a someValuesFrom restriction on property - **?p** with respect to owl:Thing , **?u ?p ?v** implies **?u - rdf:type ?x** for any **?u** , **?v** . - - - **cls-hv1**: If **?x** is a hasValue restriction on property **?p** - with value **?v** , **?u rdf:type ?x** implies **?u ?p ?v** for any - **?u** . - - - **cls-hv2**: If **?x** is a hasValue restriction on property **?p** - with value **?v** , **?u ?p ?v** implies **?u rdf:type ?x** for any - **?u** . - - - **cls-nothing2**: Anything having type **owl:Nothing** is an - inconsistency. - + - **prp-irp**: If `?p` is irreflexive, `?x ?p ?x` is inconsistent. + - **prp-symp**: If `?p` is symmetric, `?x ?p ?y` implies `?y ?p ?x` . + - **prp-spo1**: If `?p1` has superproperty `p2` , `?x ?p1 ?y` implies `?x ?p2 ?`y . + - **prp-inv1**: If `?p1` and `?p2` are inverse properties, `?x ?p1 ?y` implies `?y ?p1 ?x` . + - **prp-inv2**: If `?p1` and `?p2` are inverse properties, `?x ?p2 ?y` implies `?y ?p1 ?x` . + - **cls-svf2**: If `?x` is a someValuesFrom restriction on property `?p` with respect to owl:Thing , `?u ?p ?v` implies `?u rdf:type ?x` for any `?u` , `?v` . + - **cls-hv1**: If `?x` is a hasValue restriction on property `?p` with value `?v` , `?u rdf:type ?x` implies `?u ?p ?v` for any`?u` . + - **cls-hv2**: If `?x` is a hasValue restriction on property `?p` with value `?v` , `?u ?p ?v` implies `?u rdf:type ?x` for any `?u` . + - **cls-nothing2**: Anything having type `owl:Nothing` is an inconsistency. - **cax-sco**: Whenever a type is seen, inherit any supertypes. ### Join Rules @@ -792,39 +632,20 @@ later during the same phase. Since the reducer assumes incoming triples are received before outgoing triples, only the former need to be held in memory. - - **prp-asyp**: If **?p** is asymmetric, **?x ?p ?y** and **?y ?p ?x** - is inconsistent. - - - **prp-trp**: If **?p** is transitive, **?x ?p ?y** and **?y ?p ?z** - implies **?x ?p ?z**. - - - **prp-pdw**: If properties **?p** and **?q** are disjoint, **?x ?p - ?y** and **?x ?q ?y** is inconsistent - - - **cax-dw**: If classes **c1** and **c2** are disjoint, having both - types is inconsistent. - - - **cls-com**: If classes **c1** and **c2** are complementary, having - both types is inconsistent. - - - **cls-svf1**: If **?x** is a someValuesFrom restriction on property - **?p** with respect to type **?y**, **?u ?p ?v** and **?v rdf:type - ?y** imply **?u rdf:type ?x**. - - - **cls-avf**: If **?x** is an allValuesFrom restriction on property - **?p** with respect to type **?y**, **?u** **?p ?v** and **?u - rdf:type ?x** imply **?v rdf:type ?y**. - - - **cls-maxc1**: **?x ?p ?y** is inconsistent if **?p** has maximum - cardinality 0. - - - **cls-maxqc2**: **?x ?p ?y** is inconsistent if **?p** has maximum - qualified cardinality 0 with respect to **owl:Thing**. + - **prp-asyp**: If `?p` is asymmetric, `?x ?p ?y` and `?y ?p ?x` is inconsistent. + - **prp-trp**: If `?p` is transitive, `?x ?p ?y` and `?y ?p ?z` implies `?x ?p ?z`. + - **prp-pdw**: If properties `?p` and `?q` are disjoint, `?x ?p ?y` and `?x ?q ?y` is inconsistent + - **cax-dw**: If classes **c1** and **c2** are disjoint, having both types is inconsistent. + - **cls-com**: If classes **c1** and **c2** are complementary, having both types is inconsistent. + - **cls-svf1**: If `?x` is a someValuesFrom restriction on property `?p` with respect to type `?y`, `?u ?p ?v` and `?v rdf:type ?y` imply `?u rdf:type ?x`. + - **cls-avf**: If `?x` is an allValuesFrom restriction on property `?p` with respect to type `?y`, `?u` `?p ?v` and `?u rdf:type ?x` imply `?v rdf:type ?y`. + - **cls-maxc1**: `?x ?p ?y` is inconsistent if `?p` has maximum cardinality 0. + - **cls-maxqc2**: `?x ?p ?y` is inconsistent if `?p` has maximum qualified cardinality 0 with respect to `owl:Thing`. ## Usage The complete forward-chaining inference task is performed by the class -**mvm.rya.reasoning.mr.ReasoningDriver**. This will run a series of +`org.apache.rya.reasoning.mr.ReasoningDriver`. This will run a series of MapReduce jobs on the input until no further inferences can be made. Input must be configured using properties given in a Hadoop configuration file and/or through command line parameters. Optional @@ -835,20 +656,16 @@ parameters control the output generated. The following properties are all required to connect to Accumulo: - **ac.instance** (instance name) - - **ac.zk** (Zookeepers) - - **ac.username** - - **ac.pwd** - - **rdf.tablePrefix** (e.g. **rya_**: the dataset to perform inference on) File input is not generally recommended, but is implemented for testing purposes. To read from a file instead of an Accumulo table, specify -either **input** to use a file/directory on Hadoop Distributed File -System (HDFS), or C) **reasoning.inputLocal** to use a local +either `input` to use a file/directory on Hadoop Distributed File +System (HDFS), or C) `reasoning.inputLocal` to use a local file/directory (it will be uploaded to HDFS). This requires that the data can be loaded entirely in memory, and does not correctly handle bnodes in instance data. Each individual MapReduce job parses the file @@ -860,25 +677,20 @@ The following properties are optional: - **reasoning.workingDir**: An HDFS path to which all intermediate files and outputs will be written (defaults to **tmp/reasoning**) - - **reasoning.debug**: Keep intermediate sequence files and print detailed information about individual input output records to output files starting with **debug** (defaults to false) - - **reasoning.output**: Run the OutputTool job to collect inferred triples and inconsistencies in respective output files (defaults to true, set to false if the content of the inferred data is not important) - - **reasoning.stats**: Print a table of detailed metrics to standard out instead of the default information about the run (defaults to false). - - **reasoning.step**: Used internally to keep track of which iteration the reasoning engine is on. Can be set manually to resume a previous execution (defaults to 0, meaning reasoning starts from the beginning -- set to the previously completed step to resume). - - **rdf.format**: When using file input, use this to specify the RDF serialization format (defaults to RDF/XML) @@ -908,86 +720,66 @@ Useful MapReduce properties include (Hadoop 2 versions): ### Example -Given the following **conf.xml** (assuming a running Accumulo instance +Given the following `conf.xml` (assuming a running Accumulo instance with these parameters): - -> **** -> -> **** -> -> **** -> -> **ac.instance** -> -> **dev** -> -> **** -> -> **** -> -> **ac.zk** -> -> **localhost:2181** -> -> **** -> -> **** -> -> **ac.username** -> -> **root** -> -> **** -> -> **** -> -> **ac.pwd** -> -> **root** -> -> **** -> -> **** -> -> **reasoning.workingDir** -> -> **example_output** -> -> **** -> -> **** - +``` + + + + ac.instance + dev + + + ac.zk + localhost:2181 + + + ac.username + root + + + ac.pwd + root + + + reasoning.workingDir + example_output + + +``` The following command will run the reasoner on the triples found in Rya -table **rya_spo** and produce file output in HDFS under -**example_output/final**: +table rya_spo and produce file output in HDFS under +example_output/final. Be sure to change the version to whatever you are using. -> **hadoop jar target/rya.reasoning-3.2.10-SNAPSHOT-shaded.jar -> mvm.rya.reasoning.mr.ReasoningDriver -conf conf.xml -> -Drdf.tablePrefix=rya_** +``` +hadoop jar target/rya.reasoning-3.2.13-SNAPSHOT-shaded.jar \ + org.apache.rya.reasoning.mr.ReasoningDriver -conf conf.xml \ + -Drdf.tablePrefix=rya_ +``` ### Output Summary statistics will be printed to standard out, including the number of instance triples inferred and the number of inconsistencies found. By default, this information will be printed at each iteration's -completion. If **reasoning.stats** is set to true, detailed statistics +completion. If `reasoning.stats` is set to true, detailed statistics will be printed at the end instead. The inferences themselves will be written to files under -**${reasoning.workingDir}/final**, unless output was disabled. Triples +`${reasoning.workingDir}/final`, unless output was disabled. Triples are written as raw N-Triples, while inconsistencies are printed with their derivations. Triples and inconsistencies will be written to different files; each file will only exist if at least one of the corresponding fact types was generated. If debug was set to true, data files and additional info for each step -**i** will be written to **${reasoning.workingDir}/step-${i}a** (output +**i** will be written to `${reasoning.workingDir}/step-${i}a` (output of the forward chaining reasoner for that step) and -**${reasoning.workingDir}/step-${i}** (output of the duplicate +`${reasoning.workingDir}/step-${i}` (output of the duplicate elimination job for that step). Some additional information can be found in the Hadoop logs if the log -level was set to DEBUG (with the **mapreduce.{map/reduce}.log.level** +level was set to DEBUG (with the `mapreduce.{map/reduce}.log.level` properties). ### Conformance Testing @@ -1005,23 +797,23 @@ by the reasoner, contained in the schema (since the current implementation is mainly concerned with consistency detection, it may correctly derive a schema fact but will not manifest it as a triple), or trivially implied (e.g. parsing a conclusion specified in XML using the -**** tag will result in a triple **[bnode] rdf:type -owl:Ontology** , which the reasoner would not explicitly generate). Once +`` tag will result in a triple `[bnode] rdf:type owl:Ontology` , +which the reasoner would not explicitly generate). Once all tests are run, either success or failure is reported for each. If a test has multiple types, e.g. both consistency and entailment, success requires passing all appropriate checks. The command expects two arguments: the input file containing the tests, and a path to use for a temporary working directory for Mini Accumulo. -Input format can be specified with **rdf.format**; defaults to RDF/XML. +Input format can be specified with `rdf.format`; defaults to RDF/XML. HDFS is still used for intermediate storage and reasoner output -(**reasoning.workingDir**). +(`reasoning.workingDir`). Example: ``` hadoop jar rya.reasoning-3.2.10-SNAPSHOT-shaded.jar \ - mvm.rya.reasoning.mr.ConformanceTest -conf conf.xml \ + org.apache.rya.reasoning.mr.ConformanceTest -conf conf.xml \ owl-test/profile-RL.rdf temp ``` ## Design @@ -1036,7 +828,7 @@ practice, full OWL reasoners exist, but have limited scalability. Most traditional reasoners use tableau algorithms, which maintain a set of logical assertions as a tree, and repeatedly reduce them to entail new assertions. Each time a reduction requires a nondeterministic choice --- e.g. A or B -- this corresponds to a branch in the tree. When a +-- for example: A or B -- this corresponds to a branch in the tree. When a reduction results in an atomic assertion that would create a contradiction, the current branch is inconsistent, and the search for alternative reductions backtracks and chooses a new path. When all paths @@ -1048,13 +840,13 @@ designed to support scalable if/then rules, so this is a natural choice. RL allows almost all of the OWL modeling vocabulary but its semantics are limited specifically so that nondeterministic inferences are never necessary. For example, no RL rule will imply that an individual must -belong to one of two different classes. One might assert that c1 is a -subclass of (c2 union c3), but this does not entail any inferences under -RL. (However, if (c1 union c2) is a subclass of c3, the RL rules do -entail that both c1 and c2 are subclasses of c3. The union construct can +belong to one of two different classes. One might assert that `c1` is a +subclass of (`c2 union c3`), but this does not entail any inferences under +RL. (However, if (`c1 union c2`) is a subclass of `c3`, the RL rules do +entail that both `c1` and `c2` are subclasses of `c3`. The union construct can have implications in RL, but only deterministic ones.) -Our approach is similar to that of WebPie[1] (Web-scale Parallel +Our approach is similar to that of [WebPie[1]](http://www.few.vu.nl/~jui200/webpie.html) (Web-scale Parallel Inference Engine), with one notable difference being that we apply all rules on instance data in the same MapReduce job, using one standard partitioning scheme (by single node: a triple is partitioned based on @@ -1090,12 +882,11 @@ example, with transitivity). Many of the RL rules can be applied this way, if we can additionally assume that each reducer has access to the complete schema, or TBox. For -example, if **:c1** and **:c2** are known to be disjoint classes, a -reasoner responsible for processing triples involving node **:x** can -detect an inconsistency if it receives both **:x rdf:type :c** and **:x -rdf:type :y** . But this only works if the disjoint class relationship +example, if `:c1` and `:c2` are known to be disjoint classes, a +reasoner responsible for processing triples involving node `:x` can +detect an inconsistency if it receives both `:x rdf:type :c` and `:x rdf:type :y` . But this only works if the disjoint class relationship was passed to that reducer, and if we process the input in parallel, the -mapper that sees **:c1 owl:disjointWith :c2** has no way of predicting +mapper that sees `:c1 owl:disjointWith :c2` has no way of predicting which reducers will need this information. Therefore, we do an initial pass through the data to aggregate all semantically meaningful information about classes and properties (the schema), and ensure that @@ -1125,9 +916,9 @@ Together, this yields the following MapReduce jobs and workflow: 1) Setup: Distribute Schema object to every mapper and reducer. - 2) Map: Reads triples from Accumulo and any previous steps. For a given triple, use the schema to determine whether local reasoners for the subject and/or object might be able to use that triple. If so, output the triple with the subject and/or object as a key. (For example, if the triple is **:s :p :o** and the schema has a domain for **:p** , output **<:s, (:s :p :o)>** so the reducer for **:s** can apply the domain rule. If the triple has a range as well, also output **<:o, (:s :p :o)> )**. + 2) Map: Reads triples from Accumulo and any previous steps. For a given triple, use the schema to determine whether local reasoners for the subject and/or object might be able to use that triple. If so, output the triple with the subject and/or object as a key. (For example, if the triple is `:s :p :o` and the schema has a domain for `:p` , output `<:s, (:s :p :o)>` so the reducer for `:s` can apply the domain rule. If the triple has a range as well, also output `<:o, (:s :p :o)> )`. - 3) Shuffle/Sort: pairs are grouped based on the key (node) and sorted based on how the node relates to the triple: incoming edges precede outgoing edges. (For example, if both **(:a :b :c)** and **(:c :d :e)** are relevant to the reducer for **:c** , then **<:c, (:a :b :c)>** will be received before **<:c, (:c :d :e)>** .) This allows joins to use less memory than they would otherwise need. + 3) Shuffle/Sort: pairs are grouped based on the key (node) and sorted based on how the node relates to the triple: incoming edges precede outgoing edges. (For example, if both `(:a :b :c)` and `(:c :d :e)` are relevant to the reducer for `:c` , then `<:c, (:a :b :c)>` will be received before `<:c, (:c :d :e)>` .) This allows joins to use less memory than they would otherwise need. 4) Reduce: Receives a series of triples involving a single URI. Creates a LocalReasoner object for that URI, which combines those triples @@ -1166,229 +957,137 @@ one, and then by applying schema inference rules to add inferred schema information. ``` SchemaFilter: # MapReduce job - Map: - for triple in input: - if isSchemaTriple(triple): - output(null, triple) - Reduce(null, triples): # Single reducer + Map: + for triple in input: + if isSchemaTriple(triple): + output(null, triple) + Reduce(null, triples): # Single reducer schema <- new Schema() - for triple in triples: - schema.processTriple(triple) - schema.computeClosure() - output(schema) to file + for triple in triples: + schema.processTriple(triple) + schema.computeClosure() + output(schema) to file ``` Where: ``` isSchemaTriple(triple): -(s p o) <- triple -if (p is rdf:type AND o in { owl:TransitiveProperty, - owl:IrreflexiveProperty, owl:SymmetricProperty, ... }) - OR (p in { rdfs:subClassOf, rdfs:subPropertyOf, owl:inverseOf, - owl:disjointWith, ... }): - return true -else: - return false -Schema.processTriple(s, p, o): - (s p o) <- triple - - # If this is a schema-relevant type assignment - if p is rdf:type: - if o in { owl:TransitiveProperty, owl:IrreflexiveProperty, ... }: - # then s is a property with that characteristic - -properties[s] <- initialize property s if necessary - -if o == owl:TransitiveProperty: - -properties[s].transitive <- true - -... # Repeat for each characteristic - -# If this connects two schema constructs - -else if p in { rdfs:subClassOf, owl:disjointWith, owl:onProperty, ... -}: - -initialize s if necessary (class or property, depending on p) - -initialize o if necessary (class or property, depending on p) - -# Connect s to o according to p: - -switch(p): - -case rdfs:subClassOf: classes[s].superclasses.add(classes[o]) - -case rdfs:subPropertyOf: -properties[s].superproperties.add(properties[o]) - -case rdfs:domain: properties[s].domain.add(classes[o]) - -case rdfs:range: properties[s].range.add(classes[o]) - -# Rules scm-eqc1, scm-eqc2: equivalent class is equivalent to mutual -subClassOf: - -case owl:equivalentClass: - -classes[s].superclasses.add(classes[o]) - -classes[o].superclasses.add(classes[s]) - -# Rules scm-eqp1, scm-eqp2: equivalent property is equivalent to -mutual subPropertyOf: - -case owl:equivalentProperty: - -properties[s].superproperties.add(properties[o]) + (s p o) <- triple + if (p is rdf:type AND o in { owl:TransitiveProperty, owl:IrreflexiveProperty, owl:SymmetricProperty, ... }) + OR (p in { rdfs:subClassOf, rdfs:subPropertyOf, owl:inverseOf, owl:disjointWith, ... }): + return true + else: + return false -properties[o].superproperties.add(properties[s]) - -# inverse, disjoint, complement relations are all symmetric: - -case owl:inverseOf: - -properties[s].inverseproperties.add(properties[o]) - -properties[o].inverseproperties.add(properties[s]) - -case owl:disjointWith: - -classes[s].disjointclasses.add(o) - -classes[o].disjointclasses.add(s) - -case owl:complementOf: - -classes[s].complementaryclasses.add(o) - -classes[o].complementaryclasses.add(s) - -case owl:propertyDisjointWith: - -properties[s].disjointproperties.add(properties[o]) - -properties[o].disjointproperties.add(properties[s]) - -case owl:onProperty: - -classes[s].onProperty.add(properties[o]) - -# Link properties back to their property restrictions - -properties[o].addRestriction.(properties[s]) - -case owl:someValuesFrom: classes[s].someValuesFrom.add(o) - -case owl:allValuesFrom: classes[s].allValuesFrom.add(o) - -case owl:hasValue: classes[s].hasValue.add(o) - -case owl:maxCardinality: classes[s].maxCardinality <- (int) o +Schema.processTriple(s, p, o): + (s p o) <- triple + # If this is a schema-relevant type assignment + if p is rdf:type: + if o in { owl:TransitiveProperty, owl:IrreflexiveProperty, ... }: + # then s is a property with that characteristic + properties[s] <- initialize property s if necessary + if o == owl:TransitiveProperty: + properties[s].transitive <- true + ... # Repeat for each characteristic + + # If this connects two schema constructs + else if p in { rdfs:subClassOf, owl:disjointWith, owl:onProperty, ... }: + initialize s if necessary (class or property, depending on p) + initialize o if necessary (class or property, depending on p) + # Connect s to o according to p: + switch(p): + case rdfs:subClassOf: classes[s].superclasses.add(classes[o]) + case rdfs:subPropertyOf: properties[s].superproperties.add(properties[o]) + case rdfs:domain: properties[s].domain.add(classes[o]) + case rdfs:range: properties[s].range.add(classes[o]) + + # Rules scm-eqc1, scm-eqc2: equivalent class is equivalent to mutual subClassOf: + case owl:equivalentClass: + classes[s].superclasses.add(classes[o]) + classes[o].superclasses.add(classes[s]) + # Rules scm-eqp1, scm-eqp2: equivalent property is equivalent to mutual subPropertyOf: + case owl:equivalentProperty: + properties[s].superproperties.add(properties[o]) + properties[o].superproperties.add(properties[s]) + + # inverse, disjoint, complement relations are all symmetric: + case owl:inverseOf: + properties[s].inverseproperties.add(properties[o]) + properties[o].inverseproperties.add(properties[s]) + case owl:disjointWith: + classes[s].disjointclasses.add(o) + classes[o].disjointclasses.add(s) + case owl:complementOf: + classes[s].complementaryclasses.add(o) + classes[o].complementaryclasses.add(s) + case owl:propertyDisjointWith: + properties[s].disjointproperties.add(properties[o]) + properties[o].disjointproperties.add(properties[s]) + + case owl:onProperty: + classes[s].onProperty.add(properties[o]) + # Link properties back to their property restrictions + properties[o].addRestriction.(properties[s]) + + case owl:someValuesFrom: classes[s].someValuesFrom.add(o) + case owl:allValuesFrom: classes[s].allValuesFrom.add(o) + case owl:hasValue: classes[s].hasValue.add(o) + case owl:maxCardinality: classes[s].maxCardinality <- (int) o + case owl:maxQualifiedCardinality: classes[s].maxQualifiedCardinality <- (int) o + case owl:onClass: classes[s].onClass.add(classes[o]) -case owl:maxQualifiedCardinality: classes[s].maxQualifiedCardinality -<- (int) o -case owl:onClass: classes[s].onClass.add(classes[o]) computeSchemaClosure(): + # Rule scm-spo states that owl:subPropertyOf relationships are transitive. + # For each property, do a BFS to find all its superproperties. + for p in properties: + frontier <- { sp | sp in p.superproperties } + while frontier is not empty: + ancestors <- UNION{ f.superproperties | f in frontier } + p.superproperties <- p.superproperties UNION ancestors + frontier <- UNION{ a.superproperties | a in ancestors } - p.superproperties + + # Rules scm-hv, scm-svf2, and scm-avf2 use sub/superproperty information to + # infer sub/superclass relations between property restrictions: + for c1, c2 in classes: + # Rules apply if c1 is a restriction on a subproperty and c2 is a restriction on a superproperty. + if c1 is property restriction AND c2 is property restriction AND c1.onProperty.superproperties contains c2.onProperty: + if v exists such that c1.hasValue(v) and c2.hasValue(v): + c1.superclasses.add(c2) + else if c3 exists such that c1.someValuesFrom(c3) and c2.someValuesFrom(c3): + c1.superclasses.add(c2) + if c3 exists such that c1.allValuesFrom(c3) and c2.allValuesFrom(c3): + c2.superclasses.add(c1) + + # Repeatedly apply rules that operate on the subclass graph until complete: + do: + # scm-sco: subclass relationship is transitive + compute owl:subClassOf hierarchy # same BFS algorithm as owl:subPropertyOf + + # scm-svf1, scm-avf1: restrictions on subclasses + for p in properties: + for r1, r2 in property restrictions on p: + if c1, c2 exist such that c1.superclasses contains c2: + AND ( (r1.someValuesFrom(c1) AND r2.someValuesFrom(c2)) + OR (r1.someValuesFrom(c1) AND r2.someValuesFrom(c2)) ): + r1.superclasses.add(r2) + until no new information generated + + # Once all subclass and subproperty connections have been made, use them to + # apply domain and range inheritance (scm-dom1, scm-dom2, scm-rng1, scm-rng2): + for p in properties: + for parent in p.superproperties: + p.domain <- p.domain UNION parent.domain + p.range <- p.range UNION parent.range + for c in p.domain: + p.domain <- p.domain UNION c.superclasses + for c in p.range: + p.range <- p.range UNION c.superclasses -# Rule scm-spo states that owl:subPropertyOf relationships are -transitive. - -# For each property, do a BFS to find all its superproperties. - -for p in properties: - -frontier <- { sp | sp in p.superproperties } - -while frontier is not empty: - -ancestors <- UNION{ f.superproperties | f in frontier } - -p.superproperties <- p.superproperties UNION ancestors - -frontier <- UNION{ a.superproperties | a in ancestors } - -p.superproperties - -# Rules scm-hv, scm-svf2, and scm-avf2 use sub/superproperty -information to - -# infer sub/superclass relations between property restrictions: - -for c1, c2 in classes: - -# Rules apply if c1 is a restriction on a subproperty and c2 is a -restriction on a superproperty. - -if c1 is property restriction AND c2 is property restriction AND -c1.onProperty.superproperties contains c2.onProperty: - -if v exists such that c1.hasValue(v) and c2.hasValue(v): - -c1.superclasses.add(c2) - -else if c3 exists such that c1.someValuesFrom(c3) and -c2.someValuesFrom(c3): - -c1.superclasses.add(c2) - -if c3 exists such that c1.allValuesFrom(c3) and -c2.allValuesFrom(c3): - -c2.superclasses.add(c1) - -# Repeatedly apply rules that operate on the subclass graph until -complete: - -do: - -# scm-sco: subclass relationship is transitive - -compute owl:subClassOf hierarchy # same BFS algorithm as -owl:subPropertyOf - -# scm-svf1, scm-avf1: restrictions on subclasses - -for p in properties: - -for r1, r2 in property restrictions on p: - -if c1, c2 exist such that c1.superclasses contains c2: - -AND ( (r1.someValuesFrom(c1) AND r2.someValuesFrom(c2)) - -OR (r1.someValuesFrom(c1) AND r2.someValuesFrom(c2)) ): - -r1.superclasses.add(r2) - -until no new information generated - -# Once all subclass and subproperty connections have been made, use -them to - -# apply domain and range inheritance (scm-dom1, scm-dom2, scm-rng1, -scm-rng2): - -for p in properties: - -for parent in p.superproperties: - -p.domain <- p.domain UNION parent.domain - -p.range <- p.range UNION parent.range - -for c in p.domain: - -p.domain <- p.domain UNION c.superclasses - -for c in p.range: - -p.range <- p.range UNION c.superclasses ``` #### Forward Chaining Instance Reasoning -The ForwardChain MapReduce job scans through both the input triples and +The `ForwardChain` `MapReduce` job scans through both the input triples and any triples that have been derived in any previous iterations, maps triples according to their subject or object (depending on which might be able to use them for reasoning: can be both or neither, resulting in @@ -1396,222 +1095,129 @@ be able to use them for reasoning: can be both or neither, resulting in resources, and saves all generated triples and inconsistencies to an intermediate output directory. -Main ForwardChain logic: - -**ForwardChain(i): # MapReduce job, iteration i** - -**Setup: distribute schema file to each node** - -**Map(triples): # from input or previous job** - -**schema <- load from file** - -**for fact in facts:** - -**(s p o) <- fact.triple** - -**relevance <- getRelevance(fact.triple, schema)** - -**if relevance.relevantToSubject == true:** - -**output(s, fact)** - -**if relevance.relevantToObject == true:** - -**output(o, fact)** - -**Group (node, fact) according to node;** - -**Sort such that incoming edges (with respect to node) come first:** - -**(c, (a b c)) < (c, (c d e))** - -**Reduce(node, facts): # from map output** - -**schema <- load from file** - -**reasoner <- new LocalReasoner(node, i)** - -**for fact in facts:** - -**reasoner.processFact(fact)** - -**output(node.collectOutput)** - +Main `ForwardChain` logic: +``` +ForwardChain(i): # MapReduce job, iteration i + Setup: distribute schema file to each node + + Map(triples): # from input or previous job + schema <- load from file + for fact in facts: + (s p o) <- fact.triple + relevance <- getRelevance(fact.triple, schema) + if relevance.relevantToSubject == true: + output(s, fact) + if relevance.relevantToObject == true: + output(o, fact) + + Group (node, fact) according to node; + Sort such that incoming edges (with respect to node) come first: + (c, (a b c)) < (c, (c d e)) + + Reduce(node, facts): # from map output + schema <- load from file + reasoner <- new LocalReasoner(node, i) + for fact in facts: + reasoner.processFact(fact) + output(node.collectOutput) +``` Where a fact consists of a triple, iteration generated (if any), and sources (if any), and where: - -**getRelevance(triple, schema):** - -**# Determine relevance according to rules and their implementations in -the reasoner** - -**(s p o) <- triple** - -**if p is rdf:type:** - -**# If this triple is a schema-relevant type assignment** - -**if o in schema.classes:** - -**relevantToSubject <- true** - -**if p in schema.properties:** - -**if schema.properties[p].domain is not empty:** - -**relevantToSubject = true** - -**if schema.properties[p].range is not empty:** - -**relevantToObject = true** - -**if schema.properties[p].isTransitive:** - -**relevantToSubject = true** - -**relevantToObject = true** - -**... # Check for all implemented rules** - +``` +getRelevance(triple, schema): + # Determine relevance according to rules and their implementations in the reasoner + (s p o) <- triple + if p is rdf:type: + # If this triple is a schema-relevant type assignment + if o in schema.classes: + relevantToSubject <- true + if p in schema.properties: + if schema.properties[p].domain is not empty: + relevantToSubject = true + if schema.properties[p].range is not empty: + relevantToObject = true + if schema.properties[p].isTransitive: + relevantToSubject = true + relevantToObject = true + ... # Check for all implemented rules +``` Some examples of LocalReasoner rule application: -**processFact(fact):** - -**(s p o) <- fact.triple** - -**newFacts = {}** - -**if reasoner.node == o:** - -**# Limit recursion on self-referential edges** - -**unless s==o AND fact.iteration == reasoner.currentIteration:** - -**newFacts <- newFacts UNION processIncomingEdge(fact)** - -**if reasoner.node == s:** - -**if p is rdf:type:** - -**newFacts <- newFacts UNION processType(fact)** - -**else:** - -**newFacts <- newFacts UNION processOutgoingEdge(fact)** - -**results = {}** - -**for newFact in newFacts:** - -**results <- results UNION processFact(newFact)** - -**return results** - -**processIncomingEdge(fact):** - -**(s p o) <- fact.triple** - -**# Range rule** - -**for c in schema.properties[p].range:** - -**generate fact ((reasoner.node rdf:type c), source:fact, -iteration:reasoner.currentIteration)** - -**# Inverse property rule** - -**for p2 in schema.properties[p].inverseOf:** - -**generate fact ((reasoner.node p2 s), source:fact, -iteration:reasoner.currentIteration)** - -**# Symmetric property rule** - -**if schema.properties[p].isSymmetric:** - -**generate fact ((reasoner.node p s), source:fact, -iteration:reasoner.currentIteration)** - -**# Irreflexive property rule** - -**if schema.properties[p].isIrreflexive:** - -**if s == o:** - -**generate inconsistency (source:fact, -iteration:reasoner.currentIteration)** - -**# Transitive property rule (part 1/2)** - -**if schema.properties[p].isTransitive:** - -**if fact.distance() >= 2^(reasoner.currentIteration-1): # smart -transitive closure** - -**reasoner.transitiveEdges.put(p, fact)** - -**... # Apply each supported rule applying to incoming edges** - -**processOutgoingEdge(fact):** - -**(s p o) <- fact.triple** - -**# Domain rule** - -**for c in schema.properties[p].domain:** - -**generate fact ((reasoner.node rdf:type c), source:fact, -iteration:reasoner.currentIteration)** - -**# Transitive property rule (part 2/2)** - -**if schema.properties[p].isTransitive:** - -**for leftFact in reasoner.transitiveEdges[p]:** - -**(s0 p node) <- leftFact** - -**generate fact ((s0 p o), source:{fact, leftFact}, -iteration:reasoner.currentIteration)** - -**... # Apply each supported rule applying to outgoing edges** - -**processType(fact):** - -**(reasoner.node rdf:type c) <- fact.triple** - -**reasoner.typeFacts.put(c, fact)** - -**# Disjoint class rule** - -**for c2 in schema.classes[c].disjointclasses:** - -**if reasoner.typeFacts contains c2:** - -**generate inconsistency (source:{fact, reasoner.typeFacts[c]}, -iteration:reasoner.currentIteration)** - -**# Subclass rule** - -**for parent in schema.classes[c].superclasses:** - -**processType(fact((reasoner.node rdf:type parent), source:fact, -iteration:reasoner.currentIteration))** - -**... # Apply each supported rule applying to types** +``` +processFact(fact): + (s p o) <- fact.triple + newFacts = {} + if reasoner.node == o: + # Limit recursion on self-referential edges + unless s==o AND fact.iteration == reasoner.currentIteration: + newFacts <- newFacts UNION processIncomingEdge(fact) + if reasoner.node == s: + if p is rdf:type: + newFacts <- newFacts UNION processType(fact) + else: + newFacts <- newFacts UNION processOutgoingEdge(fact) + results = {} + for newFact in newFacts: + results <- results UNION processFact(newFact) + return results + +processIncomingEdge(fact): + (s p o) <- fact.triple + # Range rule + for c in schema.properties[p].range: + generate fact ((reasoner.node rdf:type c), source:fact, iteration:reasoner.currentIteration) + # Inverse property rule + for p2 in schema.properties[p].inverseOf: + generate fact ((reasoner.node p2 s), source:fact, iteration:reasoner.currentIteration) + # Symmetric property rule + if schema.properties[p].isSymmetric: + generate fact ((reasoner.node p s), source:fact, iteration:reasoner.currentIteration) + # Irreflexive property rule + if schema.properties[p].isIrreflexive: + if s == o: + generate inconsistency (source:fact, iteration:reasoner.currentIteration) + # Transitive property rule (part 1/2) + if schema.properties[p].isTransitive: + if fact.distance() >= 2^(reasoner.currentIteration-1): # smart transitive closure + reasoner.transitiveEdges.put(p, fact) + ... # Apply each supported rule applying to incoming edges + +processOutgoingEdge(fact): + (s p o) <- fact.triple + # Domain rule + for c in schema.properties[p].domain: + generate fact ((reasoner.node rdf:type c), source:fact, iteration:reasoner.currentIteration) + # Transitive property rule (part 2/2) + if schema.properties[p].isTransitive: + for leftFact in reasoner.transitiveEdges[p]: + (s0 p node) <- leftFact + generate fact ((s0 p o), source:{fact, leftFact}, iteration:reasoner.currentIteration) + ... # Apply each supported rule applying to outgoing edges + +processType(fact): + (reasoner.node rdf:type c) <- fact.triple + reasoner.typeFacts.put(c, fact) + # Disjoint class rule + for c2 in schema.classes[c].disjointclasses: + if reasoner.typeFacts contains c2: + generate inconsistency (source:{fact, reasoner.typeFacts[c]}, iteration:reasoner.currentIteration) + # Subclass rule + for parent in schema.classes[c].superclasses: + processType(fact((reasoner.node rdf:type parent), source:fact, iteration:reasoner.currentIteration)) + ... # Apply each supported rule applying to types +``` ### Classes -The main reasoning logic is located in **mvm.rya.reasoning**, while +The main reasoning logic is located in `org.apache.rya.reasoning`, while MapReduce tools and utilities for interacting with Accumulo are located -in **mvm.rya.reasoning.mr**. Reasoning logic makes use of RDF constructs -in the **org.openrdf.model** API, in particular: Statement, URI, +in `org.apache.rya.reasoning.mr`. Reasoning logic makes use of RDF constructs +in the `org.openrdf.model` API, in particular: Statement, URI, Resource, and Value. -#### mvm.rya.reasoning +#### org.apache.rya.reasoning - - **OWL2**: In general, the Sesame/OpenRDF[2] Application + - **OWL2**: In general, [the Sesame/OpenRDF/RDF4J[2]](http://archive.rdf4j.org) Application Programming Interface (API) is used to represent RDF constructs and refer to the RDF, RDFS (Resource Description Framework Schema), and OWL vocabularies. However, the API only covers OWL 1 constructs. The @@ -1625,62 +1231,62 @@ Resource, and Value. relationships. The Schema object maps URIs to instances of OwlProperty and Resources to instances of OwlClass. -> The schema is built one triple at a time: the Schema takes in a single -> triple, determines what kind of schema information it represents, and -> instantiates and/or connects class and/or property objects -> accordingly. Some RL rules are applied during this process. An example -> of this is the rule that an equivalentClass relationship is equivalent -> to mutual subClassOf relationships. The remainder of the RL schema -> rules (those that require traversing multiple levels of the schema -> graph) must be applied explicitly. Invoke the closure() method after -> all triples are incorporated to fully apply these rules (e.g., the -> rules that infer a property's domain based on the domains of its -> superproperties and/or the superclasses of its domains). -> -> Once the schema has been constructed, it can be used in reasoning. The -> Schema object serves as a repository for OwlClass and OwlProperty -> objects, accessed by their URIs (or Resources), which in turn contain -> detailed schema information about themselves. -> -> The Schema class also provides static method isSchemaTriple for -> checking whether a triple contains schema-relevant information at all. - - - An **OwlProperty** or **OwlClass** represents a property or a class, + The schema is built one triple at a time: the Schema takes in a single + triple, determines what kind of schema information it represents, and + instantiates and/or connects class and/or property objects + accordingly. Some RL rules are applied during this process. An example + of this is the rule that an equivalentClass relationship is equivalent + to mutual subClassOf relationships. The remainder of the RL schema + rules (those that require traversing multiple levels of the schema + graph) must be applied explicitly. Invoke the closure() method after + all triples are incorporated to fully apply these rules (e.g., the + rules that infer a property's domain based on the domains of its + superproperties and/or the superclasses of its domains). + + Once the schema has been constructed, it can be used in reasoning. The + Schema object serves as a repository for OwlClass and OwlProperty + objects, accessed by their URIs (or Resources), which in turn contain + detailed schema information about themselves. + + The Schema class also provides static method isSchemaTriple for + checking whether a triple contains schema-relevant information at all. + + - An `OwlProperty` or `OwlClass` represents a property or a class, respectively. Each object holds a reference to the RDF entity that identifies it (using to the openrdf api): a URI for each OwlProperty, and a Resource for each class (because a class is more general, it can be a URI or a bnode). -> Both maintain connections to other schema constructs, according to the -> relevant schema connections that can be made for each type. In -> general, these correspond to outgoing edges with the appropriate -> schema triple. Internally, they are represented as sets of those -> entities they are connected to, one set for each type of relationship. -> For example, each OwlClass contains a set of its superclasses. These -> are accessed via getter methods that return sets of URIs or Resources, -> as appropriate. Both also apply some RL schema rules. Some rules are -> applied by the getter methods (for example, every class being its own -> subclass), while others must be explicitly invoked by the Schema (for -> example, determining that one property restriction is a subclass of -> another based on the relationships between their corresponding -> properties and classes). -> -> OwlProperty also includes boolean fields corresponding to whether the -> property is transitive, symmetric, asymmetric, etc. (All default to -> false.) An OwlProperty also contains a set of references to any -> property restrictions that apply to it. -> -> In addition to common connections to other classes like superclasses -> and disjoint classes, OwlClass instances can also represent property -> restriction information. This is represented in the same way as the -> other schema connections: a set of classes corresponding to any -> allValuesFrom triples, a set of classes corresponding to any -> someValuesFrom triples, etc. Typically, only one such connection -> should exist (plus one onProperty relationship to specify what -> property this restriction applies to), but this representation is used -> for the sake of generality. In principle, an RDF graph could contain -> two such triples, though the semantic implications are not -> well-defined. + Both maintain connections to other schema constructs, according to the + relevant schema connections that can be made for each type. In + general, these correspond to outgoing edges with the appropriate + schema triple. Internally, they are represented as sets of those + entities they are connected to, one set for each type of relationship. + For example, each OwlClass contains a set of its superclasses. These + are accessed via getter methods that return sets of URIs or Resources, + as appropriate. Both also apply some RL schema rules. Some rules are + applied by the getter methods (for example, every class being its own + subclass), while others must be explicitly invoked by the Schema (for + example, determining that one property restriction is a subclass of + another based on the relationships between their corresponding + properties and classes). + + OwlProperty also includes boolean fields corresponding to whether the + property is transitive, symmetric, asymmetric, etc. (All default to + false.) An OwlProperty also contains a set of references to any + property restrictions that apply to it. + + In addition to common connections to other classes like superclasses + and disjoint classes, OwlClass instances can also represent property + restriction information. This is represented in the same way as the + other schema connections: a set of classes corresponding to any + allValuesFrom triples, a set of classes corresponding to any + someValuesFrom triples, etc. Typically, only one such connection + should exist (plus one onProperty relationship to specify what + property this restriction applies to), but this representation is used + for the sake of generality. In principle, an RDF graph could contain + two such triples, though the semantic implications are not + well-defined. - **Fact** and **Derivation**: A Fact represents a piece of information, typically a triple, that either was contained in the @@ -1725,20 +1331,20 @@ Resource, and Value. the property is transitive, it will be relevant to both subject and object. -> When the reasoner receives a triple, it determines the direction of -> the edge, checks it against the appropriate set of RL rules, applies -> those rules if necessary, and recursively processes any facts newly -> generated. The LocalReasoner will perform as much inference as it can -> in its neighborhood, but many triples may ultimately need to be passed -> to reasoners for other nodes in order to produce all implied -> information. -> -> Most RL rules are handled in the LocalReasoner object itself, but it -> also contains a TypeReasoner specifically responsible for those rules -> involving the central node's types. Unlike other facts, the type facts -> aren't collected automatically and must be explicitly gathered with a -> call to collectTypes. (Collecting type assignments one at a time would -> lead to a great deal of redundancy.) + When the reasoner receives a triple, it determines the direction of + the edge, checks it against the appropriate set of RL rules, applies + those rules if necessary, and recursively processes any facts newly + generated. The LocalReasoner will perform as much inference as it can + in its neighborhood, but many triples may ultimately need to be passed + to reasoners for other nodes in order to produce all implied + information. + + Most RL rules are handled in the LocalReasoner object itself, but it + also contains a TypeReasoner specifically responsible for those rules + involving the central node's types. Unlike other facts, the type facts + aren't collected automatically and must be explicitly gathered with a + call to collectTypes. (Collecting type assignments one at a time would + lead to a great deal of redundancy.) - **TypeReasoner**: Conducts reasoning having to do with a single node's types. Several rules involve types, so they are all handled @@ -1751,12 +1357,12 @@ Resource, and Value. domain rule will generate the same type assignment for every outgoing edge with the associated predicate. -> Also has the ability to hold information that should be asserted if -> and only if the node turns out to be a particular type, and return -> that information if/when that happens. This is particularly useful for -> property restriction rules. + Also has the ability to hold information that should be asserted if + and only if the node turns out to be a particular type, and return + that information if/when that happens. This is particularly useful for + property restriction rules. -#### mvm.rya.reasoning.mr +#### org.apache.rya.reasoning.mr Contains MapReduce tools and utilities for interacting with HDFS, Accumulo, and Rya tables in Accumulo. @@ -1779,15 +1385,15 @@ otherwise. The individual MapReduce jobs each have different mappers for different kinds of input: Accumulo (main source of initial input, takes in -**** ), RDF file (alternative initial -input source, takes in **** ), and -HDFS sequence file (intermediate output/input, takes in **** or **** ). +`` ), +RDF file (alternative initial +input source, takes in` ` ), and +HDFS sequence file (intermediate output/input, takes in +`` or `` ). - **ReasoningDriver**: Main driver class for reasoning application. - Executes high-level algorithm by running **SchemaFilter**, - **ForwardChain**, **DuplicateElimination**, and **OutputTool**. + Executes high-level algorithm by running `SchemaFilter`, + `ForwardChain`, `DuplicateElimination`, and `OutputTool`. - **MRReasoningUtils**: Defines configuration parameters and contains static methods for configuring input and output (using those @@ -1797,8 +1403,8 @@ NullWritable>** or **** ). - **ResourceWritable**: WritableComparable wrapper for org.openrdf.model.Resource, so it can be used as a key/value in MapReduce tasks. Also contains an integer field to enable arbitrary - secondary sort. Provides static classes **PrimaryComparator** to use - the Resource alone, and **SecondaryComparator** to use resource + secondary sort. Provides static classes `PrimaryComparator` to use + the Resource alone, and `SecondaryComparator` to use resource followed by key. - **SchemaWritable**: Writable subclass of Schema, allowing the schema @@ -1818,20 +1424,19 @@ NullWritable>** or **** ). - Mappers call Schema.isSchemaTriple on each input triple, and output those triples found to contain schema information. One - mapper is defined for each input source: **SchemaTableMapper**, - **SchemaRdfMapper**, and **SchemaFileMapper**. + mapper is defined for each input source: `SchemaTableMapper`, + `SchemaRdfMapper`, and `SchemaFileMapper`. - - The reducer **SchemaFilterReducer** receives all schema-relevant + - The reducer `SchemaFilterReducer` receives all schema-relevant triples, feeds them one at a time into a Schema object, invokes Schema.closure() to do final schema reasoning, and writes that one Schema object to file output. - - Mapper inputs: **** , **** , **** + - Mapper inputs: `` , `` , `` - - Mapper output/reducer input: ** ** + - Mapper output/reducer input: ` ` - - Reducer output: **** + - Reducer output: `` - **ForwardChain:** MapReduce job responsible for actual forward-chaining reasoning. @@ -1841,8 +1446,8 @@ NullWritable>** or **** ). Fact as the value and the subject and/or object as the key, depending on the relevance. (For each triple, the mapper can produce zero, one, or two outputs.) Defined by generic class - **ForwardChainMapper** and its subclasses for different inputs: - **TableMapper**, **RdfMapper**, and **FileMapper**. + `ForwardChainMapper` and its subclasses for different inputs: + `TableMapper`, `RdfMapper`, and `FileMapper`. - Reducers receive a node, instantiate a LocalReasoner for that node, and feed a stream of facts/edges involving that node to @@ -1851,34 +1456,32 @@ NullWritable>** or **** ). collects any newly generated facts or inconsistencies and outputs them. After reading all triples, the reducer calls LocalReasoner.getTypes to get any more new facts involving - types. Defined by **ReasoningReducer**. + types. Defined by `ReasoningReducer`. - - Mapper inputs: **** , **** , **** + - Mapper inputs: `` , `` , `` - - Mapper output/reducer input: ** ** + - Mapper output/reducer input: ` ` - - Reducer outputs: **** , **** + - Reducer outputs: `` , `` - **DuplicateElimination**: MapReduce job responsible for eliminating redundant information. This includes redundant inconsistencies, so unlike ForwardChain, this job needs to take inconsistencies (Derivations) as input. - - Map phase (generic class **DuplicateEliminationMapper** with + - Map phase (generic class `DuplicateEliminationMapper` with subclasses for different inputs): Output Facts and their - Derivations. For input triples (**DuplicateTableMapper** and - **DuplicateRdfMapper**), wrap the triple in a Fact and output an + Derivations. For input triples (`DuplicateTableMapper` and + `DuplicateRdfMapper`), wrap the triple in a Fact and output an empty Derivation. For intermediate triples - (**DuplicateFileMapper**), split the Fact and Derivation, + (`DuplicateFileMapper`), split the Fact and Derivation, sending the Fact with no Derivation as the key, and the Derivation itself as the value. For inconsistencies - (**InconsistencyMapper**), wrap the Derivation in an empty Fact + (`InconsistencyMapper`), wrap the Derivation in an empty Fact (a Fact with no triple), use that as a key, and use the Derivation itself as the value. - - Reducers (**DuplicateEliminationReducer**) receive a Fact and a + - Reducers (`DuplicateEliminationReducer`) receive a Fact and a sequence of possible Derivations. If any of the Derivations are empty, it's an input fact and shouldn't be output at all (because it is already known). Otherwise, output the Fact plus @@ -1887,14 +1490,11 @@ NullWritable>** or **** ). except that the output should be the Derivation alone, as an inconsistency. - - Mapper inputs: **** , **** , <**Fact, NullWritable>** , - **** + - Mapper inputs: `` , `` , <`Fact, NullWritable>` , `` - - Mapper output/reducer input: ** ** + - Mapper output/reducer input: ` ` - - Reducer outputs: **** , **** + - Reducer outputs: `` , `` - **OutputTool**: MapReduce job that collects output from all previous iterations in one place. @@ -1909,12 +1509,11 @@ NullWritable>** or **** ). representations for each. The reducer then writes those String representations to two different output files. - - Mapper inputs: **** , **** + - Mapper inputs: `` , `` - - Mapper output/reducer input: ** ** + - Mapper output/reducer input: ` ` - - Reducer output: **** + - Reducer output: `` - **RunStatistics**: Simple tool to collect statistics from each job executed during the run. Uses Hadoop counters to get the number of diff --git a/extras/rya.manual/src/site/markdown/indexing.md b/extras/rya.manual/src/site/markdown/indexing.md index 3faf98a13..9c2027cb9 100644 --- a/extras/rya.manual/src/site/markdown/indexing.md +++ b/extras/rya.manual/src/site/markdown/indexing.md @@ -24,7 +24,7 @@ This section covers the optional indexes implemented in Apache Accumulo and Mong ## Overview of indexing -The non-optional core indexes are SPO, POS, OSP. These quickly find statements where any combination of one, two, or three of the subject, predicate, and object are known. ALso, since each of these core indexes contain the entire statement and context graph for all statements, each index acts as the repository for the entire RDF store. +The non-optional core indexes are SPO, POS, OSP. These quickly find statements where any combination of one, two, or three of the subject, predicate, and object are known. Also, since each of these core indexes contain the entire statement and context graph for all statements, each index acts as the repository for the entire RDF store. Apache Rya has a variety of optional indexes that can be enabled. @@ -33,24 +33,21 @@ Other indexes find groups of related statements as in the Geo-temporal, smarturi The following is a list of the index projects. All are under the extras folder since they are optional: rya/extras/ -indexing -- the following are grouped as one project: +The following sections cover indexes: + + - Entity + - Freetext + - Temporal + - Reasoning + - Backward Chaining + - Forward Chaining + - Geo + - rya.geoindexing/geo.common + - rya.geoindexing/geo.geomesa + - rya.geoindexing/geo.geowave + - rya.geoindexing/geo.mongo + - shell - -``` -entity -freetext -temporal -yes rya.forwardchain -Yes -rya.geoindexing/geo.common -rya.geoindexing/geo.geomesa -rya.geoindexing/geo.geowave -rya.geoindexing/geo.mongo - -No rya.giraph - -shell -``` ### Enabling Indexing Each section will describe the how to enable its index. There are two install methods for Rya that impact how indexes are configured: - legacy - on the fly configuration @@ -59,7 +56,7 @@ Each section will describe the how to enable its index. There are two install m #### Legacy - on the fly configuration -The legacy method is not recommended for new Rya installations. It relies on the Rya driver code loading a consistent set of configuration data in memory from an XML file or setter methods. Most Rya features will detect missing persistent storage components and lazily create them. This includes the core indexes and any optional indexes that are enabled in the configuration. For example if one starts by connecting to new Accumulo installation with no tables, Rya will create the SPO, POS and OSP tables when they are needed. This is not the recommended because the configuration of the Rya application code can get out of sync with the backing database causing serialization errors and incomplete indexes. For example, if one run of Rya uses and maintains a Geo index, and a second run connects to the same backing store, and disables Geo indexing, the index will be missing any new statement insertions made in the second run. +The legacy method is not recommended for new Rya installations. It relies on the Rya driver code loading a consistent set of configuration data in memory from an XML file or setter methods. Most Rya features will detect missing persistent storage components and lazily create them. This includes the core indexes and any optional indexes that are enabled in the configuration. For example if one starts by connecting to new Accumulo installation with no tables, Rya will create the SPO, POS and OSP tables when they are needed. This is not recommended because the configuration of the Rya application code can get out of sync with the backing database causing serialization errors and incomplete indexes. For example, if one run of Rya uses and maintains a Geo index, and a second run connects to the same backing store, and disables Geo indexing, the index will be missing any new statement insertions made in the second run. #### Installer - persisted RyaDetails @@ -87,7 +84,7 @@ The following are found in: extras/indexingExample/src/main/java ### 2. Index: Temporal The temporal index quickly locates dates and times (instants) or ranges of contiguous instants (intervals) using an expression using temporal relations. -#### Enable and Options +#### Configuration Temporal indexing is enabled in the installer configuration builder by setting method `setUseAccumuloTemporalIndex()` to true. For example: ```java @@ -142,7 +139,7 @@ WHERE { FILTER( tempo:after(?time, '2001-01-01T01:01:03-08:00') ) . } ``` -#### Temporal Architecture Accumulo +#### Temporal Implementation Accumulo Temporal under Accumulo is maintained in a single table. Each statement that is indexed has four entries in the temporal table: O, SPO, SO and PO. Where O is the object that is always a datetime, S is subject, and P is predicate. Row Keys are in these two forms. Brackets denotes optional: [x] denotes x is optional: 1. constraintPrefix datetime @@ -160,14 +157,14 @@ It will Log a warning if the object is not parse-able. It attempts to parse with OpenRdf's Literal.calendarValue() . If that fails, tries: org.joda.time.DateTime.parse() . -#### Temporal Architecture MongoDB +#### Temporal Implementation MongoDB MongoDB uses its native indexing on the core collection. ### 3. Index: Entity Improves queries on **Entities**, also known as "star" queries, using the Accumulo's DocumentIndexIntersectingIterator. -#### Entity Enable and Options +#### Entity Configuration Entity indexing is enabled in the installer configuration builder by setting method `setUseAccumuloEntityIndex()` to true. For example: ```java From 77b148208751f6f4d4b59a8828600a9f6d2eccc3 Mon Sep 17 00:00:00 2001 From: "David W. Lotts" Date: Fri, 23 Mar 2018 17:41:07 -0400 Subject: [PATCH 5/7] Manual update, todo still break indexes into separate files. Complete Geo docs. --- .../src/site/images/GeoAreasDecomposed.png | Bin 0 -> 11467 bytes .../rya.manual/src/site/images/GeoHash1.png | Bin 0 -> 23720 bytes .../rya.manual/src/site/images/GeoHash2.png | Bin 0 -> 17632 bytes .../src/site/images/GeoNearSimilarGeoHash.png | Bin 0 -> 52277 bytes .../src/site/images/GeoSpaceFillingCurve.png | Bin 0 -> 4045 bytes .../rya.manual/src/site/markdown/indexing.md | 120 ++++++++++++++++++ 6 files changed, 120 insertions(+) create mode 100644 extras/rya.manual/src/site/images/GeoAreasDecomposed.png create mode 100644 extras/rya.manual/src/site/images/GeoHash1.png create mode 100644 extras/rya.manual/src/site/images/GeoHash2.png create mode 100644 extras/rya.manual/src/site/images/GeoNearSimilarGeoHash.png create mode 100644 extras/rya.manual/src/site/images/GeoSpaceFillingCurve.png diff --git a/extras/rya.manual/src/site/images/GeoAreasDecomposed.png b/extras/rya.manual/src/site/images/GeoAreasDecomposed.png new file mode 100644 index 0000000000000000000000000000000000000000..b283427a54e6afb21c23c80051f28838ff2a5f27 GIT binary patch literal 11467 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 F{|B6#3d;Zh literal 0 HcmV?d00001 diff --git a/extras/rya.manual/src/site/images/GeoHash1.png b/extras/rya.manual/src/site/images/GeoHash1.png new file mode 100644 index 0000000000000000000000000000000000000000..989ec22d563d468f7e1ec8fe73f7d1ff16c553ee GIT binary patch literal 23720 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 literal 0 HcmV?d00001 diff --git a/extras/rya.manual/src/site/images/GeoHash2.png b/extras/rya.manual/src/site/images/GeoHash2.png new file mode 100644 index 0000000000000000000000000000000000000000..28f50c2d8964d42982f6e05ce76fe297ae0a0129 GIT binary patch literal 17632 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 literal 0 HcmV?d00001 diff --git a/extras/rya.manual/src/site/markdown/indexing.md b/extras/rya.manual/src/site/markdown/indexing.md index 9c2027cb9..2af0079fe 100644 --- a/extras/rya.manual/src/site/markdown/indexing.md +++ b/extras/rya.manual/src/site/markdown/indexing.md @@ -443,6 +443,126 @@ The search expression: "\"ssl certificate\" authority key" searches for the phrase "ssl certificate" and ("authority" or "key" or "ssl" or "certificate" ). + +### Index: Geo +This index allows optimized searching for objects that are geo literals, Well Known Text (WKT) and Geospatial Markup Language (GML). + +There are three immplementations of geo indexing in Rya: + + - rya.geoindexing/geo.geomesa + + Accumulo only using the [GeoMesa library](http://www.geomesa.org/)[3] + + - rya.geoindexing/geo.geowave + + Accumulo only using the [GeoWave library](https://locationtech.github.io/geowave/)[4] + + - rya.geoindexing/geo.mongo + + MongoDB only, using MongoDB's native support for geo. + +[3] http://www.geomesa.org/ + +[4] https://locationtech.github.io/geowave/ + +To enable any of the Geo indexes, there three things you must do: +- build Rya with the geo profile +- use two replacement classes +- enable specific geo implementation in ConfigUtils + +#### Building with the Geo profile +Any of the Geo implementations must be built since they use an incompatible license and so can not be distributed as part of an Apache project. Use the profile '''geoindexing''' in the maven command: + + mvn clean install -P geoindexing + +This will set up the correct dependencies and build the geo jar. + +#### Geo Sail and Optional Config Utils +The following two classes should be substituted when using a Geo indexe: + + - ```RyaSailFactory``` replace with: ```GeoRyaSailFactory``` + - ```ConfigUtils``` replace with: ```org.apache.rya.indexing.accumulo.geo.OptionalConfigUtils``` + +#### Geo Enable and Options + +Geo indexing is enabled in the installer configuration builder by setting +method `setUse Index()` to true. For example: + +```java +AccumuloIndexingConfiguration.builder().setUseAccumuloTemporalIndex(true); +``` +When using Rya with the shell command: install-with-parameters, or in legacy (not recommended) set the configuration key: + + +```java +OptionalConfigUtils.USE_GEO = "sc.use_geo" +``` + + +To Limit the indexing of inserted statements particular predicates, set the following configuration key to a list of predicate URI's. + +```java +OptionalConfigUtils.GEO_PREDICATES_LIST = "sc.geo.predicates" +``` + +##### Geo Option: ??? + +#### Geo Usage +##### Query Language +Geospatial “Simple Functions”: +Within, Equals, Disjoint, Intersects, Touches, Crosses, Contains, Overlaps +Well-know Text Representation: +Point, Linestring, Polygon  + +SPARQL Integration +GeoSPARQL for storage and query + +Data View +Workhorse: GeoMesa by CCRI +Open Source Spatio-Temporal Indexing layer +GeoTools interface, Accumulo storage +Model as RDF “Feature Types” with SPOC “Attributes” +Single table, server side iterators + +##### GeoMesa Data Model +OpenGIS spatial features model +Features: points, lines, polygons +E.g.: Fire hydrant, river, political boundaries +Attributes of a feature: Strings, numbers, etc +E.g.: color, river flow rate, political state name +GeoTools provides the DataStore interfaces and tools +RDF “Feature Type” with SPOC “Attributes” + +Submit Queries using GeoTools Filters +Language: Extended Common Query Language (ECQL) +API: Filter Factory + +DataStore Implementation: Accumulo Data Layer +Single partitioned table +Server side iterators +Space Filling curve index +Geohashed spatial components + +#### Geo Architecture +Space Filling Curve and GeoHashing + +Interleave bits from Latitude and Longitude into Geohash string. + +![hash the lat long](../images/GeoHash1.png "From Lat,Long to geohash string") +![hash the lat long](../images/GeoHash2.png "Lat,Long to 6 bit geohash string") + +The sequence of all strings form single dimensional curve that corresponds to the planet's surface. + +![hash the lat long](../images/GeoSpaceFillingCurve.png "Lat,Long to 6 bit geohash string") + +Points nearby on surface are likely to be nearby on the curve. + +![hash the lat long](../images/GeoNearSimilarGeoHash.png "nearby places likely have similar geohash") + +Bounded areas can be decomposed into a list of geohash ranges, searched as Accumulo range scans. + +![bounded areas range of geohashes](../images/GeoAreasDecomposed.png "bounded areas described by a list geohash ranges") + =============template============== ### Index: ???? This index allows optimized searching ????. From 007bb1e8505f94078e46054a62e5a5a05340bfc3 Mon Sep 17 00:00:00 2001 From: "David W. Lotts" Date: Mon, 26 Mar 2018 16:27:14 -0400 Subject: [PATCH 6/7] Manual update, still todo: break indexes into separate files. Complete Geo docs. --- .../rya.manual/src/site/markdown/indexing.md | 54 ++++++++++--------- 1 file changed, 30 insertions(+), 24 deletions(-) diff --git a/extras/rya.manual/src/site/markdown/indexing.md b/extras/rya.manual/src/site/markdown/indexing.md index 2af0079fe..c958d6f64 100644 --- a/extras/rya.manual/src/site/markdown/indexing.md +++ b/extras/rya.manual/src/site/markdown/indexing.md @@ -486,56 +486,62 @@ The following two classes should be substituted when using a Geo indexe: #### Geo Enable and Options Geo indexing is enabled in the installer configuration builder by setting -method `setUse Index()` to true. For example: +method `setUse TODO Index()` to true. For example: ```java -AccumuloIndexingConfiguration.builder().setUseAccumuloTemporalIndex(true); +AccumuloIndexingConfiguration.builder().setUse TODO Index(true); ``` -When using Rya with the shell command: install-with-parameters, or in legacy (not recommended) set the configuration key: +When using Rya with the shell command: install-with-parameters, or in legacy (not recommended) set the configuration key: ```java OptionalConfigUtils.USE_GEO = "sc.use_geo" ``` +For Mongo backed Rya, the above ```USE_GEO``` is enough. If you intend to use the Accumulo backed Rya there are two geo implementations: GeoMesa and GeoWave. GeoMesa is the default, so no further configuraton is needed. If you intend to use Accumulo with GeoWave, or you have a custom Geo implementation, set the `geo_indexer_type`: +```java +GEO_INDEXER_TYPE = "sc.geo.geo_indexer_type" +conf.set(OptionalConfigUtils.GEO_INDEXER_TYPE, GeoIndexerType.GEO_MESA.toString()); +``` +The built in geo_indexer_type's supported in Rya are: +```java +// rya/extras/rya.geoindexing/geo.common/src/main/java/org/apache/rya/indexing/GeoIndexerType.java +public enum GeoIndexerType { +GEO_MESA("org.apache.rya.indexing.accumulo.geo.GeoMesaGeoIndexer"), +GEO_WAVE("org.apache.rya.indexing.accumulo.geo.GeoWaveGeoIndexer"), +MONGO_DB("org.apache.rya.indexing.mongodb.geo.MongoGeoIndexer"), +``` -To Limit the indexing of inserted statements particular predicates, set the following configuration key to a list of predicate URI's. +To Limit the indexing of inserted statements with particular predicates, set the following configuration key to a list of predicate URI's. ```java OptionalConfigUtils.GEO_PREDICATES_LIST = "sc.geo.predicates" ``` + ##### Geo Option: ??? #### Geo Usage -##### Query Language -Geospatial “Simple Functions”: +##### Query Language Accumulo GeoMesa +GeoMesa is an Open Source Spatio-Temporal Indexing layer by CCRI. +It uses the GeoTools API, and an Accumulo storage Model. the model stores RDF “Feature Type” with Subject, Predicate, Object, Context as “Attributes” +It stores this in a single table, using server side iterators. + +It supports Geospatial “Simple Functions” in SPARQL filters: Within, Equals, Disjoint, Intersects, Touches, Crosses, Contains, Overlaps Well-know Text Representation: Point, Linestring, Polygon  -SPARQL Integration -GeoSPARQL for storage and query - -Data View -Workhorse: GeoMesa by CCRI -Open Source Spatio-Temporal Indexing layer -GeoTools interface, Accumulo storage -Model as RDF “Feature Types” with SPOC “Attributes” -Single table, server side iterators +The SPARQL Integration uses GeoSPARQL for storage and query. ##### GeoMesa Data Model OpenGIS spatial features model Features: points, lines, polygons -E.g.: Fire hydrant, river, political boundaries -Attributes of a feature: Strings, numbers, etc -E.g.: color, river flow rate, political state name -GeoTools provides the DataStore interfaces and tools -RDF “Feature Type” with SPOC “Attributes” - -Submit Queries using GeoTools Filters -Language: Extended Common Query Language (ECQL) -API: Filter Factory +For example: Fire hydrant, river, political boundaries. + +Features may have Attributes: Strings, numbers, and others. For example: color, river flow rate, political state name. GeoTools provides the DataStore interfaces and tools. + +Submit Queries using GeoTools Filters (API: Filter Factory) translated from SPARQL filters. This is expressed using *Extended Common Query Language (ECQL)* DataStore Implementation: Accumulo Data Layer Single partitioned table From a8fb79e0ffa9f486558eb8395d44605dff304cbd Mon Sep 17 00:00:00 2001 From: "David W. Lotts" Date: Tue, 12 Mar 2019 15:47:21 -0400 Subject: [PATCH 7/7] Manual chapter on various indexes. wiping my local clone for new pc. --- .../rya.manual/src/site/markdown/indexing.md | 50 ++++++++++++------- 1 file changed, 33 insertions(+), 17 deletions(-) diff --git a/extras/rya.manual/src/site/markdown/indexing.md b/extras/rya.manual/src/site/markdown/indexing.md index c958d6f64..fa0bd5f70 100644 --- a/extras/rya.manual/src/site/markdown/indexing.md +++ b/extras/rya.manual/src/site/markdown/indexing.md @@ -485,17 +485,13 @@ The following two classes should be substituted when using a Geo indexe: #### Geo Enable and Options -Geo indexing is enabled in the installer configuration builder by setting -method `setUse TODO Index()` to true. For example: +Geo indexing available in configuration builder. -```java -AccumuloIndexingConfiguration.builder().setUse TODO Index(true); -``` - -When using Rya with the shell command: install-with-parameters, or in legacy (not recommended) set the configuration key: +When using Rya with the shell command: install-with-parameters, or in legacy mode, set the configuration key true: ```java OptionalConfigUtils.USE_GEO = "sc.use_geo" +conf.setBoolean(OptionalConfigUtils.USE_GEO, true); ``` For Mongo backed Rya, the above ```USE_GEO``` is enough. If you intend to use the Accumulo backed Rya there are two geo implementations: GeoMesa and GeoWave. GeoMesa is the default, so no further configuraton is needed. If you intend to use Accumulo with GeoWave, or you have a custom Geo implementation, set the `geo_indexer_type`: @@ -511,6 +507,7 @@ GEO_MESA("org.apache.rya.indexing.accumulo.geo.GeoMesaGeoIndexer"), GEO_WAVE("org.apache.rya.indexing.accumulo.geo.GeoWaveGeoIndexer"), MONGO_DB("org.apache.rya.indexing.mongodb.geo.MongoGeoIndexer"), ``` +##### Geo Option: GEO_PREDICATES_LIST To Limit the indexing of inserted statements with particular predicates, set the following configuration key to a list of predicate URI's. @@ -518,10 +515,7 @@ To Limit the indexing of inserted statements with particular predicates, set the OptionalConfigUtils.GEO_PREDICATES_LIST = "sc.geo.predicates" ``` - -##### Geo Option: ??? - -#### Geo Usage +#### Geo indexing Usage ##### Query Language Accumulo GeoMesa GeoMesa is an Open Source Spatio-Temporal Indexing layer by CCRI. It uses the GeoTools API, and an Accumulo storage Model. the model stores RDF “Feature Type” with Subject, Predicate, Object, Context as “Attributes” @@ -535,11 +529,17 @@ Point, Linestring, Polygon  The SPARQL Integration uses GeoSPARQL for storage and query. ##### GeoMesa Data Model -OpenGIS spatial features model -Features: points, lines, polygons +GeoMesa use the OpenGIS spatial features model. +Geo Features: + - points + - lines + - polygons + For example: Fire hydrant, river, political boundaries. -Features may have Attributes: Strings, numbers, and others. For example: color, river flow rate, political state name. GeoTools provides the DataStore interfaces and tools. +Features may have Attributes: Strings, numbers, and others. For example: color, river flow rate, political state name. + +GeoTools provides the DataStore interfaces and tools. Submit Queries using GeoTools Filters (API: Filter Factory) translated from SPARQL filters. This is expressed using *Extended Common Query Language (ECQL)* @@ -549,10 +549,20 @@ Server side iterators Space Filling curve index Geohashed spatial components -#### Geo Architecture -Space Filling Curve and GeoHashing +##### Query Language Accumulo GeoWave +TODO -Interleave bits from Latitude and Longitude into Geohash string. +##### Query Language Mongo Geo + +TODO + +### Geo index Architecture + +#### GeoMesa Architecture + +Internally, the geo index uses a #Space Filling Curve# and #GeoHashing#. + +Interleave bits from Latitude and Longitude into a Geohash string. ![hash the lat long](../images/GeoHash1.png "From Lat,Long to geohash string") ![hash the lat long](../images/GeoHash2.png "Lat,Long to 6 bit geohash string") @@ -569,6 +579,12 @@ Bounded areas can be decomposed into a list of geohash ranges, searched as Accum ![bounded areas range of geohashes](../images/GeoAreasDecomposed.png "bounded areas described by a list geohash ranges") +#### GeoWave Architecture +TODO + +#### GeoMongo Architecture +TODO + =============template============== ### Index: ???? This index allows optimized searching ????.