Skip to content

Commit dc222e8

Browse files
author
Peter Gustafsson
committed
fix(file_processors): Preserve Docling chunk structure metadata
Signed-off-by: Peter Gustafsson <peter.gustafsson6@gmail.com>
1 parent 0443aa6 commit dc222e8

5 files changed

Lines changed: 115 additions & 14 deletions

File tree

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Copyright (c) The OGX Contributors.
2+
# All rights reserved.
3+
#
4+
# This source code is licensed under the terms described in the LICENSE file in
5+
# the root directory of this source tree.
6+
7+
from typing import Any
8+
9+
10+
def extract_structural_metadata(doc_chunk: Any) -> dict[str, Any]:
11+
chunk_meta = getattr(doc_chunk, "meta", None)
12+
if chunk_meta is None:
13+
return {}
14+
15+
metadata: dict[str, Any] = {}
16+
headings = getattr(chunk_meta, "headings", None)
17+
if headings:
18+
metadata["headings"] = headings
19+
20+
page_numbers = {
21+
page_number
22+
for doc_item in getattr(chunk_meta, "doc_items", [])
23+
for provenance in (getattr(doc_item, "prov", None) or [])
24+
if (page_number := getattr(provenance, "page_no", None)) is not None
25+
}
26+
if page_numbers:
27+
metadata["page_numbers"] = sorted(page_numbers)
28+
29+
return metadata

src/ogx/providers/inline/file_processor/docling/docling.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
VectorStoreChunkingStrategy,
3535
)
3636

37+
from ._metadata import extract_structural_metadata
3738
from .config import DoclingFileProcessorConfig
3839

