Merge entities#
The merge script processes CSV files with merge instructions and consolidates duplicate entities. Each run merges one CSV file against one triplestore snapshot; the triplestore must be re-indexed from the RDF files before the next run.
Usage#
uv run python -m oc_meta.run.merge.entities <CSV_PATH> <META_CONFIG> <RESP_AGENT> [OPTIONS]
Argument |
Description |
|---|---|
|
Merge instruction CSV file, or folder with merge instruction CSVs |
|
Path to Meta config file |
|
Responsible agent URI for provenance |
Option |
Default |
Description |
|---|---|---|
|
ra br id |
Entity types to merge (space-separated) |
|
stop.out |
File to trigger graceful stop |
Examples#
Basic merge:
uv run python -m oc_meta.run.merge.entities \
merges/ \
meta_config.yaml \
https://w3id.org/oc/meta/prov/pa/1
Merge only bibliographic resources:
uv run python -m oc_meta.run.merge.entities \
merges/ \
meta_config.yaml \
https://w3id.org/oc/meta/prov/pa/1 \
--entity_types br
CSV input format#
Each CSV file should have:
surviving_entity,merged_entities
https://w3id.org/oc/meta/br/0601,https://w3id.org/oc/meta/br/0602; https://w3id.org/oc/meta/br/0603
merged_entities accepts semicolon-separated values with or without spaces around the semicolon.
Use output from find duplicates.
What the merge does#
For each row, the script:
Loads the merge closure from the triplestore. For a batch containing only identifiers, the closure consists of the surviving and merged identifiers plus every entity connected through
datacite:hasIdentifierto an identifier that will be deleted. Other batches use the general closure: the entities named in the row, theirfrbr:partOfcontainer chain (issue, volume, journal), every entity that refers to any of these, and every agent role attached to the loaded bibliographic resources together with its responsible agent and identifiers. Loading the entities that refer to a resource that may be deleted keeps the merge from leaving dangling references.Copies identifiers from merged entities to the surviving entity
Fills metadata gaps (title, date, etc.) from merged entities
Merges matching containers: an issue, volume or journal of a merged entity is merged into the survivor’s container of the same type only when the two are equivalent (they share an identifier, or have the same sequence number, or the same title). Containers that disagree are kept distinct, and a level is merged only if its parent level was merged. The publisher role follows the same rule, keyed on the responsible agent behind it.
Updates references in other entities pointing to any merged entity or merged container
Keeps a single author/editor chain per role type through
oc_graphenricher: the surviving entity’s chain wins and the merged entities’ chains of that type are discarded, including on equivalent containers merged through the container cascade. When the survivor has no chain of a role type, the merged entity with the most roles of that type donates its chain, so the survivor never ends up with two parallel chains nor loses the only one available. Duplicate publisher roles held by the same responsible agent are collapsed as well.Records provenance for the merge operation
Marks merged entities as merged
Writes updated RDF back to files
Within a batch, clusters are merged in a fixed order regardless of the CSV row order: identifier rows first, then responsible agents, then bibliographic resources from the top of the container hierarchy down (journal, volume, issue, article). A merge can delete entities other rows name (a BR or RA merge deduplicates identifiers with the same scheme and literal, and the container cascade deletes merged containers), so every cluster runs before anything can delete its entities. If a cluster still names a deleted entity, the batch fails before writing anything.
Identifier-only batches find incoming references with VALUES queries sent by POST in groups of 1000 identifiers, then import the resulting closure in groups of 1000 entities. References that point only to surviving identifiers are not loaded because they do not change. Mixed and non-identifier batches retain the general closure and groups of 10 entities.
The script does not upload to the triplestore. It processes a batch of merges against a single triplestore snapshot held in memory and writes the result to files; the triplestore is re-indexed from those files afterwards.
One file per run#
A run merges the first CSV file with pending rows and then stops: applying a second file without re-indexing would read triplestore state the first file already changed, resurrecting deleted entities in the RDF files and losing merged metadata. To make the biggest possible batch, put as many rows as possible in a single CSV file.
After a file is merged the script writes a reindex_required.out sentinel next to the CSV file or CSV folder, and the next invocation refuses to start while the sentinel exists. The cycle is:
Run the merge command: the first pending CSV file is merged and the sentinel is created.
Re-index the triplestore from the RDF files.
Delete
reindex_required.out.Run the command again for the next file.
Each file is merged as a single batch: one triplestore snapshot is loaded into one in-memory graph, so merges that touch a shared entity (for example the same journal reached through the container cascade) see each other’s changes. Nothing runs in parallel.
Programmatic use#
Merge instructions can also be passed in memory, without CSV files:
from oc_meta.run.merge.entities import EntityMerger
merger = EntityMerger("meta_config.yaml", "https://w3id.org/oc/meta/prov/pa/1")
merger.process_rows(
[
{
"surviving_entity": "https://w3id.org/oc/meta/br/0601",
"merged_entities": ["https://w3id.org/oc/meta/br/0602"],
}
]
)
process_rows merges the whole list as one batch and writes the RDF files, without any CSV bookkeeping.
Graceful interruption#
To stop processing cleanly:
touch stop.out
The script will:
Finish current merge operations
Save progress
Exit with status code 0
To resume, run the same command again. Already-processed files are skipped.
Errors#
If processing a CSV file fails, the command raises the error and exits unsuccessfully. Rows from the failed file remain Done=False and no sentinel is written, so rerunning the command retries them after the cause is fixed.
Progress tracking#
The script stores progress in the CSV Done column. Each file is merged as a single batch: its rows are all marked done together once the batch is written. Files whose rows are all done are skipped, so after re-indexing and removing the sentinel the next run picks up the next pending file.
For long-running merges, the log reports each phase (closure size, entities imported, merges applied).