Relationships – Project Haystack


OverviewRelationship TypesExampleQuerying RelationshipsTransitive RelationshipsReciprocal Of


Relationships model how entities are related to one another. A relationship tag is applied to ref tag definitions to inform what the ref implies. Entities cross reference each other with ref tags using the id tag as the primary identifier. This design is just like the relational model with primary key (the id tag) and foreign keys (other ref tags). We use the term relationship to denote instance-to-instance relationships versus def-to-def associations.

Relationship Types

Haystack defines two primary types of relationships:

Containment Relationships

Containment relationships are defined by the contains and containedBy tags. Refs that model containment follow a naming convention of {parent}Ref and are applied on the child entity to refer to the parent entity. Refs that implement the containment relationship include siteRef, spaceRef, and equipRef.

Flow Relationships

Flow relationships are defined by the inputs and outputs tags. Refs that model flow follow a naming convention of {substance}Ref and are applied on the inputting entity to refer to the source of the substance. Refs that implement flow relationships include airRef, elecRef, chilledWaterRef, and steamRef.


Lets look at a simple example:

// hot water plant entity
id: @hwp, hot, water, plant, equip

// AHU entity that inputs hot water from the plant above
id: @ahu, ahu, equip, hotWaterHeating, hotWaterRef: @hwp

The plant's primary identifier is modeled by the id tag with a ref value of @hwp. The id tag uniquely identifies this plant within the data set. The second dict models an ahu that has the reference tag hotWaterRef referencing the plant by its id.

The actual definition of hotWaterRef looks like this:

def: ^hotWaterRef
is: ^ref
of: ^hot-water-output
tagOn: ^hot-water-input
inputs: ^hot-water
doc: "Hot water flows from the the referent to this entity"

This definition tells us that the hotWaterRef tag is a ref subtype that should reference a hot-water-output. When applied to an instance, it implies that the entity receives hot-water from the referenced entity.

Querying Relationships


Project Haystack uses filters as its simple declarative query language. The most basic way to query relationships is by tags. The following filter queries AHUs that receive hot water from our plant in the example above:

ahu and hotWaterRef == @hwp

This filter would match any dict that has the ahu tag and the hotWaterRef tag with a value equal to the identifier @hwp.

This technique is also heavily used to match points associated with a given equipment. For example to find the discharge temp sensor for the AHU identified as @ahu would be:

discharge and temp and sensor and point and equipRef == @ahu

We can use the proposed def operator ? to perform more abstract relationship queries. This operator lets us use reflection and subtype inference to construct queries that don't require detailed knowledge of how a system was tagged. Using our plant example above we can query AHUs that receive anything:

ahu and inputs?

This query matches dicts that have the ahu tag and any reflected def with the inputs tag. The ? operator performs indirection to query the tags on the defs. For example:

inputs   // does the dict have a tag named "inputs"
inputs?  // does the dict implement a def with a tag name "inputs"

The rules to map a dict to its implemented defs is specified in the reflection chapter.

We can enhance our query to filter dicts that receive hot water like this:


The term to the right of the first dash is used to match the value of the def's inputs tag. We can read the filter above as follows: does the dict implement a def with the inputs tag where the value that fits hot-water. Subtyping may be used too; any of the following filters would match our AHU also:

inputs-water?   // hot-water is a subtype of water
inputs-liquid?  // hot-water is a subtype of liquid

We can also give the ? operator a value on the right hand side as follows:

inputs-hot-water? @hwp

The filter above matches using the rules discussed with one new requirement: the tag's value must be equal to @hwp. We can break down the steps as follows:

  1. Does the dict implement a def with inputs (it does: hotWaterRef)
  2. Does the value of that tag in the def subtype from hot-water (it does)
  3. Does the value of that tag in the dict equal @hwp (it does)
  4. If all of the above are true, the overall filter is a match

This syntax is fairly simple to use, but provides a lot of flexibility under the hood to integrate the ontology into your queries.

Transitive Relationships

Relationship tags can be marked as transitive. A transitive relationship means that if the relationship applies between A and B and also between B and C, then it is inferred to apply between A and C. Lets look at an example to illustrate:

id: @ahu, ahu, equip
id: @fan, discharge, fan, equip, equipRef=@ahu
id: @status, speed, cmd, point, equipRef=@fan

We have three entities in our example:

  • AHU identified with @ahu id
  • Discharge fan contained by the AHU
  • Fan speed command point contained by the fan

In this example, containment is modeled by equipRef, which is tagged with the containedBy relationship. Furthermore, the containedBy definition is tagged as transitive.

Put it all together and we have a transitive containment relationship we can query. The following filter will match both the fan equip and the speed point dicts:

containedBy? @ahu

The filter above will match any dict if it is contained by the given AHU either directly or indirectly.

This filter matches the fan equip as follows:

  1. Does the dict implement a def with the containedBy tag (it does: equipRef)
  2. Is the value of that tag in the dict equal to @ahu (yes)
  3. Match

The filter matches the fan speed point as follows

  1. Does the dict implement a def with the containedBy tag (it does: equipRef)
  2. Does the value of that tag in the dict equal @ahu (it does not, its value is @fan)
  3. Is the def marked as transitive (yes, containedBy is transitive)
  4. If so, then does the entity referenced by the tag (@fan in this case) itself match the containedBy filter? It does because of the previous steps we evaluated

Transitive relationships don't require the same reference tag is used. For example, we might traverse a equipRef first, and a spaceRef tag second. What matters is that there is a recursive set of matching relationships.

Reciprocal Of

Its also desirable to query relationships from either endpoint without regard to which endpoint actually declares the reference tag. Using our plant example we want to be able to query both sides as follows:

inputs-hot-water? @hwp   // inputs hot water from the plant
outputs-hot-water? @ahu  // outputs hot water to the AHU

We call these reciprocal relationships. They are explicitly configured on the relationship defs via the reciprocalOf tag.

Lets follow the process for this filter that should match the @hwp entity:

outputs-hot-water? @ahu

Following the rules we have discussed above so far, we would not find a match. There is no tag on the plant that directly references the AHU. However because outputs specifies it is an inverse of inputs, we would then have to check for a match in the reverse direction. The inverse is constructed as follows when matching the @hwp entity:

  1. Replace outputs with its inverse which is inputs
  2. Leave the subject term to the right of the first dash
  3. Replace the reference value @ahu with the dict being processed, which is @hwp
  4. Replace the dict being processed with the entity identified by @ahu

Following those rules, we would end up with an inverse filter that looks just the one we have already examined and we know will match:

inputs-hot-water? @hwp

Together transitive and inverse relationships provide tremendous flexibility to query Haystack data without regard to the underlying reference tags used.