系列教程|系列教程 | 用Jina搭建PDF搜索引擎Part 1

?

PDF Search 系列教程来咯,在 Part 1 中,我们将演示如何从 PDF 中提取、处理并存储图像及文本。
随着神经搜索 (Neural Search) 技术的普及,越来越多开发者,开始尝试用 Jina 解决非结构化数据的索引和搜索问题。本系列教程中,我们将演示 如何用 Jina 搭建一个PDF 搜索引擎。
具体内容如下:
  • Part 1 将介绍如何从 PDF 中提取、处理并存储图像及文本
  • Part 2 将演示如何将这些信息输入到 CLIP 中(CLIP 是一个可以理解图像及文本的深度学习模型)。提取 PDF 图像及文本信息后,CLIP 将生成索引,输入图像或文本,即可进行语义相似性搜索。
  • Part 3 通过客户端及 Streamlit 前端,对索引进行搜索。
  • Part 4 为其他相关演示,如提取元数据等。
前序简介:预期目标 & 技术栈 预期目标: 搭建一个 PDF 搜索引擎,用户输入文本或上传图片,搜索引擎即可返回类似的图片和文本片段,并附带原始 PDF 链接。
本文将着重讲解如何将一个 900 多页的 PDF 处理成可供搜索的向量。
本教程将涉及以下技术栈:
DocArray:a data structure for unstructured data. 通过这个工具可以封装 PDF 文件、文本块、图像块以及搜索引擎的其他输入/输出。
【系列教程|系列教程 | 用Jina搭建PDF搜索引擎Part 1】Jina:为 DocArray Document 搭建流水线及神经搜索引擎,并将其扩展到云端。
Jina Hub:无需逐一创建处理单元,可直接使用云端可复用模块。
教程详解:提取 PDF 中的文本及图像 提取 PDF 中的文本及图像,有以下方法可供选择:
1. 用 Jina Hub 上的 PDFSegmenter Executor,提取 PDF 中文本块和图像块。
  1. 用 ImageMagick 和 OCR 对 PDF 中的每一页进行截图。
  2. 将 PDF 转换为 HTML,图片提取到目录,再次将 HTML 转换为文本(这里我们使用的是 Pandoc )。
本文将使用方法 1,提取 PDF 中的文本及图像。
1、创建 PDF(也可使用已有文件)
首先,我们需要一个示例文件,从维基百科中选择一个词条,并导出为 PDF 作为示例文档。 本教程中我们用到的是 Rabbit 词条(也可以称为文章)。
系列教程|系列教程 | 用Jina搭建PDF搜索引擎Part 1
文章图片
?
本教程中使用的浏览器为 Chrome
注意:
  • 禁用页眉、页脚等设置,以免索引中出现类似 4/798 页等无关信息。
  • 可以尝试通过改变页面大小来避免分页
2、提取 PDF 中的文本及图像
借助 Jina Hub 中的 Executor,在 Flow 中运行并提取 PDF 中的数据。 在 Jina 中,Flow 是执行重要任务的 Pipeline,可以建立可搜索的 PDF 文档索引,或通过索引进行搜索。
每个 Flow 包括多个 Executor,每个 Executor 负责一个小任务。这些 Executor 串联在一起,对 Document 进行端到端的处理。
这里我们用到了 Jina Hub 上的 Executor--PDFSegmenter。
使用 Jina Sandbox,即可释放本地资源,将运行转移到云端:
from docarray import DocumentArray from jina import Flowdocs = DocumentArray.from_files("data/*.pdf", recursive=True)flow = ( Flow() .add(uses="jinahub+sandbox://PDFSegmenter", install_requirements=True, name="segmenter") )with flow: indexed_docs = flow.index(docs)

将 PDF 文档转换为 DocumentArray 形式。 在 Jina 中,每一段数据(文本、图像、PDF 等)都是一个Document,一组Document 组成一个 DocumentArray。
通过 documentary.from _ files () 即可从一个目录自动加载所有内容。
DocumentArray 输入到 Flow 后,处理过的 DocumentArray 将存储在 indexed _ docs 中。
在 rabbit.pdf 中, Indexed _ docs 只包含了一个包括文本块和图像块的 Document。
系列教程|系列教程 | 用Jina搭建PDF搜索引擎Part 1
文章图片
?
下图为 DocumentArray 摘要,其中包含了 indexed_docs.summary()
系列教程|系列教程 | 用Jina搭建PDF搜索引擎Part 1
文章图片
?
通过 indexed_docs[0].chunks.summary() 查看部分文本块或图像块:
系列教程|系列教程 | 用Jina搭建PDF搜索引擎Part 1
文章图片
?
如上图所示,Document 中一共包括 58 个块,分为 tensor(图像)和字符串(文本)。
从每个 chunk 中打印 chunk.content
chunks = indexed_docs[0].chunksfor chunk in chunks: print(chunk.content)

系列教程|系列教程 | 用Jina搭建PDF搜索引擎Part 1
文章图片
?
图像的形式为 tensor
系列教程|系列教程 | 用Jina搭建PDF搜索引擎Part 1
文章图片
?
文本则是一段很长的字符串
3、处理数据
对数据进行以下处理:
  • 将文本片段分片为更小的块,如句子。上述长字符串包含了过多信息,通过 sentencize,可以从每一个文本块中得到一个明确的语义信息。
  • 对图像进行归一化处理,便于后续在深度学习模型中进行编码。
