We use the specification Allgemeines Metadatenprofil für Bildungsressourcen to describe our learning resources. It uses JSON-LD as a format with schema.org as the main vocabulary. For example, the metadata for the article "Addition" is:
{
"@context": [
"https://w3id.org/kim/lrmi-profile/draft/context.jsonld",
{
"@language": "de"
}
],
"id": "https://serlo.org/1495",
"type": [
"LearningResource",
"Article"
],
"dateCreated": "2014-03-01T20:36:44+00:00",
"dateModified": "2021-03-08T20:51:17+00:00",
"description": "Addition, auch Plusrechnen genannt, geh\\u00f6rt zu den Grundrechenarten der Mathematik. Lerne, was ein Summand ist. \\u21d2 Hier lernst du, dass das Assoziativgesetz und Kommutativgesetz gelten. \\u21d2 Veranschaulichung durch Merktabellen und Zahlengeraden. F\\u00fcr den Anfang kannst du auch schriftlich addieren! Viele \\u00dcbungsaufgaben sind verf\\u00fcgbar.\\u2713 Lernen mit Serlo!",
"headline": "Addition",
"identifier": {
"type": "PropertyValue",
"propertyID": "UUID",
"value": 1495
},
"inLanguage": [
"de"
],
"isAccessibleForFree": true,
"isFamilyFriendly": true,
"learningResourceType": "Article",
"license": {
"id": "https://creativecommons.org/licenses/by-sa/4.0/deed.de"
},
"maintainer": "https://serlo.org/",
"name": "Addition",
"publisher": [
{
"id": "https://serlo.org/"
}
],
"version": "https://serlo.org/197588"
}
Each property is a schema.org property. For example, identifier
is the same as the property https://schema.org/identifier. The site https://json-ld.org/#developers lists tools for working with JSON-LD.
The metadata can be accessed via our GraphQL endpoint at https://api.serlo-staging.dev/graphql (serlo-staging.dev
is our testing environment). In the query namespace metadata
, we have two properties: publisher
and entities
. publisher
points to the metadata about Serlo Education e.V. (i.e. the publisher). Via entities
you can access the metadata about our learning resources. entities
supports pagination via the properties first
and after
, filtering by language via instance
and filtering by modification date via modifiedAfter
:
extend type Query {
metadata: MetadataQueryNamespace!
}
type MetadataQueryNamespace {
# Returns metadata about Serlo Education e.V.
publisher: JSONObject!
# Returns metadata about learning resources with pagination and filter options
entities(
first: Int, # Number of metadata objects which shall
# be returned (default is 100)
after: String, # Cursor to the entity after which the
# new metadata shall be returned (see `endCursor` in `HasNextPageInfo`)
instance: Instance, # Filter for the subdomain / language
modifiedAfter: String # Filters entities which have been modified
# after the given date (format YYYY-MM-DDTHH:MM:SSZ)
): EntitiesMetadataConnection!
}
type EntitiesMetadataConnection {
# Array of metadata objects
nodes: [JSONObject!]!
# Information whether there are more resources to query
pageInfo: HasNextPageInfo!
}
type HasNextPageInfo {
# Is true when more metadata objects can be queried
hasNextPage: Boolean!
# Cursor which needs to be passed to `after` in order to fetch more metadata objects
endCursor: String
}
The site https://graphql.org/code/ lists client tools for GraphQL. In the following sections you will also find code examples for accessing our metadata via the GraphQL API.
In case you program a crawler which shall run regularly you might find the modifiedAfter
argument helpful. By setting it to the date of the last execution you can fetch the metadata of all learning resources which have been modified since then. Note that the returned metadata objects are always ordered by id
even if modifiedAfter
is set.
DOCS
on the right side.import json
from IPython.display import display, Markdown, HTML
def display_json(value, title="The result"):
json_formated = json.dumps(value, indent=2)
display_markdown(f"#### {title}")
display_markdown(f"```json\n{json_formated}\n```")
def display_len(list_object, explanation="elements were fetched"):
display_markdown("**Result:** %s %s" % (len(list_object), explanation))
def display_markdown(text):
display(Markdown(text))
import requests
def get_publisher():
req = requests.post(
"https://api.serlo-staging.dev/graphql",
headers = {
"Content-Type": "application/json",
},
json = {
"query": """
query {
metadata {
publisher
}
}
"""
}
)
return req.json()
display_json(get_publisher())
{
"data": {
"metadata": {
"publisher": {
"@context": [
"https://w3id.org/kim/lrmi-profile/draft/context.jsonld",
{
"@language": "de"
}
],
"id": "https://serlo.org/",
"type": [
"EducationalOrganization",
"NGO"
],
"name": "Serlo Education e.V.",
"alternateName": "Serlo",
"url": "https://de.serlo.org/",
"description": "Serlo.org bietet einfache Erkl\u00e4rungen, Kurse, Lernvideos, \u00dcbungen und Musterl\u00f6sungen mit denen Sch\u00fcler*innen und Studierende nach ihrem eigenen Bedarf und in ihrem eigenen Tempo lernen k\u00f6nnen. Die Lernplattform ist komplett kostenlos und werbefrei.",
"image": "https://assets.serlo.org/5ce4082185f5d_5df93b32a2e2cb8a0363e2e2ab3ce4f79d444d11.jpg",
"logo": "https://de.serlo.org/_assets/img/serlo-logo.svg",
"address": {
"type": "PostalAddress",
"streetAddress": "Daiserstra\u00dfe 15 (RGB)",
"postalCode": "81371",
"addressLocality": "M\u00fcnchen",
"addressRegion": "Bayern",
"addressCountry": "Germany"
},
"email": "de@serlo.org"
}
}
}
}
import requests
req = requests.post(
"https://api.serlo-staging.dev/graphql",
headers = {
"Content-Type": "application/json",
},
json = {
"query": """
query {
metadata {
entities(first: 2) {
nodes
}
}
}
"""
}
)
display_json(req.json())
{
"data": {
"metadata": {
"entities": {
"nodes": [
{
"@context": [
"https://w3id.org/kim/lrmi-profile/draft/context.jsonld",
{
"@language": "de"
}
],
"id": "https://serlo.org/1495",
"type": [
"LearningResource",
"Article"
],
"dateCreated": "2014-03-01T20:36:44+00:00",
"dateModified": "2021-03-08T20:51:17+00:00",
"description": "Addition, auch Plusrechnen genannt, geh\u00f6rt zu den Grundrechenarten der Mathematik. Lerne, was ein Summand ist. \u21d2 Hier lernst du, dass das Assoziativgesetz und Kommutativgesetz gelten. \u21d2 Veranschaulichung durch Merktabellen und Zahlengeraden. F\u00fcr den Anfang kannst du auch schriftlich addieren! Viele \u00dcbungsaufgaben sind verf\u00fcgbar.\u2713 Lernen mit Serlo!",
"headline": "Addition",
"identifier": {
"type": "PropertyValue",
"propertyID": "UUID",
"value": 1495
},
"inLanguage": [
"de"
],
"isAccessibleForFree": true,
"isFamilyFriendly": true,
"learningResourceType": "Article",
"license": {
"id": "https://creativecommons.org/licenses/by-sa/4.0/deed.de"
},
"maintainer": "https://serlo.org/",
"name": "Addition",
"publisher": [
{
"id": "https://serlo.org/"
}
],
"version": "https://serlo.org/197588"
},
{
"@context": [
"https://w3id.org/kim/lrmi-profile/draft/context.jsonld",
{
"@language": "de"
}
],
"id": "https://serlo.org/1497",
"type": [
"LearningResource",
"Article"
],
"dateCreated": "2014-03-01T20:36:51+00:00",
"dateModified": "2021-09-06T11:11:40+00:00",
"description": "",
"headline": "Kleinstes gemeinsames Vielfaches",
"identifier": {
"type": "PropertyValue",
"propertyID": "UUID",
"value": 1497
},
"inLanguage": [
"de"
],
"isAccessibleForFree": true,
"isFamilyFriendly": true,
"learningResourceType": "Article",
"license": {
"id": "https://creativecommons.org/licenses/by-sa/4.0/deed.de"
},
"maintainer": "https://serlo.org/",
"name": "Kleinstes gemeinsames Vielfaches",
"publisher": [
{
"id": "https://serlo.org/"
}
],
"version": "https://serlo.org/224107"
}
]
}
}
}
}
import requests
def fetch_entities(first, after=None):
req = requests.post(
"https://api.serlo-staging.dev/graphql",
headers = {
"Content-Type": "application/json",
},
json = {
"query": """
query($first: Int, $after: String) {
metadata {
entities(first: $first, after: $after) {
nodes
pageInfo {
hasNextPage
endCursor
}
}
}
}
""",
"variables": { "first": first, "after": after }
}
)
return req.json()
# Fetching the first page with the first two metadata elements
first_result = fetch_entities(first=2)
# Display the `PageInfo` object of the first request
# Note that `hastNextPage` is true, so there are more objects which can be fetched
first_result_page_info = first_result["data"]["metadata"]["entities"]["pageInfo"]
display_json(first_result_page_info, title="The PageInfo object of the first request")
# Fetching the second page with the next two elements
# Note that `endCursor` of the first request is passed as the `after` argument in order to get the next elements
second_result = fetch_entities(first=2, after=first_result_page_info["endCursor"])
display_json(second_result, title="Metadata of the second page")
{
"hasNextPage": true,
"endCursor": "MTQ5Nw=="
}
{
"data": {
"metadata": {
"entities": {
"nodes": [
{
"@context": [
"https://w3id.org/kim/lrmi-profile/draft/context.jsonld",
{
"@language": "de"
}
],
"id": "https://serlo.org/1499",
"type": [
"LearningResource",
"Article"
],
"dateCreated": "2014-03-01T20:37:01+00:00",
"dateModified": "2021-09-06T11:41:11+00:00",
"description": "Binomische Formeln einfach erkl\u00e4rt. Verwendung der binomischen Formel zum Aufl\u00f6sen von Klammern und Faktorisieren. Mit vielen Beispielen und \u00dcbungen! Erfahre mehr zu leichten Beweisen der binomischen Formel mithilfe des Quadrats. \u21d2 Ein Kochrezept zur allgemeinen Vorhergehensweise. Video\u2713",
"headline": "Binomische Formeln",
"identifier": {
"type": "PropertyValue",
"propertyID": "UUID",
"value": 1499
},
"inLanguage": [
"de"
],
"isAccessibleForFree": true,
"isFamilyFriendly": true,
"learningResourceType": "Article",
"license": {
"id": "https://creativecommons.org/licenses/by-sa/4.0/deed.de"
},
"maintainer": "https://serlo.org/",
"name": "Binomische Formeln",
"publisher": [
{
"id": "https://serlo.org/"
}
],
"version": "https://serlo.org/224109"
},
{
"@context": [
"https://w3id.org/kim/lrmi-profile/draft/context.jsonld",
{
"@language": "de"
}
],
"id": "https://serlo.org/1501",
"type": [
"LearningResource",
"Article"
],
"dateCreated": "2014-03-01T20:37:01+00:00",
"dateModified": "2021-09-08T10:24:28+00:00",
"description": "",
"headline": "Ergebnismenge",
"identifier": {
"type": "PropertyValue",
"propertyID": "UUID",
"value": 1501
},
"inLanguage": [
"de"
],
"isAccessibleForFree": true,
"isFamilyFriendly": true,
"learningResourceType": "Article",
"license": {
"id": "https://creativecommons.org/licenses/by-sa/4.0/deed.de"
},
"maintainer": "https://serlo.org/",
"name": "Ergebnismenge",
"publisher": [
{
"id": "https://serlo.org/"
}
],
"version": "https://serlo.org/224215"
}
],
"pageInfo": {
"hasNextPage": true,
"endCursor": "MTUwMQ=="
}
}
}
}
}
The following example shows how the PageInfo
object can be used to fetch all entities in a loop:
import requests
def fetch_all_entities(first=500):
result = []
endCursor = None
while True:
current_page = fetch_entities(first, after=endCursor)["data"]["metadata"]["entities"]
result += current_page["nodes"]
if current_page["pageInfo"]["hasNextPage"]:
endCursor = current_page["pageInfo"]["endCursor"]
else:
break
return result
def fetch_entities(first, after=None):
req = requests.post(
"https://api.serlo-staging.dev/graphql",
headers = {
"Content-Type": "application/json",
},
json = {
"query": """
query($first: Int, $after: String) {
metadata {
entities(first: $first, after: $after) {
nodes
pageInfo {
hasNextPage
endCursor
}
}
}
}
""",
"variables": { "first": first, "after": after }
}
)
return req.json()
all_entities = fetch_all_entities()
display_len(all_entities)
Result: 8371 elements were fetched
instance
and modifiedAfter
¶import requests
def fetch_all_entities(first=500, instance=None, modifiedAfter=None):
result = []
endCursor = None
while True:
current_page = fetch_entities(first, after=endCursor, instance=instance, modifiedAfter=modifiedAfter)
current_page = current_page["data"]["metadata"]["entities"]
result += current_page["nodes"]
if current_page["pageInfo"]["hasNextPage"]:
endCursor = current_page["pageInfo"]["endCursor"]
else:
break
return result
def fetch_entities(first, after=None, instance=None, modifiedAfter=None):
req = requests.post(
"https://api.serlo-staging.dev/graphql",
headers = {
"Content-Type": "application/json",
},
json = {
"query": """
query($first: Int, $after: String, $instance: Instance, $modifiedAfter: String) {
metadata {
entities(first: $first, after: $after, instance: $instance, modifiedAfter: $modifiedAfter) {
nodes
pageInfo {
hasNextPage
endCursor
}
}
}
}
""",
"variables": {
"first": first,
"after": after,
"instance": instance,
"modifiedAfter": modifiedAfter
}
}
)
return req.json()
# == Fetch elements by language / subdomain ==
german_entities = fetch_all_entities(instance="de")
display_len(german_entities, explanation="entities fetched from de.serlo.org")
# == Fetch elements which are modified in 2021 ==
# Format for modifiedAfter is YYYY-MM-DDTHH:MM:SSZ
entities2021 = fetch_all_entities(modifiedAfter="2021-01-01T00:00:00Z")
display_len(entities2021, explanation="entities fetched which are modified in 2021")
Result: 7458 entities fetched from de.serlo.org
Result: 3179 entities fetched which are modified in 2021
%%bash
curl https://api.serlo-staging.dev/graphql \
--silent -X POST \
-H "Content-Type: application/json" \
--data '{
"query": "query { metadata { publisher } }"
}' | jq -C
{ "data": { "metadata": { "publisher": { "@context": [ "https://w3id.org/kim/lrmi-profile/draft/context.jsonld", { "@language": "de" } ], "id": "https://serlo.org/", "type": [ "EducationalOrganization", "NGO" ], "name": "Serlo Education e.V.", "alternateName": "Serlo", "url": "https://de.serlo.org/", "description": "Serlo.org bietet einfache Erklärungen, Kurse, Lernvideos, Übungen und Musterlösungen mit denen Schüler*innen und Studierende nach ihrem eigenen Bedarf und in ihrem eigenen Tempo lernen können. Die Lernplattform ist komplett kostenlos und werbefrei.", "image": "https://assets.serlo.org/5ce4082185f5d_5df93b32a2e2cb8a0363e2e2ab3ce4f79d444d11.jpg", "logo": "https://de.serlo.org/_assets/img/serlo-logo.svg", "address": { "type": "PostalAddress", "streetAddress": "Daiserstraße 15 (RGB)", "postalCode": "81371", "addressLocality": "München", "addressRegion": "Bayern", "addressCountry": "Germany" }, "email": "de@serlo.org" } } } }
%%bash
curl https://api.serlo-staging.dev/graphql \
--silent -X POST \
-H "Content-Type: application/json" \
--data '{
"query": "query { metadata { entities(first: 2) { nodes } } }"
}' | jq -C
{ "data": { "metadata": { "entities": { "nodes": [ { "@context": [ "https://w3id.org/kim/lrmi-profile/draft/context.jsonld", { "@language": "de" } ], "id": "https://serlo.org/1495", "type": [ "LearningResource", "Article" ], "dateCreated": "2014-03-01T20:36:44+00:00", "dateModified": "2021-03-08T20:51:17+00:00", "description": "Addition, auch Plusrechnen genannt, gehört zu den Grundrechenarten der Mathematik. Lerne, was ein Summand ist. ⇒ Hier lernst du, dass das Assoziativgesetz und Kommutativgesetz gelten. ⇒ Veranschaulichung durch Merktabellen und Zahlengeraden. Für den Anfang kannst du auch schriftlich addieren! Viele Übungsaufgaben sind verfügbar.✓ Lernen mit Serlo!", "headline": "Addition", "identifier": { "type": "PropertyValue", "propertyID": "UUID", "value": 1495 }, "inLanguage": [ "de" ], "isAccessibleForFree": true, "isFamilyFriendly": true, "learningResourceType": "Article", "license": { "id": "https://creativecommons.org/licenses/by-sa/4.0/deed.de" }, "maintainer": "https://serlo.org/", "name": "Addition", "publisher": [ { "id": "https://serlo.org/" } ], "version": "https://serlo.org/197588" }, { "@context": [ "https://w3id.org/kim/lrmi-profile/draft/context.jsonld", { "@language": "de" } ], "id": "https://serlo.org/1497", "type": [ "LearningResource", "Article" ], "dateCreated": "2014-03-01T20:36:51+00:00", "dateModified": "2021-09-06T11:11:40+00:00", "description": "", "headline": "Kleinstes gemeinsames Vielfaches", "identifier": { "type": "PropertyValue", "propertyID": "UUID", "value": 1497 }, "inLanguage": [ "de" ], "isAccessibleForFree": true, "isFamilyFriendly": true, "learningResourceType": "Article", "license": { "id": "https://creativecommons.org/licenses/by-sa/4.0/deed.de" }, "maintainer": "https://serlo.org/", "name": "Kleinstes gemeinsames Vielfaches", "publisher": [ { "id": "https://serlo.org/" } ], "version": "https://serlo.org/224107" } ] } } } }
The following example provides a bash function which you can use to test requests against the entities
property with any combination of arguments.
%%bash
function get_entities_with {
QUERY='
query(
$first: Int,
$after: String,
$instance: Instance,
$modifiedAfter: String
) {
metadata {
entities(
first: $first,
after: $after,
instance: $instance,
modifiedAfter: $modifiedAfter
) {
nodes
pageInfo {
hasNextPage
endCursor
}
}
}
}
'
REQUEST_BODY="{
\"query\": \"$(echo "$QUERY" | tr -d "\n")\",
\"variables\": "$1"
}"
curl https://api.serlo-staging.dev/graphql \
--silent -X POST \
-H "Content-Type: application/json" \
--data "$REQUEST_BODY" | jq -C
}
echo "== Get the first English entity == "
get_entities_with '{ "first": 1, "instance": "en" }'
echo
echo "== Get the following entity with the endCursor of the first request =="
get_entities_with '{ "first": 1, "instance": "en", "after": "MzI5OTY=" }'
echo
echo "== Get an entity which was modified after 2021-11-01 =="
get_entities_with '{ "first": 1, "modifiedAfter": "2021-11-01T00:00:00Z" }'
== Get the first English entity == { "data": { "metadata": { "entities": { "nodes": [ { "@context": [ "https://w3id.org/kim/lrmi-profile/draft/context.jsonld", { "@language": "en" } ], "id": "https://serlo.org/32996", "type": [ "LearningResource", "Article" ], "dateCreated": "2014-11-16T15:02:57+00:00", "dateModified": "2021-10-21T19:48:02+00:00", "description": "", "headline": "Gaussian elimination", "identifier": { "type": "PropertyValue", "propertyID": "UUID", "value": 32996 }, "inLanguage": [ "en" ], "isAccessibleForFree": true, "isFamilyFriendly": true, "learningResourceType": "Article", "license": { "id": "http://creativecommons.org/licenses/by/4.0/" }, "maintainer": "https://serlo.org/", "name": "Gaussian elimination", "publisher": [ { "id": "https://serlo.org/" } ], "version": "https://serlo.org/227897" } ], "pageInfo": { "hasNextPage": true, "endCursor": "MzI5OTY=" } } } } } == Get the following entity with the endCursor of the first request == { "data": { "metadata": { "entities": { "nodes": [ { "@context": [ "https://w3id.org/kim/lrmi-profile/draft/context.jsonld", { "@language": "en" } ], "id": "https://serlo.org/33401", "type": [ "LearningResource", "Article" ], "dateCreated": "2014-11-30T11:27:40+00:00", "dateModified": "2021-08-19T20:29:05+00:00", "description": "", "headline": "System of linear equations", "identifier": { "type": "PropertyValue", "propertyID": "UUID", "value": 33401 }, "inLanguage": [ "en" ], "isAccessibleForFree": true, "isFamilyFriendly": true, "learningResourceType": "Article", "license": { "id": "http://creativecommons.org/licenses/by/4.0/" }, "maintainer": "https://serlo.org/", "name": "System of linear equations", "publisher": [ { "id": "https://serlo.org/" } ], "version": "https://serlo.org/222974" } ], "pageInfo": { "hasNextPage": true, "endCursor": "MzM0MDE=" } } } } } == Get an entity which was modified after 2021-11-01 == { "data": { "metadata": { "entities": { "nodes": [ { "@context": [ "https://w3id.org/kim/lrmi-profile/draft/context.jsonld", { "@language": "de" } ], "id": "https://serlo.org/1525", "type": [ "LearningResource", "Article" ], "dateCreated": "2014-03-01T20:37:31+00:00", "dateModified": "2022-01-07T23:05:21+00:00", "description": "Hier lernst du Schritt für Schritt in verschiedenen Situation (Klammer mal Klammer, Faktor mal Klammer und Produkt in der Klammer) das richtige Ausmultiplizieren mit der Klammer.", "headline": "Klammern ausmultiplizieren", "identifier": { "type": "PropertyValue", "propertyID": "UUID", "value": 1525 }, "inLanguage": [ "de" ], "isAccessibleForFree": true, "isFamilyFriendly": true, "learningResourceType": "Article", "license": { "id": "https://creativecommons.org/licenses/by-sa/4.0/deed.de" }, "maintainer": "https://serlo.org/", "name": "Klammern ausmultiplizieren", "publisher": [ { "id": "https://serlo.org/" } ], "version": "https://serlo.org/234900" } ], "pageInfo": { "hasNextPage": true, "endCursor": "MTUyNQ==" } } } } }
This metadata API is a prototype. Before deploying the metadata API in production we plan to integrate at least the following features:
We look forward to hearing from you if you get in touch with us in case you need a particular feature. You can either open an issue at https://github.com/serlo/api.serlo.org/issues or reach us via de@serlo.org.
In case you are interested in how we generate the metadata you can find the most relevant source code in this pull request. It includes the code to generate the metadata from the data in our database. The metadata is passed from our database layer (see repository serlo.org-database-layer) to our GraphQL server (see repository api.serlo.org) from which it can be fetched. You can find a brief description of our infrastructure on this page.
All of our source code is licensed under the open source license Apache License 2.0 and can be used freely. The full license notice is available here.
This documentation with all code examples are licensed under the CC0 license.
This metadata API is a prototype. It might change in the future depending on the feedback we will receive. You can find the current documentation at https://lenabi.serlo.org/metadata-api If you want to use our metadata API we are happy if get in contact with us. You can reach us at de@serlo.org.