Relationships drive the "Has part" / "Is part of" links on resource detail pages and the "Browse all X records..." search filter.
The field dct_isPartOf_sm is defined in the index mapping (text + keyword subfield) so the "Browse all" filter works. If the index was created before this mapping was added, recreate the index and run make reindex, or run a full reindex so that documents include dct_isPartOf_sm and (if dynamic mapping was used) the keyword subfield is available for filtering.
- DB table:
resource_relationships(subject_id, predicate, object_id). Populated from theresourcestable columns (e.g.dct_isPartOf_sm,pcdm_memberOf_sm). - Import behavior: OpenGeoMetadata harvests and Kithe Bridge syncs sync
resource_relationshipsautomatically for imported records and unchanged resources that point at them. This includes inverse replacement links such asdct:isReplacedByfrom a new record'sdct_replaces_smvalue. - Source direction: A child stores its parent ID in
dct_source_sm. The relationship table exposes that asdct:sourceon the child and the inversedct:isSourceOfon the parent. Resource pages label these "Source record" and "Derived records," respectively. - Make task (from project root):
Runs
make populate-relationships
scripts/populate_relationships.pyinside the API container. Use this after bulk imports that do not already sync relationships incrementally, or when relationship-sync code changes and you want a full rebuild. - Search filter: "Browse all X records..." uses:
- Has part:
include_filters[dct_isPartOf_sm][]=<parent_id>— children must have the parent ID inresources.dct_isPartOf_sm. - Collection records:
include_filters[pcdm_memberOf_sm][]=<collection_id>— member resources must have the collection ID inresources.pcdm_memberOf_sm. Filters are applied in Elasticsearch using the indexed fields (with.keywordfor exact match). So:
- Run
make populate-relationshipsafter manual/bulk DB edits that bypass the normal importers. - Run
make reindexafter manual/bulk DB edits so Elasticsearch has up-to-datedct_isPartOf_smandpcdm_memberOf_smfor the search filters.
- Has part:
Use the ParadeDB (PostgreSQL) container. From the project root, with Docker running:
docker compose exec paradedb psql -U postgres -d btaa_geospatial_api -c "..."Replace eee6150b-ce2f-4837-9d17-ce72a0c1c26f with your resource ID.
Rows in resource_relationships where this resource is the parent (dct:hasPart):
SELECT predicate, subject_id AS child_id, object_id AS parent_id, r.dct_title_s AS child_title
FROM resource_relationships rr
JOIN resources r ON r.id = rr.subject_id
WHERE rr.object_id = 'eee6150b-ce2f-4837-9d17-ce72a0c1c26f'
AND rr.predicate = 'dct:hasPart'
ORDER BY r.dct_title_s;These are the documents that should match the "Browse all" search filter.
SELECT id, dct_title_s, "dct_isPartOf_sm"
FROM resources
WHERE "dct_isPartOf_sm" IS NOT NULL
AND 'eee6150b-ce2f-4837-9d17-ce72a0c1c26f' = ANY("dct_isPartOf_sm")
ORDER BY dct_title_s;If this returns 0 rows, the DB doesn’t have any children with that parent ID in dct_isPartOf_sm; the search filter will correctly return 0 results. If it returns 25 rows but search still returns 0, run make reindex so Elasticsearch gets the updated dct_isPartOf_sm values.
SELECT predicate, COUNT(*) FROM resource_relationships GROUP BY predicate ORDER BY predicate;