3.1 将文本进行分句 (sentencizing)
句子示例如下:
  • It was a dark and stormy night.
  • What do a raven and a writing desk have in common?
  • Turn to p.13 to read about J.R.R. Tolkien pinging google.com in 3.4 seconds.
使用 Jina Hub 的 Sentencizer Executor,运行这些字符串。
from docarray import DocumentArray, Document from jina import Executordocs = DocumentArray( [ Document(text="It was a dark and stormy night."), Document(text="What do a raven and a writing desk have in common?"), Document(text="Turn to p.13 to read about J.R.R. Tolkien pinging google.com in 3.4 seconds") ] )exec = Executor.from_hub("jinahub://Sentencizer")exec.segment(docs, parameters={})for doc in docs: for chunk in doc.chunks: print(chunk.text)print("---")

输入上述三个句子后,得到以下输出:
系列教程|系列教程 | 用Jina搭建PDF搜索引擎Part 1
文章图片
?
上图可知 p.13 中的标点符号,被识别成了句号。这里可以借助 SpacySentencizer 进行优化。
SpacySentencizer 是一个 Executor,可以将spaCy 的 sentencizer 集成到 Jina。
只需修改第 12 行代码如下:
from docarray import DocumentArray, Document from jina import Executordocs = DocumentArray( [ Document(text="It was a dark and stormy night."), Document(text="What do a raven and a writing desk have in common?"), Document(text="Turn to p.13 to read about J.R.R. Tolkien pinging google.com in 3.4 seconds") ] )exec = Executor.from_hub("jinahub://SpacySentencizer")exec.segment(docs, parameters={})for doc in docs: for chunk in doc.chunks: print(chunk.text)print("---")

现在的结果如下图所示:
系列教程|系列教程 | 用Jina搭建PDF搜索引擎Part 1
文章图片
?
图像的形式为 tensor
将 Executor 添加到 Flow 中:
from docarray import DocumentArray from jina import Flowdocs = DocumentArray.from_files("data/*.pdf", recursive=True)flow = ( Flow() .add(uses="jinahub+sandbox://PDFSegmenter", install_requirements=True, name="segmenter") .add(uses=ChunkSentencizer, name="chunk_sentencizer") )with flow: indexed_docs = flow.index(docs)

3.2 对图像进行归一化处理
from jina import Executor, requests import numpy as npclass ImageNormalizer(Executor): @requests(on="/index") def normalize_chunks(self, docs, **kwargs): for doc in docs: for chunk in doc.chunks[...]: if chunk.blob: chunk.convert_blob_to_image_tensor()if hasattr(chunk, "tensor"): if chunk.tensor is not None: chunk.convert_image_tensor_to_uri() chunk.tags["image_datauri"] = chunk.uri chunk.tensor = chunk.tensor.astype(np.uint8) chunk.set_image_tensor_shape((64, 64)) chunk.set_image_tensor_normalization()

代码解读:
1-6: 通用 Executor 调用代码。第 5 行规定Executor 只有在调用索引 endpoint 时才能处理 Document。
8: 通过 [ ... ] 启用递归,依次对 chunk 进行处理。
9: 出现 blob 后将其转换为张量,以适应 CLIP 编码器。
12-18: 假设出现张量,我们需要把未处理张量的数据 uri 添加到元数据(即 tags)中,以便于后续检索并在前端展示图像。
为了防止文本块与图像块互相干扰:
from docarray import DocumentArray from jina import Flowdocs = DocumentArray.from_files("data/*.pdf", recursive=True)flow = ( Flow() .add(uses="jinahub+sandbox://PDFSegmenter", install_requirements=True, name="segmenter") .add(uses=ChunkSentencizer, name="chunk_sentencizer") .add(uses=ImageNormalizer, name="image_normalizer") )with flow: indexed_docs = flow.index(docs)

通过上述过程,我们实现了:
  • 构建一个全新的 PDF
  • 将 PDF 分成文本和图像两部分
  • 进一步将文本块分割成句子块
  • 对图像进行归一化处理
效果如下图所示:
系列教程|系列教程 | 用Jina搭建PDF搜索引擎Part 1
文章图片
?
如图所示,文本块和图片块并没有处在同一个 level
通过一个新的 Executor--ChunkMerger,将文本块和图像块放在同一个 level:
from jina import Executor, requests import numpy as npclass ImageNormalizer(Executor): @requests(on="/index") def normalize_chunks(self, docs, **kwargs): ...class ChunkMerger(Executor): @requests(on="/index") def merge_chunks(self, docs, **kwargs): for doc in docs:# level 0 document for chunk in doc.chunks: if doc.text: docs.pop(chunk.id) doc.chunks = doc.chunks[...]

完成分句 (sentencize) 后,将其直接放到 Flow 中,代码如下:
from docarray import DocumentArray from executors import ChunkSentencizer, ChunkMerger, ImageNormalizer from jina import Flowdocs = DocumentArray.from_files("data/*.pdf", recursive=True)flow = ( Flow() .add(uses="jinahub+sandbox://PDFSegmenter", install_requirements=True, name="segmenter") .add(uses=ChunkSentencizer, name="chunk_sentencizer") .add(uses=ChunkMerger, name="chunk_merger") .add(uses=ImageNormalizer, name="image_normalizer") )with flow: indexed_docs = flow.index(docs)

以上就是本系列教程 Part 1 的全部内容。在 Part 2 中,我们将为 Flow 添加一个编码器,使用 CLIP 将文本和图像编码为向量,从而简化的语义搜索的过程。
欢迎大家关注 Jina AI, 持续关注本系列教程更新~
?

    推荐阅读