From c5542bd3fbdcd6e211e1686aa289939ed29427f4 Mon Sep 17 00:00:00 2001 From: bogdankostic Date: Tue, 1 Mar 2022 17:42:11 +0100 Subject: [PATCH] Add `RouteDocuments` and `JoinAnswers` nodes (#2256) * Add SplitDocumentList and JoinAnswer nodes * Update Documentation & Code Style * Add tests + adapt tutorial * Update Documentation & Code Style * Remove branch from installation path in Tutorial * Update Documentation & Code Style * Fix typing * Update Documentation & Code Style * Change name of SplitDocumentList to RouteDocuments * Update Documentation & Code Style * Adapt tutorials to new name * Add test for JoinAnswers * Update Documentation & Code Style * Adapt name of test for JoinAnswers node Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- docs/_src/tutorials/tutorials/15.md | 102 ++- haystack/__init__.py | 2 +- haystack/nodes/__init__.py | 2 +- haystack/nodes/other/__init__.py | 2 + haystack/nodes/other/join_answers.py | 64 ++ haystack/nodes/other/route_documents.py | 72 ++ haystack/pipelines/base.py | 52 +- .../haystack-pipeline-1.2.0.schema.json | 92 +++ .../haystack-pipeline-1.2.0rc0.schema.json | 92 +++ test/test_pipeline.py | 51 +- tutorials/Tutorial15_TableQA.ipynb | 654 +++++++++++++++++- tutorials/Tutorial15_TableQA.py | 33 +- 12 files changed, 1160 insertions(+), 58 deletions(-) create mode 100644 haystack/nodes/other/join_answers.py create mode 100644 haystack/nodes/other/route_documents.py diff --git a/docs/_src/tutorials/tutorials/15.md b/docs/_src/tutorials/tutorials/15.md index c6bdb6325..b2c606c5f 100644 --- a/docs/_src/tutorials/tutorials/15.md +++ b/docs/_src/tutorials/tutorials/15.md @@ -38,8 +38,9 @@ Make sure you enable the GPU runtime to experience decent speed in this tutorial # The TaPAs-based TableReader requires the torch-scatter library !pip install torch-scatter -f https://data.pyg.org/whl/torch-1.10.0+cu113.html -# If you run this notebook on Google Colab, you might need to -# restart the runtime after installing haystack. +# Install pygraphviz for visualization of Pipelines +!apt install libgraphviz-dev +!pip install pygraphviz ``` ### Start an Elasticsearch server @@ -94,7 +95,7 @@ Just as text passages, tables are represented as `Document` objects in Haystack. from haystack.utils import fetch_archive_from_http doc_dir = "data" -s3_url = "https://s3.eu-central-1.amazonaws.com/deepset.ai-farm-qa/datasets/documents/ottqa_tables_sample.json.zip" +s3_url = "https://s3.eu-central-1.amazonaws.com/deepset.ai-farm-qa/datasets/documents/ottqa_sample.zip" fetch_archive_from_http(url=s3_url, output_dir=doc_dir) ``` @@ -246,6 +247,101 @@ prediction = table_qa_pipeline.run("How many twin buildings are under constructi print_answers(prediction, details="minimum") ``` +# Open-Domain QA on Text and Tables +With haystack, you not only have the possibility to do QA on texts or tables, solely, but you can also use both texts and tables as your source of information. + +To demonstrate this, we add 1,000 sample text passages from the OTT-QA dataset. + + +```python +# Add 1,000 text passages from OTT-QA to our document store. + + +def read_ottqa_texts(filename): + processed_passages = [] + with open(filename) as passages: + passages = json.load(passages) + for title, content in passages.items(): + title = title[6:] + title = title.replace("_", " ") + document = Document(content=content, content_type="text", meta={"title": title}) + processed_passages.append(document) + + return processed_passages + + +passages = read_ottqa_texts(f"{doc_dir}/ottqa_texts_sample.json") +document_store.write_documents(passages, index=document_index) +``` + + +```python +document_store.update_embeddings(retriever=retriever, update_existing_embeddings=False) +``` + +## Pipeline for QA on Combination of Text and Tables +We are using one node for retrieving both texts and tables, the `TableTextRetriever`. In order to do question-answering on the Documents coming from the `TableTextRetriever`, we need to route Documents of type `"text"` to a `FARMReader` (or alternatively `TransformersReader`) and Documents of type `"table"` to a `TableReader`. + +To achieve this, we make use of two additional nodes: +- `SplitDocumentList`: Splits the List of Documents retrieved by the `TableTextRetriever` into two lists containing only Documents of type `"text"` or `"table"`, respectively. +- `JoinAnswers`: Takes Answers coming from two different Readers (in this case `FARMReader` and `TableReader`) and joins them to a single list of Answers. + + +```python +from haystack.nodes import FARMReader, RouteDocuments, JoinAnswers + +text_reader = FARMReader("deepset/roberta-base-squad2") +# In order to get meaningful scores from the TableReader, use "deepset/tapas-large-nq-hn-reader" or +# "deepset/tapas-large-nq-reader" as TableReader models. The disadvantage of these models is, however, +# that they are not capable of doing aggregations over multiple table cells. +table_reader = TableReader("deepset/tapas-large-nq-hn-reader") +route_documents = RouteDocuments() +join_answers = JoinAnswers() +``` + + +```python +text_table_qa_pipeline = Pipeline() +text_table_qa_pipeline.add_node(component=retriever, name="TableTextRetriever", inputs=["Query"]) +text_table_qa_pipeline.add_node(component=route_documents, name="RouteDocuments", inputs=["TableTextRetriever"]) +text_table_qa_pipeline.add_node(component=text_reader, name="TextReader", inputs=["RouteDocuments.output_1"]) +text_table_qa_pipeline.add_node(component=table_reader, name="TableReader", inputs=["RouteDocuments.output_2"]) +text_table_qa_pipeline.add_node(component=join_answers, name="JoinAnswers", inputs=["TextReader", "TableReader"]) +``` + + +```python +# Let's have a look on the structure of the combined Table an Text QA pipeline. +from IPython import display + +text_table_qa_pipeline.draw() +display.Image("pipeline.png") +``` + + +```python +# Example query whose answer resides in a text passage +predictions = text_table_qa_pipeline.run(query="Who is Aleksandar Trifunovic?") +``` + + +```python +# We can see both text passages and tables as contexts of the predicted answers. +print_answers(predictions, details="minimum") +``` + + +```python +# Example query whose answer resides in a table +predictions = text_table_qa_pipeline.run(query="What is Cuba's national tree?") +``` + + +```python +# We can see both text passages and tables as contexts of the predicted answers. +print_answers(predictions, details="minimum") +``` + ## About us This [Haystack](https://github.com/deepset-ai/haystack/) notebook was made with love by [deepset](https://deepset.ai/) in Berlin, Germany diff --git a/haystack/__init__.py b/haystack/__init__.py index ae67727cf..f023dc7ef 100644 --- a/haystack/__init__.py +++ b/haystack/__init__.py @@ -102,7 +102,7 @@ except ImportError: from haystack.modeling.evaluation import eval from haystack.modeling.logger import MLFlowLogger, StdoutLogger, TensorBoardLogger -from haystack.nodes.other import JoinDocuments, Docs2Answers +from haystack.nodes.other import JoinDocuments, Docs2Answers, JoinAnswers, RouteDocuments from haystack.nodes.query_classifier import SklearnQueryClassifier, TransformersQueryClassifier from haystack.nodes.file_classifier import FileTypeClassifier from haystack.utils import preprocessing diff --git a/haystack/nodes/__init__.py b/haystack/nodes/__init__.py index 52f2ae002..bc3d86239 100644 --- a/haystack/nodes/__init__.py +++ b/haystack/nodes/__init__.py @@ -21,7 +21,7 @@ from haystack.nodes.file_converter import ( AzureConverter, ParsrConverter, ) -from haystack.nodes.other import Docs2Answers, JoinDocuments +from haystack.nodes.other import Docs2Answers, JoinDocuments, RouteDocuments, JoinAnswers from haystack.nodes.preprocessor import BasePreProcessor, PreProcessor from haystack.nodes.query_classifier import SklearnQueryClassifier, TransformersQueryClassifier from haystack.nodes.question_generator import QuestionGenerator diff --git a/haystack/nodes/other/__init__.py b/haystack/nodes/other/__init__.py index 8341a135b..4cfb55cea 100644 --- a/haystack/nodes/other/__init__.py +++ b/haystack/nodes/other/__init__.py @@ -1,2 +1,4 @@ from haystack.nodes.other.docs2answers import Docs2Answers from haystack.nodes.other.join_docs import JoinDocuments +from haystack.nodes.other.route_documents import RouteDocuments +from haystack.nodes.other.join_answers import JoinAnswers diff --git a/haystack/nodes/other/join_answers.py b/haystack/nodes/other/join_answers.py new file mode 100644 index 000000000..e96652dfc --- /dev/null +++ b/haystack/nodes/other/join_answers.py @@ -0,0 +1,64 @@ +from typing import Optional, List, Dict, Tuple + +from haystack.schema import Answer +from haystack.nodes import BaseComponent + + +class JoinAnswers(BaseComponent): + """ + A node to join `Answer`s produced by multiple `Reader` nodes. + """ + + def __init__( + self, join_mode: str = "concatenate", weights: Optional[List[float]] = None, top_k_join: Optional[int] = None + ): + """ + :param join_mode: `"concatenate"` to combine documents from multiple `Reader`s. `"merge"` to aggregate scores + of individual `Answer`s. + :param weights: A node-wise list (length of list must be equal to the number of input nodes) of weights for + adjusting `Answer` scores when using the `"merge"` join_mode. By default, equal weight is assigned to each + `Reader` score. This parameter is not compatible with the `"concatenate"` join_mode. + :param top_k_join: Limit `Answer`s to top_k based on the resulting scored of the join. + """ + + assert join_mode in ["concatenate", "merge"], f"JoinAnswers node does not support '{join_mode}' join_mode." + assert not ( + weights is not None and join_mode == "concatenate" + ), "Weights are not compatible with 'concatenate' join_mode" + + # Save init parameters to enable export of component config as YAML + self.set_config(join_mode=join_mode, weights=weights, top_k_join=top_k_join) + + self.join_mode = join_mode + self.weights = [float(i) / sum(weights) for i in weights] if weights else None + self.top_k_join = top_k_join + + def run(self, inputs: List[Dict], top_k_join: Optional[int] = None) -> Tuple[Dict, str]: # type: ignore + reader_results = [inp["answers"] for inp in inputs] + + if not top_k_join: + top_k_join = self.top_k_join + + if self.join_mode == "concatenate": + concatenated_answers = [answer for cur_reader_result in reader_results for answer in cur_reader_result] + concatenated_answers = sorted(concatenated_answers, reverse=True)[:top_k_join] + return {"answers": concatenated_answers, "labels": inputs[0].get("labels", None)}, "output_1" + + elif self.join_mode == "merge": + merged_answers = self._merge_answers(reader_results) + + merged_answers = merged_answers[:top_k_join] + return {"answers": merged_answers, "labels": inputs[0].get("labels", None)}, "output_1" + + else: + raise ValueError(f"Invalid join_mode: {self.join_mode}") + + def _merge_answers(self, reader_results: List[List[Answer]]) -> List[Answer]: + weights = self.weights if self.weights else [1 / len(reader_results)] * len(reader_results) + + for result, weight in zip(reader_results, weights): + for answer in result: + if isinstance(answer.score, float): + answer.score *= weight + + return sorted([answer for cur_reader_result in reader_results for answer in cur_reader_result], reverse=True) diff --git a/haystack/nodes/other/route_documents.py b/haystack/nodes/other/route_documents.py new file mode 100644 index 000000000..f9ba7e3ed --- /dev/null +++ b/haystack/nodes/other/route_documents.py @@ -0,0 +1,72 @@ +from typing import List, Tuple, Dict, Optional + +from haystack.nodes.base import BaseComponent +from haystack.schema import Document + + +class RouteDocuments(BaseComponent): + """ + A node to split a list of `Document`s by `content_type` or by the values of a metadata field and route them to + different nodes. + """ + + # By default (split_by == "content_type"), the node has two outgoing edges. + outgoing_edges = 2 + + def __init__(self, split_by: str = "content_type", metadata_values: Optional[List[str]] = None): + """ + :param split_by: Field to split the documents by, either `"content_type"` or a metadata field name. + If this parameter is set to `"content_type"`, the list of `Document`s will be split into a list containing + only `Document`s of type `"text"` (will be routed to `"output_1"`) and a list containing only `Document`s of + type `"text"` (will be routed to `"output_2"`). + If this parameter is set to a metadata field name, you need to specify the parameter `metadata_values` as + well. + :param metadata_values: If the parameter `split_by` is set to a metadata field name, you need to provide a list + of values to group the `Document`s to. `Document`s whose metadata field is equal to the first value of the + provided list will be routed to `"output_1"`, `Document`s whose metadata field is equal to the second + value of the provided list will be routed to `"output_2"`, etc. + """ + + assert split_by == "content_type" or metadata_values is not None, ( + "If split_by is set to the name of a metadata field, you must provide metadata_values " + "to group the documents to." + ) + + # Save init parameters to enable export of component config as YAML + self.set_config(split_by=split_by, metadata_values=metadata_values) + + self.split_by = split_by + self.metadata_values = metadata_values + + # If we split list of Documents by a metadata field, number of outgoing edges might change + if split_by != "content_type" and metadata_values is not None: + self.outgoing_edges = len(metadata_values) + + def run(self, documents: List[Document]) -> Tuple[Dict, str]: # type: ignore + if self.split_by == "content_type": + split_documents: Dict[str, List[Document]] = {"output_1": [], "output_2": []} + + for doc in documents: + if doc.content_type == "text": + split_documents["output_1"].append(doc) + elif doc.content_type == "table": + split_documents["output_2"].append(doc) + + else: + assert isinstance(self.metadata_values, list), ( + "You need to provide metadata_values if you want to split" " a list of Documents by a metadata field." + ) + split_documents = {f"output_{i+1}": [] for i in range(len(self.metadata_values))} + for doc in documents: + current_metadata_value = doc.meta.get(self.split_by, None) + # Disregard current document if it does not contain the provided metadata field + if current_metadata_value is not None: + try: + index = self.metadata_values.index(current_metadata_value) + except ValueError: + # Disregard current document if current_metadata_value is not in the provided metadata_values + continue + + split_documents[f"output_{index+1}"].append(doc) + + return split_documents, "split_documents" diff --git a/haystack/pipelines/base.py b/haystack/pipelines/base.py index c9321d354..1358f7c88 100644 --- a/haystack/pipelines/base.py +++ b/haystack/pipelines/base.py @@ -645,28 +645,38 @@ class Pipeline(BasePipeline): f"Exception while running node `{node_id}` with input `{node_input}`: {e}, full stack trace: {tb}" ) queue.pop(node_id) - next_nodes = self.get_next_nodes(node_id, stream_id) - for n in next_nodes: # add successor nodes with corresponding inputs to the queue - if queue.get(n): # concatenate inputs if it's a join node - existing_input = queue[n] - if "inputs" not in existing_input.keys(): - updated_input: dict = {"inputs": [existing_input, node_output], "params": params} - if query: - updated_input["query"] = query - if file_paths: - updated_input["file_paths"] = file_paths - if labels: - updated_input["labels"] = labels - if documents: - updated_input["documents"] = documents - if meta: - updated_input["meta"] = meta + # + if stream_id == "split_documents": + for stream_id in [key for key in node_output.keys() if key.startswith("output_")]: + current_node_output = {k: v for k, v in node_output.items() if not k.startswith("output_")} + current_docs = node_output.pop(stream_id) + current_node_output["documents"] = current_docs + next_nodes = self.get_next_nodes(node_id, stream_id) + for n in next_nodes: + queue[n] = current_node_output + else: + next_nodes = self.get_next_nodes(node_id, stream_id) + for n in next_nodes: # add successor nodes with corresponding inputs to the queue + if queue.get(n): # concatenate inputs if it's a join node + existing_input = queue[n] + if "inputs" not in existing_input.keys(): + updated_input: dict = {"inputs": [existing_input, node_output], "params": params} + if query: + updated_input["query"] = query + if file_paths: + updated_input["file_paths"] = file_paths + if labels: + updated_input["labels"] = labels + if documents: + updated_input["documents"] = documents + if meta: + updated_input["meta"] = meta + else: + existing_input["inputs"].append(node_output) + updated_input = existing_input + queue[n] = updated_input else: - existing_input["inputs"].append(node_output) - updated_input = existing_input - queue[n] = updated_input - else: - queue[n] = node_output + queue[n] = node_output i = 0 else: i += 1 # attempt executing next node in the queue as current `node_id` has unprocessed predecessors diff --git a/json-schemas/haystack-pipeline-1.2.0.schema.json b/json-schemas/haystack-pipeline-1.2.0.schema.json index d425ee098..ac86f1de8 100644 --- a/json-schemas/haystack-pipeline-1.2.0.schema.json +++ b/json-schemas/haystack-pipeline-1.2.0.schema.json @@ -59,6 +59,9 @@ { "$ref": "#/definitions/ImageToTextConverterComponent" }, + { + "$ref": "#/definitions/JoinAnswersComponent" + }, { "$ref": "#/definitions/JoinDocumentsComponent" }, @@ -86,6 +89,9 @@ { "$ref": "#/definitions/RCIReaderComponent" }, + { + "$ref": "#/definitions/RouteDocumentsComponent" + }, { "$ref": "#/definitions/SentenceTransformersRankerComponent" }, @@ -1093,6 +1099,51 @@ ], "additionalProperties": false }, + "JoinAnswersComponent": { + "type": "object", + "properties": { + "name": { + "title": "Name", + "description": "Custom name for the component. Helpful for visualization and debugging.", + "type": "string" + }, + "type": { + "title": "Type", + "description": "Haystack Class name for the component.", + "type": "string", + "const": "JoinAnswers" + }, + "params": { + "title": "Parameters", + "type": "object", + "properties": { + "join_mode": { + "title": "Join Mode", + "default": "concatenate", + "type": "string" + }, + "weights": { + "title": "Weights", + "type": "array", + "items": { + "type": "number" + } + }, + "top_k_join": { + "title": "Top K Join", + "type": "integer" + } + }, + "additionalProperties": false, + "description": "Each parameter can reference other components defined in the same YAML file." + } + }, + "required": [ + "type", + "name" + ], + "additionalProperties": false + }, "JoinDocumentsComponent": { "type": "object", "properties": { @@ -1646,6 +1697,47 @@ ], "additionalProperties": false }, + "RouteDocumentsComponent": { + "type": "object", + "properties": { + "name": { + "title": "Name", + "description": "Custom name for the component. Helpful for visualization and debugging.", + "type": "string" + }, + "type": { + "title": "Type", + "description": "Haystack Class name for the component.", + "type": "string", + "const": "RouteDocuments" + }, + "params": { + "title": "Parameters", + "type": "object", + "properties": { + "split_by": { + "title": "Split By", + "default": "content_type", + "type": "string" + }, + "metadata_values": { + "title": "Metadata Values", + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false, + "description": "Each parameter can reference other components defined in the same YAML file." + } + }, + "required": [ + "type", + "name" + ], + "additionalProperties": false + }, "SentenceTransformersRankerComponent": { "type": "object", "properties": { diff --git a/json-schemas/haystack-pipeline-1.2.0rc0.schema.json b/json-schemas/haystack-pipeline-1.2.0rc0.schema.json index 520d8ee0c..4387b996b 100644 --- a/json-schemas/haystack-pipeline-1.2.0rc0.schema.json +++ b/json-schemas/haystack-pipeline-1.2.0rc0.schema.json @@ -59,6 +59,9 @@ { "$ref": "#/definitions/ImageToTextConverterComponent" }, + { + "$ref": "#/definitions/JoinAnswersComponent" + }, { "$ref": "#/definitions/JoinDocumentsComponent" }, @@ -95,6 +98,9 @@ { "$ref": "#/definitions/SklearnQueryClassifierComponent" }, + { + "$ref": "#/definitions/SplitDocumentListComponent" + }, { "$ref": "#/definitions/TableReaderComponent" }, @@ -1093,6 +1099,51 @@ ], "additionalProperties": false }, + "JoinAnswersComponent": { + "type": "object", + "properties": { + "name": { + "title": "Name", + "description": "Custom name for the component. Helpful for visualization and debugging.", + "type": "string" + }, + "type": { + "title": "Type", + "description": "Haystack Class name for the component.", + "type": "string", + "const": "JoinAnswers" + }, + "params": { + "title": "Parameters", + "type": "object", + "properties": { + "join_mode": { + "title": "Join Mode", + "default": "concatenate", + "type": "string" + }, + "weights": { + "title": "Weights", + "type": "array", + "items": { + "type": "number" + } + }, + "top_k_join": { + "title": "Top K Join", + "type": "integer" + } + }, + "additionalProperties": false, + "description": "Each parameter can reference other components defined in the same YAML file." + } + }, + "required": [ + "type", + "name" + ], + "additionalProperties": false + }, "JoinDocumentsComponent": { "type": "object", "properties": { @@ -1836,6 +1887,47 @@ ], "additionalProperties": false }, + "SplitDocumentListComponent": { + "type": "object", + "properties": { + "name": { + "title": "Name", + "description": "Custom name for the component. Helpful for visualization and debugging.", + "type": "string" + }, + "type": { + "title": "Type", + "description": "Haystack Class name for the component.", + "type": "string", + "const": "SplitDocumentList" + }, + "params": { + "title": "Parameters", + "type": "object", + "properties": { + "split_by": { + "title": "Split By", + "default": "content_type", + "type": "string" + }, + "metadata_values": { + "title": "Metadata Values", + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false, + "description": "Each parameter can reference other components defined in the same YAML file." + } + }, + "required": [ + "type", + "name" + ], + "additionalProperties": false + }, "TableReaderComponent": { "type": "object", "properties": { diff --git a/test/test_pipeline.py b/test/test_pipeline.py index 4299076c4..1b216e8f2 100644 --- a/test/test_pipeline.py +++ b/test/test_pipeline.py @@ -3,10 +3,12 @@ from pathlib import Path import os import json from unittest.mock import Mock + +import pandas as pd import pytest import responses -from haystack import __version__ +from haystack import __version__, Document, Answer, JoinAnswers from haystack.document_stores.base import BaseDocumentStore from haystack.document_stores.deepsetcloud import DeepsetCloudDocumentStore from haystack.document_stores.elasticsearch import ElasticsearchDocumentStore @@ -17,7 +19,7 @@ from haystack.nodes.retriever.base import BaseRetriever from haystack.nodes.retriever.sparse import ElasticsearchRetriever from haystack.pipelines import Pipeline, DocumentSearchPipeline, RootNode, ExtractiveQAPipeline from haystack.pipelines.base import _PipelineCodeGen -from haystack.nodes import DensePassageRetriever, EmbeddingRetriever +from haystack.nodes import DensePassageRetriever, EmbeddingRetriever, RouteDocuments from conftest import MOCK_DC, DC_API_ENDPOINT, DC_API_KEY, DC_TEST_INDEX, SAMPLES_PATH, deepset_cloud_fixture @@ -1041,6 +1043,51 @@ def test_documentsearch_document_store_authentication(retriever_with_docs, docum assert kwargs["headers"] == auth_headers +def test_route_documents_by_content_type(): + # Test routing by content_type + docs = [ + Document(content="text document", content_type="text"), + Document( + content=pd.DataFrame(columns=["col 1", "col 2"], data=[["row 1", "row 1"], ["row 2", "row 2"]]), + content_type="table", + ), + ] + + route_documents = RouteDocuments() + result, _ = route_documents.run(documents=docs) + assert len(result["output_1"]) == 1 + assert len(result["output_2"]) == 1 + assert result["output_1"][0].content_type == "text" + assert result["output_2"][0].content_type == "table" + + +def test_route_documents_by_metafield(test_docs_xs): + # Test routing by metadata field + docs = [Document.from_dict(doc) if isinstance(doc, dict) else doc for doc in test_docs_xs] + route_documents = RouteDocuments(split_by="meta_field", metadata_values=["test1", "test3", "test5"]) + result, _ = route_documents.run(docs) + assert len(result["output_1"]) == 1 + assert len(result["output_2"]) == 1 + assert len(result["output_3"]) == 1 + assert result["output_1"][0].meta["meta_field"] == "test1" + assert result["output_2"][0].meta["meta_field"] == "test3" + assert result["output_3"][0].meta["meta_field"] == "test5" + + +@pytest.mark.parametrize("join_mode", ["concatenate", "merge"]) +def test_join_answers(join_mode): + inputs = [{"answers": [Answer(answer="answer 1", score=0.7)]}, {"answers": [Answer(answer="answer 2", score=0.8)]}] + + join_answers = JoinAnswers(join_mode=join_mode) + result, _ = join_answers.run(inputs) + assert len(result["answers"]) == 2 + assert result["answers"] == sorted(result["answers"], reverse=True) + + result, _ = join_answers.run(inputs, top_k_join=1) + assert len(result["answers"]) == 1 + assert result["answers"][0].answer == "answer 2" + + def clean_faiss_document_store(): if Path("existing_faiss_document_store").exists(): os.remove("existing_faiss_document_store") diff --git a/tutorials/Tutorial15_TableQA.ipynb b/tutorials/Tutorial15_TableQA.ipynb index b29637ea6..d448118ce 100644 --- a/tutorials/Tutorial15_TableQA.ipynb +++ b/tutorials/Tutorial15_TableQA.ipynb @@ -57,8 +57,9 @@ "# The TaPAs-based TableReader requires the torch-scatter library\n", "!pip install torch-scatter -f https://data.pyg.org/whl/torch-1.10.0+cu113.html\n", "\n", - "# If you run this notebook on Google Colab, you might need to\n", - "# restart the runtime after installing haystack." + "# Install pygraphviz for visualization of Pipelines\n", + "!apt install libgraphviz-dev\n", + "!pip install pygraphviz" ] }, { @@ -87,7 +88,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": 4, "metadata": { "id": "S4PGj1A6wKWu" }, @@ -151,24 +152,24 @@ "from haystack.utils import fetch_archive_from_http\n", "\n", "doc_dir = \"data\"\n", - "s3_url = \"https://s3.eu-central-1.amazonaws.com/deepset.ai-farm-qa/datasets/documents/ottqa_tables_sample.json.zip\"\n", + "s3_url = \"https://s3.eu-central-1.amazonaws.com/deepset.ai-farm-qa/datasets/documents/ottqa_sample.zip\"\n", "fetch_archive_from_http(url=s3_url, output_dir=doc_dir)" ] }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 7, "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "SKjw2LuXxlGh", - "outputId": "c24f8ca0-1a58-44ea-f01d-414db4c8f1f4" + "outputId": "92c67d24-d6fb-413e-8dd7-53075141d508" }, "outputs": [ { - "name": "stdout", "output_type": "stream", + "name": "stdout", "text": [ " Result ... Score\n", "0 Winner ... 6-1 , 6-1\n", @@ -276,7 +277,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 10, "metadata": { "id": "XM-ijy6Zz11L" }, @@ -289,18 +290,18 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 11, "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "YHfQWxVI0N2e", - "outputId": "05976ac9-bee3-4eb8-b36d-01f1db5250db" + "outputId": "1d8dc4d2-a184-489e-defa-d445d76c458f" }, "outputs": [ { - "name": "stdout", "output_type": "stream", + "name": "stdout", "text": [ " Name ... Status\n", "0 Twin Towers II ... Never built\n", @@ -364,18 +365,18 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 13, "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "ILuAXkyN4F7x", - "outputId": "7bdb7190-fcf8-4296-c237-cffc78dac4aa" + "outputId": "4bd19dcb-df8e-4a4d-b9d2-d34650e9e5c2" }, "outputs": [ { - "name": "stdout", "output_type": "stream", + "name": "stdout", "text": [ " Name ... Status\n", "0 Twin Towers II ... Never built\n", @@ -412,20 +413,23 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 14, "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "ilbsecgA4vfN", - "outputId": "5f4e8f0b-bc9e-485b-c933-546fcad2b411" + "outputId": "f845f43e-43e8-48fe-d0ef-91b17a5eff0e" }, "outputs": [ { - "name": "stdout", "output_type": "stream", + "name": "stdout", "text": [ - "{ 'answers': [ Answer(answer='12', type='extractive', score=1.0, context= Name ... Status\n", + "\n", + "Query: How many twin buildings are under construction?\n", + "Answers:\n", + "[ ]\n" ] } ], @@ -472,18 +475,18 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 15, "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "It8XYT2ZTVJs", - "outputId": "5bd712a0-9f22-4fc0-a4f1-b01b15cb9916" + "outputId": "7d31af60-e04a-485d-f0ee-f29592b03928" }, "outputs": [ { - "name": "stdout", "output_type": "stream", + "name": "stdout", "text": [ "Predicted answer: 12\n", "Meta field: {'aggregation_operator': 'COUNT', 'answer_cells': ['Three Sixty West', 'Gateway Towers', 'Rustomjee Crown', 'Lokhandwala Minerva', 'Lamar Towers', 'Indonesia One Towers', 'India Bulls Sky Forest Tower', 'Capital Towers', 'One Avighna Park', 'The Destiny ( Tower )', 'Oberoi Esquire Towers', 'Bhoomi Celestia']}\n" @@ -509,7 +512,7 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 16, "metadata": { "id": "G-aZZvyv4-Mf" }, @@ -525,19 +528,22 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 17, "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "m8evexnW6dev", - "outputId": "290168b1-294e-42ed-c970-e5ddfefb3396" + "outputId": "40514084-f516-4f13-fb48-6a55cb578366" }, "outputs": [ { - "name": "stdout", "output_type": "stream", + "name": "stdout", "text": [ + "\n", + "Query: How many twin buildings are under construction?\n", + "Answers:\n", "[ { 'answer': '12',\n", " 'context': Name ... Status\n", "0 Twin Towers II ... Never built\n", @@ -717,6 +723,596 @@ "print_answers(prediction, details=\"minimum\")" ] }, + { + "cell_type": "markdown", + "source": [ + "# Open-Domain QA on Text and Tables\n", + "With haystack, you not only have the possibility to do QA on texts or tables, solely, but you can also use both texts and tables as your source of information.\n", + "\n", + "To demonstrate this, we add 1,000 sample text passages from the OTT-QA dataset." + ], + "metadata": { + "id": "8uMzl9Ml_D1B" + } + }, + { + "cell_type": "code", + "source": [ + "# Add 1,000 text passages from OTT-QA to our document store.\n", + "\n", + "\n", + "def read_ottqa_texts(filename):\n", + " processed_passages = []\n", + " with open(filename) as passages:\n", + " passages = json.load(passages)\n", + " for title, content in passages.items():\n", + " title = title[6:]\n", + " title = title.replace(\"_\", \" \")\n", + " document = Document(content=content, content_type=\"text\", meta={\"title\": title})\n", + " processed_passages.append(document)\n", + "\n", + " return processed_passages\n", + "\n", + "\n", + "passages = read_ottqa_texts(f\"{doc_dir}/ottqa_texts_sample.json\")\n", + "document_store.write_documents(passages, index=document_index)" + ], + "metadata": { + "id": "4CBcIjIq_uFx" + }, + "execution_count": 18, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "document_store.update_embeddings(retriever=retriever, update_existing_embeddings=False)" + ], + "metadata": { + "id": "j1TaNF7SiKgH" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "## Pipeline for QA on Combination of Text and Tables\n", + "We are using one node for retrieving both texts and tables, the `TableTextRetriever`. In order to do question-answering on the Documents coming from the `TableTextRetriever`, we need to route Documents of type `\"text\"` to a `FARMReader` (or alternatively `TransformersReader`) and Documents of type `\"table\"` to a `TableReader`.\n", + "\n", + "To achieve this, we make use of two additional nodes:\n", + "- `SplitDocumentList`: Splits the List of Documents retrieved by the `TableTextRetriever` into two lists containing only Documents of type `\"text\"` or `\"table\"`, respectively.\n", + "- `JoinAnswers`: Takes Answers coming from two different Readers (in this case `FARMReader` and `TableReader`) and joins them to a single list of Answers." + ], + "metadata": { + "id": "c2sk_uNHj0DY" + } + }, + { + "cell_type": "code", + "source": [ + "from haystack.nodes import FARMReader, RouteDocuments, JoinAnswers\n", + "\n", + "text_reader = FARMReader(\"deepset/roberta-base-squad2\")\n", + "# In order to get meaningful scores from the TableReader, use \"deepset/tapas-large-nq-hn-reader\" or\n", + "# \"deepset/tapas-large-nq-reader\" as TableReader models. The disadvantage of these models is, however,\n", + "# that they are not capable of doing aggregations over multiple table cells.\n", + "table_reader = TableReader(\"deepset/tapas-large-nq-hn-reader\")\n", + "route_documents = RouteDocuments()\n", + "join_answers = JoinAnswers()" + ], + "metadata": { + "id": "Ej_j8Q3wlxXE" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "text_table_qa_pipeline = Pipeline()\n", + "text_table_qa_pipeline.add_node(component=retriever, name=\"TableTextRetriever\", inputs=[\"Query\"])\n", + "text_table_qa_pipeline.add_node(component=route_documents, name=\"RouteDocuments\", inputs=[\"TableTextRetriever\"])\n", + "text_table_qa_pipeline.add_node(component=text_reader, name=\"TextReader\", inputs=[\"RouteDocuments.output_1\"])\n", + "text_table_qa_pipeline.add_node(component=table_reader, name=\"TableReader\", inputs=[\"RouteDocuments.output_2\"])\n", + "text_table_qa_pipeline.add_node(component=join_answers, name=\"JoinAnswers\", inputs=[\"TextReader\", \"TableReader\"])" + ], + "metadata": { + "id": "Zdq6JnF5m3aP" + }, + "execution_count": 21, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "# Let's have a look on the structure of the combined Table an Text QA pipeline.\n", + "from IPython import display\n", + "\n", + "text_table_qa_pipeline.draw()\n", + "display.Image(\"pipeline.png\")" + ], + "metadata": { + "id": "K4vH1ZEnniut", + "outputId": "85aa17a8-227d-40e4-c8c0-5d0532faa47a", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 540 + } + }, + "execution_count": 22, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "image/png": "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\n", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "execution_count": 22 + } + ] + }, + { + "cell_type": "code", + "source": [ + "# Example query whose answer resides in a text passage\n", + "predictions = text_table_qa_pipeline.run(query=\"Who is Aleksandar Trifunovic?\")" + ], + "metadata": { + "id": "strPNduPoBLe" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "# We can see both text passages and tables as contexts of the predicted answers.\n", + "print_answers(predictions, details=\"minimum\")" + ], + "metadata": { + "id": "9YiK75tSoOGA", + "outputId": "bd52f841-3846-441f-dd6f-53b02111691e", + "colab": { + "base_uri": "https://localhost:8080/" + } + }, + "execution_count": 24, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "\n", + "Query: Who is Aleksandar Trifunovic?\n", + "Answers:\n", + "[ { 'answer': 'a Serbian professional basketball coach and former player',\n", + " 'context': 'Aleksandar Trifunović ( ; born 30 May 1967 ) is a Serbian '\n", + " 'professional basketball coach and former player .'},\n", + " { 'answer': 'Johnny Höglin',\n", + " 'context': Rank Athlete Country Time\n", + "0 1 Kees Verkerk Netherlands 2:03.4\n", + "1 2 Ivar Eriksen Norway 2:05.0\n", + "2 3 Ard Schenk Netherlands 2:05.0\n", + "3 4 Magne Thomassen Norway 2:05.1\n", + "4 5 Johnny Höglin Sweden 2:05.2\n", + "5 5 Bjørn Tveter Norway 2:05.2\n", + "6 7 Svein-Erik Stiansen Norway 2:05.5\n", + "7 8 Eduard Matusevich Soviet Union 2:06.1\n", + "8 9 Peter Nottet Netherlands 2:06.3\n", + "9 10 Örjan Sandler Sweden 2:07.0\n", + "10 11 Aleksandr Kerchenko Soviet Union 2:07.1\n", + "11 12 Ants Antson Soviet Union 2:07.2\n", + "12 12 Valery Kaplan Soviet Union 2:07.2\n", + "13 14 Jouko Launonen Finland 2:07.5\n", + "14 15 Günter Traub West Germany 2:07.7\n", + "15 16 Jan Bols Netherlands 2:07.8\n", + "16 16 Manne Lavås Sweden 2:07.8\n", + "17 18 Kimmo Koskinen Finland 2:07.9\n", + "18 19 Richard Wurster United States 2:08.4\n", + "19 20 Göran Claeson Sweden 2:08.6},\n", + " { 'answer': 'Ivar Eriksen',\n", + " 'context': Rank Athlete Country Time\n", + "0 1 Kees Verkerk Netherlands 2:03.4\n", + "1 2 Ivar Eriksen Norway 2:05.0\n", + "2 3 Ard Schenk Netherlands 2:05.0\n", + "3 4 Magne Thomassen Norway 2:05.1\n", + "4 5 Johnny Höglin Sweden 2:05.2\n", + "5 5 Bjørn Tveter Norway 2:05.2\n", + "6 7 Svein-Erik Stiansen Norway 2:05.5\n", + "7 8 Eduard Matusevich Soviet Union 2:06.1\n", + "8 9 Peter Nottet Netherlands 2:06.3\n", + "9 10 Örjan Sandler Sweden 2:07.0\n", + "10 11 Aleksandr Kerchenko Soviet Union 2:07.1\n", + "11 12 Ants Antson Soviet Union 2:07.2\n", + "12 12 Valery Kaplan Soviet Union 2:07.2\n", + "13 14 Jouko Launonen Finland 2:07.5\n", + "14 15 Günter Traub West Germany 2:07.7\n", + "15 16 Jan Bols Netherlands 2:07.8\n", + "16 16 Manne Lavås Sweden 2:07.8\n", + "17 18 Kimmo Koskinen Finland 2:07.9\n", + "18 19 Richard Wurster United States 2:08.4\n", + "19 20 Göran Claeson Sweden 2:08.6},\n", + " { 'answer': 'Magne Thomassen',\n", + " 'context': Rank Athlete Country Time\n", + "0 1 Kees Verkerk Netherlands 2:03.4\n", + "1 2 Ivar Eriksen Norway 2:05.0\n", + "2 3 Ard Schenk Netherlands 2:05.0\n", + "3 4 Magne Thomassen Norway 2:05.1\n", + "4 5 Johnny Höglin Sweden 2:05.2\n", + "5 5 Bjørn Tveter Norway 2:05.2\n", + "6 7 Svein-Erik Stiansen Norway 2:05.5\n", + "7 8 Eduard Matusevich Soviet Union 2:06.1\n", + "8 9 Peter Nottet Netherlands 2:06.3\n", + "9 10 Örjan Sandler Sweden 2:07.0\n", + "10 11 Aleksandr Kerchenko Soviet Union 2:07.1\n", + "11 12 Ants Antson Soviet Union 2:07.2\n", + "12 12 Valery Kaplan Soviet Union 2:07.2\n", + "13 14 Jouko Launonen Finland 2:07.5\n", + "14 15 Günter Traub West Germany 2:07.7\n", + "15 16 Jan Bols Netherlands 2:07.8\n", + "16 16 Manne Lavås Sweden 2:07.8\n", + "17 18 Kimmo Koskinen Finland 2:07.9\n", + "18 19 Richard Wurster United States 2:08.4\n", + "19 20 Göran Claeson Sweden 2:08.6},\n", + " { 'answer': '5',\n", + " 'context': Position # Player Moving from\n", + "0 F 12 Nikola Kalinić Radnički Kragujevac\n", + "1 SF 6 Nemanja Dangubić Mega Vizura\n", + "2 C 33 Maik Zirbes Brose Baskets\n", + "3 PG 3 Marcus Williams Lokomotiv Kuban\n", + "4 PG 24 Stefan Jović Radnički Kragujevac\n", + "5 C 14 Đorđe Kaplanović FMP\n", + "6 SF 5 Nikola Čvorović FMP\n", + "7 SG 7 Aleksandar Aranitović Crvena zvezda U18\n", + "8 SG 20 Aleksa Radanov Crvena zvezda U18},\n", + " { 'answer': 'Vasile Sărucan',\n", + " 'context': Rank Name Nationality Result\n", + "0 1 Hans Baumgartner West Germany 8.12\n", + "1 2 Igor Ter-Ovanesyan Soviet Union 7.91\n", + "2 3 Vasile Sărucan Romania 7.88\n", + "3 4 Valeriu Jurcă Romania 7.72\n", + "4 5 Philippe Housiaux Belgium 7.70\n", + "5 6 Andreas Gloerfeld West Germany 7.70\n", + "6 7 Jan Kobuszewski Poland 7.66\n", + "7 8 Jaroslav Brož Czechoslovakia 7.66\n", + "8 9 Alan Lerwill Great Britain 7.61\n", + "9 10 Mikhail Bariban Soviet Union 7.58\n", + "10 11 Valeriy Podluzhniy Soviet Union 7.54\n", + "11 12 Kari Palmen Finland 7.51\n", + "12 13 Georgi Marin Bulgaria 7.51\n", + "13 14 Jesper Tørring Denmark 7.46\n", + "14 15 Milan Spasojević Yugoslavia 7.23\n", + "15 16 Salih Mercan Turkey 6.98\n", + "16 17 Henrik Kalocsai Hungary 5.67},\n", + " { 'answer': 'Belgium',\n", + " 'context': Rank Name Nationality Result\n", + "0 1 Hans Baumgartner West Germany 8.12\n", + "1 2 Igor Ter-Ovanesyan Soviet Union 7.91\n", + "2 3 Vasile Sărucan Romania 7.88\n", + "3 4 Valeriu Jurcă Romania 7.72\n", + "4 5 Philippe Housiaux Belgium 7.70\n", + "5 6 Andreas Gloerfeld West Germany 7.70\n", + "6 7 Jan Kobuszewski Poland 7.66\n", + "7 8 Jaroslav Brož Czechoslovakia 7.66\n", + "8 9 Alan Lerwill Great Britain 7.61\n", + "9 10 Mikhail Bariban Soviet Union 7.58\n", + "10 11 Valeriy Podluzhniy Soviet Union 7.54\n", + "11 12 Kari Palmen Finland 7.51\n", + "12 13 Georgi Marin Bulgaria 7.51\n", + "13 14 Jesper Tørring Denmark 7.46\n", + "14 15 Milan Spasojević Yugoslavia 7.23\n", + "15 16 Salih Mercan Turkey 6.98\n", + "16 17 Henrik Kalocsai Hungary 5.67},\n", + " { 'answer': 'Poland',\n", + " 'context': Rank Name Nationality Result\n", + "0 1 Hans Baumgartner West Germany 8.12\n", + "1 2 Igor Ter-Ovanesyan Soviet Union 7.91\n", + "2 3 Vasile Sărucan Romania 7.88\n", + "3 4 Valeriu Jurcă Romania 7.72\n", + "4 5 Philippe Housiaux Belgium 7.70\n", + "5 6 Andreas Gloerfeld West Germany 7.70\n", + "6 7 Jan Kobuszewski Poland 7.66\n", + "7 8 Jaroslav Brož Czechoslovakia 7.66\n", + "8 9 Alan Lerwill Great Britain 7.61\n", + "9 10 Mikhail Bariban Soviet Union 7.58\n", + "10 11 Valeriy Podluzhniy Soviet Union 7.54\n", + "11 12 Kari Palmen Finland 7.51\n", + "12 13 Georgi Marin Bulgaria 7.51\n", + "13 14 Jesper Tørring Denmark 7.46\n", + "14 15 Milan Spasojević Yugoslavia 7.23\n", + "15 16 Salih Mercan Turkey 6.98\n", + "16 17 Henrik Kalocsai Hungary 5.67},\n", + " { 'answer': 'Hafþór Júlíus Björnsson',\n", + " 'context': # Name Nationality Pts\n", + "0 1 Hafþór Júlíus Björnsson Iceland 31.5\n", + "1 2 Robert Oberst United States 29\n", + "2 3 Lauri Nami Estonia 24\n", + "3 4 Nick Best United States 14.5\n", + "4 5 Laurence Shahlaei UK 12\n", + "5 6 Wu Long China 6},\n", + " { 'answer': 'Estonia',\n", + " 'context': # Name Nationality Pts\n", + "0 1 Hafþór Júlíus Björnsson Iceland 31.5\n", + "1 2 Robert Oberst United States 29\n", + "2 3 Lauri Nami Estonia 24\n", + "3 4 Nick Best United States 14.5\n", + "4 5 Laurence Shahlaei UK 12\n", + "5 6 Wu Long China 6},\n", + " { 'answer': 'Iceland',\n", + " 'context': # Name Nationality Pts\n", + "0 1 Hafþór Júlíus Björnsson Iceland 31.5\n", + "1 2 Robert Oberst United States 29\n", + "2 3 Lauri Nami Estonia 24\n", + "3 4 Nick Best United States 14.5\n", + "4 5 Laurence Shahlaei UK 12\n", + "5 6 Wu Long China 6},\n", + " { 'answer': 'Egor Antropov ( born May 8 , 1992 ) is a Russian '\n", + " 'professional ice hockey defenceman',\n", + " 'context': 'Egor Antropov ( born May 8 , 1992 ) is a Russian '\n", + " 'professional ice hockey defenceman . He is currently '\n", + " 'playing with Piráti Chomutov of the Czech Extral'},\n", + " { 'answer': 'Zurab Magomedovich Yevloyev ( ; born February 20 , 1980 ) '\n", + " 'is a Russian professional football player',\n", + " 'context': 'Zurab Magomedovich Yevloyev ( ; born February 20 , 1980 ) '\n", + " 'is a Russian professional football player . In 2010 , he '\n", + " 'played for FC Angusht Nazran in the'}]\n" + ] + } + ] + }, + { + "cell_type": "code", + "source": [ + "# Example query whose answer resides in a table\n", + "predictions = text_table_qa_pipeline.run(query=\"What is Cuba's national tree?\")" + ], + "metadata": { + "id": "QYOHDSmLpzEg" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "# We can see both text passages and tables as contexts of the predicted answers.\n", + "print_answers(predictions, details=\"minimum\")" + ], + "metadata": { + "id": "4kw53uWep3zj", + "outputId": "b332cc17-3cb8-4e20-d79d-bb4cf656f277", + "colab": { + "base_uri": "https://localhost:8080/" + } + }, + "execution_count": 26, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "\n", + "Query: What is Cuba's national tree?\n", + "Answers:\n", + "[ { 'answer': 'Cuban royal palm',\n", + " 'context': Country ... Scientific name\n", + "0 Afghanistan ... \n", + "1 Albania ... Olea europaea\n", + "2 Antigua and Barbuda ... Bucida buceras\n", + "3 Argentina ... Erythrina crista-galli , Schinopsis balansae\n", + "4 Australia ... Acacia pycnantha\n", + "5 Bahamas ... Guaiacum sanctum\n", + "6 Bangladesh ... Mangifera indica\n", + "7 Belize ... Swietenia macrophylla\n", + "8 Bhutan ... Cupressus cashmeriana\n", + "9 Brazil ... Caesalpinia echinata\n", + "10 Cambodia ... Borassus flabellifer\n", + "11 Canada ... Acer\n", + "12 Chile ... Araucaria araucana\n", + "13 Colombia ... Ceroxylon quindiuense\n", + "14 Costa Rica ... Enterolobium cyclocarpum\n", + "15 Croatia ... Quercus robur\n", + "16 Cuba ... Roystonea regia\n", + "17 Cyprus ... Quercus alnifolia\n", + "18 Czech Republic ... Tilia cordata\n", + "19 Denmark ... Fagus sylvatica\n", + "\n", + "[20 rows x 3 columns]},\n", + " { 'answer': 'Quercus sagraeana , the Cuban oak',\n", + " 'context': 'Quercus sagraeana , the Cuban oak , is a medium-sized '\n", + " 'evergreen tree native to western Cuba in the Cuban pine '\n", + " 'forests ecoregion .'},\n", + " { 'answer': \"Glenn O'Brien\",\n", + " 'context': Book title ... Notes\n", + "0 Sex ... The book contains erotica influenced photographs taken by Steven Meisel and ...\n", + "1 Madonna : The Girlie Show ... The photographs in the book showcased behind-the-scenes of the 1993 Girlie S...\n", + "2 The Making of Evita ... Featuring an introduction by Madonna , The Making of Evita chronicles the cr...\n", + "3 The Emperor 's New Clothes : An All-Star Retelling of the Classic Fairy Tale ... This fully illustrated retelling of the classic fairy tale by Hans Christian...\n", + "4 X-Static Process ... In 2002 , Madonna had collaborated with photographer Steven Klein for an art...\n", + "5 Nobody Knows Me ... Available for one month only via Madonna 's official website . Contained 52 ...\n", + "6 Madonna Confessions ... Behind-the-scenes and on-stage pictures from Madonna 's 2006 Confessions Tou...\n", + "7 I Am Because We Are ... The book contains excerpts from interviews with Malawian children , their bi...\n", + "8 Madonna : Sticky & Sweet ... Behind-the-scenes and on-stage photography from Madonna 's Sticky & Sweet To...\n", + "9 Tom Munro ... Munro 's self-titled first monograph book consists of photographs taken by h...\n", + "10 Mayumi 's Kitchen : Macrobiotic Cooking for Body and Soul ... Mayumi Nishimura worked as Madonna 's private chef for seven years , and she...\n", + "\n", + "[11 rows x 6 columns]},\n", + " { 'answer': 'Guy Oseary',\n", + " 'context': Book title ... Notes\n", + "0 Sex ... The book contains erotica influenced photographs taken by Steven Meisel and ...\n", + "1 Madonna : The Girlie Show ... The photographs in the book showcased behind-the-scenes of the 1993 Girlie S...\n", + "2 The Making of Evita ... Featuring an introduction by Madonna , The Making of Evita chronicles the cr...\n", + "3 The Emperor 's New Clothes : An All-Star Retelling of the Classic Fairy Tale ... This fully illustrated retelling of the classic fairy tale by Hans Christian...\n", + "4 X-Static Process ... In 2002 , Madonna had collaborated with photographer Steven Klein for an art...\n", + "5 Nobody Knows Me ... Available for one month only via Madonna 's official website . Contained 52 ...\n", + "6 Madonna Confessions ... Behind-the-scenes and on-stage pictures from Madonna 's 2006 Confessions Tou...\n", + "7 I Am Because We Are ... The book contains excerpts from interviews with Malawian children , their bi...\n", + "8 Madonna : Sticky & Sweet ... Behind-the-scenes and on-stage photography from Madonna 's Sticky & Sweet To...\n", + "9 Tom Munro ... Munro 's self-titled first monograph book consists of photographs taken by h...\n", + "10 Mayumi 's Kitchen : Macrobiotic Cooking for Body and Soul ... Mayumi Nishimura worked as Madonna 's private chef for seven years , and she...\n", + "\n", + "[11 rows x 6 columns]},\n", + " { 'answer': 'Guy Oseary',\n", + " 'context': Book title ... Notes\n", + "0 Sex ... The book contains erotica influenced photographs taken by Steven Meisel and ...\n", + "1 Madonna : The Girlie Show ... The photographs in the book showcased behind-the-scenes of the 1993 Girlie S...\n", + "2 The Making of Evita ... Featuring an introduction by Madonna , The Making of Evita chronicles the cr...\n", + "3 The Emperor 's New Clothes : An All-Star Retelling of the Classic Fairy Tale ... This fully illustrated retelling of the classic fairy tale by Hans Christian...\n", + "4 X-Static Process ... In 2002 , Madonna had collaborated with photographer Steven Klein for an art...\n", + "5 Nobody Knows Me ... Available for one month only via Madonna 's official website . Contained 52 ...\n", + "6 Madonna Confessions ... Behind-the-scenes and on-stage pictures from Madonna 's 2006 Confessions Tou...\n", + "7 I Am Because We Are ... The book contains excerpts from interviews with Malawian children , their bi...\n", + "8 Madonna : Sticky & Sweet ... Behind-the-scenes and on-stage photography from Madonna 's Sticky & Sweet To...\n", + "9 Tom Munro ... Munro 's self-titled first monograph book consists of photographs taken by h...\n", + "10 Mayumi 's Kitchen : Macrobiotic Cooking for Body and Soul ... Mayumi Nishimura worked as Madonna 's private chef for seven years , and she...\n", + "\n", + "[11 rows x 6 columns]},\n", + " { 'answer': 'Belize',\n", + " 'context': Country ... Scientific name\n", + "0 Afghanistan ... \n", + "1 Albania ... Olea europaea\n", + "2 Antigua and Barbuda ... Bucida buceras\n", + "3 Argentina ... Erythrina crista-galli , Schinopsis balansae\n", + "4 Australia ... Acacia pycnantha\n", + "5 Bahamas ... Guaiacum sanctum\n", + "6 Bangladesh ... Mangifera indica\n", + "7 Belize ... Swietenia macrophylla\n", + "8 Bhutan ... Cupressus cashmeriana\n", + "9 Brazil ... Caesalpinia echinata\n", + "10 Cambodia ... Borassus flabellifer\n", + "11 Canada ... Acer\n", + "12 Chile ... Araucaria araucana\n", + "13 Colombia ... Ceroxylon quindiuense\n", + "14 Costa Rica ... Enterolobium cyclocarpum\n", + "15 Croatia ... Quercus robur\n", + "16 Cuba ... Roystonea regia\n", + "17 Cyprus ... Quercus alnifolia\n", + "18 Czech Republic ... Tilia cordata\n", + "19 Denmark ... Fagus sylvatica\n", + "\n", + "[20 rows x 3 columns]},\n", + " { 'answer': 'Palmyra palm',\n", + " 'context': Country ... Scientific name\n", + "0 Afghanistan ... \n", + "1 Albania ... Olea europaea\n", + "2 Antigua and Barbuda ... Bucida buceras\n", + "3 Argentina ... Erythrina crista-galli , Schinopsis balansae\n", + "4 Australia ... Acacia pycnantha\n", + "5 Bahamas ... Guaiacum sanctum\n", + "6 Bangladesh ... Mangifera indica\n", + "7 Belize ... Swietenia macrophylla\n", + "8 Bhutan ... Cupressus cashmeriana\n", + "9 Brazil ... Caesalpinia echinata\n", + "10 Cambodia ... Borassus flabellifer\n", + "11 Canada ... Acer\n", + "12 Chile ... Araucaria araucana\n", + "13 Colombia ... Ceroxylon quindiuense\n", + "14 Costa Rica ... Enterolobium cyclocarpum\n", + "15 Croatia ... Quercus robur\n", + "16 Cuba ... Roystonea regia\n", + "17 Cyprus ... Quercus alnifolia\n", + "18 Czech Republic ... Tilia cordata\n", + "19 Denmark ... Fagus sylvatica\n", + "\n", + "[20 rows x 3 columns]},\n", + " { 'answer': 'Guadeloupe',\n", + " 'context': State ... Official Language ( s )\n", + "0 Antigua and Barbuda ... English\n", + "1 Dominica ... English\n", + "2 Grenada ... English\n", + "3 Montserrat ... English\n", + "4 Saint Kitts and Nevis ... English\n", + "5 Saint Lucia ... English\n", + "6 Saint Vincent and the Grenadines ... English\n", + "7 Anguilla ... English\n", + "8 British Virgin Islands ... English\n", + "9 Guadeloupe ... French\n", + "10 Martinique ... French\n", + "\n", + "[11 rows x 10 columns]},\n", + " { 'answer': 'Basse-Terre',\n", + " 'context': State ... Official Language ( s )\n", + "0 Antigua and Barbuda ... English\n", + "1 Dominica ... English\n", + "2 Grenada ... English\n", + "3 Montserrat ... English\n", + "4 Saint Kitts and Nevis ... English\n", + "5 Saint Lucia ... English\n", + "6 Saint Vincent and the Grenadines ... English\n", + "7 Anguilla ... English\n", + "8 British Virgin Islands ... English\n", + "9 Guadeloupe ... French\n", + "10 Martinique ... French\n", + "\n", + "[11 rows x 10 columns]},\n", + " { 'answer': 'East Caribbean dollar',\n", + " 'context': State ... Official Language ( s )\n", + "0 Antigua and Barbuda ... English\n", + "1 Dominica ... English\n", + "2 Grenada ... English\n", + "3 Montserrat ... English\n", + "4 Saint Kitts and Nevis ... English\n", + "5 Saint Lucia ... English\n", + "6 Saint Vincent and the Grenadines ... English\n", + "7 Anguilla ... English\n", + "8 British Virgin Islands ... English\n", + "9 Guadeloupe ... French\n", + "10 Martinique ... French\n", + "\n", + "[11 rows x 10 columns]},\n", + " { 'answer': 'Jenkins',\n", + " 'context': NRHP reference number ... County\n", + "0 72000402 ... Wilkes\n", + "1 ... Meriwether\n", + "2 ... Bartow\n", + "3 71000280 ... Jenkins\n", + "4 ... Chatham\n", + "5 89002015 ... Thomas\n", + "6 ... Glynn\n", + "7 75000615 ... Walton\n", + "8 84001156 ... Sumter\n", + "9 79000713 ... Cobb\n", + "10 82002491 ... Twiggs\n", + "11 74000703 ... Taliaferro\n", + "12 80001039 ... Floyd\n", + "13 90000805 ... Gwinnett\n", + "14 73000620 ... Decatur\n", + "15 79000731 ... Houston\n", + "16 95000741 ... Grady\n", + "17 97000559 ... Greene\n", + "18 74000662 ... Brooks\n", + "19 75000616 ... Washington\n", + "\n", + "[20 rows x 4 columns]},\n", + " { 'answer': \"Primula farinosa , the bird's-eye primrose\",\n", + " 'context': \"Primula farinosa , the bird's-eye primrose , is a small \"\n", + " 'perennial plant in the family Primulaceae , native to '\n", + " 'Northern Europe and northern Asia , and '},\n", + " { 'answer': 'Poospiza',\n", + " 'context': 'Poospiza is a genus of finch-like tanagers found in both '\n", + " 'the South American lowlands and the Andes mountains . '\n", + " 'Generally they are arboreal feeders in '},\n", + " { 'answer': 'golden-crowned sparrow',\n", + " 'context': 'The golden-crowned sparrow ( Zonotrichia atricapilla ) is '\n", + " 'a large American sparrow found in the western part of '\n", + " 'North America .'},\n", + " { 'answer': 'Banksia sessilis var . cordata is a variety of Banksia '\n", + " 'sessilis ( Parrot Bush',\n", + " 'context': 'Banksia sessilis var . cordata is a variety of Banksia '\n", + " 'sessilis ( Parrot Bush ) , with unusually large leaves and '\n", + " 'flower heads . It is a rare variety '},\n", + " { 'answer': 'rain',\n", + " 'context': 's and operates hotels at Machu Picchu Natural Reserve , '\n", + " 'the southeastern rain forest of the Amazon in Puerto '\n", + " 'Maldonado , Tambopata , the Sacred Valley'}]\n" + ] + } + ] + }, { "cell_type": "markdown", "metadata": { @@ -757,5 +1353,5 @@ } }, "nbformat": 4, - "nbformat_minor": 2 -} + "nbformat_minor": 0 +} \ No newline at end of file diff --git a/tutorials/Tutorial15_TableQA.py b/tutorials/Tutorial15_TableQA.py index 5c282e21c..b77e719de 100644 --- a/tutorials/Tutorial15_TableQA.py +++ b/tutorials/Tutorial15_TableQA.py @@ -6,7 +6,7 @@ from haystack.utils import launch_es, fetch_archive_from_http, print_answers from haystack.document_stores import ElasticsearchDocumentStore from haystack import Document, Pipeline from haystack.nodes.retriever import TableTextRetriever -from haystack.nodes import TableReader +from haystack.nodes import TableReader, FARMReader, RouteDocuments, JoinAnswers def tutorial15_tableqa(): @@ -115,6 +115,37 @@ def tutorial15_tableqa(): prediction = table_qa_pipeline.run("How many twin buildings are under construction?") print_answers(prediction, details="minimum") + ### Pipeline for QA on Combination of Text and Tables + # We are using one node for retrieving both texts and tables, the TableTextRetriever. + # In order to do question-answering on the Documents coming from the TableTextRetriever, we need to route + # Documents of type "text" to a FARMReader ( or alternatively TransformersReader) and Documents of type + # "table" to a TableReader. + + text_reader = FARMReader("deepset/roberta-base-squad2") + # In order to get meaningful scores from the TableReader, use "deepset/tapas-large-nq-hn-reader" or + # "deepset/tapas-large-nq-reader" as TableReader models. The disadvantage of these models is, however, + # that they are not capable of doing aggregations over multiple table cells. + table_reader = TableReader("deepset/tapas-large-nq-hn-reader") + route_documents = RouteDocuments() + join_answers = JoinAnswers() + + text_table_qa_pipeline = Pipeline() + text_table_qa_pipeline.add_node(component=retriever, name="TableTextRetriever", inputs=["Query"]) + text_table_qa_pipeline.add_node(component=route_documents, name="RouteDocuments", inputs=["TableTextRetriever"]) + text_table_qa_pipeline.add_node(component=text_reader, name="TextReader", inputs=["RouteDocuments.output_1"]) + text_table_qa_pipeline.add_node(component=table_reader, name="TableReader", inputs=["RouteDocuments.output_2"]) + text_table_qa_pipeline.add_node(component=join_answers, name="JoinAnswers", inputs=["TextReader", "TableReader"]) + + # Example query whose answer resides in a text passage + predictions = text_table_qa_pipeline.run(query="Who is Aleksandar Trifunovic?") + # We can see both text passages and tables as contexts of the predicted answers. + print_answers(predictions, details="minimum") + + # Example query whose answer resides in a table + predictions = text_table_qa_pipeline.run(query="What is Cuba's national tree?") + # We can see both text passages and tables as contexts of the predicted answers. + print_answers(predictions, details="minimum") + if __name__ == "__main__": tutorial15_tableqa()