Skip to main content

Download notebook · View source · Open in Colab

Work with Connections

Connections represent relationships between objects, such as works_at or contains. They are directed graph edges and can have properties of their own. This example connects a dish image to an ingredient entity, traverses that connection, updates its properties, and deletes it.

The examples require Python 3.10 or later, an ApertureDB connection profile or environment credentials, and permission to create, read, update, and delete objects. A unique documentation_run property separates the example data from other records. Cleanup uses that value, so keep the setup variables available until cleanup completes.

Connect to the database​

The examples require a running database and configured client credentials. The quick start guide describes the setup.

# Install the required client packages if needed
%pip install --quiet "aperturedb==0.5.1"
Note: you may need to restart the kernel to use updated packages.
from aperturedb.CommonLibrary import create_connector

# Create the connector for ApertureDB
client = create_connector()

import uuid

documentation_run = uuid.uuid4().hex
ingredient_class = "CookbookIngredient_" + documentation_run
connection_class = "CookbookIngredientLink_" + documentation_run

def checked_query(query, input_blobs=None):
response, output_blobs = client.query(query, input_blobs or [])
assert isinstance(response, list) and len(response) == len(query), response
for command in response:
result = next(iter(command.values()))
assert result.get("status") == 0, response
return response, output_blobs

Check connection​

GetStatus confirms that the client can reach and authenticate to the database.

query = [{
"GetStatus": {
}
}]

# Execute the query to get back a JSON response for GetStatus
response, blobs = checked_query(query)

client.print_last_response()
[
{
"GetStatus": {
"info": "OK",
"status": 0,
"system": "ApertureDB",
"version": "0.19.15"
}
}
]

Connect Dishes to Ingredients — Build a Graph​

The query adds a dish image and an ingredient entity, then creates a connection between them. References identify the endpoints within the query.

The connection commands manage these edges. The connect parameter can also create a connection when adding an object. The graph schema overview describes how objects and connections form a data model.

The Python SDK loaders handle bulk ingestion.

from pathlib import Path
from urllib.request import urlopen
import hashlib

fixture_path = Path("data/focaccia_brittany.jpg")
fixture_path.parent.mkdir(exist_ok=True)
fixture_url = "https://raw.githubusercontent.com/aperture-data/Cookbook/e333f6c59070b9165033d9ddd5af852a6b9624ba/notebooks/simple/data/focaccia_brittany.jpg"
fixture_sha256 = "4b104ba601ab3bf1126579e9dc795842551fc25be9ab2640795da2fb461f7061"
if not fixture_path.exists() or hashlib.sha256(fixture_path.read_bytes()).hexdigest() != fixture_sha256:
with urlopen(fixture_url, timeout=60) as response:
fixture_path.write_bytes(response.read())
assert hashlib.sha256(fixture_path.read_bytes()).hexdigest() == fixture_sha256
print("Verified fixture:", fixture_path.name)

query = [{
"AddEntity": {
"_ref": 1, # Forward reference to this entity, to be used in subsequent commands in the same query
"class": ingredient_class, # Every entity belongs to a class, in this case Ingredient
"properties": {
'documentation_run': documentation_run, # You can define as many key, value properties per entity, as needed
"name": "butter", # The data type for "name" property of an Ingredient will now always be of the type "string".
"macronutrient": "fat",
"subgroup": "dairy",
"category": "vegetarian"
},
"if_not_found": ['all', ['$name', '==', 'butter'], ['$documentation_run', '==', documentation_run]]
}}, {
"AddImage": {
"_ref": 2, # Forward reference to this image, to be used in subsequent commands in the same query
"properties": {
'documentation_run': documentation_run,
"name": "Focaccia",
"id": 35,
"contributor": "Vishakha",
"category": "starter",
"cuisine": "Italian",
"location": "Brittany",
"caption": "fresh made focaccia with herbs from the garden"
},
"if_not_found": ['all', ['$id', '==', 35], ['$documentation_run', '==', documentation_run]]
}}, {
"AddConnection": { # Now connect the results of the previous two commands using their handles, also see connect:{} parameter
"class": connection_class, # Connections themselves can have classes
"src": 1, # backward reference to 'Ingredient' entity added above
"dst": 2, # backward reference to 'Focaccia' image added above
"properties": {
'documentation_run': documentation_run, # Connections can also have their own set of key,value properties
"best_image": True
}
}
}]

# Read the image data as a binary blob
fd = open("data/focaccia_brittany.jpg", 'rb')
image_arr = [ fd.read() ]
fd.close()

response, blobs = checked_query(query, image_arr)

client.print_last_response()
Verified fixture: focaccia_brittany.jpg
[
{
"AddEntity": {
"status": 0
}
},
{
"AddImage": {
"status": 0
}
},
{
"AddConnection": {
"status": 0
}
}
]

Read or Query properties of the added Connection​

FindConnection returns the stored connection and its property values.

query = [{
"FindConnection": {
'constraints': ['$documentation_run', '==', documentation_run],
"with_class": connection_class, # Every connection belongs to a class, in this case self_image
"results": { # The results object controls what is returned
"all_properties": True # Return all properties of the connection
}
}
}]

response, blobs = checked_query(query)

client.print_last_response()

assert response[0]["FindConnection"]["returned"] == 1
assert response[0]["FindConnection"]["connections"][0]["best_image"] is True
[
{
"FindConnection": {
"connections": [
{
"_uniqueid": "5.0.1001",
"best_image": true,
"documentation_run": "0dcf486788e0459ba23cc389e189164d"
}
],
"returned": 1,
"status": 0
}
}
]

Use the Connection from Ingredient to Traverse (graph) and Find Dish (image)​

Queries can use the is_connected_to parameter to traverse connections in the ApertureDB graph.

