Pipeline merging
What is merging and why do we need it
The pipeline feeds from different sources. Records from different sources may contain information relating to other records withing the same source or in a different one. We want to present this information in the catalogue API in a unified view to present the user with as much context as we can and to improve discoverability.
To achieve this the pipeline merges works by:
extracting links to other works from the source data
constructing a graph from those links and identifying sets of connected works in the graph
selecting one of a set of connected works as target
adding relevant information to the target from the other works in the set
redirecting all the others works in the set to the target
What happens when merging?
These are a few cases that may happen when merging:
A:1 -> B:1 if A:1 is the canonical results in:
A:1 merged with B:1
B:1 redirected to A:1
A:1 -> B:1 -> C:1 if A:1 is the canonical results in:
A:1 merged with B:1 and C:1
B1: redirected to A:1
C:1 redirected to A:1
assuming the previous case has happened, an update comes for B:2 which no longer links to C:1:
A:1 merged to B:2
B:2 redirected to A:1
C:1 no longer redirected
How this currently works
Matcher
The matcher receives individual works and updates stores the state of the graph.
It receives A:1 which says that A:1 -> B:1
it stores the link A:1 -> B1
it stores A:1 and B:1 as belonging to the same component AB
it sends
[[A:1, B:1]]
to the merger
It receives B:1 -> C:1
it stores the link B:1 -> C:1
Updates A:1, B:1 And C:1 as belonging to ABC
it sends
[[A:1, B:1, C:1]]
to the merger
It receives B:2 breaking the link to C:1
it stores B:2 with no links removing the link to C:1
Updates A:1, B:2 ad belonging to AB
it sends
[[A:1,B:2],[C:1]]
to the merger
Merger
The merger reads the ids and versions received by the matcher from the recorder store and decides if and how to merge based on internal rules. If it decides to merge it updates the works as:
Fills
numberOfSources
field on the work selected as target with the number of works merged into itModifies all the other works as redirected poiting to the target
if it decides not to merge, it sends the works unchanged
Previous behaviour
At some point (ie probably a few weeks ago) the merger used to send a merged
boolean flag to the ingestor that basically indicated if the work had been tampered with by the merger.
Ingestor
Because updates to works can get to the ingestor out of order, the ingestor assigns a version to each work. Works are ingested if they're version is greater or equal than the one already present in the index.
The version is calculated as based on the transformer version and the number of sources: transformerVersion*1000 + numberOfSources
.
This means that
a new
transformerVersion
always gets ingesteda work for the same
transformerVersion
and more sources gets ingested if there are already works with the sametransformerVersion
but less sources
Previous behaviour
The ingestor used to calculate the version as transformerVersion * 10 + (merged?1:0)
This meant that
a new
transformerVersion
always got ingestedan out of order updates from the merger for the same
transformerVersion
might have been ingested incorrectly cause they would have all the same elastic search versionunlink kind of worked because
merged
would have been set totrue
so, given elastic search greater or equal than versioning, the unlinked version would have been ingested (provided no message out of order issues)
Problems
There are a number of issues with the current approach:
We need to multiply
transformerVersion
by 1000 to make it take precedence overnumberOfSources
becausenumberOfSources
can be quite high (650 is the recorded max). If we don't do that we run into data consistency problems in the index. As we add more sources we will find ourselves incrementing this number regularlyThe unliking case only works if the update that causes the unlink is on the target work, therefore incrementing
transformerVersion
. If, as in the example above, the update is on a work that gets redirected to the target, it won't be reflected in the APIThe ingestor is responsible for figuring out the version based on information passed on by the merger. This causes coupling between the merger and the ingestor. The ingestor is currently aware of merging happening at some point, which it shouldn't be. It also makes the versioning logic very hard to follow and modify
Discarded ideas
Jamie and I came up with multiple ideas to tweak the current behaviour, each one with some problems:
Go back to the
merged
flag: it has issues with multiple sources and it has issues with images versioning because image versions are derived from worksAdd unlink in the version function like
(transformerVersion * 10000) + (unlinked?1:0)*1000 + numberOfSources
: bleurgh and also when something is ingested as unlinked it's impossible to override it unless thetransformerVersion
changesUse something like the sum of all
transformerVersion
s of all works sent by the matcher: this would succeed in unlinking, but again would fail to add a work to a group that was the result of a previous unlink operation
Solution proposed
The solution suggested by Nic is that we pass through in the work a field with the timestamp of the last update. The merger should then use the maximum of the timestamps of all works sent by the matcher as the version for each of the works. The ingestor doesn't need to use greater than or equal to versioning anymore, but we will leave it so that we don't necessarily have to reindex in a new index if we change transformer rules.
The updated timestamp needs to be recorded in the adapter in different ways depending on the source:
Sierra: the sierra API already provides a timestamp with down to the second granularity for each update. The Sierra adapter already stores it in the Sierra VHS. The sierra transfomer can read this value and copy it into the work.
Miro: Miro data doesn't change so we can just set it to zero in the transformer
Calm: Calm data has a
retrievedAt
field in the Calm VHS. It comes from the HTTP header of the response from the Calm API and it just represents when the response was sent from the server. There is also aModified
field which indicates when the record was modified in Calm, but it has a down to the day granularity. Therefore, we will useretrievedAt
.METS: there is currently no info about when a bag was created/updated in the adapter store. However, this is provided by the storage service in the
createdDate
field in the bag response, so we can change the adapter to read and store this. This requires an adapter change and a VHS migration
Last updated