类chatDoc文档定位功能开发指南

类chatDoc文档定位功能开发指南

最近做一个类似chatDoc的文档定位功能,这里记录一下开发过程中如何对文件定位的。

1. 用户场景

用户在网页中上传pdf文件,保存后文件内容会被LLM(大语言模型)拆分解析,存入向量数据库中。用户在查看解析结果时,需要在页面上显示解析结果的来源,即pdf文件的具体位置。

2. 解决方案

2.1 问题分析

要定位到pdf文件到具体位置,首先就要渲染pdf,其次要能获取到解析结果来源的位置信息或者文字信息,然后操纵pdf文件,将pdf文件定位到指定位置或高亮匹配到的文字。

2.2 技术选型

没什么好聊的,pdfjs是其他开源库比如vue-pdf的基础,而我要对pdf文件进行更加细致的操作(我只是担心黑盒),所以不能选择二次封装的库,就选了pdfjs。仓库GitHub地址

2.3 开发过程

(进入正题)

2.3.1 pdfjs的引入

这里有两个要注意的点: pdfjs有两种引入方式,一种是通过下载pdfjs的包,然后当作静态资源引入,另一种是通过npm安装,而后者的话,要执行的是 npm install pdfjs-dist(pdfjs这个名字被抢注了),我在项目里采用的是后者。

在引入的时,要注意自己项目中使用的webpack或者vite的配置或版本,因为pdfjs的代码中有一些es6或更新的语法,如果webpack或者vite的配置不支持的话,会报错。保险起见可以引入2.x版本。
还有一点是,在代码中引入这个包时,注意包的导出方式,看看有没有 export default,如果有的话,就要用 import pdfjsLib from 'pdfjs-dist',如果没有的话,就要用 import * as pdfjsLib from 'pdfjs-dist'
在引入pdfjs之后,还要给pdfjs配置相同版本的worker,不然会报一个警告:Warning: Setting up fake worker

然后在vue文件中引入:

js 复制代码
// vue 版本 2.7.13
// pdfjs 版本 2.7.570
import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf' 
// 这里要将legacy下的pdf.worker.min.js放到在静态资源文件夹中,我在 vite 和 webpack 里都是这样干的,有其他方法请指点
pdfjsLib.GlobalWorkerOptions.workerSrc = '/static/js/pdf.worker.min.js'

2.3.2 pdf文件的渲染

引入pdfjs之后,接下来就要渲染pdf文件内容了。渲染的方式决定着我能否高亮匹配的文字。

  • 仅渲染图片 这种方式渲染出来的内容,是一张张图片,这样的话,无法在页面上高亮文字。

  • 仅渲染文字 这种方式渲染,能高亮文字,但是会丢失文件中的图像信息。

  • 文字和图片都渲染 这种方式渲染,能高亮文字,也能显示图片,pdfjs 的 viewer.js文件 和 vue-pdf 之类的二次封装库都是这样渲染的。

小结:采用第三种方式渲染pdf文件,这样既能高亮文字,也不会丢失信息。这种渲染模式的实现原理是,先用 canvas 渲染出图片,然后渲染文字,将文字覆盖在图片上,设置透明度,这样在网页上看起来像是高亮了图片中的文字。

html 复制代码
<!-- pdfView.vue -->
<template>
  <div ref="pdfViewer" class="pdf-viewer"></div>
</template>
  
<script>
import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf' 
pdfjsLib.GlobalWorkerOptions.workerSrc = '/static/js/pdf.worker.min.js'

export default {
  name: "PdfViewer",
  data() {
    return {
      url: "/path/pdfFile.pdf"
    }
  },
  mounted() {
    this.loadPdf();
  },
  methods: {
    // 加载pdf
    loadPdf() {
      pdfjsLib.getDocument(this.url).promise.then(pdf => {
        this.renderPdf(pdf);
      });
    },
    // 渲染pdf全部页面
    renderPdf(pdf) {
      for (let i = 1; i <= pdf.numPages; i++) {
        pdf.getPage(i).then(page => {
          this.renderPage(page);
        });
      }
    },
    async renderPage(page) {
      // 先渲染图片
      const viewport = page.getViewport({ scale: 1 });
      const canvas = document.createElement("canvas");
      const context = canvas.getContext("2d");
      canvas.height = viewport.height;
      canvas.width = viewport.width;
      const renderContext = {
        canvasContext: context,
        viewport: viewport,
      };
      await page.render(renderContext).promise;
      // 再渲染文字
      const textContent = await page.getTextContent();
      const textLayer = document.createElement("div");
      pdfjs.renderTextLayer({
        textContent: textContent,
        container: textLayer,
        viewport: viewport,
        textDivs: [],
      });
      textLayer.className = "textLayer";
      // 将文字覆盖在图片上
      const canvasWrapper = document.createElement("div");
      canvasWrapper.className = "canvasWrapper";
      canvasWrapper.appendChild(canvas);
      canvasWrapper.appendChild(textLayer);
      this.$refs.pdfViewer.appendChild(canvasWrapper);
    },
  }
}
</script>
<style scoped>
.pdf-viewer {
  width: 80vw;
  height: 80vh;
  margin: 0 auto;
  border: 1px solid #ccc;
  overflow: auto;
}
</style>
<style>
/* 不能用scoped,因为动态创建的元素不会被编译带上hash */
/* 不加上这些css修饰,元素会错位,样式来自vue-pdf-embed这个库 */
.canvasWrapper {
  position: relative;
}
.textLayer {
  text-align: initial;
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  overflow: hidden;
  opacity: 0.2;
  line-height: 1;
  text-size-adjust: none;
  forced-color-adjust: none;
}
.textLayer span,
.textLayer br {
  color: transparent;
  position: absolute;
  white-space: pre;
  cursor: text;
  transform-origin: 0% 0%;
}
</style>

