Skip to content

fix(harvester): recover records skipped by live pagination - #886

Open
TahaKhan998 wants to merge 1 commit into
CERNDocumentServer:masterfrom
TahaKhan998:fix/issue-437-harvester-missing-records
Open

fix(harvester): recover records skipped by live pagination#886
TahaKhan998 wants to merge 1 commit into
CERNDocumentServer:masterfrom
TahaKhan998:fix/issue-437-harvester-missing-records

Conversation

@TahaKhan998

Copy link
Copy Markdown

Closes #437
INSPIRE search results can change while we're paginating. If a record gets updated mid-harvest, it can jump to a page we already passed, so we never pick it up even though hits.total said it was there.
What we do now: during the normal harvest we keep the IDs we actually saw, plus the total from the first page. If those don't match at the end, we re-query the same search with fields=id to get the full ID list, figure out what's missing (all_ids - seen_ids), and fetch only those records with q=... AND id:... like we already do for single-id harvests. If everything matched on the first pass, we don't do any of that extra work.


# Pagination can skip records when INSPIRE results shift mid-harvest.
# A total mismatch can also mean records entered/left the live query.
if not q or expected_total is None or len(seen_ids) == expected_total:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can a record be added to the set and cause a shift in another direction? what if one record is added and another removed?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah good point, if one record drops out and another joins the total can stay the same and we'd never hit recovery with the count check alone.

i changed it to always do the id scan at the end and recover whatever's in all_ids - seen_ids, so we're comparing the actual sets not just the count.

# Re-scan with IDs only, then harvest the missing records by id.
all_ids = set()
ids_url = self._build_url(q, fields="id")
while ids_url:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what if something shifts in the rescan?

@TahaKhan998 TahaKhan998 Jul 30, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes the rescan can shift too since it's still paginated.

i changed so now we run the id scan twice with size=1000, and if the two passes don't match we union them and log a warning. anything still missing gets fetched individually by id instead of re-paginating the full harvest. i also added a test for this case(test_reader_recovers_when_count_matches).

i also have another approach in mind where we collect all ids upfront at the start and then harvest each record by id instead of paginating through search at all. that'd be more robust but more expensive api-wise, so i kept this for now since it fits the current flow. happy to switch if you'd prefer that.

@TahaKhan998
TahaKhan998 force-pushed the fix/issue-437-harvester-missing-records branch 2 times, most recently from 3c6abbe to 8ee741a Compare August 4, 2026 13:04
@TahaKhan998
TahaKhan998 force-pushed the fix/issue-437-harvester-missing-records branch from 8ee741a to 2f637d7 Compare August 4, 2026 13:28

@palkerecsenyi palkerecsenyi left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, it's a nice and efficient way of doing it! I just have one question

"ID scan found fewer INSPIRE records than reported. "
f"| details: found={len(all_ids)}, reported={scan_total}"
)
retry_ids, _ = self._scan_ids(q, headers)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why would the initial scan for IDs return a total hits count that's less than the number of hits it returned? Surely within one single API request the count would be consistent? Maybe I am misunderstanding this

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It’s not one request, _scan_ids paginates. When we first request records from a query, the first page says we have X records in this query, and that’s what scan_total is. Then when we go through each record and each page, shifting might happen while that happens and we might miss a few records while the ids are being scanned. Then scan_total and the number of ids might not be the same, so we refetch all ids with retry_ids and union the differences between all_ids and retry_ids.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh okay makes sense, thanks for the explanation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

hervester: make sure all records were processed

3 participants