from IPython.display import Image, display

query = [{
"FindEntity": {
"_ref": 1, # Forward reference to this entity, to be used in subsequent commands in the same query
"with_class": ingredient_class, # Only look for entities of class 'Ingredient'
"constraints": ['all', ['$name', '==', 'butter'], ['$documentation_run', '==', documentation_run]],
"results": {
"list": ["name", "category"] # Retrieve only the properties 'name' and 'category'.
}
}}, {
"FindImage": {
'constraints': ['$documentation_run', '==', documentation_run],
"is_connected_to": { # Look for images that have connections to the entity found above.
"ref": 1, # backward reference to 'Ingredient' entity found above
"connection_class": connection_class, # Only consider connections of the 'ingredient' class
"constraints": ['$best_image', '==', True]
},
"blobs": True, # Retrieve the image data as a binary blob
"results": {
"list": ["name", "id"] # Retrieve only the properties 'name' and 'id'.
}
}
}]

response, blobs = checked_query(query)

client.print_last_response()
for image_blob in blobs:
display(Image(data=image_blob))

assert response[1]["FindImage"]["returned"] == 1
assert response[1]["FindImage"]["entities"][0]["name"] == "Focaccia"
assert len(blobs) == 1 and blobs[0]
[
{
"FindEntity": {
"entities": [
{
"category": "vegetarian",
"name": "butter"
}
],
"returned": 1,
"status": 0
}
},
{
"FindImage": {
"blobs_start": 0,
"entities": [
{
"_blob_index": 0,
"id": 35,
"name": "Focaccia"
}
],
"returned": 1,
"status": 0
}
}
]

jpeg

Update properties of Connection already in ApertureDB​

UpdateConnection changes existing property values or adds properties to matching connections.

query = [{
"UpdateConnection": {
"with_class": connection_class, # Find any connections of class 'ingredient' ...
"constraints": ['all', ['$best_image', '==', True], ['$documentation_run', '==', documentation_run]],
"properties": {
"best_image": False, # And update them by setting 'best_image' to 'False'.
},
}
}]

response, blobs = checked_query(query)

client.print_last_response()
[
{
"UpdateConnection": {
"count": 1,
"status": 0
}
}
]
# Check that the connection was modified
query = [{
"FindEntity": {
"_ref": 1, # Forward reference to this entity, to be used in subsequent commands in the same query
"with_class": ingredient_class, # Only look for entities of class 'Ingredient
"constraints": ['all', ['$name', '==', 'butter'], ['$documentation_run', '==', documentation_run]],
"results": {
"list": ["name", "category"] # Retrieve only the properties 'name' and 'category'.
}
}}, {
"FindImage": {
'constraints': ['$documentation_run', '==', documentation_run],
"is_connected_to": { # Look for images that have connections to the entity found above.
"ref": 1, # Backward reference to 'Ingredient' entity found above
"connection_class": connection_class, # Only consider connections of the 'ingredient' class
"constraints": ['$best_image', '==', True]
},
"blobs": True, # Retrieve the image data as a binary blob
"results": {
"list": ["name", "id"] # Retrieve only the properties 'name' and 'id'.
}
}
}]

response, blobs = checked_query(query)

# We expect that this search shouldn't find anything.
client.print_last_response()
for image_blob in blobs:
display(Image(data=image_blob))

assert response[1]["FindImage"]["returned"] == 0
assert not blobs
[
{
"FindEntity": {
"entities": [
{
"category": "vegetarian",
"name": "butter"
}
],
"returned": 1,
"status": 0
}
},
{
"FindImage": {
"returned": 0,
"status": 0
}
}
]

Delete the Connection​

DeleteConnection removes the relationship while leaving its two endpoint objects in the database.

query = [{
"DeleteConnection": {
'constraints': ['$documentation_run', '==', documentation_run],
"with_class": connection_class # delete all connections with class 'ingredient'
}
}
]

response, blobs = checked_query(query)

client.print_last_response()
[
{
"DeleteConnection": {
"count": 1,
"status": 0
}
}
]

Verify deletion​

After deletion, the same connection query returns no matches.

query = [{
"FindConnection": {
'constraints': ['$documentation_run', '==', documentation_run],
"with_class": connection_class, # Look for the connections that we deleted
"results": {
"all_properties": True
}
}
}]

response, blobs = checked_query(query)

# We expect 0 results
client.print_last_response()

assert response[0]["FindConnection"]["returned"] == 0
[
{
"FindConnection": {
"returned": 0,
"status": 0
}
}
]

Cleanup the other objects​

Deleting a connection leaves its endpoint objects intact. The cleanup queries remove this run's dish image and ingredient entity.

query = [{
"DeleteEntity": {
"with_class": ingredient_class,
"constraints": ['all', ['$name', '==', 'butter'], ['$documentation_run', '==', documentation_run]]
}
}]

response, blobs = checked_query(query)

client.print_last_response()
[
{
"DeleteEntity": {
"count": 1,
"status": 0
}
}
]
query = [{
"DeleteImage": {
"constraints": ['all', ['$name', '==', 'Focaccia'], ['$documentation_run', '==', documentation_run]]
}
}]

response, blobs = checked_query(query)

client.print_last_response()
# Verify that cleanup removed this run's vertices as well as its edge.
remaining, _ = checked_query([
{"FindEntity": {"with_class": ingredient_class, "constraints": ["$documentation_run", "==", documentation_run], "results": {"all_properties": True}}},
{"FindImage": {"constraints": ["$documentation_run", "==", documentation_run], "results": {"all_properties": True}}},
])
assert all(next(iter(command.values()))["returned"] == 0 for command in remaining)
[
{
"DeleteImage": {
"count": 1,
"status": 0
}
}
]

What's next?​