Download notebook · View source · Open in Colab
SPARQL wrapper
The SPARQL wrapper translates RDF/SPARQL queries into ApertureDB queries. Applications that already use SPARQL can query ApertureDB objects, properties, and graph connections through this interface.
The wrapper supports a subset of the ApertureDB Query Language. Direct ApertureDB queries offer more functionality and can be more efficient.
%pip install --quiet "aperturedb==0.5.1" rdflib
# We're going to use the pandas library to display binding sets in a more readable format.
import pandas as pd
pd.set_option('display.width', 200)
pd.set_option('display.max_colwidth', 50)
Connect and check prerequisites
Configure a connection through APERTUREDB_JSON or a saved SDK profile; see client configuration. This notebook reads an existing Cookbook graph and does not load or delete it. It requires images with caption, ingredients with name, and HasIngredient connections from images to ingredients. The final KNN section additionally needs a 512-dimensional ViT-B/16 descriptor set linked to those images. Creating that set requires a separate embedding ingestion step and the matching model weights; this notebook does not download or train a model.
from aperturedb.CommonLibrary import create_connector
client = create_connector()
response, _ = client.query([
{"FindConnection": {"with_class": "HasIngredient", "results": {"count": True}}},
{"FindDescriptorSet": {"with_name": "ViT-B/16", "dimensions": True}}
])
assert isinstance(response, list) and all(next(iter(r.values()))["status"] == 0 for r in response), response
assert response[0]["FindConnection"]["count"] > 0, "Load the Cookbook images/ingredients/HasIngredient fixture first."
sets = response[1]["FindDescriptorSet"].get("entities", [])
assert len(sets) == 1, "The KNN section requires the ViT-B/16 descriptor fixture."
assert sets[0]["_dimensions"] == 512, "The fixture descriptor set must have 512 dimensions."
Create SPARQL wrapper object
The wrapper holds the database connection and maps the ApertureDB schema to RDF terms. Debug mode retains intermediate query and response data, which the later examples display.
from aperturedb.SPARQL import SPARQL
import json
# Create SPARQL object. We'll use the debugging feature to print interesting information.
sparql = SPARQL(client=client, debug=True)
Schema and RDF mappings
On creation, the wrapper reads the database schema and builds:
- Turtle namespaces
- A mapping from property URIs to the object classes that use each property
- A mapping from connection URIs to possible source and destination object classes
Namespaces
Namespaces shorten URIs in RDF serializations. p:name represents the ApertureDB property name, and c:HasIngredient represents the connection class HasIngredient.
Each object class, such as o:Ingredient, also has a namespace. The wrapper uses it to construct URIs from ApertureDB unique IDs. The output lists the namespaces generated from this database.
# Internal feature: You don't normally have to look at this
print("namespaces:", json.dumps({k: str(v) for k, v in sparql.namespaces.items()}, indent=2))
Properties
Each property in the schema becomes an RDF property in the p namespace, represented as a functional data property. A property name can occur on several object classes, so the wrapper maps each property to its possible domain classes. The output lists those classes; it does not show the property's range type.
# Internal feature: You don't normally have to look at this
print("properties:", json.dumps({sparql.graph.qname(k): str(v)
for k, v in sparql.properties.items()}, indent=2))
Connections
Each connection class becomes an RDF property in the c namespace, represented as a non-functional object property. Its domain and range describe the possible source and destination object classes.
# Internal feature: You don't normally have to look at this
print("connections:", json.dumps({sparql.graph.qname(k): str(v)
for k, v in sparql.connections.items()}, indent=2))
Create query and run it
This query finds dish images connected to both chicken and butter ingredients. A SPARQL SELECT returns variable bindings. The wrapper's display helper converts those bindings to a pandas DataFrame and shortens URIs to qualified names.
# Find all images with chicken and butter as ingredients
query = """
SELECT ?s ?caption {
?s c:HasIngredient [p:name "chicken"] , [p:name "butter"] ;
p:caption ?caption .
} LIMIT 10
"""
df = sparql.to_dataframe(sparql.query(query))
assert not df.empty and "s" in df.columns, "Expected results from the declared Cookbook fixture."
print(df)
Retrieve image data
SPARQL results contain RDF values rather than image bytes. The wrapper's get_blob, get_image, and show_image methods retrieve and display the image associated with a result. Each retrieval sends a separate ApertureDB query.
sparql.show_images(df.s)
Query translation
The wrapper integrates with rdflib through a custom evaluator for Basic Graph Pattern (BGP) nodes in the SPARQL algebra.
Triples
rdflib parses the query into triples and assigns identifiers to blank nodes. Those triples become the input to the ApertureDB evaluator. The output shows the triples for the preceding query.
# Internal debug feature: Not available by default
print(sparql._format_triples(sparql.triples))
ApertureDB query
The wrapper translates the triples into an ApertureDB query. Debug mode exposes the generated commands, including their references and constraints.
# Internal debug feature: Not available by default
print(json.dumps(sparql.input_query, indent=2))
ApertureDB response
The ApertureDB response contains the command results used to construct the SPARQL result.
# Internal debug feature: Not available by default
print(json.dumps(sparql.output_response, indent=2))
The wrapper converts the response into solutions that map each selected variable to its value.
# Internal debug feature: Not available by default
solutions = [ {str(k): sparql._format_node(v) for k, v in solution.items()} for solution in sparql.solutions ]
print(len(solutions))
for solution in solutions:
print(solution)
Match either ingredient
This query matches images containing chicken or butter. The VALUES clause supplies the two ingredient names. rdflib evaluates the graph pattern once for each value, resulting in two ApertureDB calls.
# Find all images with chicken or butter as ingredients
query = """
SELECT ?s ?caption WHERE {
VALUES ?ingredient { "chicken" "butter" }
?s c:HasIngredient [p:name ?ingredient] ;
p:caption ?caption .
} LIMIT 10
"""
df = sparql.to_dataframe(sparql.query(query))
assert not df.empty and "s" in df.columns, "Expected results from the declared Cookbook fixture."
print(df)
sparql.show_images(df.s)
Aggregate and sort matches
This query counts matching results, groups them, and sorts the groups with COUNT, GROUP BY, and ORDER BY. rdflib performs these aggregation and ordering operations.
# Find the top 10 ingredients
query = """
SELECT (COUNT(*) AS ?count) ?ingredient WHERE {
?s c:HasIngredient [p:name ?ingredient] .
} GROUP BY ?ingredient ORDER BY DESC(?count) LIMIT 10
"""
df = sparql.to_dataframe(sparql.query(query))
assert not df.empty and "ingredient" in df.columns, "Expected results from the declared Cookbook fixture."
print(df)
KNN search
The wrapper extends SPARQL with knn:similarTo for descriptor search. Its object specifies the query vector, descriptor set, neighbor count, and optional search parameters. The vector is represented as a JSON string.
?descriptor knn:similarTo [
knn:vector "[1,2,3,4,5]" ; # vector for descriptor as a JSON string
knn:set "RecipeEmbeddings" ; # descriptor set
knn:k_neighbors 5 ; # number of results
knn:knn_first true ; # optional
knn:engine "HNSW" ; # optional
knn:metric "L2" ; # optional
knn:distance ?distance # optional, binds to distance literals
] .
?descriptor binds to a matching ApertureDB Descriptor. The encode_descriptor helper converts the query vector to the JSON string used in the following query.
import numpy as np
descriptor = np.random.default_rng(0).random(512)
# A reproducible synthetic query vector demonstrates the API, not semantic relevance.
query = f"""
SELECT ?i ?distance ?d ?caption WHERE {{
?d knn:similarTo [
knn:set 'ViT-B/16' ;
knn:k_neighbors 20 ;
knn:vector "{sparql.encode_descriptor(descriptor)}" ;
knn:distance ?distance
] ;
c:ANY ?i . # Use fake connection because we can't say c:_DescriptorConnection
?i p:caption ?caption .
}}
"""
The query returns bindings for the matching images, descriptors, and distances.
df = sparql.to_dataframe(sparql.query(query))
assert not df.empty and "i" in df.columns, "Expected results from the declared Cookbook fixture."
print(df)
The image helper retrieves and displays the images referenced by the result.
sparql.show_images(df.i)
client.close()