Hiërarchische scoping via boom-aggregatie
Bij asset-analyses moet je vaak alle jobs, kosten of metingen onder een top-asset aggregeren — bijvoorbeeld "alle jobs onder Kartonmachine 8" of "totale kosten van gebouw A inclusief alle onderdelen". Dit artikel beschrijft het patroon om hiërarchische scope correct af te bakenen via de parent-relatie (PartOfProcessFunction, PartOfEquipment, PartOfBuilding), en waarom tekstmatch op omschrijving geen betrouwbaar alternatief is.
Beschikbaarheid: Professional, Premium, Enterprise (REST API)
Probleem: tekstmatch onderschat of overschat scopebewerken
Een intuïtieve aanpak voor "alle jobs op asset X" is:
$filter=Description like '%X%'
Dit levert twee typen fouten op:
- False positives: jobs met een irrelevante verwijzing naar de naam in de omschrijving worden meegeteld (bijv. "KM8 omgeving opruimen" valt onder gebouwbeheer, niet onder de machine zelf).
- False negatives: jobs op kind-procesfuncties die de naam van het top-asset niet in hun omschrijving hebben, missen (bijv. een job op "Pomp 8-P-101" zonder "KM8" in de tekst).
In praktijkanalyses kan het verschil oplopen tot 50% onder- of overrapportage. Tekstmatch is geschikt voor vrije zoekopdrachten, niet voor aggregatie-scope.
Oplossing: afbakenen via parent-relatiebewerken
Elke hiërarchische entiteit in Ultimo heeft een parent-veld:
| Entiteit | Parent-relatie | Veldnaam |
|---|---|---|
| ProcessFunction | Onderdeel van procesfunctie | PartOfProcessFunction |
| Equipment | Onderdeel van installatie | PartOfEquipment |
| Building | Onderdeel van complex | PartOfBuilding |
| Department | Onderdeel van afdeling | PartOfDepartment |
Het aggregatie-patroon bestaat uit drie stappen:
- Haal de volledige boom van de gekozen entiteit op.
- Bouw lokaal een parent-child index op en bepaal welke nodes onder welke top-sectie vallen.
- Filter de doel-entiteit (Job, Cost, Measurement) op basis van deze set.
Stap 1 — boom ophalenbewerken
Haal alle procesfuncties binnen een prefix-range op, inclusief de parent-relatie:
GET .../object/ProcessFunction
?$filter=Id ge '08' and Id lt '09'
&$expand=PartOfProcessFunction
&$select=Id,Description,PartOfProcessFunction/Id
&$top=1000
Gebruik nextPageLink uit de respons voor volgende pagina's. Zie odata-filters voor de paginatie-regels en de valkuil rond handmatige keyset-paginatie.
Waarom een prefix-range en geen
Id like '08%'? Een range-filter (Id ge 'X' and Id lt 'Y') kan server-side een index-lookup gebruiken;like '08%'forceert een full-scan. Op datasets van duizenden records is dat een meetbaar verschil.
Stap 2 — boom lokaal opbouwenbewerken
In Python, met de opgehaalde records:
# records = [{"Id": "08KM8", "PartOfProcessFunction": {"Id": None}}, ...]
parent_of = {
r["Id"]: (r.get("PartOfProcessFunction") or {}).get("Id")
for r in records
}
def ancestors(node_id):
"""Keten van parent-IDs tot aan de root."""
seen = set()
cur = node_id
while cur and cur not in seen:
seen.add(cur)
yield cur
cur = parent_of.get(cur)
# Top-secties = directe kinderen van de root
ROOT = "08KM8"
top_sections = {nid for nid, parent in parent_of.items() if parent == ROOT}
# Map: elke node -> welke top-sectie hoort het bij?
def section_of(node_id):
for a in ancestors(node_id):
if a in top_sections:
return a
return None
section_members = {sec: set() for sec in top_sections}
for nid in parent_of:
sec = section_of(nid)
if sec:
section_members[sec].add(nid)
Een boom van enkele duizenden procesfuncties is in seconden opgebouwd en is herbruikbaar voor alle verdere queries.
Stap 3 — doel-entiteit filteren op scopebewerken
Zodra je weet welke procesfunctie-IDs onder elke top-sectie vallen, kun je jobs of kosten daarop filteren.
Optie A — één brede query, lokaal aggregerenbewerken
Voor analyses met veel subgroepen is één brede query meestal efficiënter:
GET .../object/Job
?$filter=Status in (1,2,4) and ProcessFunction/Id ge '08' and ProcessFunction/Id lt '09'
&$expand=ProcessFunction
&$select=Id,ProcessFunction/Id,WorkOrderType/Id,TargetDate
&$top=1000
Vervolgens map je elke job in Python naar zijn top-sectie via section_of(job.ProcessFunction.Id) en aggregeer je lokaal.
Optie B — per sectie een aparte querybewerken
Voor dashboards met één specifieke scope:
$filter=Status in (1,2,4) and ProcessFunction/Id in ('08SL', '08SL001', '08SL002', ...)
De in-operator heeft een praktische limiet van circa 100 waarden per filter. Bij meer kinderen split de query in meerdere calls of gebruik optie A.
Volledig voorbeeld: open jobs per top-sectiebewerken
# Stap 1 & 2: boom ophalen en section_members bepalen (zie hierboven)
# Stap 3: alle open jobs in de scope ophalen
jobs = fetch_all(
"Job",
filter="Status in (1,2,4) and ProcessFunction/Id ge '08' and ProcessFunction/Id lt '09'",
expand="ProcessFunction",
select="Id,ProcessFunction/Id,WorkOrderType/Id,TargetDate",
)
# Stap 4: aggregeren per top-sectie
from collections import Counter
per_section = Counter()
unmatched = 0
for j in jobs:
pf_id = j["ProcessFunction"]["Id"]
sec = section_of(pf_id)
if sec:
per_section[sec] += 1
else:
unmatched += 1
for sec, count in per_section.most_common():
print(f"{sec}: {count} open jobs")
print(f"Niet-gematched: {unmatched}")
Valkuilenbewerken
Ontbrekende records tussen paginatie-batchesbewerken
Bij handmatige paginatie ($top + $skip of Id gt 'X') kunnen records die tijdens het ophalen worden toegevoegd of gewijzigd tussen batches ontbreken. Een ontbrekende parent-procesfunctie betekent dat alle kinderen die via die parent naar de top-sectie routeren niet gemapped kunnen worden. Altijd nextPageLink gebruiken.
Orphan-nodesbewerken
Als een procesfunctie een parent heeft die niet in de dataset zit (bijv. door autorisatiefilter op de API key), wordt het kind niet aan een top-sectie toegekend. Controleer bij je aggregatie altijd het aantal "niet-gematchede" records en rapporteer dit als aparte categorie. In praktijk is 1-3% niet-gematched acceptabel; hoger wijst op een scope- of autorisatie-probleem.
Cycli in de hiërarchiebewerken
Ultimo laat technisch gezien cycli toe in parent-child relaties (bijv. A → B → A). De ancestors()-functie hierboven breekt op een seen-set om oneindige lussen te voorkomen. Cycli zijn een data-kwaliteits-bug en horen gerapporteerd te worden aan de klant.
Context-overschrijdende relatiesbewerken
Bij multi-context implementaties (TD + Facilitair + Fleet) kan dezelfde boom objecten uit verschillende contexten bevatten. Filter expliciet op Context als de analyse context-specifiek is, anders tel je facilitaire jobs mee in een technische-dienst-analyse.
Equipment-hiërarchie (identiek patroon)bewerken
Voor Equipment werkt hetzelfde patroon met PartOfEquipment:
GET .../object/Equipment
?$filter=Id ge 'PUMP' and Id lt 'PUMQ'
&$expand=PartOfEquipment
&$select=Id,Description,PartOfEquipment/Id
Dezelfde ancestors-/section-of-logica is van toepassing.
Gebouw-hiërarchie (drie niveaus vast)bewerken
Gebouwen hebben een vaste driedelige structuur: Building → BuildingPart → BuildingFloor. Dit is geen recursieve boom maar een gefixeerde hiërarchie. Voor aggregatie per gebouw volstaat:
$filter=Building/Id eq '0001'
Recursieve ancestor-logica is hier niet nodig.
Wanneer wel tekstmatch gebruiken?bewerken
Tekstmatch blijft nuttig voor:
- Vrije zoekopdrachten door eindgebruikers ("zoek jobs over een pomp").
- Ad-hoc verkennende queries waar precisie niet kritisch is.
- Entiteiten zonder bruikbare hiërarchie (bijv. vrije tekst-categorieën).
Voor managementrapportages, KPI-dashboards en analyses op asset-niveau moet de scope via de boom worden bepaald.
Gerelateerde artikelenbewerken
- odata-filters — OData filter referentie, paginatie, valkuilen
- rest-api — REST API basis
- Assets — Asset tree, procesfunctie vs. equipment
Brondatabewerken
Dit artikel is consultant-synthese. Voor ground-truth data over specifieke Ultimo-objecten gebruik de onderstaande tools.
- Entiteit-data —
lookup_entity("<n>")·lookup_table_schema("<n>")Alle properties, DB-kolomnamen, triggers en computed columns. Bronnen:Entities.xml,database-schema.json. - Workflows per entiteit —
find_workflows("", entity="<n>")Alle Before/After Save events en andere ActionFields voor een entiteit. Bron:workflows.xml. - Schermen —
lookup_screen("<ScreenName>")· Schermen index Schermdefinities incl. tabel, autorisatielevel, screen-level. Bron:ultimo_screens_names.xml. - AET-settings / feature toggles —
find_aet_settings(query)· AET index Feature toggles en systeem-configuratie. Bron:ApplicationElementTreeData.json. - Kennisbank-breed zoeken —
search(query)Doorzoekt alle wiki-artikelen, entities, workflows, schermen, templates en ActionFields tegelijk.