Versioning Query Conventions for Annotations and Search¶
Overview¶
This document defines the architectural conventions for how annotations, vector search, and document processing interact with the dual-tree versioning system. These conventions ensure consistent behavior across all query operations while maintaining historical accuracy and corpus independence.
Fundamental Principles¶
P1: Version Immutability¶
Every annotation, embedding, and processing result is permanently tied to a specific Document version. These artifacts never migrate between versions, preserving complete historical accuracy.
P2: Default to Current¶
All queries default to the latest version (Document.is_current=True) unless a specific version is explicitly requested. This represents the "working set" users interact with.
P3: Corpus Context Primacy¶
Within a corpus context, only documents with active filesystem paths are considered. A document may be globally current but locally deleted in a specific corpus.
Annotation Conventions¶
A1: Annotation Version Binding¶
- Rule: Annotations are created against a specific
document_idwhich IS a version - Implication: When a document is updated, old annotations remain with old version
- Rationale: Page numbers, bounding boxes, and content may change between versions
A2: Structural vs Corpus Annotations¶
Structural Annotations (Global Scope)¶
- Created: During document parsing, tied to specific version
- Immutable: Never change after creation
- Versioning: Each document version has its own structural annotations
- Query Pattern:
# Get structural annotations for current version Annotation.objects.filter( document__version_tree_id=version_tree_id, document__is_current=True, structural=True )
Corpus Annotations (Corpus Scope)¶
- Created: By users/analyses within corpus context
- Version-Specific: Tied to the document version active when created
- Visibility: Only shown if document has active path in corpus
- Query Pattern:
# Get corpus annotations for current version with active path Annotation.objects.filter( document__is_current=True, corpus_id=corpus_id, structural=False ).exclude( # Exclude if document deleted in this corpus document__documentpath__corpus_id=corpus_id, document__documentpath__is_current=True, document__documentpath__is_deleted=True )
A3: Query Defaults¶
Current Version Query (Default)¶
def get_document_annotations(document_path, corpus_id, user):
# Find current document at this path
current_path = DocumentPath.objects.get(
corpus_id=corpus_id,
path=document_path,
is_current=True,
is_deleted=False
)
# Get annotations for current version
return Annotation.objects.filter(
document_id=current_path.document_id,
corpus_id=corpus_id
)
Historical Version Query (Explicit)¶
def get_document_annotations_at_version(document_id, corpus_id, user):
# Direct query for specific version
return Annotation.objects.filter(
document_id=document_id, # Specific version
corpus_id=corpus_id
)
A4: Deletion Behavior¶
- Soft Delete: When document deleted from corpus, annotations remain but are hidden
- Restore: When document restored, previous annotations become visible again
- True Delete: Annotations only removed when no corpus contains the document
Vector Search Conventions¶
V1: Embedding Version Binding¶
- Rule: Each Document version has its own embeddings
- Storage: Embeddings stored with specific document_id
- Immutable: Embeddings never change after generation
V2: Search Scope Rules¶
Default Search (Current Only)¶
def vector_search(query, corpus_id):
# Build base queryset
qs = Annotation.objects.filter(
document__is_current=True # Only current versions
)
if corpus_id:
# Further filter by corpus active paths
qs = qs.filter(corpus_id=corpus_id).exclude(
document__documentpath__corpus_id=corpus_id,
document__documentpath__is_current=True,
document__documentpath__is_deleted=True
)
# Apply vector similarity search
return qs.search_by_embedding(query_vector)
Cross-Version Search (Special Case)¶
def vector_search_all_versions(query, corpus_id):
# Search across all versions (for finding similar historical content)
qs = Annotation.objects.filter(
document__version_tree_id__in=active_trees
)
# ... apply vector search
V3: Corpus Filtering¶
- Active Documents Only: Exclude documents with
is_deleted=Truepaths - Path State Check: Always verify DocumentPath state before including in results
- Performance: Use EXISTS subquery for efficient filtering
Processing Pipeline Conventions¶
PP1: Pipeline Version Targeting¶
- Input: Pipelines receive specific document_id (version)
- Output: Results tied to that specific version
- No Migration: Results don't carry forward to new versions
PP2: Import and Update Flow¶
def import_document(corpus, path, content, user):
# Create or update document (versioning handled here)
doc, status, path_record = import_document(...)
if status in ['created', 'updated']:
# Process the NEW version
parse_document.delay(doc.id) # Specific version
generate_embeddings.delay(doc.id) # Specific version
generate_thumbnail.delay(doc.id) # Specific version
return doc
PP3: Reprocessing Rules¶
- New Version: Always process from scratch
- No Inheritance: Don't copy results from previous versions
- Rationale: Content may have changed, models may have improved
Query Patterns¶
Pattern 1: Get Current Annotations for Path¶
def get_current_annotations_for_path(corpus_id, path):
# Step 1: Find current document at path
current_path = DocumentPath.objects.filter(
corpus_id=corpus_id,
path=path,
is_current=True,
is_deleted=False
).first()
if not current_path:
return Annotation.objects.none()
# Step 2: Get annotations for that version
return Annotation.objects.filter(
document_id=current_path.document_id,
corpus_id=corpus_id
)
Pattern 2: Get All Versions' Annotations¶
def get_all_versions_annotations(corpus_id, path):
# Get all document paths (including historical)
all_paths = DocumentPath.objects.filter(
corpus_id=corpus_id,
path=path
).values_list('document_id', flat=True)
# Get annotations for all versions
return Annotation.objects.filter(
document_id__in=all_paths,
corpus_id=corpus_id
).order_by('document__created')
Pattern 3: Time Travel Query¶
def get_annotations_at_time(corpus_id, path, timestamp):
# Find document state at timestamp
path_at_time = DocumentPath.objects.filter(
corpus_id=corpus_id,
path=path,
created__lte=timestamp
).order_by('-created').first()
if not path_at_time or path_at_time.is_deleted:
return Annotation.objects.none()
# Get annotations created before timestamp
return Annotation.objects.filter(
document_id=path_at_time.document_id,
corpus_id=corpus_id,
created__lte=timestamp
)
Implementation Checklist¶
1. AnnotationQueryOptimizer Updates¶
- Add version-aware query methods
- Update
get_document_annotationsto checkis_current - Add corpus path deletion checks
- Create
get_annotations_for_pathmethod
2. Vector Store Updates¶
- Filter base queryset to current documents
- Add DocumentPath deletion checks
- Update
_build_base_querysetmethod - Add version parameter to search methods
3. GraphQL Updates¶
- Add
versionparameter to annotation queries - Default to current version when not specified
- Update resolvers to use version-aware methods
- Add time-travel query support
4. Frontend Updates¶
- Show version indicator in UI
- Add version selector for historical viewing
- Update annotation queries to handle versions
- Clear indication when viewing non-current version
Migration Considerations¶
Existing Data¶
- All existing annotations are already tied to specific document versions
- After dual-tree migration, these will be on
is_current=Truedocuments - No data migration needed for annotations themselves
Backward Compatibility¶
- Existing queries passing
document_idcontinue to work - New version-aware methods are additive
- GraphQL schema changes are backward compatible
Performance Optimizations¶
Indexes Required¶
-- For efficient current document filtering
CREATE INDEX idx_document_version_current
ON documents(version_tree_id, is_current)
WHERE is_current = TRUE;
-- For corpus path state queries
CREATE INDEX idx_docpath_corpus_current_deleted
ON document_paths(corpus_id, path, is_current, is_deleted)
WHERE is_current = TRUE AND is_deleted = FALSE;
-- For annotation version queries
CREATE INDEX idx_annotation_doc_corpus
ON annotations(document_id, corpus_id);
Query Optimization Tips¶
- Use EXISTS subqueries for path state checks
- Prefetch document and path relationships
- Cache current version lookups
- Use select_related for document version info
Testing Strategy¶
Test Scenarios¶
- Version Update: Annotations don't carry forward
- Soft Delete: Annotations hidden but not lost
- Restore: Previous annotations reappear
- Cross-Corpus: Independent annotation visibility
- Time Travel: Historical state reconstruction
- Search Scope: Only current versions in results
Edge Cases¶
- Document current globally but deleted locally
- Multiple versions with same content hash
- Annotations on documents with no current version
- Search across version boundaries
Conclusion¶
These conventions ensure that: 1. Historical accuracy is preserved 2. Users work with current content by default 3. Corpus independence is maintained 4. Performance remains optimal 5. The system is predictable and consistent
The key insight is that annotations and embeddings are version-specific artifacts that don't migrate. This simplifies reasoning about the system and ensures data integrity across version changes.