首页 > 数据库 >RAG实战(一):EPUB加载、文本切割与向量入库

RAG实战(一):EPUB加载、文本切割与向量入库

来源:互联网 2026-07-31 17:06:03

# 目录 1. 整体数据管道总览 2. EPubLoader:按章节加载电子书 3. RecursiveCharacterTextSplitter:把章节切成小块 4. 批量向量化与入库 5. 总结 --- ## 一、整体数据管道总览 RAG 的第一步其实不是急着去检索,而是先把原始文档转换成可检索

# 目录 1. 整体数据管道总览 2. EPubLoader:按章节加载电子书 3. RecursiveCharacterTextSplitter:把章节切成小块 4. 批量向量化与入库 5. 总结 --- ## 一、整体数据管道总览 RAG 的第一步其实不是急着去检索,而是先把原始文档转换成可检索的向量。整个数据管道需要四个组件接力完成:Loader 从文件加载文档 → Splitter 切成小块 → Embedding 转为向量 → Milvus 存储并建索引。听起来像是流水线,但每一步都决定着后续检索的质量。

--- ## 二、EPubLoader:按章节加载电子书 EPUB 这种电子书格式,内部天然按章节组织内容,LangChain 的 `EPubLoader` 能直接解析并按照章节输出 Document 对象。用法很简单: ```ja vascript import { EPubLoader } from '@langchain/community/document_loaders/fs/epub'; const loader = new EPubLoader('./天龙八部.epub', { splitChapters: true // 按章节拆分,每个章节生成一个 Document }); const documents = await loader.load(); console.log(`加载完成,共 ${documents.length} 个章节`); ``` 这里 `splitChapters: true` 是关键。EPUB 内部有章节标记,Loader 会据此把整本书拆成独立的 Document 数组,每个 Document 的 `pageContent` 就是一整章的文本。如果不开这个选项,整本书会被读成一个巨大的 Document,后续切割时就会丢失章节边界信息——那可就白瞎了 EPUB 的结构化优势。 --- ## 三、RecursiveCharacterTextSplitter:把章节切成小块 想想看,一章动辄几千字,Embedding 接口哪能一次性吞下这么长的文本?LLM 的上下文窗口也放不下整章。所以必须把一个章节**切成多个小块(chunk)**。 ```ja vascript import { RecursiveCharacterTextSplitter } from '@langchain/textsplitters'; const textSplitter = new RecursiveCharacterTextSplitter({ chunkSize: 500, // 每块最多 500 个字符 chunkOverlap: 50, // 相邻两块重叠 50 个字符 }); ``` ### 两个核心参数 | 参数 | 值 | 作用 | |------|----|------| | `chunkSize` | 500 | 每块最大字数。太大超出 embedding 窗口,太小丢失上下文 | | `chunkOverlap` | 50 | 相邻 chunk 之间的重叠字数。防止关键信息刚好卡在 chunk 边界被切断 | 这个重叠的意义在哪?举个例子:如果一刀切在第 250 字,"段誉施展凌波微步"中的"凌波微"在上一块,"步"在下一块——两块的向量都无法完整表达"凌波微步"这个武功名。但 50 字的重叠足够让"凌波微步"完整地出现在某一块中,向量就能准确捕捉它的语义。 ### 逐章切割 ```ja vascript let totalInserted = 0; const documentLen = documents.length; for (let chapterIndex = 0; chapterIndex < documentLen; chapterIndex++) { const chapter = documents[chapterIndex]; const chapterContent = chapter.pageContent; const chunks = await textSplitter.splitText(chapterContent); console.log(`第 ${chapterIndex + 1} 章拆分为 ${chunks.length} 个片段`); if (chunks.length === 0) { console.log(`跳过空章节`); continue; } const insertedCount = await insertChunksBatch( chunks, bookId, chapterIndex + 1 ); totalInserted += insertedCount; } ``` 逐章处理的好处很明显:可以保留章节号字段,后续检索时能看到结果来自哪一章;避免一次性加载整本书撑爆内存;每个 chunk 的 `chapter_num` 字段让检索结果更结构化,不再是纯粹的一堆文本。 --- ## 四、批量向量化与入库 ### Collection Schema 设计 ```ja vascript await client.createCollection({ collection_name: 'ebook', fields: [ { name: 'id', data_type: DataType.VarChar, max_length: 100, is_primary_key: true }, { name: 'book_id', data_type: DataType.VarChar, max_length: 100 }, // 哪本书 { name: 'book_name', data_type: DataType.VarChar, max_length: 200 }, // 书名 { name: 'chapter_num', data_type: DataType.Int32 }, // 第几章 { name: 'index', data_type: DataType.Int32 }, // 章节内第几个chunk { name: 'content', data_type: DataType.VarChar, max_length: 10000 }, // 原文 { name: 'vector', data_type: DataType.FloatVector, dim: 1024 }, // 向量 ] }); ```

