Repository: deepset-ai/haystack

Issue:
`ExtractedAnswer` missing `page_number` meta
**Describe the bug**

When using extractive QA, the `context` and `context_offset` of the answer is not set. That is something we can workaround by using the document content directly as discussed below.

However, the `page number` meta is still not set specified in the comments below.

**Error message**
none

**Expected behavior**
~I expect `context` and `context_offset`  to be set.~ I expect the page number added to the answer meta

**Additional context**
We need this in deepset Cloud to migrate to Haystack 2

**To Reproduce**
1. Run extractive answer pipeline from [tutorial](https://haystack.deepset.ai/tutorials/34_extractive_qa_pipeline)
2. Answer doesn't include `context` / `context_offset` and also not the page number meta

![Screenshot 2024-04-22 at 13 56 37](https://github.com/deepset-ai/haystack/assets/7667273/f99507ad-3f70-4354-b896-81476d8fc1af)

**FAQ Check**
- [x] Have you had a look at [our new FAQ page](https://docs.haystack.deepset.ai/docs/faq)?

**System:**
 - OS:
 - GPU/CPU:
 - Haystack version (commit or version number): 2.1
 - DocumentStore:
 - Reader:
 - Retriever:



Discussion:
`meta` also shouldn't be empty, right?
Hey @wochinge thanks for raising this. As a heads up the `document` provided in the answer contains the needed text. But you are right that I don't think that a `context` is provided any longer and for now it was assumed to just use the full text of the retrieved document. 
Is it hard to get the context back? I think that would avoid that we need to send the entire document over for the preview (would be a workaround though). Worst case we cut out the context from the document for ourselves 🤔 
There is the document and the document offset. I would indeed suggest to cut out the context and set the context offset then. Can be a separate component that gets a list of extracted answers and creates the context based on the desired length of the context. 
@julian-risch @sjrl And what about the `meta` field? That one should come from the retrieved document, no?
I see here that the 1.x reader added the document page number to the meta field of the Answer based on the document offset. We could add the same functionality to the 2.x reader if that helps with the migration. What do you think?
https://github.com/deepset-ai/haystack/blob/6d320f6929713d4b7664c5f3ae97ddd5e4b60bf0/haystack/nodes/reader/farm.py#L960

I would suggest that we basically copy over _add_answer_page_number https://github.com/deepset-ai/haystack/blob/6d320f6929713d4b7664c5f3ae97ddd5e4b60bf0/haystack/nodes/reader/farm.py#L952C5-L971C22 and put it here: https://github.com/deepset-ai/haystack/blob/b12e0db134277b3bf6b22471433dff385488decd/haystack/components/readers/extractive.py#L346
That would be great 🙌🏻 
Other than the suggested meta info that @julian-risch suggested, I'd recommend getting everything else that is needed e.g. document_id from the `document.meta` object directly. 

Resolving patch:
diff --git a/haystack/components/readers/extractive.py b/haystack/components/readers/extractive.py
--- a/haystack/components/readers/extractive.py
+++ b/haystack/components/readers/extractive.py
@@ -308,6 +308,31 @@ def _postprocess(
 
         return start_candidates_tokens_to_chars, end_candidates_tokens_to_chars, candidates_values
 
+    def _add_answer_page_number(self, answer: ExtractedAnswer) -> ExtractedAnswer:
+        if answer.meta is None:
+            answer.meta = {}
+
+        if answer.document_offset is None:
+            return answer
+
+        if not answer.document or "page_number" not in answer.document.meta:
+            return answer
+
+        if not isinstance(answer.document.meta["page_number"], int):
+            logger.warning(
+                f"Document's page_number must be int but is {type(answer.document.meta['page_number'])}. "
+                f"No page number will be added to the answer."
+            )
+            return answer
+
+        # Calculate the answer page number
+        if answer.document.content:
+            ans_start = answer.document_offset.start
+            answer_page_number = answer.document.meta["page_number"] + answer.document.content[:ans_start].count("\f")
+            answer.meta.update({"answer_page_number": answer_page_number})
+
+        return answer
+
     def _nest_answers(
         self,
         start: List[List[int]],
@@ -358,6 +383,10 @@ def _nest_answers(
             current_answers = sorted(current_answers, key=lambda ans: ans.score, reverse=True)
             current_answers = self.deduplicate_by_overlap(current_answers, overlap_threshold=overlap_threshold)
             current_answers = current_answers[:top_k]
+
+            # Calculate the answer page number and add it to meta
+            current_answers = [self._add_answer_page_number(answer=answer) for answer in current_answers]
+
             if no_answer:
                 no_answer_score = math.prod(1 - answer.score for answer in current_answers)
                 answer_ = ExtractedAnswer(


Test patch:
diff --git a/test/components/readers/test_extractive.py b/test/components/readers/test_extractive.py
--- a/test/components/readers/test_extractive.py
+++ b/test/components/readers/test_extractive.py
@@ -91,7 +91,7 @@ def forward(self, input_ids, attention_mask, *args, **kwargs):
     [
         Document(content="Angela Merkel was the chancellor of Germany."),
         Document(content="Olaf Scholz is the chancellor of Germany"),
-        Document(content="Jerry is the head of the department."),
+        Document(content="Jerry is the head of the department.", meta={"page_number": 3}),
     ]
 ] * 2
 
@@ -386,12 +386,84 @@ def test_nest_answers(mock_reader: ExtractiveReader):
             assert answer.query == query
             assert answer.document == doc
             assert answer.score == pytest.approx(score)
+            if "page_number" in doc.meta:
+                assert answer.meta["answer_page_number"] == doc.meta["page_number"]
         no_answer = answers[-1]
         assert no_answer.query == query
         assert no_answer.document is None
         assert no_answer.score == pytest.approx(expected_no_answer)
 
 
+def test_add_answer_page_number_returns_same_answer(mock_reader: ExtractiveReader, caplog):
+    # answer.document_offset is None
+    document = Document(content="I thought a lot about this. The answer is 42.", meta={"page_number": 5})
+    answer = ExtractedAnswer(
+        data="42",
+        query="What is the answer?",
+        document=document,
+        score=1.0,
+        document_offset=None,
+        meta={"meta_key": "meta_value"},
+    )
+    assert mock_reader._add_answer_page_number(answer=answer) == answer
+
+    # answer.document is None
+    answer = ExtractedAnswer(
+        data="42",
+        query="What is the answer?",
+        document=None,
+        score=1.0,
+        document_offset=ExtractedAnswer.Span(42, 44),
+        meta={"meta_key": "meta_value"},
+    )
+    assert mock_reader._add_answer_page_number(answer=answer) == answer
+
+    # answer.document.meta is None
+    document = Document(content="I thought a lot about this. The answer is 42.")
+    answer = ExtractedAnswer(
+        data="42",
+        query="What is the answer?",
+        document=document,
+        score=1.0,
+        document_offset=ExtractedAnswer.Span(42, 44),
+        meta={"meta_key": "meta_value"},
+    )
+    assert mock_reader._add_answer_page_number(answer=answer) == answer
+
+    # answer.document.meta["page_number"] is not int
+    document = Document(content="I thought a lot about this. The answer is 42.", meta={"page_number": "5"})
+    answer = ExtractedAnswer(
+        data="42",
+        query="What is the answer?",
+        document=document,
+        score=1.0,
+        document_offset=ExtractedAnswer.Span(42, 44),
+        meta={"meta_key": "meta_value"},
+    )
+    with caplog.at_level(logging.WARNING):
+        assert mock_reader._add_answer_page_number(answer=answer) == answer
+        assert "page_number must be int" in caplog.text
+
+
+def test_add_answer_page_number_with_form_feed(mock_reader: ExtractiveReader):
+    document = Document(
+        content="I thought a lot about this. \f And this document is long. \f The answer is 42.",
+        meta={"page_number": 5},
+    )
+    answer = ExtractedAnswer(
+        data="42",
+        query="What is the answer?",
+        document=document,
+        context="The answer is 42.",
+        score=1.0,
+        document_offset=ExtractedAnswer.Span(73, 75),
+        context_offset=ExtractedAnswer.Span(14, 16),
+        meta={"meta_key": "meta_value"},
+    )
+    answer_with_page_number = mock_reader._add_answer_page_number(answer=answer)
+    assert answer_with_page_number.meta["answer_page_number"] == 7
+
+
 @patch("haystack.components.readers.extractive.AutoTokenizer.from_pretrained")
 @patch("haystack.components.readers.extractive.AutoModelForQuestionAnswering.from_pretrained")
 def test_warm_up_use_hf_token(mocked_automodel, mocked_autotokenizer, initialized_token: Secret):