Haystack docs home page

Open-Domain QA on Tables

Open In Colab

This tutorial shows you how to perform question-answering on tables using the TableTextRetriever or ElasticsearchRetriever as retriever node and the TableReader as reader node.

Prepare environment

Colab: Enable the GPU runtime

Make sure you enable the GPU runtime to experience decent speed in this tutorial. Runtime -> Change Runtime type -> Hardware accelerator -> GPU

# Make sure you have a GPU running
# Install the latest release of Haystack in your own environment
#! pip install farm-haystack

# Install the latest master of Haystack
!pip install --upgrade pip
!pip install git+https://github.com/deepset-ai/haystack.git#egg=farm-haystack[colab]

# 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

# Install pygraphviz for visualization of Pipelines
!apt install libgraphviz-dev
!pip install pygraphviz

Start an Elasticsearch server

You can start Elasticsearch on your local machine instance using Docker. If Docker is not readily available in your environment (e.g. in Colab notebooks), then you can manually download and execute Elasticsearch from source.

# Recommended: Start Elasticsearch using Docker via the Haystack utility function
from haystack.utils import launch_es

# In Colab / No Docker environments: Start Elasticsearch from source
! wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-7.9.2-linux-x86_64.tar.gz -q
! tar -xzf elasticsearch-7.9.2-linux-x86_64.tar.gz
! chown -R daemon:daemon elasticsearch-7.9.2

import os
from subprocess import Popen, PIPE, STDOUT

es_server = Popen(
    ["elasticsearch-7.9.2/bin/elasticsearch"], stdout=PIPE, stderr=STDOUT, preexec_fn=lambda: os.setuid(1)  # as daemon
# wait until ES has started
! sleep 30
# Connect to Elasticsearch
from haystack.document_stores import ElasticsearchDocumentStore

# We want to use a small model producing 512-dimensional embeddings, so we need to set embedding_dim to 512
document_index = "document"
document_store = ElasticsearchDocumentStore(
    host="localhost", username="", password="", index=document_index, embedding_dim=512

Add Tables to DocumentStore

To quickly demonstrate the capabilities of the TableTextRetriever and the TableReader we use a subset of 1000 tables of the Open Table-and-Text Question Answering (OTT-QA) dataset.

Just as text passages, tables are represented as Document objects in Haystack. The content field, though, is a pandas DataFrame instead of a string.

# Let's first fetch some tables that we want to query
# Here: 1000 tables from OTT-QA
from haystack.utils import fetch_archive_from_http

doc_dir = "data/tutorial15"
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)
# Add the tables to the DocumentStore

import json
from haystack import Document
import pandas as pd

def read_ottqa_tables(filename):
    processed_tables = []
    with open(filename) as tables:
        tables = json.load(tables)
        for key, table in tables.items():
            current_columns = table["header"]
            current_rows = table["data"]
            current_df = pd.DataFrame(columns=current_columns, data=current_rows)
            current_doc_title = table["title"]
            current_section_title = table["section_title"]
            document = Document(
                meta={"title": current_doc_title, "section_title": current_section_title},

    return processed_tables

tables = read_ottqa_tables(f"{doc_dir}/ottqa_tables_sample.json")
document_store.write_documents(tables, index=document_index)

# Showing content field and meta field of one of the Documents of content_type 'table'

Initialize Retriever, Reader & Pipeline


Retrievers help narrowing down the scope for the Reader to a subset of tables where a given question could be answered. They use some simple but fast algorithm.

Here: We use the TableTextRetriever capable of retrieving relevant content among a database of texts and tables using dense embeddings. It is an extension of the DensePassageRetriever and consists of three encoders (one query encoder, one text passage encoder and one table encoder) that create embeddings in the same vector space. More details on the TableTextRetriever and how it is trained can be found in this paper.


  • ElasticsearchRetriever that uses BM25 algorithm
from haystack.nodes.retriever import TableTextRetriever

retriever = TableTextRetriever(
    embed_meta_fields=["title", "section_title"],
# Add table embeddings to the tables in DocumentStore
## Alternative: ElasticsearchRetriever
# from haystack.nodes.retriever import ElasticsearchRetriever
# retriever = ElasticsearchRetriever(document_store=document_store)
# Try the Retriever
from haystack.utils import print_documents

retrieved_tables = retriever.retrieve("How many twin buildings are under construction?", top_k=5)
# Get highest scored table


The TableReader is based on TaPas, a transformer-based language model capable of grasping the two-dimensional structure of a table. It scans the tables returned by the retriever and extracts the anser. The available TableReader models can be found here.

Notice: The TableReader will return an answer for each table, even if the query cannot be answered by the table. Furthermore, the confidence scores are not useful as of now, given that they will always be very high (i.e. 1 or close to 1).

from haystack.nodes import TableReader

reader = TableReader(model_name_or_path="google/tapas-base-finetuned-wtq", max_seq_len=512)
# Try the TableReader on one Table (highest-scored retrieved table from previous section)

table_doc = document_store.get_document_by_id("List_of_tallest_twin_buildings_and_structures_in_the_world_1")
from haystack.utils import print_answers

prediction = reader.predict(query="How many twin buildings are under construction?", documents=[table_doc])
print_answers(prediction, details="all")

The offsets in the offsets_in_document and offsets_in_context field indicate the table cells that the model predicts to be part of the answer. They need to be interpreted on the linearized table, i.e., a flat list containing all of the table cells.

In the Answer's meta field, you can find the aggreagtion operator used to construct the answer (in this case COUNT) and the answer cells as strings.

print(f"Predicted answer: {prediction['answers'][0].answer}")
print(f"Meta field: {prediction['answers'][0].meta}")


The Retriever and the Reader can be sticked together to a pipeline in order to first retrieve relevant tables and then extract the answer.

Notice: Given that the TableReader does not provide useful confidence scores and returns an answer for each of the tables, the sorting of the answers might be not helpful.

# Initialize pipeline
from haystack import Pipeline

table_qa_pipeline = Pipeline()
table_qa_pipeline.add_node(component=retriever, name="TableTextRetriever", inputs=["Query"])
table_qa_pipeline.add_node(component=reader, name="TableReader", inputs=["TableTextRetriever"])
prediction = table_qa_pipeline.run("How many twin buildings are under construction?")
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.

# 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})

    return processed_passages

passages = read_ottqa_texts(f"{doc_dir}/ottqa_texts_sample.json")
document_store.write_documents(passages, index=document_index)
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.
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()
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"])
# Let's have a look on the structure of the combined Table an Text QA pipeline.
from IPython import display

# 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")

About us

This Haystack notebook was made with love by deepset in Berlin, Germany

We bring NLP to the industry via open source!
Our focus: Industry specific language models & large scale QA systems.

Some of our other work:

Get in touch: Twitter | LinkedIn | Slack | GitHub Discussions | Website

By the way: we're hiring!