RAG实战(一):EPUB加载、文本切割与向量入库

长期稳定更新的攒劲资源: >>>点此立即查看<<<

和上一篇 AI 日记本相比,这次多了 `book_id` 和 `chapter_num` 两个结构化字段。它们的价值在于:后续检索时不仅能拿到相关文本,还能精确知道结果来自哪本书的第几章——这就把模糊的语义搜索变成了带上下文的结构化查询。 ### 批量插入 ```ja vascript async function insertChunksBatch(chunks, bookId, chapterNum) { if (chunks.length === 0) return 0; const insertData = await Promise.all( chunks.map(async (chunk, chunkIndex) => { const vector = await getEmbedding(chunk); return { id: `${bookId}_${chapterNum}_${chunkIndex}`, book_id: bookId, book_name: BOOK_NAME, chapter_num: chapterNum, index: chunkIndex, content: chunk, vector: vector }; }) ); const insertResult = await client.insert({ collection_name: 'ebook', data: insertData }); return Number(insertResult.insert_cnt) || 0; } ``` `Promise.all` 将同一章内所有 chunk 的 embedding 请求**并行发出**。一章 20 个 chunk 就同时发 20 个请求,等全部拿到向量后再一次性 `client.insert()` 写入数据库。并行请求省时间,批量写入省网络往返,性能和可靠性都能兼顾。 ### 建立索引 ```ja vascript await client.createIndex({ collection_name: 'ebook', field_name: 'vector', index_type: IndexType.IVF_FLAT, metric_type: MetricType.COSINE, params: { nlist: 1024 } }); ``` `nlist` 是 K-Means 聚类的簇数——1024 个簇意味着查询时只需在与 query 最近的几个簇内计算相似度,而不是全量比对。对于数十万条向量数据,索引是毫秒级响应的前提,没有这一步,检索速度会慢到让人怀疑人生。 --- ## 五、总结 1. **EPubLoader + splitChapters**:按章节加载 EPUB,每个章节变成独立 Document。保留章节边界,让后续检索结果可以追溯到具体章节。 2. **RecursiveCharacterTextSplitter**:`chunkSize: 500` 控制每块字数,`chunkOverlap: 50` 防止关键信息被边界切断。逐章切割,空章节跳过。 3. **Collection 多字段设计**:除了 `vector` 和 `content`,加入 `book_id`、`chapter_num`、`index` 让数据不仅有语义可搜,还有结构可查。 4. **Promise.all 并行 + 批量写入**:同一章 chunk 并行请求 embedding,一次性 insert,性能和可靠性兼顾。 5. **IVF_FLAT + nlist**:聚类索引让查询从 O(n) 全量比对降为 O(log n) 近簇搜索。 第一篇完成数据管道——从 epub 文件到向量数据库。下一篇,用自然语言搜天龙八部,看看"段誉会什么武功"能捞出哪些片段。 --- *—— 一本带助手的智慧之书。*

侠游戏发布此文仅为了传递信息,不代表侠游戏网站认同其观点或证实其描述

热游推荐

更多
湘ICP备2026025700号-3 湘公网安备 43070302000280号
All Rights Reserved
本站为非盈利网站,不接受任何广告。本站所有软件,都由网友
上传,如有侵犯你的版权,请发邮件给xiayx666@163.com
抵制不良色情、反动、暴力游戏。注意自我保护,谨防受骗上当。
适度游戏益脑,沉迷游戏伤身。合理安排时间,享受健康生活。