Indexing Failure Recovery#
When pqa index (or get_directory_index) processes a directory of papers, failures can occur at two distinct layers: PDF/text parsing and metadata fetching. The recovery mechanisms at each layer differ significantly.
Failure Marking and Skip-on-Rerun#
The core persistence primitive is FAILED_DOCUMENT_ADD_ID = "ERROR", a sentinel stored in the SearchIndex.index_files dictionary (a zlib-compressed pickle at {index_directory}/{index_name}/files.zip) in place of a real content hash.
When any exception escapes process_file, the file is immediately marked and the index is flushed to disk :
await search_index.mark_failed_document(file_location)
await search_index.save_index()
mark_failed_document writes "ERROR" as the file's hash. filecheck returns True for any entry in index_files regardless of value, so files marked "ERROR" are permanently skipped on subsequent runs unless the index is manually cleared or rebuilt.
⚠️ Known limitation (open issue): Once a file is marked
ERROR, later index runs silently skip it even when the original failure was transient (e.g., a 429 rate-limit). There is currently no--reprocess-failedCLI flag orIndexSettingsfield to retry them. See issue #1275 for context.
Parse/Add Failures (ValueError, ImpossibleParsingError)#
process_file catches all exceptions from Docs.aadd(), but only swallows ValueError and ImpossibleParsingError :
if not isinstance(e, ValueError | ImpossibleParsingError):
raise
ImpossibleParsingError— raised when a document is fundamentally unparseable (corrupt/unsupported format). Swallowed; the file is markedERRORand the run continues.ValueError— also swallowed (rationale is marked# TODOCin the source).- Any other exception — re-raised into the
anyio.TaskGroup, which cancels the entireget_directory_indexrun.
Metadata Fetch Failures (HTTP 4xx/5xx, Timeouts, Retries)#
Metadata lookup for DOIs and titles goes through DOIOrTitleBasedProvider.query, which catches and suppresses multiple failure modes before they can reach process_file:
| Exception | Behaviour |
|---|---|
DOINotFoundError | Logged as warning, returns None |
aiohttp.ClientResponseError (e.g. 403, 500) | Logged as warning, returns None |
RetryError (tenacity exhausted) | Logged as warning, returns None |
TimeoutError | Logged as warning, returns None |
When a provider returns None, the document is indexed with a plain Doc (no enriched metadata) rather than failing.
Transient HTTP retries are handled upstream in _get_with_retrying, which uses tenacity with up to 5 attempts and incremental backoff for:
aiohttp.ServerDisconnectedErroraiohttp.ClientConnectionResetErroraiohttp.ClientResponseErrorwith status 500 or 504
As of the current codebase (is_retryable), HTTP 429 (Too Many Requests) is not in the retryable set, meaning a 429 from Semantic Scholar escapes to DOIOrTitleBasedProvider.query — where it is caught as aiohttp.ClientResponseError and returns None. PR #1353 (open as of this writing) tracks a related bug where a migration from aiohttp to httpx briefly caused 429s to propagate all the way up and crash index runs.
Concurrency note:
IndexSettings.concurrency(default 5, low intentionally for users without API keys) controls the semaphore over parallelprocess_filecalls. Reducing this value decreases the rate of outbound metadata requests and can reduce 429 frequency. SettingSEMANTIC_SCHOLAR_API_KEY/CROSSREF_MAILTOenvironment variables raises provider rate limits.
Key Source Files#
| File | Role |
|---|---|
paperqa/agents/search.py | process_file, mark_failed_document, filecheck, FAILED_DOCUMENT_ADD_ID |
paperqa/clients/client_models.py | DOIOrTitleBasedProvider.query — graceful metadata failure suppression |
paperqa/utils.py | ImpossibleParsingError, is_retryable, _get_with_retrying |
paperqa/settings.py | IndexSettings (concurrency, batch_size) |