Categorie: api Bijgewerkt: 2026-04-23 rest-api odata processfunction equipment analyse aggregatie

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:

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:

  1. Haal de volledige boom van de gekozen entiteit op.
  2. Bouw lokaal een parent-child index op en bepaal welke nodes onder welke top-sectie vallen.
  3. 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:

Voor managementrapportages, KPI-dashboards en analyses op asset-niveau moet de scope via de boom worden bepaald.


Gerelateerde artikelenbewerken

Brondatabewerken

Dit artikel is consultant-synthese. Voor ground-truth data over specifieke Ultimo-objecten gebruik de onderstaande tools.