Skip to content

Declare relationships and properties

Properties and relationships make model semantics explicit and easy to query. Use this guide to choose stable readings, field names, and access patterns as you turn a model design into PyRel declarations.

The following sections give a high-level overview of what relationships and properties are, when to use each one, and naming conventions to use when you declare them.

Relationships have the following components:

  • A reading: a Python f-string that describes the relationship and its fields.
  • One or more fields: “slots” in the reading that correspond to related concepts, defined by a {Concept} or {Concept:field_name} placeholder in the reading f-string.

For example, here is a relationship with one unnamed field and one named field:

from relationalai.semantics import Model
m = Model("MyModel")
Customer = m.Concept("Customer")
Order = m.Concept("Order")
# Declare a relationship between the Customer and Order concepts
Customer.orders = m.Relationship(f"{Customer} places {Order:order}")
  • The relationship is declared with Model.Relationship and assigned to Customer.orders.
  • The reading is f"{Customer} places {Order:order}".
  • The fields are Customer (unnamed) and Order:order (named order).

A relationship maps one or more input fields to an output field, where the reading describes the meaning of the relationship and the fields define how to access them in queries and definitions.

For example, the Customer.orders relationship declared above, and reproduced here for convenience, maps the Customer field (input) to the Order:order field (output):

# input field
# \
# vvvvvvvv
Customer.orders = m.Relationship(f"{Customer} places {Order:order}")
# ^^^^^^^^^^^
# /
# output field
  • As the names “input” and “output” suggest, a relationship is a bit like a function. In queries and definitions, you call it with input arguments and to get a reference to the matching output values.
  • Unlike a Python function, though, a relationship has no implementation code of its own.

What the difference is between properties and relationships

Section titled “What the difference is between properties and relationships”

A relationship is a general association between entities that can have multiple fields and is declared with Model.Relationship. A property is a specialized relationship that represents a single-valued attribute of an entity and is declared with Model.Property.

Use the following table to help you choose which one to use:

What to useWhen to use it
Model.PropertyChoose when the inputs uniquely determine one output value, like a person’s birth date or an order’s status.
Model.RelationshipChoose when an input can have many outputs, or the association has multiple fields, like email addresses, memberships, or an order line with a product and quantity.

A relationship can have any number of fields. The following sections describe the most common shapes and when to use each one.

Binary relationships (2 fields): map one thing to another

Section titled “Binary relationships (2 fields): map one thing to another”

A binary relationship has two fields and is the most common way to represent a simple association between two concepts, like “customer places order” or “person works at company”.

Unary relationships (1 field): Boolean flags

Section titled “Unary relationships (1 field): Boolean flags”

A unary relationship has one field and represents a Boolean flag for a concept, like “Order is cancelled” or “Shipment is delayed”:

from relationalai.semantics import Model, String
m = Model("MyModel")
Order = m.Concept("Order", identify_by={"id": String})
# Declare a unary relationship to flag cancelled orders
Order.cancelled = m.Relationship(f"{Order} is cancelled")
  • The single field is an input field. There is no output field. Whether or not the flag is true for a given entity is determined by the presence or absence of the relationship.
  • Unary relationships are preferable to Boolean-valued binary relationships (for example, Order.cancelled = m.Relationship(f"{Order} has been cancelled {Boolean:status})) because they often use less space in the model and can be more performant to query.

N-ary relationships (3+ fields): include additional context

Section titled “N-ary relationships (3+ fields): include additional context”

Choose an n-ary relationship when the association naturally has extra fields, like a timestamp for when a relationship began:

from relationalai.semantics import Model, String, DateTime
m = Model("MyModel")
Product = m.Concept("Product", identify_by={"sku": String})
# Declare a relationship with three fields
Product.inventory = m.Relationship(
f"{Product} on hand as of {DateTime:timestamp} is {Integer:quantity}"
)
  • The output field is always the last field in the reading, so some care must be taken when declaring n-ary relationships to ensure the reading is clear and the fields are easy to access.
  • The more fields a relationship has, the more expensive it can be to query, so n-ary relationships should be used judiciously. Stick to three or four fields maximum, if possible.

Declaring a property adds a single-valued attribute you can select, filter, and constrain. Use this when an entity has an attribute that should have at most one value per entity (for example, age, status, or birth_date). A property declaration does not load data by itself.

Declare the property with Model.Property using a reading f-string:

from relationalai.semantics import Integer, Model
m = Model("MyModel")
Person = m.Concept("Person")
Person.age = m.Property(
f"{Person} is {Integer} years old",
short_name="person_age", # Optional
)
  • Person.age = m.Property(...) declares a property and attaches it to the Person concept.
  • The reading string f"{Person} is {Integer} years old" defines one input field (the Person) and one output field (the Integer).
  • Because this is a property, the output field is single-valued for each input entity, so each person can have at most one age.
  • The optional short_name="person_age" argument registers this property with the key "person_age" in the Model.relationship_index dictionary. If you don’t provide a short_name, the property will not be accessible via Model.relationship_index, but will still be viewable in Model.relationships.

(Advanced) Pass a list of Field objects for a relationship

Section titled “(Advanced) Pass a list of Field objects for a relationship”

Declare the property with Model.Property by providing a list of Field objects instead of a reading string:

from relationalai.semantics import Integer, Model
from relationalai.semantics.frontend.base import Field
m = Model("MyModel")
Person = m.Concept("Person")
Person.age = m.Property(
fields=[Field("person", Person), Field("age", Integer)],
short_name="person_age", # Optional
)
  • Person.age = m.Property(fields=[...]) declares the property without parsing a reading string.
  • Field("person", Person) defines an input field named person with type Person.
  • Field("age", Integer) defines the output field named age with type Integer.
  • Because this is a property, the output field is single-valued for each input entity, so each person can have at most one age.
  • The optional short_name="person_age" argument registers this property with the key "person_age" in the Model.relationship_index dictionary. If you don’t provide a short_name, the property will not be accessible via Model.relationship_index, but will still be viewable in Model.relationships.
  • If you can express the property clearly as a short reading f-string, the reading-string version is usually easier to scan and maintain.
  • The explicit Field list version is useful when you are generating fields programmatically or when the reading string would be too long or complex to be helpful.
  • PyRel does generate a default reading from the fields that includes internal type IDs, but it is usually less human-readable.

Declare a relationship with Model.Relationship

Section titled “Declare a relationship with Model.Relationship”

Declaring a relationship adds a reusable association you can call like a function, select in queries, and traverse across concepts. Choose this for one-to-many and many-to-many associations, and for any association that naturally has multiple fields. Attaching the relationship to a concept attribute is usually easier to discover than keeping it as a standalone variable.

Declare the relationship with Model.Relationship using a reading f-string:

from relationalai.semantics import Integer, Model, String
m = Model("MyModel")
Person = m.Concept("Person", identify_by={"name": String})
Company = m.Concept("Company", identify_by={"name": String})
Person.works_at = m.Relationship(
f"{Person} works at {Company}",
short_name="person_works_at", # Optional
)
  • The relationship is declared with Model.Relationship and assigned to Person.works_at.
  • The reading string f"{Person} works at {Company}" defines an input field of type Person and an output field of type Company.
  • Because this is a relationship, the output field can be multi-valued for each input entity, so a person can work at multiple companies.
  • The optional short_name="person_works_at" argument registers this relationship with the key "person_works_at" in the Model.relationship_index dictionary. If you don’t provide a short_name, the relationship will not be accessible via Model.relationship_index, but will still be viewable in Model.relationships.

Declare the relationship with Model.Relationship by providing a list of Field objects instead of a reading string:

from relationalai.semantics import Integer, Model, String
from relationalai.semantics.frontend.base import Field
m = Model("MyModel")
Person = m.Concept("Person", identify_by={"name": String})
Company = m.Concept("Company", identify_by={"name": String})
Person.works_at = m.Relationship(
fields=[Field("employee", Person), Field("company", Company)],
short_name="person_works_at", # Optional
)
  • Person.works_at = m.Relationship(fields=[...]) declares a relationship without parsing a reading string.
  • Field("employee", Person) defines an input field named employee with type Person.
  • Field("company", Company) defines the output field named company with type Company.
  • Because this is a relationship, the output field can be multi-valued for each input entity, so a person can work at multiple companies.
  • The optional short_name="person_works_at" argument registers this relationship with the key "person_works_at" in the Model.relationship_index dictionary. If you don’t provide a short_name, the relationship will not be accessible via Model.relationship_index, but will still be viewable in Model.relationships.
  • If you can express the relationship clearly as a short reading f-string, the reading-string version is usually easier to scan and maintain.
  • The explicit Field list version is useful when you are generating fields programmatically or when the reading string would be too long or complex to be helpful.
  • If you omit reading, PyRel generates a default reading from the fields that includes internal type IDs. It is usually less human-readable.

Declare an alternate reading for a relationship

Section titled “Declare an alternate reading for a relationship”

An alternate reading gives the same underlying relationship a different human-readable phrase or direction. Use this when people naturally talk about the same association in multiple ways, for example by using the inverse of a binary relationship or using different phrasing in the reading.

Create an alternate (inverse) reading by calling Relationship.alt on a base relationship:

from relationalai.semantics import Model
m = Model("MyModel")
Team = m.Concept("Team")
Project = m.Concept("Project")
# Declare a base relationship with explicit field names.
Team.projects = m.Relationship(f"{Team} works on {Project}")
# Add an alternate reading over the same underlying fields.
Project.team = Team.projects.alt(f"{Project} is worked on by {Team}")
  • Team.projects declares the base relationship with stable field names (team and project).
  • Team.projects.alt(...) creates an alternate reading that refers to the same underlying relationship and fields, but with different phrasing and direction. It is assigned to Project.team to make it easy to discover when starting from the Project concept.
  • Both Team.projects and Project.team can be used interchangeably in queries and definitions, and they will return the same results because they refer to the same underlying relationship.
  • An alternate reading is another way to refer to the same underlying relationship. It does not create a new relationship or new fields.

  • Relationship.alt must use the same field concepts and field names as the base relationship. Reordering is OK, but changing names or types is not. For example, the following would raise an error because it changes the field names instead of just reordering them:

    # Fails because it changes the field names.
    Project.team = Team.works_on.alt(f"{Project:p} involves {Team:t}")

View all relationships declared in a model

Section titled “View all relationships declared in a model”

Viewing relationships helps you debug what your model currently knows about, especially in notebooks and interactive exploration. Choose Model.relationships when you want everything the model has created. Choose Model.relationship_index when you want to look up a relationship by a stable name.

To view all relationships and properties created in a model, inspect Model.relationships:

from relationalai.semantics import Integer, Model
m = Model("MyModel")
Person = m.Concept("Person")
Company = m.Concept("Company")
Person.age = m.Property(f"{Person} has {Integer:age}")
Person.works_at = m.Relationship(f"{Person} works at {Company}")
# Print all relationships and properties declared in the model
print(m.relationships)
  • m.relationships is a list of every relationship/property object created via m.Relationship and m.Property.
  • This is a good first stop when you are not sure what has been declared in the current model instance.

To look up a relationship or property by short name, use Model.relationship_index:

from relationalai.semantics import Integer, Model
m = Model("MyModel")
Person = m.Concept("Person")
Person.age = m.Property(
f"{Person} has {Integer:age}",
short_name="person_age", # Declare a short name to make it accessible in relationship_index
)
# Use the short name to look up the relationship in the relationship_index dictionary
print(m.relationship_index["person_age"])
  • short_name="person_age" is the key used to register the property in m.relationship_index.
  • m.relationship_index["person_age"] returns the underlying Relationship or Property object associated with that short name.