Qanything 2.0源码解析系列4: 图片解析逻辑
2024-9-20
| 2024-9-25
Words 10776Read Time 27 min
type
Post
status
Published
date
Sep 20, 2024
slug
summary
对Qanything2.0的图片解析逻辑进行剖析,包括ocr、doc切分合并、向量化
tags
category
技术分享
icon
password
😀
前言: 这篇文章介绍Qanything针对图片类型文件的处理逻辑
qanything_kernel/core/retriever/general_document.py文件中LocalFileForInsert 类的split_file_to_docs 方法的这一段。
核心是两块:
  1. 将图片内容进行ocr识别
  1. 识别的文字内容组装成Document 类型的doc

📝 ocr识别

核心方法

引用的话语

get_ocr_result_sync方法

ocr服务启动

执行上述方法之前,应先将ocr服务启动起来,不然直接异常报错了。
在项目scripts/entrypoint.sh文件中看到ocr的启动命令是:
存放路径:qanything_kernel/dependent_server/ocr_server/ocr_models下
 
对于这样一张图片:
notion image
ocr识别的结果为:
 

组装doc

为每个doc注入元数据(不重要)

主要是为doc注入file_id,kb_id之类的元数据,还有个操作是合并短的doc,对pdf、doc这类文件起作用,对图片类文件不起作用,因为一个图片只有一个doc。所以下面这个方法可以忽略。
到这里得到了以整个ocr结果为整体的一个doc,后续会对这个doc进行split。
  1. 第一步拆分,将这一个doc拆分成长度都小于800的n个父doc,父doc和父doc之间没有overlap, 父doc存入mysql数据库Documents表中;
  1. 第二步拆分,将n个父doc,每个都拆分成m个长度小于400的子doc,子doc和子doc之间有100个chunk_overlap,子doc进行embedding编码后存入milvus向量数据库中。
至于为什么要两步拆分,后续有解释。

存milvus向量数据库

  1. 先用num_tokens_embed这个方法判断一下这个doc的tokenizer后长度有没有超过800,不超过不做处理,超过的话需要对doc切分(split)。
  1. split用的是这个
DEFAULT_PARENT_CHUNK_SIZE=800, chunk_overlap=0
  1. 对于这样一个text,先从separators=["\n\n", "\n", "。", "!", "!", "?", "?", ";", ";", "……", "…", "、", ",", ",", " ", ""]中从左到右判断哪个符号可以对这个text进行拆分,\n\n不行,\n可以。
  1. 按照\n对text进行拆分,然后每个句子和前面的\n进行合并,处理完就变成了一个列表。
    1. 遍历上述列表,看看是不是还有长度超过800的chunk,如果有,\n已经用过了,用\n后面的符号按照上述流程,继续拆分这个chunk。这也就是RecursiveCharacterTextSplitter这个名字的来源,递归的去处理。
      1. 💡
        注意这里的长度是通过num_tokens_embed函数计算出来的,不一定是文字本身的长度,但是可以这么理解。
        遍历的过程中如果发现这个chunk的长度小于800,将其添加到_good_splits 列表中,直到遇到大于800的chunk。遇到之后两步操作:
        • 如果_good_splits 不为空,则将_good_splits列表内容进行merge
        • 继续按照上述逻辑递归的处理这个chunk
        如果遍历完这个列表都没遇到大于800的chunk,最终对_good_splits 进行一遍merge操作。
    1. merge的逻辑
      1. 以我们这个text为例,执行完c步骤后,_good_splits 的内容和b步骤的列表是一模一样的,总共是38行,所以列表长度也是38,因为所有chunk的长度都小于800,不涉及递归的操作。
        遍历这个_good_splits,定义一个total统计遍历过的所有chunk的总长度,直到遍历到当前的chunk,发现总长度超过800,则对当前chunk之前的所有chunk进行merge(不包含当前chunk)。合并逻辑直接按照切分符号拼接成字符串,拼接成一个大chunk(切分符号是空字符串)。
        chunk_overlap的逻辑也在merge的逻辑内:
        为了提高文档检索的效果,一般都会设置一个overlap,增加每个document(简称doc)的上下文关联。
        举个例子,比如对于这样一个splits,设置了chunk_overlap那么每个doc开头都会和上一个doc的末尾有重叠,这样就增加了每个doc的上下文关联。
        merge逻辑的源代码:
         
        c步骤的结果经d步骤处理完之后变成了两个doc,每个doc长度都在800内,且没有overlap,因为参数传递的chunk_overlap=0。这两个doc会存入mysql Documents表中。
         
    3.为每个doc注入doc_id
    这里有两个doc,每个doc长度都在800内,且没有overlap。
    这个很简单,就是file_id + ‘_’ + doc的位置 doc_ids = [file_id + '_' + str(i) for i, _ in enumerate(documents)]
     
    1. 对前面生成的两个doc继续split。
    这个split和前面的split参数有些不同,DEFAULT_CHILD_CHUNK_SIZE=400,chunk_overlap=100.
    为每个子doc注入doc_id:
    前面生成了两个doc,以第一个doc生成的子doc为例,子doc的长度是400左右,且个其他子doc之间有100个token的重复。子doc的doc_id都是e1368aa4c0cf4d88bda9874ae3686811_0。
     
    1. 子doc向量化并存入milvus向量数据库
    本来有两个父doc,第一个doc产生了三个子doc,第二个doc产生了一个子doc,总共4个子doc,现在要对着4个子doc进行操作。4个doc如下,删除了一些无用的属性。
    1. 起bce-embedding的服务,得到四个doc的embedding向量
    b. 存milvus向量数据库。
     
    💡
    两个父doc存入了mysql数据库中,四个子doc进行向量化存入了milvus数据库中。
    为什么有了父doc,直接不直接将父doc向量化存milvus数据库中呢。Qanything父doc之间没有overlap,但我们也可以直接设置一个呀。为什么还要对父doc进行split。
    💡
    以我的理解回答上述问题: 是为了问答接口的检索逻辑和大模型回答这样做的。 1. 父doc的长度是800,子doc的长度是400,短的chunk有助于提高milvus的搜索准确率,当用户问了一个问题之后,能在milvus向量数据库中检索到更相关的片段。 2. 大模型问答的时候,其实是拿到了子doc的doc_id对应的父doc,这样给大模型提供了更多的上下文信息,提高大模型的回答效果。

    🤗 总结归纳

    以上就是图片解析逻辑的全部流程了。
    1. 图片ocr识别文字,这些文字注入元数据,组成一个doc
    1. 对doc进行split并merge,doc和doc之间的chunk_overlap为0,也就是没有overlap,每个doc长度在800内。
    1. 对每个doc再split并merge,doc的子doc之间有100的chunk_overlap,子doc长度400内
    1. 对所有的子doc使用bce-embedding向量化并存入milvus向量数据库

    📎 参考文章

    • Qanything v2.0源码
      💡
      有问题,欢迎您在底部评论区留言,一起交流~
      Qanything 2.0源码解析系列5:问答接口中的检索逻辑(以图片为例)Qanything 2.0 源码解析系列3 : 文件解析服务
      Loading...