From ef9e4f4467a2e265bad72b048a1a3186e40969b1 Mon Sep 17 00:00:00 2001 From: Tanay Soni Date: Mon, 8 Jun 2020 11:07:19 +0200 Subject: [PATCH] Add PDF text extraction (#109) --- .travis.yml | 2 +- README.rst | 7 +- haystack/indexing/file_converters/__init__.py | 0 haystack/indexing/file_converters/base.py | 44 ++++ .../indexing/file_converters/pdftotext.py | 230 ++++++++++++++++++ haystack/indexing/{io.py => utils.py} | 73 +++--- requirements.txt | 2 + test/conftest.py | 19 +- test/samples/pdf/sample_pdf_1.pdf | Bin 0 -> 44524 bytes test/samples/pdf/sample_pdf_2.pdf | Bin 0 -> 26093 bytes test/test_db.py | 8 +- test/test_imports.py | 4 +- test/test_pdf_conversion.py | 52 ++++ tutorials/Tutorial1_Basic_QA_Pipeline.ipynb | 10 +- tutorials/Tutorial1_Basic_QA_Pipeline.py | 10 +- ...ic_QA_Pipeline_without_Elasticsearch.ipynb | 10 +- ...Basic_QA_Pipeline_without_Elasticsearch.py | 8 +- tutorials/Tutorial5_Evaluation.ipynb | 2 +- tutorials/Tutorial5_Evaluation.py | 2 +- 19 files changed, 421 insertions(+), 62 deletions(-) create mode 100644 haystack/indexing/file_converters/__init__.py create mode 100644 haystack/indexing/file_converters/base.py create mode 100644 haystack/indexing/file_converters/pdftotext.py rename haystack/indexing/{io.py => utils.py} (50%) create mode 100644 test/samples/pdf/sample_pdf_1.pdf create mode 100644 test/samples/pdf/sample_pdf_2.pdf create mode 100644 test/test_pdf_conversion.py diff --git a/.travis.yml b/.travis.yml index 90578ad3d..e9d1ed25b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,5 @@ language: python -sudo: false +sudo: true cache: pip python: - "3.7" diff --git a/README.rst b/README.rst index 3d1167b25..fc7f7db07 100644 --- a/README.rst +++ b/README.rst @@ -228,6 +228,11 @@ You will find the Swagger API documentation at http://127.0.0.1:80/docs .. image:: https://raw.githubusercontent.com/deepset-ai/haystack/master/docs/img/annotation_tool.png -7. Development +7. Indexing PDF files +______________________ + +Haystack has a customizable PDF text extraction pipeline with cleaning functions for header, footers, and tables. It supports complex document layouts with multi-column text. + +8. Development ------------------- * Unit tests can be executed by running :code:`tox`. \ No newline at end of file diff --git a/haystack/indexing/file_converters/__init__.py b/haystack/indexing/file_converters/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/haystack/indexing/file_converters/base.py b/haystack/indexing/file_converters/base.py new file mode 100644 index 000000000..c4121da47 --- /dev/null +++ b/haystack/indexing/file_converters/base.py @@ -0,0 +1,44 @@ +from abc import abstractmethod +from pathlib import Path + + +class BaseConverter: + """ + Base class for implementing file converts to transform input documents to text format for indexing in database. + """ + + def __init__( + self, + remove_numeric_tables: bool = None, + remove_header_footer: bool = None, + remove_whitespace: bool = None, + remove_empty_lines: bool = None, + valid_languages: [str] = None, + ): + """ + :param remove_numeric_tables: This option uses heuristics to remove numeric rows from the tables. + The tabular structures in documents might be noise for the reader model if it + does not have table parsing capability for finding answers. However, tables + may also have long strings that could possible candidate for searching answers. + The rows containing strings are thus retained in this option. + :param remove_header_footer: use heuristic to remove footers and headers across different pages by searching + for the longest common string. This heuristic uses exact matches and therefore + works well for footers like "Copyright 2019 by XXX", but won't detect "Page 3 of 4" + or similar. + :param remove_whitespace: strip whitespaces before or after each line in the text. + :param remove_empty_lines: remove more than two empty lines in the text. + :param valid_languages: validate languages from a list of languages specified in the ISO 639-1 + (https://en.wikipedia.org/wiki/ISO_639-1) format. + This option can be used to add test for encoding errors. If the extracted text is + not one of the valid languages, then it might likely be encoding error resulting + in garbled text. + """ + self.remove_numeric_tables = remove_numeric_tables + self.remove_header_footer = remove_header_footer + self.remove_whitespace = remove_whitespace + self.remove_empty_lines = remove_empty_lines + self.valid_languages = valid_languages + + @abstractmethod + def extract_pages(self, file_path: Path) -> [str]: + pass diff --git a/haystack/indexing/file_converters/pdftotext.py b/haystack/indexing/file_converters/pdftotext.py new file mode 100644 index 000000000..2dbc09b7b --- /dev/null +++ b/haystack/indexing/file_converters/pdftotext.py @@ -0,0 +1,230 @@ +import logging +import re +import subprocess +from functools import partial, reduce +from itertools import chain +from pathlib import Path + +import fitz +import langdetect + +from haystack.indexing.file_converters.base import BaseConverter + +logger = logging.getLogger(__name__) + + +class PDFToTextConverter(BaseConverter): + def __init__( + self, + remove_numeric_tables: bool = False, + remove_whitespace: bool = None, + remove_empty_lines: bool = None, + remove_header_footer: bool = None, + valid_languages: [str] = None, + ): + """ + :param remove_numeric_tables: This option uses heuristics to remove numeric rows from the tables. + The tabular structures in documents might be noise for the reader model if it + does not have table parsing capability for finding answers. However, tables + may also have long strings that could possible candidate for searching answers. + The rows containing strings are thus retained in this option. + :param remove_whitespace: strip whitespaces before or after each line in the text. + :param remove_empty_lines: remove more than two empty lines in the text. + :param remove_header_footer: use heuristic to remove footers and headers across different pages by searching + for the longest common string. This heuristic uses exact matches and therefore + works well for footers like "Copyright 2019 by XXX", but won't detect "Page 3 of 4" + or similar. + :param valid_languages: validate languages from a list of languages specified in the ISO 639-1 + (https://en.wikipedia.org/wiki/ISO_639-1) format. + This option can be used to add test for encoding errors. If the extracted text is + not one of the valid languages, then it might likely be encoding error resulting + in garbled text. + """ + verify_installation = subprocess.run(["pdftotext -v"], shell=True) + if verify_installation.returncode == 127: + raise Exception( + """pdftotext is not installed. It is part of xpdf or poppler-utils software suite. + + Installation on Linux: + wget --no-check-certificate https://dl.xpdfreader.com/xpdf-tools-linux-4.02.tar.gz && + tar -xvf xpdf-tools-linux-4.02.tar.gz && sudo cp xpdf-tools-linux-4.02/bin64/pdftotext /usr/local/bin + + Installation on MacOS: + brew install xpdf + + You can find more details here: https://www.xpdfreader.com + """ + ) + + super().__init__( + remove_numeric_tables=remove_numeric_tables, + remove_whitespace=remove_whitespace, + remove_empty_lines=remove_empty_lines, + remove_header_footer=remove_header_footer, + valid_languages=valid_languages, + ) + + def extract_pages(self, file_path: Path) -> [str]: + + page_count = fitz.open(file_path).pageCount + + pages = [] + for page_number in range(1, page_count + 1): + # pdftotext tool provides an option to retain the original physical layout of a PDF page. This behaviour + # can be toggled by using the layout param. + # layout=True + # + table structures get retained better + # - multi-column pages(eg, research papers) gets extracted with text from multiple columns on same line + # layout=False + # + keeps strings in content stream order, hence multi column layout works well + # - cells of tables gets split across line + # + # Here, as a "safe" default, layout is turned off. + page = self._extract_page(file_path, page_number, layout=False) + lines = page.splitlines() + cleaned_lines = [] + for line in lines: + words = line.split() + digits = [word for word in words if any(i.isdigit() for i in word)] + + # remove lines having > 40% of words as digits AND not ending with a period(.) + if self.remove_numeric_tables: + if words and len(digits) / len(words) > 0.4 and not line.strip().endswith("."): + logger.debug(f"Removing line '{line}' from {file_path}") + continue + + if self.remove_whitespace: + line = line.strip() + + cleaned_lines.append(line) + + page = "\n".join(cleaned_lines) + + if self.remove_empty_lines: + page = re.sub(r"\n\n+", "\n\n", page) + + pages.append(page) + page_number += 1 + + if self.valid_languages: + document_text = "".join(pages) + if not self._validate_language(document_text): + logger.warning( + f"The language for {file_path} is not one of {self.valid_languages}. The file may not have " + f"been decoded in the correct text format." + ) + + if self.remove_header_footer: + pages, header, footer = self.find_and_remove_header_footer( + pages, n_chars=300, n_first_pages_to_ignore=1, n_last_pages_to_ignore=1 + ) + logger.info(f"Removed header '{header}' and footer {footer} in {file_path}") + + return pages + + def _extract_page(self, file_path: Path, page_number: int, layout: bool): + """ + Extract a page from the pdf file at file_path. + + :param file_path: path of the pdf file + :param page_number: page number to extract(starting from 1) + :param layout: whether to retain the original physical layout for a page. If disabled, PDF pages are read in + the content stream order. + """ + if layout: + command = ["pdftotext", "-layout", "-f", str(page_number), "-l", str(page_number), file_path, "-"] + else: + command = ["pdftotext", "-f", str(page_number), "-l", str(page_number), file_path, "-"] + output_page = subprocess.run(command, capture_output=True, shell=False) + page = output_page.stdout.decode(errors="ignore") + return page + + def _validate_language(self, text: str): + """ + Validate if the language of the text is one of valid languages. + """ + try: + lang = langdetect.detect(text) + except langdetect.lang_detect_exception.LangDetectException: + lang = None + + if lang in self.valid_languages: + return True + else: + return False + + def _ngram(self, seq: str, n: int): + """ + Return ngram (of tokens - currently splitted by whitespace) + :param seq: str, string from which the ngram shall be created + :param n: int, n of ngram + :return: str, ngram as string + """ + + # In order to maintain the original whitespace, but still consider \n and \t for n-gram tokenization, + # we add a space here and remove it after creation of the ngrams again (see below) + seq = seq.replace("\n", " \n") + seq = seq.replace("\t", " \t") + + seq = seq.split(" ") + ngrams = ( + " ".join(seq[i : i + n]).replace(" \n", "\n").replace(" \t", "\t") for i in range(0, len(seq) - n + 1) + ) + + return ngrams + + def _allngram(self, seq: str, min_ngram: int, max_ngram: int): + lengths = range(min_ngram, max_ngram) if max_ngram else range(min_ngram, len(seq)) + ngrams = map(partial(self._ngram, seq), lengths) + res = set(chain.from_iterable(ngrams)) + return res + + def find_longest_common_ngram(self, sequences: [str], max_ngram: int = 30, min_ngram: int = 3): + """ + Find the longest common ngram across different text sequences (e.g. start of pages). + Considering all ngrams between the specified range. Helpful for finding footers, headers etc. + + :param sequences: list[str], list of strings that shall be searched for common n_grams + :param max_ngram: int, maximum length of ngram to consider + :param min_ngram: minimum length of ngram to consider + :return: str, common string of all sections + """ + + seqs_ngrams = map(partial(self._allngram, min_ngram=min_ngram, max_ngram=max_ngram), sequences) + intersection = reduce(set.intersection, seqs_ngrams) + + try: + longest = max(intersection, key=len) + except ValueError: + # no common sequence found + longest = "" + return longest if longest.strip() else None + + def find_and_remove_header_footer( + self, pages: [str], n_chars: int, n_first_pages_to_ignore: int, n_last_pages_to_ignore: int + ): + """ + Heuristic to find footers and headers across different pages by searching for the longest common string. + For headers we only search in the first n_chars characters (for footer: last n_chars). + Note: This heuristic uses exact matches and therefore works well for footers like "Copyright 2019 by XXX", + but won't detect "Page 3 of 4" or similar. + + :param pages: list of strings, one string per page + :param n_chars: number of first/last characters where the header/footer shall be searched in + :param n_first_pages_to_ignore: number of first pages to ignore (e.g. TOCs often don't contain footer/header) + :param n_last_pages_to_ignore: number of last pages to ignore + :return: (cleaned pages, found_header_str, found_footer_str) + """ + + # header + start_of_pages = [p[:n_chars] for p in pages[n_first_pages_to_ignore:-n_last_pages_to_ignore]] + found_header = self.find_longest_common_ngram(start_of_pages) + if found_header: + pages = [page.replace(found_header, "") for page in pages] + + # footer + end_of_pages = [p[-n_chars:] for p in pages[n_first_pages_to_ignore:-n_last_pages_to_ignore]] + found_footer = self.find_longest_common_ngram(end_of_pages) + if found_footer: + pages = [page.replace(found_footer, "") for page in pages] + return pages, found_header, found_footer diff --git a/haystack/indexing/io.py b/haystack/indexing/utils.py similarity index 50% rename from haystack/indexing/io.py rename to haystack/indexing/utils.py index 003da53e8..e8ec48485 100644 --- a/haystack/indexing/io.py +++ b/haystack/indexing/utils.py @@ -4,57 +4,53 @@ from farm.data_handler.utils import http_get import tempfile import tarfile import zipfile +from typing import Callable +from haystack.indexing.file_converters.pdftotext import PDFToTextConverter logger = logging.getLogger(__name__) -def write_documents_to_db(document_store, document_dir, clean_func=None, only_empty_db=False, split_paragraphs=False): +def convert_files_to_dicts(dir_path: str, clean_func: Callable = None, split_paragraphs: bool = False) -> [dict]: """ - Write all text files(.txt) in the sub-directories of the given path to the connected database. + Convert all files(.txt, .pdf) in the sub-directories of the given path to Python dicts that can be written to a + Document Store. - :param document_dir: path for the documents to be written to the database + :param dir_path: path for the documents to be written to the database :param clean_func: a custom cleaning function that gets applied to each doc (input: str, output:str) - :param only_empty_db: If true, docs will only be written if db is completely empty. - Useful to avoid indexing the same initial docs again and again. + :param split_paragraphs: split text in paragraphs. + :return: None """ - file_paths = Path(document_dir).glob("**/*.txt") - # check if db has already docs - if only_empty_db: - n_docs = document_store.get_document_count() - if n_docs > 0: - logger.info(f"Skip writing documents since DB already contains {n_docs} docs ... " - "(Disable `only_empty_db`, if you want to add docs anyway.)") - return None + file_paths = [p for p in Path(dir_path).glob("**/*")] + if ".pdf" in [p.suffix.lower() for p in file_paths]: + pdf_converter = PDFToTextConverter() + else: + pdf_converter = None - # read and add docs - docs_to_index = [] + documents = [] for path in file_paths: - with open(path) as doc: - text = doc.read() - if clean_func: - text = clean_func(text) + if path.suffix.lower() == ".txt": + with open(path) as doc: + text = doc.read() + elif path.suffix.lower() == ".pdf": + pages = pdf_converter.extract_pages(path) + text = "\n".join(pages) + else: + raise Exception(f"Indexing of {path.suffix} files is not currently supported.") - if split_paragraphs: - for para in text.split("\n\n"): - if not para.strip(): # skip empty paragraphs - continue - docs_to_index.append( - { - "name": path.name, - "text": para - } - ) - else: - docs_to_index.append( - { - "name": path.name, - "text": text - } - ) - document_store.write_documents(docs_to_index) - logger.info(f"Wrote {len(docs_to_index)} docs to DB") + if clean_func: + text = clean_func(text) + + if split_paragraphs: + for para in text.split("\n\n"): + if not para.strip(): # skip empty paragraphs + continue + documents.append({"name": path.name, "text": para}) + else: + documents.append({"name": path.name, "text": text}) + + return documents def fetch_archive_from_http(url, output_dir, proxies=None): @@ -97,3 +93,4 @@ def fetch_archive_from_http(url, output_dir, proxies=None): archive.extractall(output_dir) # temp_file gets deleted here return True + diff --git a/requirements.txt b/requirements.txt index c33be74f0..aee6ff538 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,4 +9,6 @@ elasticsearch elastic-apm tox coverage +langdetect # for PDF conversions +PyMuPDF # for PDF conversions # optional: sentence-transformers diff --git a/test/conftest.py b/test/conftest.py index 14d28c713..ea8dd7e1c 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -1,7 +1,8 @@ import tarfile import time import urllib.request -from subprocess import Popen, PIPE, STDOUT + +from subprocess import Popen, PIPE, STDOUT, run import pytest @@ -19,3 +20,19 @@ def elasticsearch_fixture(elasticsearch_dir): thetarfile.extractall(path=elasticsearch_dir) es_server = Popen([elasticsearch_dir / "elasticsearch-7.6.1/bin/elasticsearch"], stdout=PIPE, stderr=STDOUT) time.sleep(30) + + +@pytest.fixture(scope="session") +def xpdf_fixture(): + verify_installation = run(["pdftotext"], shell=True) + if verify_installation.returncode == 127: + commands = """ wget --no-check-certificate https://dl.xpdfreader.com/xpdf-tools-linux-4.02.tar.gz && + tar -xvf xpdf-tools-linux-4.02.tar.gz && sudo cp xpdf-tools-linux-4.02/bin64/pdftotext /usr/local/bin""" + run([commands], shell=True) + + verify_installation = run(["pdftotext -v"], shell=True) + if verify_installation.returncode == 127: + raise Exception( + """pdftotext is not installed. It is part of xpdf or poppler-utils software suite. + You can download for your OS from here: https://www.xpdfreader.com/download.html.""" + ) diff --git a/test/samples/pdf/sample_pdf_1.pdf b/test/samples/pdf/sample_pdf_1.pdf new file mode 100644 index 0000000000000000000000000000000000000000..87259b897f83b462f521276bf32d210ea008bcd3 GIT binary patch literal 44524 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

1p2->@_y0X+Ac~V za}tFSVHraG6v9B;!c_T)j>9GDHF(4zG8!c6&cLIzp#gfccnHMAE6Geu=ihAvQF3^P zJ1#cdH`)(yCbbqT%T^CXff;MU>aQ_h3f6KkwA(Zmfps}WpI7?pCF>jBVG&Z=lwMy9 zwx~L1Rg%uG6}EN>&Q2MDed#8qBIy>77lCx2Vj5aoT9ziL9uBGMuj`$>*NQ4D<1L+h zE`Iv4wm1AVZ$ImlWp;*%Y_I~(n! z>Dzpf3?G8$9+JIyIh!ABAHR6__xn);U`D4jq2lyADT4Jtjh~Rx`GQ85H+iIFR&?YiKmp)11IAgt&Zl z39uV92pm0ws`Dbh3C=ci({bL93!DieX^fd&vznNJ5u9vo4UBC~n}r!Alz>c$t$siP zNpBrePci7=`mSIEn^=WCSSl{dY2MTtna9xxR*friN>JbXhT>uImPh>krkb)bN z@k~yQ=s;d}dpD;?eO&l)N@06IF^c-_2IH+WHj0O!d1==NN`=0++dTgfX=bZ$$MJrV z+Qgj1!1yX?Pr9A9ZGYUj!wX{q)2ju^1J8S(tX5plK5Dv;luW}OzA+@qp$#^z5hJ*E zri$Y=lwQeDjz#I}?hTm1!;6oI=2VdL{jlt|c5~ErttAk{`{owy-8W#azC$4;CDV0X z7AkVXr?Q73yxAZpnJ)QaWxmv@y+-Y=OQ%t`vt2{Gr*jOwRsU~#(0@AWaj-J|%Kg<( z{I-zjKRb*+w2}{H;=gl(RFaUD7ZLmGX!(2R`A63a#qkeE&;J)4j{d_i?{9RtUkV!o z{fB+tpI^V5JO2Luy9D=NMY+ESbAJ+-SeZXe9X~Y6U#5y|^uLPx-@Sf`ZGXxC)0uvW zYrmwmKW`a1K18^WV}4!zUuKUVs}%ia_W0o%|63`4<#RAW{W|uSq2(`YK&B6G(ckhu z(rh1M9?OT|^efK%o44&>O7Pe6{QC5#GW`#G{`;|ijr?Dq>9-s6m#3Rw_4V&<>yIY# z=h68;G^xLn;QnxN=K9e0{U{Ydcxmi&O zEe$DfAYVB8sQ@rBDl^C$A5x@D2Kqb46*&djZxn(ng(=K5;)e)z#}AvWRHLN)U~3rZ zFP!f@4;$|<2i_SM>8%GFFAIueJrhi@SR7yOmolWDirx_nUmB2{GoVWsU+AXYY?^Nt zYR6B=&}W@C-Bx)Tzb_8th4(KXDGth%ES{R0jTTMk=-lwuRnMAEu9xeayy2W(TRW$l zZLnE<@!UGCZcW>*Ep39^^g2DIxO>8FIq$fMY)rS*!4r1B$gt&|_sFE%ayz(NTv^2s zwN2#dT3IqLGzO|bhi0T-u^CIEdbpC>UmnHdnvr)848xoW<7v1fXF1KHnw)M8=V z(A&_8WOM;!Jw;I&S_=aSsBALYYGXl3%Nv1Ak6eui>bUB+)>TEQXyEvwlJ=KSxSdrH zkhT^Q{H2cji`5@#N!YRWqFg2?+v+igEIpP7#46o_p%S)tjscgvowW!}sRiswD;-oA*iAc{} z1WD_y&D=7?g%7QW;+#4<#0zu{reL9?+9CrdOHp~>IZ+#QP4x%q8mk-xZOM=P8f5x= z&eM$aC$Xxl9$?j*ECpvzs89VTw2k!NzXghVj9L+y>2Ke#1yDZ+GQ6Cma#s!Sy%?+P z(9U&G-{g+q0CgroESg7yBLh4Md?F`kbAK)B$+Jqjgr_Fr_HCE|=^i!cpC8PXQE$gMo z8}JW5FX!xu;=2_6uQNBjO>nicwsY!$pkHtJH256xd4{^ezk89IY`k=V+3KH-jc~G7 z=w?;7d$G9*FNAzL*OU2SVDm#?XQAI?Y!XOK{qqzQ)w|X6=)!}u5m)A^%kaI|s)K?08kr7q=UVaE={Yz2Fc1~PL?@94G%iz> zVI6I5{6V#iZ#e7RJyrG#3HNtbjP?deHDDGxCTgW!Xf@qy-edZCv@{&f<0%H3uVz>D zB3+i8c50To=QNkW=+$9}F?>spv~&rWjZkbK5OaooXrGQlAwH9a6kbwH%(zFinAPa&HjI)XXEA9_vsXN&SD@yq(IBq=>H?j@$# zGf;|PwuSD4pIs`el|ow_UY7=Qd?gfL!>@-?RYAWh(coMFtrv?PxkIH?h;GdwX|9NQ zw*Q=QPA{!Op2U zET!6$IV02D@GP!tJExy??cRPA-E^12WXp86oCmAwglwOuUO91J;$pC!Z+`NfR4e6t zc!4SBen3{L6;+36<#m{!9j^v*HH*#BZC86l2U-Vr1GuF>WSC;(m!=R9gN#KE4IN{_&XkbNB^d6f9!J0urzae-4#ep|YmS_2}Ei zY3kn~OELu)sP`!ZTFZy4`3jJ@_`%TlUUb>@Is0Up@IdidpU^BlAAc@JM%U-*B%q$p|2#SVgXS^AaK2WL9f`-U@C<&W{# zAb@%FVGEF7ry0n+b5#`hqhDeHu?5;PsdxhgNku_YcL{^go|v|xJTzN@s#B)3(0)@V zx^uJpfZL9tvH0M7$Ca074I^gAoI9|BHS1z0{oz4UZ*RYxDKlR1!^4l)ik-uqOUL|p zY0mWKW226yygf-P9-(`Pvd$i?D$xwd(lM}0!I7mcX1v@Cmn`WJNFpnJtK}XDO>}+e zk7NK%cvT=`1APyw$kLyK5-d;xEwaTT^0x@!&t1Ukx2y4`)MazP&vElc^WcmF37Wej z=&A!vA1Q{4@Gr*UREAm@e+rd7c7dA{sp{W2DLJPqa1|yAN-#7)`Ww&X%M5($aL(7?vTll8RerdMl4Vu)`~rS9;A8( z^Y6GWhz_WAT*ww-^#v(0uuew3 z`e9<3m%tAmmln?~w?3|q1)Hdq!Dvf^O9Elqy|x?MZiGg19u6QCZ`AS7j~0+pJ)r7h z{NvF3#y=wHk&jiv9oNf6W0(o@OFgO9=S-fG9~i#oj5W|ZAi)KReU`%qTSNh0m9jLq z93)2%P#@QTekGwjYkNTC4ux{p1zh4eCmpAjY@>n@hc9;t{_M4j+)EFck3JL+y&pb&2VCR|IPcL{{^j>D zOfEjoM_$&L1Ud;QR*?p)O!olAekbLdj4!HyDgT4dIXDl58;B$1Epo7hCRUq@-{PrR zX}m5+9}Y#T|BMEJ*-rfy&k_ydMYq2My-r|;uRw7=B2fE8)7PaH8~ zd{Ylv_2g2?Wxr@nZYp?>dMYM#Nab1`TQ~eq7-KH_?{`@{lfvnZ=5kHDL(ou?BN427 zU}x;&-rt%b>R99n`v)bRVCj?z&p&w|<8E7LnR9zveLHZnZm%cGromk z&|=Jk=CY!c6FXuNdPEX=mx*9h8$cS%5s|;#^nAD(NNGOVusi_&l*_GoB^#bd2|Bzm zOV;|n^W6O=wb-El){TeBy)0BeB57${+`_}*HblvOuQIxtK^hlXx&DAuOGh?8kPT^? za+ee!3;hm+m!%DApz4XB#B{MCoCLXx)8(AE+1hq4dKMwl{-q#9Ly+YS-vmvH6P!@aJsJ&g?IBfv4P)~HSauv;@ie|rP*2Rw9OJ=zF z=d7PxiE`Z)TU?lx%pxCOn73ixo^+TqV69+ZRWVJOJ4zPIT*q`Z^i=R%#(>kqQF+2K zfCrbee;eT`zm-@Pl_X&a;-&f_=}Dty1$v=uI1@HV@kx09CimJ^ z{;BP|hmI029c9+ttW=fzw%2wCNRf=wFKN1Z?jdMOGP3|SW}ZPW06VKX-t6=_MsI8r%|Y5`OH zMvuP!Z6T+kooy~RLQIfm2TyZ~1kBka2o4rhP!JqkwynQqv&p*29iMb%=T6Ht0B_iO zoRev`2n;Mznpg&}JmTAawVHhipN5K|pZ$1faupG2M}96JrxZyqSB_uJs9#WSa8O!f z5DB-rpAu63r_B`-q%09{%$(?M!hIzby5n&jfdKaDb>DRW3eh4Q-yalWJ|#=AMHrm& zp9)S!U51}zG|Li~v0+|vKQVC({zx`oLx7%yNW!XY5p*&X2}wR%7BcxfOjeTtcBK#0 z!_^e3i1iig%*SHFL?;Fo2Y;myp`tOnesB9Zl5`^1)sf{PL3^v&K3+u5FtGm+G!*cZ zx;VOtq<&0^(D)8wP@F>kN0x<$tR2Z71i@-E$Bhez_Qm<$%LAzC{#H|46A5X+G587e zfs3x(xl?Q0N578p`jlh#cKf;B{rYihYk2q^G(0>HIkRUuGwp0ct9M>ef2IS+SG?T6 zO`w%FmCM!t^5+kVS;15)3y33Di}rwB36OG4QubnayJ1&@x4mJrp*7=%0BR(~=O+t8 zze*Wd5gyDlr*fy!OBpLUkUf3eXB~MCg%>CRRg^Xa$AZ8hWPAt6Y%iD<`2Is+ht5wt zb0=-N=^c9rOB|1s;LMr8n3I4J2R;K1{JR|4SJ+c7aAw@UhwFc(u~G2UX#A=jLbW)} z;d*hI?K9{T;A6#6@a7n|h=W}?yJWg0`ic+=zR254q;)$sE3B|<%Qw*O?}5$8MV-4C zertU|ZooZ$00Fy)MMl&1OGX#tauCPMLTd>sGXf$Ok zSRb^%jVjKFh$1EkRQ7D5FD86wRw1rqsh7d>|aq<;OY9T{lx@ za-+a6A8!#&=!6z31*spOMFatX=@vjuUXN6c`yoLlt|Ml>BU*MluFd+z;uMqki_u2r zcT(ur8w0npJ=)toDIjH2+&Hk>%(smJvPUhqwnH`!B3#;@9Hj(Ers+m#7^l1o^B{?h zDMiPbqvv`YVtqA=E{2~$2M2+>MY43|?W9{s2~}^qcin|jT%~erY6*~L1VIA0^+0Vf zDW$<*=i^An&cT*tP8)=SaLFJ-@{vkCF?PqeUNSWNC$=kW3J4Yx148O;zXq{q&A=Iw zrvLEw+%raR5~A1A4jE_8P1U8W%PGb*=MKO*sv0h_@7I}8BKBwJ<-2KKt?+sn|M)Kb zWVwp<-doi1Qu(5Ij6VL<%yv*0+quVu5)?5x8zv__Z3&Xf9%E~M-#>mK_hvg?MFe2r z0XgQbDb>_ZO$CEWr#2=y*?|Ti=z1>}2HBWw94OGDS46nYbEgy%>DZ$GU?asTj4E#* zF=dx1!%sd@hEHchmRg8JMK2Cfk3`D7niMac?28p2PTf2auGaq#lJSs z%Ey5%(Gq^Mk-2ST-Z9!x`f%!lf4HEtn}@@5l#MRojumx1y<%iaeg^o!wLYJmEcxI> zNAA**z(;6t@Mw~zoyEt@MK+foI5wZI?Vz$~8g-n2WiKM{qBzhdtisYppbd8+Z~24j zGn$9krg?|-y$~SKS3zKKW;>rw*ArJL1^>%tap7;I_=1Yi7^!j)QkMmgLsK9|WQ1IfycpxbRghUK0T(C*}qW?tf=Tw>TvU*Oi1O_DbGxnC@PZaVhBh8_e~H6IkF{E zDXf3M#WWEsWr*RF61uEII(xV6C?eterC?m z>{Au@c6czJ-lps)ZQsq*K8hpw-;|Cna`s~*6ZU1ft3vH^msd@zczezYz3N1>ajA6S zx;?kH7AfbNq(;4G=ms`leQa=uO>A26&$ew%X${h>Uo*!)Oj9hS>MgH!j7=C35uMW7 zzpNs#>ccy+3W?rqe5{@_!+dcgxAor3Nt(qC8DnRTNzIG78?+$bJ8iJ($n2ZviW|op z+4<7OL)z4r{Jofx{KZ2XM=Q73$<8x7NanvS~s-lz{caJMed~`HLfUQ{vn@A{4WnZKo+hQtuB*VrQTE@j|JG zD!8XC;`}_*17{0|_kvHB50iUZpKNNmecDRV^?uteuGz05>(6Vm_8&=I+PreG>qjf@ zjK~;1W}<(Ry3IfJk#tS}_K40!c}43V`M$El>E;ugM8^{k4ZBATn0oBpjIP2@-@N$I z+7yki!O}^Qx;f3h(z3wKWM8K@-}KqPj(k1Z_WN`O zxU*Y%_Kh6Lw!mw<9238G4X(8sGGqD;L;L0Q*QyLH4+>7U)E%z9y!!U-{e|79Jres> zNv>Fbk{6<}z_)zWS?v*}m=HNaZt~@FP8H^4ciK55C2cO=S-mMrr*kyg)yRqyx1|-X z-#4$mDI*>H5aIMeZT$Q zAA@S{EKCi%bR}odGRpy;voBhDG%3%UtWs>h)>MyPqa8W9eRcX*nIl#v7E}j7AAafH ztXH@1w6DIhFz{T4t=ryE2jZ=aVWAxt(%<=Im0#F~p4gu3`01SmJ-5tRW_|CW<~`Tf z+2VdRx3AqhyP(7wevp>QC~9CBWBYMw$?=-fRrOSYe7EYbW|+YXt7tlyS6gKtFkx7J z%AMiLC7FSP8q7x}tq#qft}t7+)jqs_*lz32pXL@^kN>lFr(MM=Q(0cg)!u&k%nQmF zBxs*Dt}zVutj2{e2YQ-13X0T$NSVt@RZN^ly(lg`0_5KVw6WpwVQQ_+B}NtIKUelt zPY}o4aT+8ALtMP$BcV+a=X?oP9^xtsj8|!OdRc&Cnh#XFs8{P;tvr0RY85E>$z1xR z31QSe0AvXjDjiIym_T3zhA|icl4){+U4UWZ;177SV#EKN@|;rmYOOi~vWgt2=smk+ z3{8XVWQ6Q*3<|k;Ek*v4E&m6@CbGsiI&dTmAH!6qO) z=<7v!B=QUb4JtIm<1v`nPjOrhwI*U7iQ0zKFR+b4ZNph~|8NeCR|F2q>Y{legd_e! zP^j+`7-}0FFJc=(qP7tXx>tNaMb^Th)AU6 zr%2Q{D4JKM7w}-oNDL!boG)z>@>mLq5hNi;<|QE>i01%^S8f-K2fP zAR(+FN*#PD>HC5}!Ri^S LF=KrFr&;|4#k|z* literal 0 HcmV?d00001 diff --git a/test/samples/pdf/sample_pdf_2.pdf b/test/samples/pdf/sample_pdf_2.pdf new file mode 100644 index 0000000000000000000000000000000000000000..6384246e891c59abf174e7225ed7f793e814ed69 GIT binary patch literal 26093 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 S4>n+N3&DvwIj^b75&sVaB5i5_ literal 0 HcmV?d00001 diff --git a/test/test_db.py b/test/test_db.py index 89eba0f37..2a0f3ca42 100644 --- a/test/test_db.py +++ b/test/test_db.py @@ -2,12 +2,13 @@ from time import sleep from haystack.database.elasticsearch import ElasticsearchDocumentStore from haystack.database.sql import SQLDocumentStore -from haystack.indexing.io import write_documents_to_db +from haystack.indexing.utils import convert_files_to_dicts def test_sql_write_read(): sql_document_store = SQLDocumentStore() - write_documents_to_db(document_store=sql_document_store, document_dir="samples/docs") + documents = convert_files_to_dicts(dir_path="samples/docs") + sql_document_store.write_documents(documents) documents = sql_document_store.get_all_documents() assert len(documents) == 2 doc = sql_document_store.get_document_by_id("1") @@ -17,7 +18,8 @@ def test_sql_write_read(): def test_elasticsearch_write_read(elasticsearch_fixture): document_store = ElasticsearchDocumentStore() - write_documents_to_db(document_store=document_store, document_dir="samples/docs") + documents = convert_files_to_dicts(dir_path="samples/docs") + document_store.write_documents(documents) sleep(2) # wait for documents to be available for query documents = document_store.get_all_documents() assert len(documents) == 2 diff --git a/test/test_imports.py b/test/test_imports.py index e4920132d..67460178c 100644 --- a/test/test_imports.py +++ b/test/test_imports.py @@ -2,7 +2,7 @@ def test_module_imports(): from haystack import Finder from haystack.database.sql import SQLDocumentStore from haystack.indexing.cleaning import clean_wiki_text - from haystack.indexing.io import write_documents_to_db, fetch_archive_from_http + from haystack.indexing.utils import convert_files_to_dicts, fetch_archive_from_http from haystack.reader.farm import FARMReader from haystack.reader.transformers import TransformersReader from haystack.retriever.tfidf import TfidfRetriever @@ -11,7 +11,7 @@ def test_module_imports(): assert Finder is not None assert SQLDocumentStore is not None assert clean_wiki_text is not None - assert write_documents_to_db is not None + assert convert_files_to_dicts is not None assert fetch_archive_from_http is not None assert FARMReader is not None assert TransformersReader is not None diff --git a/test/test_pdf_conversion.py b/test/test_pdf_conversion.py new file mode 100644 index 000000000..130caa5f8 --- /dev/null +++ b/test/test_pdf_conversion.py @@ -0,0 +1,52 @@ +import logging +from pathlib import Path + +from haystack.indexing.file_converters.pdftotext import PDFToTextConverter + +logger = logging.getLogger(__name__) + + +def test_extract_pages(xpdf_fixture): + converter = PDFToTextConverter() + pages = converter.extract_pages(file_path=Path("samples/pdf/sample_pdf_1.pdf")) + assert len(pages) == 4 # the sample PDF file has four pages. + assert pages[0] != "" # the page 1 of PDF contains text. + assert pages[2] == "" # the page 3 of PDF file is empty. + + +def test_table_removal(xpdf_fixture): + converter = PDFToTextConverter(remove_numeric_tables=True) + pages = converter.extract_pages(file_path=Path("samples/pdf/sample_pdf_1.pdf")) + + # assert numeric rows are removed from the table. + assert "324" not in pages[0] + assert "54x growth" not in pages[0] + assert "$54.35" not in pages[0] + + # assert text is retained from the document. + assert "Adobe Systems made the PDF specification available free of charge in 1993." in pages[0] + + +def test_language_validation(xpdf_fixture, caplog): + converter = PDFToTextConverter(valid_languages=["en"]) + pages = converter.extract_pages(file_path=Path("samples/pdf/sample_pdf_1.pdf")) + assert "The language for samples/pdf/sample_pdf_1.pdf is not one of ['en']." not in caplog.text + + converter = PDFToTextConverter(valid_languages=["de"]) + pages = converter.extract_pages(file_path=Path("samples/pdf/sample_pdf_1.pdf")) + assert "The language for samples/pdf/sample_pdf_1.pdf is not one of ['de']." in caplog.text + + +def test_header_footer_removal(xpdf_fixture): + converter = PDFToTextConverter(remove_header_footer=True) + converter_no_removal = PDFToTextConverter(remove_header_footer=False) + + pages1 = converter.extract_pages(file_path=Path("samples/pdf/sample_pdf_1.pdf")) # file contains no header/footer + pages2 = converter_no_removal.extract_pages(file_path=Path("samples/pdf/sample_pdf_1.pdf")) # file contains no header/footer + for p1, p2 in zip(pages1, pages2): + assert p2 == p2 + + pages = converter.extract_pages(file_path=Path("samples/pdf/sample_pdf_2.pdf")) # file contains header and footer + for page in pages: + assert "header" not in page + assert "footer" not in page \ No newline at end of file diff --git a/tutorials/Tutorial1_Basic_QA_Pipeline.ipynb b/tutorials/Tutorial1_Basic_QA_Pipeline.ipynb index 98ae1876c..ef8ca8963 100644 --- a/tutorials/Tutorial1_Basic_QA_Pipeline.ipynb +++ b/tutorials/Tutorial1_Basic_QA_Pipeline.ipynb @@ -42,7 +42,7 @@ "source": [ "from haystack import Finder\n", "from haystack.indexing.cleaning import clean_wiki_text\n", - "from haystack.indexing.io import write_documents_to_db, fetch_archive_from_http\n", + "from haystack.indexing.utils import convert_files_to_dicts, fetch_archive_from_http\n", "from haystack.reader.farm import FARMReader\n", "from haystack.reader.transformers import TransformersReader\n", "from haystack.utils import print_answers" @@ -164,11 +164,13 @@ "s3_url = \"https://s3.eu-central-1.amazonaws.com/deepset.ai-farm-qa/datasets/documents/wiki_gameofthrones_txt.zip\"\n", "fetch_archive_from_http(url=s3_url, output_dir=doc_dir)\n", "\n", - "\n", - "# Now, let's write the docs to our DB.\n", + "# Convert files to dicts\n", "# You can optionally supply a cleaning function that is applied to each doc (e.g. to remove footers)\n", "# It must take a str as input, and return a str.\n", - "write_documents_to_db(document_store=document_store, document_dir=doc_dir, clean_func=clean_wiki_text, only_empty_db=True, split_paragraphs=True)" + "dicts = convert_files_to_dicts(dir_path=doc_dir, clean_func=clean_wiki_text, split_paragraphs=True)\n", + "\n", + "# Now, let's write the dicts containing documents to our DB.\n", + "document_store.write_documents(dicts)" ] }, { diff --git a/tutorials/Tutorial1_Basic_QA_Pipeline.py b/tutorials/Tutorial1_Basic_QA_Pipeline.py index 9a5e4981b..acfe5d1bd 100755 --- a/tutorials/Tutorial1_Basic_QA_Pipeline.py +++ b/tutorials/Tutorial1_Basic_QA_Pipeline.py @@ -16,7 +16,7 @@ import time from haystack import Finder from haystack.database.elasticsearch import ElasticsearchDocumentStore from haystack.indexing.cleaning import clean_wiki_text -from haystack.indexing.io import write_documents_to_db, fetch_archive_from_http +from haystack.indexing.utils import convert_files_to_dicts, fetch_archive_from_http from haystack.reader.farm import FARMReader from haystack.reader.transformers import TransformersReader from haystack.utils import print_answers @@ -69,10 +69,14 @@ doc_dir = "data/article_txt_got" s3_url = "https://s3.eu-central-1.amazonaws.com/deepset.ai-farm-qa/datasets/documents/wiki_gameofthrones_txt.zip" fetch_archive_from_http(url=s3_url, output_dir=doc_dir) -# Now, let's write the docs to our DB. +# convert files to dicts containing documents that can be indexed to our datastore +dicts = convert_files_to_dicts(dir_path=doc_dir, clean_func=clean_wiki_text, split_paragraphs=True) # You can optionally supply a cleaning function that is applied to each doc (e.g. to remove footers) # It must take a str as input, and return a str. -write_documents_to_db(document_store=document_store, document_dir=doc_dir, clean_func=clean_wiki_text, only_empty_db=True, split_paragraphs=True) + +# Now, let's write the docs to our DB. +document_store.write_documents(dicts) + # ## Initalize Retriever, Reader, & Finder # diff --git a/tutorials/Tutorial3_Basic_QA_Pipeline_without_Elasticsearch.ipynb b/tutorials/Tutorial3_Basic_QA_Pipeline_without_Elasticsearch.ipynb index 3b5823054..12de56d76 100644 --- a/tutorials/Tutorial3_Basic_QA_Pipeline_without_Elasticsearch.ipynb +++ b/tutorials/Tutorial3_Basic_QA_Pipeline_without_Elasticsearch.ipynb @@ -35,7 +35,7 @@ "source": [ "from haystack import Finder\n", "from haystack.indexing.cleaning import clean_wiki_text\n", - "from haystack.indexing.io import write_documents_to_db, fetch_archive_from_http\n", + "from haystack.indexing.utils import convert_files_to_dicts, fetch_archive_from_http\n", "from haystack.reader.farm import FARMReader\n", "from haystack.reader.transformers import TransformersReader\n", "from haystack.utils import print_answers" @@ -110,11 +110,13 @@ "s3_url = \"https://s3.eu-central-1.amazonaws.com/deepset.ai-farm-qa/datasets/documents/wiki_gameofthrones_txt.zip\"\n", "fetch_archive_from_http(url=s3_url, output_dir=doc_dir)\n", "\n", - "\n", - "# Now, let's write the docs to our DB.\n", + "# convert files to dicts containing documents that can be indexed to our datastore\n", "# You can optionally supply a cleaning function that is applied to each doc (e.g. to remove footers)\n", "# It must take a str as input, and return a str.\n", - "write_documents_to_db(document_store=document_store, document_dir=doc_dir, clean_func=clean_wiki_text, only_empty_db=True, split_paragraphs=True)" + "dicts = convert_files_to_dicts(dir_path=doc_dir, clean_func=clean_wiki_text, split_paragraphs=True)\n", + "\n", + "# Now, let's write the docs to our DB.\n", + "document_store.write_documents(dicts)" ] }, { diff --git a/tutorials/Tutorial3_Basic_QA_Pipeline_without_Elasticsearch.py b/tutorials/Tutorial3_Basic_QA_Pipeline_without_Elasticsearch.py index c7f642006..eca060a50 100644 --- a/tutorials/Tutorial3_Basic_QA_Pipeline_without_Elasticsearch.py +++ b/tutorials/Tutorial3_Basic_QA_Pipeline_without_Elasticsearch.py @@ -11,7 +11,7 @@ from haystack import Finder from haystack.database.memory import InMemoryDocumentStore from haystack.database.sql import SQLDocumentStore from haystack.indexing.cleaning import clean_wiki_text -from haystack.indexing.io import write_documents_to_db, fetch_archive_from_http +from haystack.indexing.utils import convert_files_to_dicts, fetch_archive_from_http from haystack.reader.farm import FARMReader from haystack.reader.transformers import TransformersReader from haystack.retriever.tfidf import TfidfRetriever @@ -37,11 +37,13 @@ doc_dir = "data/article_txt_got" s3_url = "https://s3.eu-central-1.amazonaws.com/deepset.ai-farm-qa/datasets/documents/wiki_gameofthrones_txt.zip" fetch_archive_from_http(url=s3_url, output_dir=doc_dir) -# Now, let's write the docs to our DB. +# convert files to dicts containing documents that can be indexed to our datastore +dicts = convert_files_to_dicts(dir_path=doc_dir, clean_func=clean_wiki_text, split_paragraphs=True) # You can optionally supply a cleaning function that is applied to each doc (e.g. to remove footers) # It must take a str as input, and return a str. -write_documents_to_db(document_store=document_store, document_dir=doc_dir, clean_func=clean_wiki_text, only_empty_db=True, split_paragraphs=True) +# Now, let's write the docs to our DB. +document_store.write_documents(dicts) # ## Initalize Retriever, Reader, & Finder diff --git a/tutorials/Tutorial5_Evaluation.ipynb b/tutorials/Tutorial5_Evaluation.ipynb index e7c613990..dc887533e 100644 --- a/tutorials/Tutorial5_Evaluation.ipynb +++ b/tutorials/Tutorial5_Evaluation.ipynb @@ -1635,7 +1635,7 @@ }, "source": [ "\n", - "from haystack.indexing.io import fetch_archive_from_http\n", + "from haystack.indexing.utils import fetch_archive_from_http\n", "\n", "# Download evaluation data, which is a subset of Natural Questions development set containing 50 documents\n", "doc_dir = \"../data/nq\"\n", diff --git a/tutorials/Tutorial5_Evaluation.py b/tutorials/Tutorial5_Evaluation.py index 9c783cded..6ba9f7814 100644 --- a/tutorials/Tutorial5_Evaluation.py +++ b/tutorials/Tutorial5_Evaluation.py @@ -1,5 +1,5 @@ from haystack.database.elasticsearch import ElasticsearchDocumentStore -from haystack.indexing.io import fetch_archive_from_http +from haystack.indexing.utils import fetch_archive_from_http from haystack.retriever.elasticsearch import ElasticsearchRetriever from haystack.reader.farm import FARMReader from haystack.finder import Finder