3940
log = get_logger(name=__name__, category="providers::file_processors")
@@ -277,7 +278,6 @@ def _create_chunks(
277278
if not text or not text.strip():
278279
continue
279280

280-
headings = getattr(doc_chunk, "headings", None)
281281
chunk_window = f"{i}"
282282

283283
chunk_id = generate_chunk_id(document_id, text, chunk_window)
@@ -286,8 +286,7 @@ def _create_chunks(
286286
"document_id": document_id,
287287
**document_metadata,
288288
}
289-
if headings:
290-
meta["headings"] = headings
289+
meta.update(extract_structural_metadata(doc_chunk))
291290

292291
chunks.append(
293292
Chunk(

src/ogx/providers/remote/file_processor/docling_serve/docling_serve.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -343,10 +343,15 @@ async def _convert_and_chunk(
343343
**document_metadata,
344344
}
345345

346-
headings = raw_chunk.get("meta", {}).get("headings", None)
346+
legacy_meta = raw_chunk.get("meta") or {}
347+
headings = raw_chunk.get("headings") or legacy_meta.get("headings")
347348
if headings:
348349
meta["headings"] = headings
349350

351+
page_numbers = raw_chunk.get("page_numbers") or legacy_meta.get("page_numbers")
352+
if page_numbers:
353+
meta["page_numbers"] = page_numbers
354+
350355
chunks.append(
351356
Chunk(
352357
content=text,
@@ -426,14 +431,15 @@ async def _convert_and_chunk_async(
426431
**document_metadata,
427432
}
428433

429-
# Extract headings from meta object
430-
headings = None
431-
if hasattr(raw_chunk, "meta") and hasattr(raw_chunk.meta, "headings"):
432-
headings = raw_chunk.meta.headings
433-
434+
legacy_meta = getattr(raw_chunk, "meta", None)
435+
headings = getattr(raw_chunk, "headings", None) or getattr(legacy_meta, "headings", None)
434436
if headings:
435437
meta["headings"] = headings
436438

439+
page_numbers = getattr(raw_chunk, "page_numbers", None) or getattr(legacy_meta, "page_numbers", None)
440+
if page_numbers:
441+
meta["page_numbers"] = page_numbers
442+
437443
chunks.append(
438444
Chunk(
439445
content=text,
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Copyright (c) The OGX Contributors.
2+
# All rights reserved.
3+
#
4+
# This source code is licensed under the terms described in the LICENSE file in
5+
# the root directory of this source tree.
6+
7+
from types import SimpleNamespace
8+
9+
from ogx.providers.inline.file_processor.docling._metadata import extract_structural_metadata
10+
11+
12+
def test_extract_structural_metadata_from_native_docling_chunk():
13+
doc_chunk = SimpleNamespace(
14+
headings=["wrong location"],
15+
meta=SimpleNamespace(
16+
headings=["Introduction", "Architecture"],
17+
doc_items=[
18+
SimpleNamespace(prov=[SimpleNamespace(page_no=2), SimpleNamespace(page_no=1)]),
19+
SimpleNamespace(prov=[SimpleNamespace(page_no=2)]),
20+
SimpleNamespace(prov=None),
21+
],
22+
),
23+
)
24+
25+
assert extract_structural_metadata(doc_chunk) == {
26+
"headings": ["Introduction", "Architecture"],
27+
"page_numbers": [1, 2],
28+
}
29+
30+
31+
def test_extract_structural_metadata_omits_empty_values():
32+
doc_chunk = SimpleNamespace(meta=SimpleNamespace(headings=None, doc_items=[]))
33+
34+
assert extract_structural_metadata(doc_chunk) == {}

tests/unit/providers/file_processor/test_docling_serve.py

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,30 @@ def _make_httpx_response(json_body: dict, status_code: int = 200) -> httpx.Respo
4141

4242
CHUNK_RESPONSE = {
4343
"chunks": [
44-
{"text": "First chunk of text.", "meta": {"headings": ["Introduction"]}},
45-
{"text": "Second chunk of text.", "meta": {}},
46-
{"text": "Third chunk of text.", "meta": {"headings": ["Conclusion"]}},
44+
{
45+
"filename": "test.pdf",
46+
"chunk_index": 0,
47+
"text": "First chunk of text.",
48+
"headings": ["Introduction"],
49+
"doc_items": ["#/texts/0"],
50+
"page_numbers": [1],
51+
},
52+
{
53+
"filename": "test.pdf",
54+
"chunk_index": 1,
55+
"text": "Second chunk of text.",
56+
"headings": None,
57+
"doc_items": ["#/texts/1"],
58+
"page_numbers": None,
59+
},
60+
{
61+
"filename": "test.pdf",
62+
"chunk_index": 2,
63+
"text": "Third chunk of text.",
64+
"headings": ["Conclusion"],
65+
"doc_items": ["#/texts/2"],
66+
"page_numbers": [2, 3],
67+
},
4768
],
4869
}
4970

@@ -205,15 +226,18 @@ async def test_chunk_id_uniqueness(self, processor: DoclingServeFileProcessor, u
205226
ids = [c.chunk_id for c in response.chunks]
206227
assert len(ids) == len(set(ids))
207228

208-
async def test_headings_propagated(self, processor: DoclingServeFileProcessor, upload_file: UploadFile):
229+
async def test_structural_metadata_propagated(self, processor: DoclingServeFileProcessor, upload_file: UploadFile):
209230
request = ProcessFileRequest(chunking_strategy=VectorStoreChunkingStrategyAuto())
210231

211232
with patch("httpx.AsyncClient.post", return_value=_make_httpx_response(CHUNK_RESPONSE)):
212233
response = await processor.process_file(request, file=upload_file)
213234

214235
assert response.chunks[0].metadata["headings"] == ["Introduction"]
236+
assert response.chunks[0].metadata["page_numbers"] == [1]
215237
assert "headings" not in response.chunks[1].metadata
238+
assert "page_numbers" not in response.chunks[1].metadata
216239
assert response.chunks[2].metadata["headings"] == ["Conclusion"]
240+
assert response.chunks[2].metadata["page_numbers"] == [2, 3]
217241

218242
async def test_chunk_window_set(self, processor: DoclingServeFileProcessor, upload_file: UploadFile):
219243
request = ProcessFileRequest(chunking_strategy=VectorStoreChunkingStrategyAuto())
@@ -473,7 +497,14 @@ async def test_local_docker_allows_chunking(self, upload_file: UploadFile):
473497

474498
# Mock submit_chunk() returning a successful job
475499
mock_job = AsyncMock()
476-
mock_chunk = SimpleNamespace(text="Chunk content", meta=SimpleNamespace(headings=None))
500+
mock_chunk = SimpleNamespace(
501+
filename="test.pdf",
502+
chunk_index=0,
503+
text="Chunk content",
504+
headings=["Introduction"],
505+
doc_items=["#/texts/0"],
506+
page_numbers=[1, 2],
507+
)
477508
mock_response = SimpleNamespace(chunks=[mock_chunk])
478509
mock_job.result.return_value = mock_response
479510
mock_instance.submit_chunk.return_value = mock_job
@@ -483,4 +514,6 @@ async def test_local_docker_allows_chunking(self, upload_file: UploadFile):
483514

484515
assert result.chunks is not None
485516
assert len(result.chunks) > 0
517+
assert result.chunks[0].metadata["headings"] == ["Introduction"]
518+
assert result.chunks[0].metadata["page_numbers"] == [1, 2]
486519
assert result.metadata["conversion_method"] == "async"

0 commit comments

Comments
 (0)