Types de liens de proximité
Déclarez des relations spatiales (et spatio-temporelles) entre deux types d'objets porteurs de coordonnées — « cet aéronef se trouvait à moins de 50 km de cette base militaire au moment de l'observation » — sous forme de types de liens de premier ordre, gouvernés et interrogeables dans votre extension d'ontologie.
Cette page couvre les types de liens de proximité — une nouvelle stratégie d'inférence d'arêtes ajoutée à côté de la stratégie existante de jointure par valeur ("reference"). Elle complète le guide d'authoring des ontologies. Lisez d'abord ce guide si vous découvrez les extensions d'ontologie.
Qu'est-ce qu'un lien de proximité ?
Les types de liens standard d'une extension d'ontologie infèrent les arêtes par jointure par valeur — une propriété de l'objet source doit être égale à la colonne d'identité de l'objet cible (sémantique de clé étrangère). Cela fonctionne bien pour Payment → Counterparty ou Aircraft → AircraftType, où une clé partagée existe.
De nombreuses relations du monde réel sont spatiales ou spatio-temporelles, et non fondées sur l'égalité de clés :
- Un aéronef se trouvait près d'une base militaire (aucun ID partagé).
- Une observation de drone se trouvait près d'un aéronef au même moment.
- Un point d'atterrissement de câble sous-marin se trouve près d'une zone surveillée.
Ces relations ne peuvent pas être modélisées comme des jointures par valeur. Un type de lien kind: "proximity" infère les arêtes par distance géodésique sur l'ellipsoïde WGS84 (et, facultativement, par une fenêtre temporelle), de sorte que ces relations deviennent des arêtes de premier ordre, gouvernées et interrogeables — visibles dans le graphe d'instances, accessibles depuis la carte, le chat et le SDK.
Reference et proximity en un coup d'œil
"reference" (par défaut) | "proximity" | |
|---|---|---|
| Inférence des arêtes | Jointure par valeur sur une colonne clé | Distance géodésique WGS84 (+ fenêtre temporelle facultative) |
| Champ requis | — | maxMeters |
| Champ facultatif | — | maxMinutes, six surcharges *LatProperty/*LngProperty/*TimeProperty |
| Cardinalité | Déclarée par lien | Implicitement N-N (un nombre quelconque de cibles à portée) |
| Extensions existantes | Inchangées — kind vaut "reference" par défaut | — |
Déclarer un lien de proximité
Ajoutez kind: "proximity" et maxMeters à une entrée defineLinkType(...) dans defineOntology({ linkTypes: [...] }) :
import { defineOntology, defineLinkType } from '@scrydon/sdk-authoring/ontologies'
export default defineOntology({
id: 'my-osint-extension',
version: '1.0.0',
displayName: 'My OSINT Extension',
description: 'Aircraft, bases, and cable infrastructure.',
maturity: 'preview',
author: { name: 'Acme Intelligence' },
objectTypes: [/* ... Aircraft, MilitaryBase ... */],
linkTypes: [
defineLinkType({
slug: 'AircraftNearBase',
displayName: 'Aircraft near base',
sourceTypes: ['Aircraft'],
targetTypes: ['MilitaryBase'],
kind: 'proximity', // NEW — edge inferred by geodesic distance
maxMeters: 50000, // 50 km threshold (required for proximity)
}),
],
bindings: [/* ... */],
})La plateforme calcule les arêtes au moment de la requête sur la projection gouvernée des instances Aircraft et MilitaryBase. Aucun nouveau chemin de lecture, aucune requête brute en base de données — les mêmes règles DLP/RLS/d'habilitation qui s'appliquent à la carte et au chat régissent déjà les nœuds chargés.
Référence du schéma
| Champ | Type | Requis | Remarques |
|---|---|---|---|
slug | Slug | Oui | Identifiant unique, par ex. "AircraftNearBase". |
displayName | string | Oui | Libellé d'arête lisible affiché sur le graphe. |
description | string | Non | Description plus longue facultative. |
sourceTypes | [string] | "*" | Oui | Exactement un slug de type d'objet source, ou "*" pour un lien de compatibilité générique. La V1 ne prend pas en charge les extrémités en union. |
targetTypes | [string] | "*" | Oui | Exactement un slug de type d'objet cible, ou "*" pour un lien de compatibilité générique. La V1 ne prend pas en charge les extrémités en union. |
kind | "proximity" | Oui | Doit valoir "proximity" pour activer l'inférence spatiale. |
maxMeters | number | Oui | Seuil spatial en mètres, doit être > 0. Une arête n'est créée que lorsque la distance géodésique WGS84 entre la source et la cible est ≤ maxMeters. Il s'agit de la distance ellipsoïdale, et non d'une approximation sphérique : elle peut donc différer d'un calcul haversine d'environ 0,5 % au plus. |
maxMinutes | number | Non | Fenêtre temporelle en minutes. S'applique uniquement lorsque les instances source et cible exposent toutes deux un horodatage. Omettez-la pour des relations purement spatiales. |
sourceLatProperty | string | Non | Surcharge le nom de la colonne de latitude de la source. |
sourceLngProperty | string | Non | Surcharge le nom de la colonne de longitude de la source. |
sourceTimeProperty | string | Non | Surcharge le nom de la colonne d'horodatage de la source. |
targetLatProperty | string | Non | Surcharge le nom de la colonne de latitude de la cible. |
targetLngProperty | string | Non | Surcharge le nom de la colonne de longitude de la cible. |
targetTimeProperty | string | Non | Surcharge le nom de la colonne d'horodatage de la cible. |
Convention de coordonnées
La plateforme recherche les coordonnées dans les propriétés de chaque instance à l'aide d'une échelle de conventions. Vous n'avez besoin des six champs de surcharge que lorsque votre type d'objet utilise des noms de colonnes non standard.
Latitude — essayés dans l'ordre : latitude, lat
Longitude — essayés dans l'ordre : longitude, lng, lon
Horodatage — essayés dans l'ordre : observedAt, seenAt, timestamp, time, dateUtc, sentAt ; puis toute propriété dont le nom se termine par At ou time (sans tenir compte de la casse). Si aucun horodatage n'est trouvé, l'instance est traitée comme un point statique (la fenêtre temporelle est ignorée pour ce côté).
Contrôle de plage valide : la latitude doit être dans [-90, 90] et la longitude dans [-180, 180]. Les instances dont les coordonnées sont hors plage ou non numériques sont ignorées en silence — elles ne produisent aucune arête de proximité.
Quand utiliser les surcharges de coordonnées
Utilisez les champs de surcharge par côté lorsque les colonnes de coordonnées d'un type ne suivent pas la convention. Par exemple, un type SubmarineCable peut stocker les coordonnées de son point d'atterrissement sous anchorLatitude/anchorLongitude :
defineLinkType({
slug: 'AircraftNearCable',
displayName: 'Aircraft near cable landing',
sourceTypes: ['Aircraft'],
targetTypes: ['SubmarineCable'],
kind: 'proximity',
maxMeters: 20000,
// Aircraft uses standard lat/lng — no source override needed.
// SubmarineCable uses non-standard column names — override the target side:
targetLatProperty: 'anchorLatitude',
targetLngProperty: 'anchorLongitude',
})Le côté Aircraft résout ses coordonnées depuis latitude/longitude par convention ; le côté SubmarineCable les résout explicitement depuis anchorLatitude/anchorLongitude.
Purement spatial ou spatio-temporel
Purement spatial (sans maxMinutes) : une arête est créée chaque fois que la source et la cible se trouvent à moins de maxMeters. Les propriétés d'horodatage des types n'entrent pas en compte.
defineLinkType({
slug: 'BaseNearCable',
displayName: 'Base near cable landing',
sourceTypes: ['MilitaryBase'],
targetTypes: ['SubmarineCable'],
kind: 'proximity',
maxMeters: 30000, // purely spatial — no time window
targetLatProperty: 'anchorLatitude',
targetLngProperty: 'anchorLongitude',
})Spatio-temporel (maxMinutes défini) : une arête est créée lorsque la source et la cible se trouvent à moins de maxMeters et que les deux côtés exposent un horodatage, ces horodatages étant à moins de maxMinutes l'un de l'autre. Si l'un ou l'autre côté n'a pas d'horodatage résoluble, la fenêtre temporelle est ignorée en silence et l'arête est décidée sur la seule distance.
defineLinkType({
slug: 'DroneObservationNearAircraft',
displayName: 'Drone observation near aircraft',
sourceTypes: ['DroneObservation'],
targetTypes: ['Aircraft'],
kind: 'proximity',
maxMeters: 25000,
maxMinutes: 30, // both sides carry timestamps — time window applies
})DroneObservation porte observedAt et Aircraft porte seenAt — tous deux sont résolus par l'échelle de conventions, aucune surcharge n'est donc nécessaire.
Exemple complet — AircraftNearBase
Une extension d'ontologie complète avec deux types d'objets et un lien de proximité :
import {
defineOntology,
defineObjectType,
defineProperty,
defineLinkType,
defineIdentityRule,
defineBinding,
} from '@scrydon/sdk-authoring/ontologies'
export default defineOntology({
id: 'military-situational-awareness',
version: '1.0.0',
displayName: 'Military Situational Awareness',
description: 'Aircraft positions and military bases — connected by proximity.',
maturity: 'preview',
author: { name: 'Acme Intelligence Team' },
objectTypes: [
defineObjectType({
slug: 'Aircraft',
displayName: 'Aircraft',
kind: 'analytical',
properties: [
defineProperty({ slug: 'callsign', displayName: 'Callsign', scalarType: 'string', cardinality: 'required' }),
defineProperty({ slug: 'latitude', displayName: 'Latitude', scalarType: 'float', cardinality: 'required' }),
defineProperty({ slug: 'longitude', displayName: 'Longitude', scalarType: 'float', cardinality: 'required' }),
defineProperty({ slug: 'altitude', displayName: 'Altitude (m)', scalarType: 'float' }),
defineProperty({ slug: 'seenAt', displayName: 'Seen at', scalarType: 'timestamp' }),
],
}),
defineObjectType({
slug: 'MilitaryBase',
displayName: 'Military Base',
kind: 'analytical',
properties: [
defineProperty({ slug: 'name', displayName: 'Name', scalarType: 'string', cardinality: 'required' }),
defineProperty({ slug: 'country', displayName: 'Country', scalarType: 'string' }),
defineProperty({ slug: 'latitude', displayName: 'Latitude', scalarType: 'float', cardinality: 'required' }),
defineProperty({ slug: 'longitude', displayName: 'Longitude', scalarType: 'float', cardinality: 'required' }),
],
}),
],
linkTypes: [
defineLinkType({
slug: 'AircraftNearBase',
displayName: 'Aircraft near base',
description: 'Aircraft within 50 km of a military base.',
sourceTypes: ['Aircraft'],
targetTypes: ['MilitaryBase'],
kind: 'proximity',
maxMeters: 50000, // 50 km
// Both types use standard latitude/longitude column names — no overrides needed.
// MilitaryBase has no timestamp — purely spatial.
}),
],
identityRules: [
defineIdentityRule({ objectTypeSlug: 'Aircraft', deterministicKeys: ['callsign'] }),
defineIdentityRule({ objectTypeSlug: 'MilitaryBase', deterministicKeys: ['name'] }),
],
bindings: [
defineBinding({
objectTypeSlug: 'Aircraft',
kind: 'silver_table',
source: { table: 'silver_aircraft_positions' },
identitySpec: { keys: ['callsign'] },
columnMap: {
callsign: 'callsign',
latitude: 'lat',
longitude: 'lon',
altitude: 'altitude_m',
seenAt: 'seen_at',
},
}),
defineBinding({
objectTypeSlug: 'MilitaryBase',
kind: 'silver_table',
source: { table: 'silver_military_bases' },
identitySpec: { keys: ['name'] },
columnMap: {
name: 'base_name',
country: 'country_code',
latitude: 'lat',
longitude: 'lon',
},
}),
],
})Une fois cette extension installée, le graphe d'instances trace des arêtes libellées « Aircraft near base » entre tout aéronef et toute base situés à moins de 50 km. Comme MilitaryBase n'a pas de propriété d'horodatage, la relation est purement spatiale — la distance est le seul critère.
Surcharge du rayon dans la vue graphe
Sur le canevas graphe d'Analytics, vous pouvez faire glisser un curseur de rayon sous le panneau Display pour essayer différents seuils sans modifier l'extension. Le curseur envoie un proximityOverride facultatif au point de terminaison du graphe d'instances, qui surcharge maxMeters (et facultativement maxMinutes) pour la seule session de vue en cours — les valeurs par défaut déclarées par l'extension restent inchangées.
Vous pouvez ainsi répondre à « montre-moi tout ce qui se trouve à moins de 200 km » d'un simple glissement, plutôt que de réécrire l'extension.
La surcharge du rayon est globale pour la session de vue en cours — elle s'applique simultanément à tous les liens de proximité de l'ontologie. Le réglage par lien est une évolution prévue.
Dépannage
Aucune arête de proximité n'apparaît
-
Aucun des deux côtés n'a de coordonnées résolubles. Vérifiez que les types d'objets source et cible possèdent des propriétés de latitude et de longitude avec des valeurs dans les colonnes attendues. L'échelle de conventions (
latitude/lat,longitude/lng/lon) doit correspondre à une colonne contenant une valeur numérique dans[-90, 90]×[-180, 180]. Utilisez les surchargestargetLatProperty/targetLngPropertylorsque les colonnes ont des noms non standard. -
Le seuil est trop petit pour les données réelles. Augmentez temporairement
maxMeters(ou utilisez le curseur de rayon dans la vue graphe) pour confirmer la présence de données. Un seuil de 50 km convient pour aéronef-base ; une infrastructure de câbles répartie sur des bassins océaniques peut nécessiter 200 km ou plus. -
Les types d'objets ne sont pas liés. L'inférence de proximité ne s'exécute que sur les instances chargées dans le graphe — ce qui nécessite que le type d'objet dispose d'une liaison fonctionnelle. Vérifiez que vos entrées
defineBinding(...)sont présentes et que la table silver amont contient des lignes. -
Un seul côté a des coordonnées. Si le type source n'a pas de coordonnées résolubles, il ne fournit aucun point source et aucune arête n'est émise. Les deux côtés doivent avoir des coordonnées.
Spatio-temporel : la fenêtre temporelle ne filtre pas comme prévu
La fenêtre temporelle (maxMinutes) est appliquée uniquement lorsque les instances source et cible exposent toutes deux un horodatage résoluble. Si un côté n'a pas d'horodatage, le critère temporel est ignoré en silence et l'arête est décidée sur la seule distance. C'est voulu — un objet statique (par ex. une MilitaryBase sans horodatage) est toujours une cible de proximité valide, quel que soit le moment où une source a été observée.
Si vous souhaitez un filtrage temporel strict des deux côtés, vérifiez que les deux types possèdent une propriété d'horodatage conforme à la convention (observedAt, seenAt, timestamp, time, dateUtc, sentAt, ou toute propriété se terminant par At/time). Utilisez les surcharges sourceTimeProperty/targetTimeProperty lorsque les colonnes ont des noms non standard.