Imported from art-institute-of-chicago/data-service-artist-enrichment (
AGENTS.md). Install upstream withnpx skills add art-institute-of-chicago/data-service-artist-enrichment. Copyright stays with the author.
Data Service: Artist Enrichment
Purpose
Ingests artists from the AIC data-aggregator API, matches them against Wikidata via search + SPARQL, and harvests external authority IDs (VIAF, ULAN, LCCN, ISNI, GND, RKD, BnF, NDL, and 20+ more). Falls back to GND (German National Library) when Wikidata has no match.
Architecture
data-aggregator data-service-artist-enrichment
(MySQL, Elasticsearch) (PostgreSQL)
│ │
├── GET /agents/{id} ───────────────►│ AggregatorClient
│ (fields: title, dates, etc.) │
│ │
│ ├── WikidataService
│ │ ├── wbsearchentities (name search)
│ │ ├── wbgetentities (claims/labels)
│ │ ├── sparqlQuery (name+date fallback)
│ │ └── fetchNotableWorkLabels (P800)
│ │
│ ├── FuzzyMatcher (0-100 scoring)
│ │ ├── Name: 0-40 (exact/levenshtein/similar_text)
│ │ ├── Dates: 0-40 (birth+death year match)
│ │ └── Artworks: 0-20 (title cross-ref vs P800)
│ │
│ ├── GndService (fallback)
│ │ └── lobid.org → sameAs links (VIAF/ISNI/LCCN)
│ │
│ └── Stores in artists table
│ ├── vocab_ids (jsonb, all found IDs)
│ ├── wikidata_id
│ ├── enrichment_status
│ └── match_confidence
Files
Core Services (app/Services/)
| File | Role |
|---|---|
AggregatorClient.php |
HTTP client for data-aggregator API. fetchAgent(id), fetchArtworksForArtist(id), fetchArtists(generator). |
WikidataService.php |
Wikidata API wrapper. searchEntities(), getEntities(), sparqlQuery(), extractVocabIds(), extractLabel() (multilingual), fetchNotableWorkLabels(). |
FuzzyMatcher.php |
Scoring engine. score(artist, candidate, artworks) → 0-100. Normalizes names (lowercase, strip punctuation, collapse whitespace). |
ArtistEnrichmentService.php |
Orchestrator. 4 phases: search → fetch entities → fuzzy match → fallback. |
GndService.php |
GND fallback via lobid.org. Extracts sameAs links for cascading IDs. |
Models (app/Models/)
| Model | Table | Purpose |
|---|---|---|
Artist |
artists |
Enriched artist record. vocab_ids (jsonb), wikidata_id, enrichment_status, match_confidence. |
EnrichmentRun |
enrichment_runs |
Per-invocation tracking. Mode, timestamps, counters. |
EnrichmentLog |
enrichment_logs |
Step-by-step audit trail. Input, output, duration per step. |
Command (app/Console/Commands/)
ArtistEnrichCommand.php—artist:enrich
Config (config/)
artist-enrichment.php— Aggregator URL, Wikidata endpoints, vocab property map (25+ PIDs), matching thresholds.
Matching Algorithm
Score = Name (0–40) + Dates (0–40) + Artworks (0–20) = 0–100. Threshold: 50.
Name Match (40 max)
| Condition | Points |
|---|---|
| Exact (normalized) | 40 |
| Levenshtein ratio > 95% | 35 |
| Levenshtein > 85% | 25 |
| similar_text > 90% | 30 |
| similar_text > 80% | 20 |
| similar_text > 70% | 10 |
| Substring contains (>4 chars) | 15 |
Compares all artist names (title, sort_title, alt_titles) against all candidate names (label, aliases, multilingual labels). Returns best pairwise score.
Date Match (40 max — 20 birth + 20 death)
| Year diff | Points |
|---|---|
| Exact | 20 |
| ±1 | 15 |
| ±5 | 5 |
| Missing | 0 (not penalized) |
Artwork Cross-Reference (20 max)
- Base: notable works count × 2, capped at 5
- Deep: fetches P800 labels, compares against aggregator artwork titles (exact, contains, similar_text > 85%). +5 per match, capped at 15.
Threshold Adjustment
When artist has no birth AND no death dates (factory, workshop, institution): threshold scales proportionally (50/100 × 60 = 30) since max available score drops to 60.
SPARQL Fallback
After wbsearchentities, a SPARQL query with name + birth/death runs to catch entities without Wikipedia articles (low search rank). Results prepended before top-N fetch.
Pipeline Flow
artist:enrich
│
├── Phase 1: searchWikidata()
│ ├── wbsearchentities(title, limit=5)
│ ├── sparqlQuery(title, birth, death) ← catches low-rank items
│ └── wbsearchentities(alt_titles, limit=3)
│
├── Phase 2: fetchEntities()
│ ├── Top 8 Q-IDs → wbgetentities(claims, labels, aliases)
│ └── fetchNotableWorkLabels() for P800 cross-ref
│
├── Phase 3: matchCandidates()
│ ├── FuzzyMatcher → score each candidate
│ ├── Adjusted threshold if no dates
│ └── Best score ≥ threshold → return vocab_ids + Q-ID
│
├── Phase 4: enrichViaFallbacks()
│ └── GND (lobid.org) → sameAs → VIAF/ISNI/LCCN
│
└── Store: artist record + enrichment_logs
Commands
# Incremental (scheduled, uses last_success_at)
php artisan artist:enrich
# Full reindex
php artisan artist:enrich --full-reindex
# Single artist
php artisan artist:enrich --artist-id=36467
# Multiple artists
php artisan artist:enrich --artist-id=36467 --artist-id=40669
# Limit batch
php artisan artist:enrich --limit=100
# Verbose: show candidate scores, dates, work cross-refs
php artisan artist:enrich --artist-id=43655 -v
# Dry run (no persistence)
php artisan artist:enrich --artist-id=43655 --dry-run -v
# Custom threshold
php artisan artist:enrich --min-confidence=60
Database
| Table | Key columns |
|---|---|
artists |
id (AIC agent ID PK), vocab_ids (jsonb), wikidata_id, enrichment_status, match_confidence, match_source, last_enriched_at |
enrichment_runs |
command, mode, parameters (jsonb), status, counters, summary (jsonb) |
enrichment_logs |
enrichment_run_id, artist_id, step, status, input/output (jsonb), duration_ms |
Docker
# Located in ~/docker/docker-compose.yml (service: data-service-artist-enrichment)
sail data-service-artist-enrichment php artisan artist:enrich --artist-id=36467
# Access PostgreSQL
sail data-service-artist-enrichment psql -h pgsql -U sail -d data_service_artist_enrichment
Vocab Property Map
Wikidata claims extracted via config/artist-enrichment.php → vocab_map:
| Vocabulary | Wikidata PID | Region |
|---|---|---|
| viaf | P214 | International |
| lccn | P244 | USA |
| isni | P213 | International |
| ulan | P245 | USA (Getty) |
| gnd | P227 | Germany |
| rkdartists | P650 | Netherlands |
| benezit | P2843 | International |
| bnf | P268 | France |
| ndl | P349 | Japan |
| nla | P409 | Australia |
| nkc | P691 | Czech |
| selibr | P906 | Sweden |
| bibs | P1015 | Norway |
| bne | P950 | Spain |
| sikart | P781 | Switzerland |
| nli | P8189 | Israel |
| artnet | P3782 | Commercial |
| europeana | P7704 | EU |
| christies | P4200 | Commercial |
| invaluable | P4927 | Commercial |
| mutualart | P6578 | Commercial |
| saam | P1795 | USA |
| ngv | P2041 | Australia |
| te_papa | P3544 | NZ |
| tate | P7300 | UK |
Known Limitations
- VIAF blocked by Cloudflare — data comes through Wikidata P214 and GND sameAs links instead
- Labels without
enkey — handled byextractLabel()fallback chain (en → en-gb → en-ca → mul → first available) - No-article Wikidata items — caught by SPARQL name+date fallback, prepended to candidates
- Factory/Institution artists — threshold auto-adjusted when no birth/death dates