LRU cache for DoclingDocument
This commit is contained in:
parent
a5d8af07e9
commit
d267b2c433
3 changed files with 123 additions and 3 deletions
|
|
@ -1,12 +1,33 @@
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from cachetools import LRUCache
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from docling_core.types.doc.document import DoclingDocument
|
from docling_core.types.doc.document import DoclingDocument
|
||||||
|
|
||||||
|
|
||||||
|
_docling_document_cache: LRUCache[str, "DoclingDocument"] = LRUCache(maxsize=100)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_cached_docling_document(document_id: str, json_str: str) -> "DoclingDocument":
|
||||||
|
"""Get or parse DoclingDocument with LRU caching by document ID."""
|
||||||
|
if document_id in _docling_document_cache:
|
||||||
|
return _docling_document_cache[document_id]
|
||||||
|
|
||||||
|
from docling_core.types.doc.document import DoclingDocument
|
||||||
|
|
||||||
|
doc = DoclingDocument.model_validate_json(json_str)
|
||||||
|
_docling_document_cache[document_id] = doc
|
||||||
|
return doc
|
||||||
|
|
||||||
|
|
||||||
|
def invalidate_docling_document_cache(document_id: str) -> None:
|
||||||
|
"""Remove a document from the DoclingDocument cache."""
|
||||||
|
_docling_document_cache.pop(document_id, None)
|
||||||
|
|
||||||
|
|
||||||
class Document(BaseModel):
|
class Document(BaseModel):
|
||||||
"""
|
"""
|
||||||
Represents a document with an ID, content, and metadata.
|
Represents a document with an ID, content, and metadata.
|
||||||
|
|
@ -25,12 +46,18 @@ class Document(BaseModel):
|
||||||
def get_docling_document(self) -> "DoclingDocument | None":
|
def get_docling_document(self) -> "DoclingDocument | None":
|
||||||
"""Parse and return the stored DoclingDocument.
|
"""Parse and return the stored DoclingDocument.
|
||||||
|
|
||||||
|
Uses LRU cache (keyed by document ID) to avoid repeated parsing.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The parsed DoclingDocument, or None if not stored.
|
The parsed DoclingDocument, or None if not stored or no ID.
|
||||||
"""
|
"""
|
||||||
if self.docling_document_json is None:
|
if self.docling_document_json is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
from docling_core.types.doc.document import DoclingDocument
|
# No caching for documents without ID
|
||||||
|
if self.id is None:
|
||||||
|
from docling_core.types.doc.document import DoclingDocument
|
||||||
|
|
||||||
return DoclingDocument.model_validate_json(self.docling_document_json)
|
return DoclingDocument.model_validate_json(self.docling_document_json)
|
||||||
|
|
||||||
|
return _get_cached_docling_document(self.id, self.docling_document_json)
|
||||||
|
|
|
||||||
|
|
@ -92,8 +92,13 @@ class DocumentRepository:
|
||||||
|
|
||||||
async def update(self, entity: Document) -> Document:
|
async def update(self, entity: Document) -> Document:
|
||||||
"""Update an existing document."""
|
"""Update an existing document."""
|
||||||
|
from haiku.rag.store.models.document import invalidate_docling_document_cache
|
||||||
|
|
||||||
assert entity.id, "Document ID is required for update"
|
assert entity.id, "Document ID is required for update"
|
||||||
|
|
||||||
|
# Invalidate cache before update
|
||||||
|
invalidate_docling_document_cache(entity.id)
|
||||||
|
|
||||||
# Update timestamp
|
# Update timestamp
|
||||||
now = datetime.now().isoformat()
|
now = datetime.now().isoformat()
|
||||||
entity.updated_at = datetime.fromisoformat(now)
|
entity.updated_at = datetime.fromisoformat(now)
|
||||||
|
|
@ -116,11 +121,16 @@ class DocumentRepository:
|
||||||
|
|
||||||
async def delete(self, entity_id: str) -> bool:
|
async def delete(self, entity_id: str) -> bool:
|
||||||
"""Delete a document by its ID."""
|
"""Delete a document by its ID."""
|
||||||
|
from haiku.rag.store.models.document import invalidate_docling_document_cache
|
||||||
|
|
||||||
# Check if document exists
|
# Check if document exists
|
||||||
doc = await self.get_by_id(entity_id)
|
doc = await self.get_by_id(entity_id)
|
||||||
if doc is None:
|
if doc is None:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# Invalidate cache before delete
|
||||||
|
invalidate_docling_document_cache(entity_id)
|
||||||
|
|
||||||
# Delete associated chunks first
|
# Delete associated chunks first
|
||||||
await self.chunk_repository.delete_by_document_id(entity_id)
|
await self.chunk_repository.delete_by_document_id(entity_id)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -181,3 +181,86 @@ def test_document_get_docling_document_none():
|
||||||
|
|
||||||
assert document.docling_document_json is None
|
assert document.docling_document_json is None
|
||||||
assert document.get_docling_document() is None
|
assert document.get_docling_document() is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_document_get_docling_document_caching():
|
||||||
|
"""Test that get_docling_document uses LRU cache keyed by document ID."""
|
||||||
|
from haiku.rag.store.models.document import (
|
||||||
|
_docling_document_cache,
|
||||||
|
invalidate_docling_document_cache,
|
||||||
|
)
|
||||||
|
|
||||||
|
doc_json = {
|
||||||
|
"name": "test_doc",
|
||||||
|
"texts": [
|
||||||
|
{
|
||||||
|
"self_ref": "#/texts/0",
|
||||||
|
"text": "Test text",
|
||||||
|
"orig": "Test text",
|
||||||
|
"label": "paragraph",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"tables": [],
|
||||||
|
"pictures": [],
|
||||||
|
"groups": [],
|
||||||
|
"body": {"self_ref": "#/body", "children": []},
|
||||||
|
"furniture": {"self_ref": "#/furniture", "children": []},
|
||||||
|
}
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
json_str = json.dumps(doc_json)
|
||||||
|
|
||||||
|
# Clear cache to get clean state
|
||||||
|
_docling_document_cache.clear()
|
||||||
|
|
||||||
|
document = Document(
|
||||||
|
id="test-doc-id", content="Test content", docling_document_json=json_str
|
||||||
|
)
|
||||||
|
|
||||||
|
# First call - not in cache
|
||||||
|
assert "test-doc-id" not in _docling_document_cache
|
||||||
|
doc1 = document.get_docling_document()
|
||||||
|
assert "test-doc-id" in _docling_document_cache
|
||||||
|
|
||||||
|
# Second call - cache hit, same object
|
||||||
|
doc2 = document.get_docling_document()
|
||||||
|
assert doc1 is doc2
|
||||||
|
|
||||||
|
# Invalidation removes from cache
|
||||||
|
invalidate_docling_document_cache("test-doc-id")
|
||||||
|
assert "test-doc-id" not in _docling_document_cache
|
||||||
|
|
||||||
|
|
||||||
|
def test_document_get_docling_document_no_id_no_cache():
|
||||||
|
"""Test that documents without ID don't use cache."""
|
||||||
|
from haiku.rag.store.models.document import _docling_document_cache
|
||||||
|
|
||||||
|
doc_json = {
|
||||||
|
"name": "test_doc",
|
||||||
|
"texts": [],
|
||||||
|
"tables": [],
|
||||||
|
"pictures": [],
|
||||||
|
"groups": [],
|
||||||
|
"body": {"self_ref": "#/body", "children": []},
|
||||||
|
"furniture": {"self_ref": "#/furniture", "children": []},
|
||||||
|
}
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
json_str = json.dumps(doc_json)
|
||||||
|
|
||||||
|
# Clear cache
|
||||||
|
_docling_document_cache.clear()
|
||||||
|
|
||||||
|
# Document without ID
|
||||||
|
document = Document(content="Test content", docling_document_json=json_str)
|
||||||
|
|
||||||
|
doc1 = document.get_docling_document()
|
||||||
|
doc2 = document.get_docling_document()
|
||||||
|
|
||||||
|
# Cache should remain empty (no ID to cache by)
|
||||||
|
assert len(_docling_document_cache) == 0
|
||||||
|
|
||||||
|
# Each call parses fresh (different objects)
|
||||||
|
assert doc1 is not doc2
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue