LightOnOCR-2-1B与MySQL集成:构建文档内容检索系统

发布时间:2026/5/16 10:14:42

LightOnOCR-2-1B与MySQL集成:构建文档内容检索系统 LightOnOCR-2-1B与MySQL集成构建文档内容检索系统想象一下你手头有堆积如山的纸质档案、扫描合同或者历史报告急需把它们变成可搜索、可分析的数字资料。传统做法要么是人工录入耗时耗力还容易出错要么用一些OCR工具但识别出来的文本散落在各个文件里想找点东西还得一个个文件翻。最近我正好在做一个档案数字化项目就遇到了这个头疼的问题。后来试了试LightOnOCR-2-1B这个模型发现它识别效果确实不错但光识别出来还不够怎么把这些内容管起来、快速查出来才是真正能用的关键。所以我就琢磨着把LightOnOCR-2-1B和MySQL数据库搭在一起搞了一套从文档识别到内容检索的完整方案。简单说就是模型负责把图片、PDF里的文字“读”出来MySQL负责把这些文字存好、编好索引最后我们就能像用搜索引擎一样快速找到文档里的任何内容了。这套东西用下来处理效率比纯手工高了不止一个量级关键是查东西特别方便。下面我就把具体的做法和踩过的坑跟大家详细聊聊。1. 为什么选LightOnOCR-2-1B和MySQL在动手之前得先想清楚为什么是这两个组合。市面上OCR模型不少数据库也多的是选它们俩主要是看中了几个实在的好处。1.1 LightOnOCR-2-1B小而精的文档识别专家LightOnOCR-2-1B是个只有10亿参数的视觉语言模型专攻文档识别。别看它体积小在权威的OlmOCR-Bench测试里成绩比一些参数量大9倍的模型还要好。我选它主要是图它三点第一是识别准尤其是对复杂文档。我们处理的档案里经常有表格、双栏排版甚至带数学公式的。很多轻量级OCR一遇到这些就“懵”了要么顺序乱要么表格识别成一团。LightOnOCR-2-1B在这方面表现很稳它能理解文档结构输出带Markdown格式的结构化文本比如标题、列表、表格都能保留原样这对后续处理太重要了。第二是速度快成本低。它号称比一些主流OCR快好几倍。实际测下来在合适的GPU上处理一页平均也就零点几秒。对于我们这种要处理成千上万页的项目速度就是金钱。而且它模型小部署起来对硬件要求没那么高普通带显卡的服务器就能跑综合成本一下就下来了。第三是输出干净好处理。它直接输出整理好的文本不用我们再费劲去拼接检测框、调阅读顺序。这给后续存入数据库省了不少事。1.2 MySQL久经考验的关系型数据库数据库方面考虑过一些专门的全文搜索引擎但最后还是选了MySQL。原因很简单它足够通用、足够稳定而且我们团队最熟。通用性意味着好集成。几乎所有的编程语言和框架都有成熟的MySQL连接库我们的应用系统要调用数据库非常方便。它的安装、管理、备份这些运维操作网上资料一大堆遇到问题基本都能找到解决方案。全文检索功能够用。MySQL自带的全文索引FULLTEXT INDEX对于中文内容检索经过适当配置性能完全能满足内部档案查询的需求。我们不需要像互联网搜索引擎那样应对海量并发更看重的是查询的准确性和系统的稳定性。生态和工具完善。有各种现成的客户端工具像MySQL Workbench可以方便地查看和管理数据也有成熟的监控和优化方案。这对于一个需要长期维护的档案系统来说能减少很多后期的运维负担。简单说这个组合就是让专业的模型干专业的活识别让成熟的数据库干成熟的活存储和查询我们只需要在中间做好“粘合剂”把流程打通就行。2. 系统搭建四步走整个系统的工作流程可以分成四个核心步骤准备运行环境、设计数据库、编写处理程序、实现检索功能。下面我一步步拆开讲。2.1 第一步环境准备与模型部署首先得把LightOnOCR-2-1B模型跑起来。官方推荐用vLLM来部署这样能获得最好的推理速度。这里我直接用Docker Compose来管理省心。你需要准备一个带NVIDIA GPU的Linux服务器然后创建一个docker-compose.yml文件version: 3.8 services: ocr-server: image: vllm/vllm-openai:v0.12.0 command: --model lightonai/LightOnOCR-2-1B --trust-remote-code --port 8000 --max-num-seqs 4 --gpu-memory-utilization 0.5 volumes: - ~/.cache/huggingface:/root/.cache/huggingface deploy: resources: reservations: devices: - driver: nvidia count: all capabilities: [gpu] ports: - 8000:8000 restart: unless-stopped这个配置做了几件事从Docker Hub拉取vLLM镜像指定加载LightOnOCR-2-1B模型开放8000端口并允许使用所有GPU资源gpu-memory-utilization 0.5表示使用50%的GPU显存你可以根据自己显卡调整。在文件所在目录运行docker-compose up -d服务就在后台启动了。它会提供一个OpenAI兼容的API接口地址是http://你的服务器IP:8000/v1/chat/completions。你可以用下面的命令简单测试一下curl http://localhost:8000/v1/chat/completions \ -H Content-Type: application/json \ -d { model: lightonai/LightOnOCR-2-1B, messages: [{ role: user, content: What is in this image? }], max_tokens: 100 }如果看到返回JSON数据说明模型服务正常了。2.2 第二步设计MySQL数据库表结构模型服务好了接下来设计存数据的“仓库”。我们不需要很复杂的结构核心就是记录文档信息和对应的识别内容。我设计了两张主表在MySQL里执行下面的SQL语句创建-- 创建数据库 CREATE DATABASE IF NOT EXISTS document_archive DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE document_archive; -- 1. 文档元信息表 CREATE TABLE documents ( id INT AUTO_INCREMENT PRIMARY KEY, file_name VARCHAR(255) NOT NULL COMMENT 原始文件名, file_path VARCHAR(500) COMMENT 文件存储路径, file_type ENUM(pdf, jpg, png, tiff) NOT NULL COMMENT 文件类型, file_size BIGINT COMMENT 文件大小(字节), total_pages INT DEFAULT 1 COMMENT 总页数(PDF适用), ocr_status ENUM(pending, processing, completed, failed) DEFAULT pending COMMENT OCR处理状态, ocr_text LONGTEXT COMMENT 识别出的完整文本, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_status (ocr_status), INDEX idx_created (created_at) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci; -- 2. 文档内容分块表 (用于更细粒度的检索) CREATE TABLE document_chunks ( id INT AUTO_INCREMENT PRIMARY KEY, document_id INT NOT NULL COMMENT 关联的文档ID, page_number INT COMMENT 所在页码, chunk_index INT COMMENT 在当前页中的块序号, chunk_text TEXT NOT NULL COMMENT 文本块内容, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (document_id) REFERENCES documents(id) ON DELETE CASCADE, INDEX idx_document_page (document_id, page_number) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci; -- 在documents表的ocr_text字段上创建全文索引 (MySQL 5.7及以上) ALTER TABLE documents ADD FULLTEXT INDEX ft_ocr_text (ocr_text) WITH PARSER ngram;我来解释一下关键点documents表是核心每条记录对应一个被处理的文件。ocr_text字段用LONGTEXT类型能存下很长的识别结果。ocr_status字段用来跟踪处理状态非常有用。document_chunks表是可选的但强烈建议创建。因为有些文档可能上百页全文索引直接搜那么长的字段效率不高。我们把每页文本按段落或固定长度切分成“块”存到这里检索时更快也能更精准地定位到内容位置。FULLTEXT INDEX是全文索引MySQL用它来实现对文本内容的快速关键词搜索。后面的WITH PARSER ngram是针对中文的分词插件需要你的MySQL安装时支持MySQL 5.7以上版本通常都支持。2.3 第三步编写文档处理与入库程序现在有了模型API和数据库表就需要一个“工人”程序它负责1. 读取文档文件2. 调用模型API识别3. 把结果存进数据库。我用Python写了一个示例逻辑很清晰。首先安装必要的Python库pip install requests pymysql pillow pypdfium2然后是这个核心的处理脚本process_document.pyimport os import sys import time import base64 import pymysql import requests from pathlib import Path from PIL import Image import pypdfium2 as pdfium from io import BytesIO from typing import Optional, List # 配置部分 VLLM_API_URL http://localhost:8000/v1/chat/completions MODEL_NAME lightonai/LightOnOCR-2-1B # MySQL数据库连接配置 DB_CONFIG { host: localhost, user: 你的用户名, password: 你的密码, database: document_archive, charset: utf8mb4 } # 数据库操作类 class DocumentDB: def __init__(self): self.connection pymysql.connect(**DB_CONFIG) def insert_document_record(self, file_name, file_path, file_type, file_size, total_pages1): 插入一条新的文档记录返回文档ID with self.connection.cursor() as cursor: sql INSERT INTO documents (file_name, file_path, file_type, file_size, total_pages, ocr_status) VALUES (%s, %s, %s, %s, %s, pending) cursor.execute(sql, (file_name, file_path, file_type, file_size, total_pages)) self.connection.commit() return cursor.lastrowid def update_document_ocr_result(self, doc_id, ocr_text, statuscompleted): 更新文档的OCR识别结果和状态 with self.connection.cursor() as cursor: sql UPDATE documents SET ocr_text %s, ocr_status %s, updated_at CURRENT_TIMESTAMP WHERE id %s cursor.execute(sql, (ocr_text, status, doc_id)) self.connection.commit() def insert_chunks(self, doc_id, chunks_data): 批量插入文档内容块 with self.connection.cursor() as cursor: sql INSERT INTO document_chunks (document_id, page_number, chunk_index, chunk_text) VALUES (%s, %s, %s, %s) cursor.executemany(sql, chunks_data) self.connection.commit() def close(self): self.connection.close() # OCR处理核心函数 def pdf_to_images(pdf_path, dpi150): 将PDF的每一页转换为PIL Image对象列表 pdf pdfium.PdfDocument(pdf_path) images [] for page_number in range(len(pdf)): page pdf[page_number] bitmap page.render(scaledpi/72.0) # 72 DPI是PDF的标准 pil_image bitmap.to_pil() images.append(pil_image) return images def image_to_base64(pil_image): 将PIL Image转换为Base64字符串 buffered BytesIO() # 转换为RGB模式确保兼容性 if pil_image.mode ! RGB: pil_image pil_image.convert(RGB) pil_image.save(buffered, formatJPEG, quality95) return base64.b64encode(buffered.getvalue()).decode(utf-8) def call_ocr_api(image_base64: str) - Optional[str]: 调用vLLM OCR API识别单张图片 payload { model: MODEL_NAME, messages: [{ role: user, content: [{ type: image_url, image_url: { url: fdata:image/jpeg;base64,{image_base64} } }] }], max_tokens: 4096, # 根据文档长度调整 temperature: 0.1, # 低温度使输出更稳定 } try: response requests.post(VLLM_API_URL, jsonpayload, timeout60) response.raise_for_status() result response.json() return result[choices][0][message][content] except requests.exceptions.RequestException as e: print(fAPI调用失败: {e}) return None except KeyError as e: print(f解析API响应失败: {e}) return None def split_into_chunks(text, max_chunk_size1000): 将长文本按段落或固定大小分割成块 paragraphs text.split(\n\n) chunks [] current_chunk for para in paragraphs: if len(current_chunk) len(para) 2 max_chunk_size: current_chunk para \n\n else: if current_chunk: chunks.append(current_chunk.strip()) current_chunk para \n\n if current_chunk: chunks.append(current_chunk.strip()) return chunks # 主处理流程 def process_single_document(file_path: str): 处理单个文档的主函数 db DocumentDB() file_path_obj Path(file_path) if not file_path_obj.exists(): print(f文件不存在: {file_path}) return print(f开始处理文件: {file_path_obj.name}) # 1. 插入数据库记录 file_size file_path_obj.stat().st_size file_ext file_path_obj.suffix.lower()[1:] # 去掉点号 if file_ext pdf: images pdf_to_images(file_path) total_pages len(images) else: # 单张图片 images [Image.open(file_path)] total_pages 1 doc_id db.insert_document_record( file_namefile_path_obj.name, file_pathstr(file_path_obj), file_typefile_ext, file_sizefile_size, total_pagestotal_pages ) print(f已创建文档记录ID: {doc_id}, 总页数: {total_pages}) # 2. 逐页调用OCR识别 all_text_parts [] chunks_to_insert [] for page_idx, image in enumerate(images, start1): print(f 正在处理第 {page_idx}/{total_pages} 页...) # 转换为Base64并调用API img_base64 image_to_base64(image) page_text call_ocr_api(img_base64) if page_text: all_text_parts.append(page_text) # 将本页文本分块准备存入chunks表 page_chunks split_into_chunks(page_text) for chunk_idx, chunk_text in enumerate(page_chunks): chunks_to_insert.append((doc_id, page_idx, chunk_idx, chunk_text)) print(f 第{page_idx}页识别完成长度: {len(page_text)} 字符) else: print(f 第{page_idx}页识别失败) # 避免请求过快轻微延迟 time.sleep(0.5) # 3. 合并所有页文本更新主记录 full_text \n\n.join(all_text_parts) db.update_document_ocr_result(doc_id, full_text) # 4. 插入分块数据 if chunks_to_insert: db.insert_chunks(doc_id, chunks_to_insert) print(f已插入 {len(chunks_to_insert)} 个文本块到分块表) print(f文档处理完成文档ID: {doc_id}) db.close() # 脚本入口 if __name__ __main__: if len(sys.argv) 2: print(用法: python process_document.py 文档路径) sys.exit(1) target_file sys.argv[1] process_single_document(target_file)这个脚本有点长但逻辑是线性的连接数据库→传文件信息→一页页识别→存结果。你只需要修改开头的数据库配置然后运行python process_document.py /path/to/your/document.pdf就能处理一个文件了。2.4 第四步实现内容检索功能内容存进去了最后一步就是怎么把它查出来。检索的核心就是利用MySQL的全文索引。我写了一个简单的Flask应用来提供检索API当然你也可以集成到自己的系统里。先安装Flaskpip install flask。然后创建search_app.pyfrom flask import Flask, request, jsonify import pymysql from pymysql.cursors import DictCursor app Flask(__name__) # 数据库配置同上 DB_CONFIG { host: localhost, user: 你的用户名, password: 你的密码, database: document_archive, charset: utf8mb4, cursorclass: DictCursor } def get_db_connection(): return pymysql.connect(**DB_CONFIG) app.route(/api/search, methods[GET]) def search_documents(): 全文检索接口 query request.args.get(q, ) if not query: return jsonify({error: 请输入查询关键词}), 400 # 可选分页参数 page int(request.args.get(page, 1)) per_page int(request.args.get(per_page, 10)) offset (page - 1) * per_page conn get_db_connection() try: with conn.cursor() as cursor: # 1. 在主文档表进行全文检索 sql_main SELECT id, file_name, LEFT(ocr_text, 500) as text_snippet, -- 只返回前500字符作为摘要 MATCH(ocr_text) AGAINST(%s IN NATURAL LANGUAGE MODE) as relevance_score FROM documents WHERE ocr_status completed AND MATCH(ocr_text) AGAINST(%s IN NATURAL LANGUAGE MODE) ORDER BY relevance_score DESC LIMIT %s OFFSET %s cursor.execute(sql_main, (query, query, per_page, offset)) main_results cursor.fetchall() # 2. 在分块表进行更精确的检索定位到具体块 sql_chunks SELECT c.document_id, d.file_name, c.page_number, c.chunk_index, c.chunk_text, MATCH(c.chunk_text) AGAINST(%s IN NATURAL LANGUAGE MODE) as relevance_score FROM document_chunks c JOIN documents d ON c.document_id d.id WHERE d.ocr_status completed AND MATCH(c.chunk_text) AGAINST(%s IN NATURAL LANGUAGE MODE) ORDER BY relevance_score DESC LIMIT 20 cursor.execute(sql_chunks, (query, query)) chunk_results cursor.fetchall() return jsonify({ query: query, main_results: main_results, chunk_results: chunk_results, pagination: { page: page, per_page: per_page, total_in_page: len(main_results) } }) finally: conn.close() app.route(/api/document/int:doc_id, methods[GET]) def get_document_detail(doc_id): 获取单个文档的完整内容 conn get_db_connection() try: with conn.cursor() as cursor: sql SELECT id, file_name, file_type, file_size, total_pages, ocr_text, created_at FROM documents WHERE id %s AND ocr_status completed cursor.execute(sql, (doc_id,)) doc cursor.fetchone() if not doc: return jsonify({error: 文档不存在或未处理完成}), 404 return jsonify(doc) finally: conn.close() if __name__ __main__: app.run(host0.0.0.0, port5000, debugTrue)启动这个应用 (python search_app.py)你就可以通过HTTP接口检索了。比如搜索包含“合同条款”的文档GET http://localhost:5000/api/search?q合同条款获取ID为5的文档详情GET http://localhost:5000/api/document/5前端页面可以很简单一个输入框调用这个搜索接口然后把结果列表展示出来点击条目再查看详情。这样一个完整的文档内容检索系统就搭起来了。3. 实际应用中的经验与建议这套方案在项目里跑了一段时间整体挺顺畅但也遇到些小问题总结几点经验供你参考。关于性能LightOnOCR-2-1B的速度确实有优势但处理超大批量文档时单进程调用API还是慢。我们的优化方法是引入任务队列比如Celery Redis把待处理的文档路径扔进队列然后启动多个工作进程并行消费队列、调用OCR API这样能充分利用GPU。数据库写入也改用批量操作减少频繁提交。关于文本分块前面脚本里简单的按空行分块对于格式规整的文档够用。但如果文档格式杂乱分块效果不好会影响检索精度。后来我们改进了一下尝试按固定字符数比如800字重叠分块相邻块有部分重叠或者用一些简单的启发式规则比如识别到“第一章”、“第一节”这样的标题就分块效果更好。关于MySQL全文检索默认的ngram分词对中文短语的支持有时不够智能。如果对检索精度要求极高可以考虑在存入chunk_text之前用jieba等分词库先对文本分词并在另一个字段存储分词后的结果用空格连接然后对这个字段建全文索引。或者对于数据量不是特别大的场景直接用LIKE %关键词%进行模糊匹配虽然慢点但保证能搜到。关于错误处理生产环境一定要加强。比如OCR API可能偶尔超时或无响应我们的处理脚本增加了重试机制最多3次。数据库连接也要做好异常捕获和重连。对于识别失败statusfailed的文档我们单独有个管理界面可以手动重新提交处理或查看失败原因。扩展思路如果档案内容涉密这套方案全部部署在内网非常安全。如果想更智能可以在检索到结果后把相关的文本块扔给一个大语言模型LLM让它帮你做摘要、问答或者信息提取那就变成一个真正的智能档案助手了。4. 总结回过头看把LightOnOCR-2-1B和MySQL集成起来思路并不复杂就是选对工具、设计好数据流、然后一步步实现。但带来的价值是实实在在的它把一堆死气沉沉的图片、PDF变成了活的数据资产。LightOnOCR-2-1B负责攻坚用小巧的身板提供了高质量的识别结果MySQL负责守成用稳定可靠的方式把数据管起来、供出去。两者结合成本可控效果可见而且整个技术栈都是开源、主流的学习和维护成本都不高。如果你也在做文档数字化、知识库建设这类项目不妨试试这个方案。可以从处理几十个文档的小任务开始跑通整个流程感受一下从杂乱文档到精准检索的转变。过程中肯定会遇到些小麻烦但解决之后你会发现手里多了一套非常实用的工具以后类似的项目都能套用这个框架效率提升会非常明显。获取更多AI镜像想探索更多AI镜像和应用场景访问 CSDN星图镜像广场提供丰富的预置镜像覆盖大模型推理、图像生成、视频生成、模型微调等多个领域支持一键部署。

相关新闻