这样,我们得到了一个渲染pdf的组件,支持选中文字高亮操作。

2.3.3 文字高亮

这里需要注意,我们在之前的代码里获得的 textContent 内容,划分的文字不是一行一行或者一个自然段这样划分的,而是按照分隔符划分的,包括括号、逗号、句号、换行符、图标等等,也就是说,我们在匹配文字的时候,可能会被这个影响匹配结果,这是其一;其二是注意到我们是一页一页的获取文件信息,所以我们其实不能匹配跨页的内容

其实不用说也知道,当使用上面的组件渲染之后,查看元素就知道是怎么一回事了。(推荐查看)

那么,最简单的文字匹配就是使用 querySelector 方法获取元素,然后查看 textContent 中是否包含答案,如果包含的话,就将这个元素高亮。

2.3.4 文档定位

仅仅是高亮是不够的,毕竟看不到的高亮就相当于没高亮。于是在高亮的同时,我们需要将pdf文件滚动到高亮的位置。 这里的逻辑也比较简单,我们在渲染的时候可以拿到每一页的高度 viewport.height ,然后将高亮的元素所在的页数乘以每页的高度,就是高亮元素距离顶部的距离,然后将这个距离赋值给滚动条的 scrollTop 属性,就能将高亮元素滚动到顶部了。

我这里的话,LLM 会返回一个匹配结果的位置信息,包括所在的页码,自然段顶部距离页面顶部的距离,是否跨页,自然段底部距离页面顶部的距离。我的高亮方式是多渲染了一个框,把大段的结果先框起来,然后高亮其中的答案。要注意的一点是,再新渲染框时,记得销毁之前的框和高亮的文字也要取消高亮。也要注意框、文字、图片的层级关系。

2.4 其他问题

pdf的文件内容即可以来源于静态资源也可以是文件流(反正这个最后也要被转化) 静态资源的话直接填地址就好,而文件流的形式,则需要多做小小的转换。

js 复制代码
// axios发起请求时,设置responseType为blob
axios({
  url: '...',
  method: '...',
  responseType: 'blob'
}).then(res => {
  // res.data就是文件流
})

// 将文件流转化为blob
const blob = new Blob([file], { type: 'application/pdf' })
// 将blob转化为url
const file = new FileReader()
file.readAsDataURL(blob)
file.addEventListener('load', () => {
  const url = file.result
})
// 或
// const url = URL.createObjectURL(blob)

3. 总结

这个功能的开发过程中,遇到了很多问题,比如pdfjs的引入、pdf文件的渲染、文字高亮、文档定位等等,这些问题都是在开发过程中遇到的,也是在解决问题的过程中学到的,所以这里记录一下,抛砖引玉,希望大家能不吝赐教。

相关推荐
老陈测评10 小时前
医疗AI的下一个十年:从辅助工具走向模式重构
人工智能·健康医疗
乾元10 小时前
RAG 架构: 利用向量数据库构建企业的安全知识库
运维·网络·数据库·人工智能·安全·网络安全·架构
JEECG官方10 小时前
JeecgBoot低代码平台 Qiankun 微前端集成指南:主应用配置全流程
前端
whysqwhw10 小时前
菜鸡玩 AI
人工智能
JEECG官方10 小时前
JeecgBoot低代码平台从 WPS 切换到 OnlyOffice 的开发配置指南
前端
兜兜风d'10 小时前
PyTorch 深度学习实践——加载数据集
人工智能·pytorch·深度学习
zzzzzz31010 小时前
手把手教你搭建 OpenClaw AI 私人助理(保姆级教程)
人工智能
lichenyang45310 小时前
虚拟 DOM、Diff 算法与 Fiber
前端·javascript·面试
寻见90310 小时前
收藏!OpenClaw 配置文件保姆级详解|从入门到精通,避坑 + 优化全搞定
人工智能·agent
&Darker10 小时前
十三、大语言模型微调
人工智能·python·语言模型