Document Indexing Operations#
Primary file: api/services/dataset_service.py β DocumentService class
This article covers the lifecycle management layer for knowledge base documents: state transitions (retry/pause/recover), batch enable/disable/archive, Redis-based distributed locking, and async Celery task dispatch. For the multi-stage ingestion pipeline (Extract β Clean β Segment β Index), see Knowledge Base Document Processing. For per-tenant Redis FIFO queuing, see Tenant-Isolated Document Indexing Queue.
Retry / Pause / Recover State Management#
Four operations in DocumentService share a common pattern: check Redis flag β mutate DB β set flag β dispatch Celery task. The Redis flag acts as an in-flight guard; a second concurrent call while the flag is live raises immediately.
| Operation | Redis key | TTL | Redis op | Celery task |
|---|---|---|---|---|
retry_document() | document_{id}_is_retried | 600 s | lock() | retry_document_indexing_task |
pause_document() | document_{id}_is_paused | None | setnx | β (signals the running runner) |
recover_document() | document_{id}_is_paused | β | delete | recover_document_indexing_task |
sync_website_document() | document_{id}_is_sync | 600 s | setex | sync_website_document_indexing_task |
retry_document() atomically acquires a 600-second Redis lock for every document in the batch before mutating any state. If any lock acquisition fails (blocking=False), all already-acquired locks are released and the operation raises ValueError. Only after all locks are held does it reset each document's indexing_status to WAITING and commit in a single transaction. On DB commit failure, locks are released and the session is rolled back.
pause_document() is only valid on documents in WAITING/PARSING/CLEANING/SPLITTING/INDEXING states. It sets is_paused = True and calls redis_client.setnx with no TTL β the flag persists until recover_document() explicitly deletes it.
sync_website_document() sets mode: "scrape" on data_source_info, resets status to WAITING, and sets a 600-second SETEX flag before dispatching.
Batch Enable / Disable / Archive Operations#
batch_update_document_status() accepts action β {"enable", "disable", "archive", "un_archive"} and follows a two-pass validate-then-commit pattern :
- Validate: For each document, check if the
document_{id}_indexingRedis key is set. If present, the document is actively being indexed and the call raisesDocumentIndexingError. Then route to an action-specific helper:
| Action | Helper | Celery task dispatched | Sets document_{id}_indexing? |
|---|---|---|---|
enable | _prepare_enable_update() | add_document_to_index_task | β Yes |
disable | _prepare_disable_update() | remove_document_from_index_task | β Yes |
archive | _prepare_archive_update() | remove_document_from_index_task (only if enabled) | Conditional |
un_archive | _prepare_unarchive_update() | add_document_to_index_task (only if enabled) | Conditional |
- Commit then dispatch: All DB mutations (
enabled,archived,disabled_at) are committed in a single transaction. Only after a successful commit are Celery tasks dispatched and thedocument_{id}_indexingkey set viaSETEXwith 600 s TTL. Task dispatch errors are logged but do not trigger a rollback.
Key invariant:
disablerequires the document to already be inCOMPLETEDstatus. All other actions are idempotent β already-in-target-state documents are silently skipped. Thedocument_{id}_indexingkey is a post-commit signal, not a pre-commit lock: its presence tells a subsequent batch call that async index work is already in flight.
Redis-Based Distributed Locking#
redis_client.lock() (a RedisLock context manager) guards critical sections that must not run concurrently across multiple API worker processes.
| Call site | Lock key | Timeout | Purpose |
|---|---|---|---|
save_document_with_dataset_id() | add_document_lock_dataset_id_{dataset_id} | 600 s | Prevents position collisions and duplicate-detection races when multiple requests add files to the same dataset |
create_segment() | add_segment_lock_document_id_{document_id} | 600 s | Prevents position field collisions for concurrent single-segment appends |
multi_create_segment() | multi_add_segment_lock_document_id_{document_id} | 600 s | Same as above for bulk segment creation |
create_child_chunk() | add_child_lock_{segment_id} | 20 s | Shorter TTL is appropriate; child chunk creation is fast and does not require embedding computation |
LockNotOwnedError (raised when the lock expires before the block finishes) is caught and silently swallowed in save_document_with_dataset_id() β the caller receives an empty document list in that case.
Async Celery Task Dispatch#
All task dispatch happens after session.commit() to prevent tasks from running on uncommitted data.
New document creation: After the document row is committed, save_document_with_dataset_id() dispatches via proxy classes that route to the correct Celery queue by deployment edition and subscription plan:
DocumentIndexingTaskProxy(tenant_id, dataset_id, document_ids).delay()β for new documentsDuplicateDocumentIndexingTaskProxy(tenant_id, dataset_id, duplicate_document_ids).delay()β for detected duplicates
For routing details (Cloud vs. self-hosted, dataset vs. priority_dataset queues), see Tenant-Isolated Document Indexing Queue.
Full task inventory dispatched from DocumentService:
| Task | Trigger |
|---|---|
add_document_to_index_task | enable / un_archive (if enabled) |
remove_document_from_index_task | disable / archive (if enabled) |
retry_document_indexing_task | explicit retry |
recover_document_indexing_task | unpause |
sync_website_document_indexing_task | website re-sync |
batch_clean_document_task | document deletion |
document_indexing_update_task | document reprocessing |
enable_segments_to_index_task / disable_segments_from_index_task | segment enable/disable |
delete_segment_from_index_task | segment deletion |
document_indexing_update_task handles document reprocessing: it transitions the document to PARSING, calls IndexProcessorFactory to clean existing vector/keyword index entries (including child chunks), then performs cleanup:
- For multimodal datasets, deletes all
SegmentAttachmentBindingrows for the document, then performs orphan checking: removes attachment vectors (viaindex_processor.clean()withwith_keywords=False) andUploadFilerows only for attachments that are no longer referenced by any remaining bindings, preserving shared attachments - Deletes all
DocumentSegmentrows - After the database commit, deletes orphaned attachment storage objects with per-key error handling
The task then re-runs IndexingRunner.run() from scratch. If the clean step fails, the task recovers by re-setting PARSING state on the document and continuing. After successful completion, it optionally dispatches generate_summary_index_task for HIGH_QUALITY datasets with summary indexing enabled.
Document Deletion Cleanup#
clean_document_task runs on the dataset Celery queue and performs a multi-step teardown when a document is deleted. Steps execute sequentially in separate sessions, so a failure in one phase does not abort subsequent cleanup:
- Collect metadata β fetch
DocumentSegmentrows,SegmentAttachmentBinding+UploadFileattachment records in a single JOIN query. - Vector/keyword index cleanup β call
index_processor.clean()withwith_keywords=True,delete_child_chunks=True,delete_summaries=True. Wrapped intry/except; a transient vector-backend failure logs the error but lets PG/storage cleanup continue. - PG row cleanup β delete image
UploadFilerows extracted from segment content, then allDocumentSegmentrows, then segment attachmentUploadFileandSegmentAttachmentBindingrows, thenDatasetMetadataBindingrows. - Object storage cleanup β delete image files, the source
UploadFile, and multimodal attachment files from the storage backend. Each deletion is individually guarded withtry/except. - Billing refresh β
schedule_billing_vector_space_refresh()is called only if vector cleanup succeeded.
Note: There is no DB cascade from
DocumenttoDocumentSegmentorSegmentAttachmentBinding. Manual deletion through the task is the only cleanup path. Deleting a knowledge base while documents are actively indexing can leave orphaned PG rows if this task runs before indexing completes.
Key Files#
| File | Role |
|---|---|
api/services/dataset_service.py | DocumentService β all lifecycle operations: retry, pause, recover, batch enable/disable/archive, document/segment creation |
api/tasks/document_indexing_update_task.py | Reprocesses a document: cleans old index, deletes segments, re-runs IndexingRunner |
api/tasks/clean_document_task.py | Tears down vector index, PG rows, and object storage on document deletion |
api/core/rag/index_processor/index_processor.py | BaseIndexProcessor β clean() method used by both update and delete tasks |
api/services/document_indexing_proxy/base.py | DocumentTaskProxyBase β deployment-edition-aware Celery queue routing |
api/tasks/enable_segments_to_index_task.py | Re-enables segments in the vector/keyword index |