1. 加载数据(摄取) #
在您选定的LLM能够处理数据之前,首先需要对数据进行处理和加载。这与机器学习领域中的数据清洗/特征工程流程类似,或传统数据环境中的ETL流程相似。
1.1 数据摄取流程概述 #
该数据摄取流程通常包含三个主要阶段:
- 加载数据 - 从各种数据源获取原始数据
- 转换数据 - 对数据进行处理和格式化
- 索引并存储数据 - 将处理后的数据存储到索引中
我们将在索引和存储部分详细讨论索引和存储相关内容。在本指南中,我们将主要讨论加载器(loaders)和转换(transformations)。
1.2 加载器 #
在您选择的LLM能够处理数据之前,您需要先加载数据。LlamaIndex通过数据连接器(也称为数据适配器)来实现这一过程。Reader 数据连接器从不同数据源摄取数据,并将数据格式化为 Document 对象。一个 Document 是一组数据(目前为文本,未来将包括图像和音频)及其相关元数据的集合。
1.2.1 使用SimpleDirectoryReader加载数据 #
最易用的阅读器是我们的 SimpleDirectoryReader,它能将指定目录下的每个文件转换为文档。该工具内置于LlamaIndex,支持读取多种格式,包括Markdown、PDF、Word文档、PowerPoint幻灯片、图片、音频和视频文件。
# 导入SimpleDirectoryReader类
# 这是LlamaIndex内置的文档读取器,用于从目录中读取各种格式的文件
from llama_index.core import SimpleDirectoryReader
# 创建SimpleDirectoryReader实例,指定数据目录路径
# 这会读取./data目录下的所有支持格式的文件
documents = SimpleDirectoryReader("./data").load_data()
# 打印加载的文档数量
print(f"成功加载了 {len(documents)} 个文档")
# 显示每个文档的基本信息
for i, doc in enumerate(documents):
print(f"文档 {i+1}: {doc.metadata.get('file_name', '未知文件名')}")1.2.2 使用来自LlamaHub的Readers #
由于数据来源的可能性众多,这些连接器并未全部内置。相反,您需要从我们的数据连接器注册表中下载它们,即 LlamaHub。
在此示例中,LlamaIndex下载并安装名为数据库读取器的连接器,它会对SQL数据库执行查询并将结果的每一行作为 Document:
uv add llama-index-readers-database1.2.2.1 sqlite3 #
# 导入os模块,用于操作系统相关功能
import os
# 导入sqlite3模块,用于操作SQLite数据库
import sqlite3
# 从llama_index.core模块导入Document类,用于文档对象
from llama_index.core import Document
# 从llama_index.core模块导入VectorStoreIndex类,用于向量索引
from llama_index.core import VectorStoreIndex
# 定义创建SQLite数据库和示例数据的函数
def create_sample_database():
"""创建示例SQLite数据库和表"""
# 连接到名为sample.db的SQLite数据库(如果不存在则自动创建)
conn = sqlite3.connect("sample.db")
# 获取数据库游标对象
cursor = conn.cursor()
# 创建users表(如果不存在则新建)
cursor.execute(
"""
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
email TEXT,
department TEXT,
salary REAL
)
"""
)
# 定义要插入的示例数据
sample_data = [
(1, "张三", "zhangsan@company.com", "技术部", 8000),
(2, "李四", "lisi@company.com", "市场部", 7500),
(3, "王五", "wangwu@company.com", "人事部", 7000),
(4, "赵六", "zhaoliu@company.com", "技术部", 8500),
(5, "钱七", "qianqi@company.com", "财务部", 7200),
]
# 批量插入示例数据到users表
cursor.executemany(
"""
INSERT OR REPLACE INTO users (id, name, email, department, salary)
VALUES (?, ?, ?, ?, ?)
""",
sample_data,
)
# 提交事务,保存更改
conn.commit()
# 关闭数据库连接
conn.close()
# 打印数据库创建完成提示
print("✅ 示例数据库创建完成")
# 定义从数据库查询数据并转换为Document对象的函数
def load_data_from_database():
"""从SQLite数据库加载数据并转换为Document对象"""
# 连接到sample.db数据库
conn = sqlite3.connect("sample.db")
# 获取数据库游标对象
cursor = conn.cursor()
# 执行SQL查询,获取users表所有数据
cursor.execute("SELECT * FROM users")
# 获取所有查询结果
rows = cursor.fetchall()
# 初始化文档对象列表
documents = []
# 遍历每一行数据
for row in rows:
# 构造格式化的文本内容
text_content = f"用户信息: ID={row[0]}, 姓名={row[1]}, 邮箱={row[2]}, 部门={row[3]}, 薪资={row[4]}"
# 创建Document对象,包含文本内容和元数据
doc = Document(
text=text_content,
metadata={
"id": row[0],
"name": row[1],
"email": row[2],
"department": row[3],
"salary": row[4],
"source": "database",
},
)
# 将Document对象添加到列表
documents.append(doc)
# 关闭数据库连接
conn.close()
# 返回文档对象列表
return documents
# 定义主函数
def main():
"""主函数,演示从数据库加载数据"""
try:
# 打印演示标题
print("🗄️ 数据库文档加载演示")
# 打印分隔线
print("=" * 50)
# 创建示例数据库
create_sample_database()
# 打印加载数据提示
print("\n📊 正在从数据库加载数据...")
# 从数据库加载文档对象
documents = load_data_from_database()
# 打印加载的文档数量
print(f"✅ 从数据库加载了 {len(documents)} 个文档")
# 打印前3个文档的内容和元数据
print("\n📋 文档内容预览:")
for i, doc in enumerate(documents[:3]):
print(f"文档 {i+1}: {doc.text}")
print(f" 元数据: {doc.metadata}")
print()
# 打印数据库统计信息
print(f"📈 数据库统计:")
print(f"- 总文档数: {len(documents)}")
print(
f"- 平均文档长度: {sum(len(doc.text) for doc in documents) / len(documents):.0f} 字符"
)
# 按部门统计文档数量
departments = {}
for doc in documents:
# 获取部门名称
dept = doc.metadata["department"]
# 统计每个部门的文档数
departments[dept] = departments.get(dept, 0) + 1
# 打印部门分布统计
print(f"- 部门分布: {departments}")
# 创建向量索引
print("\n🤖 创建向量索引...")
index = VectorStoreIndex.from_documents(documents)
# 创建查询引擎
print("🔍 创建查询引擎...")
query_engine = index.as_query_engine()
# 打印自然语言查询演示标题
print("\n📝 自然语言查询演示:")
print("-" * 40)
# 查询1:技术部有哪些员工
print("查询1: 技术部有哪些员工?")
response1 = query_engine.query("技术部有哪些员工?")
print(f"回答: {response1}")
print()
# 查询2:薪资最高的员工是谁
print("查询2: 薪资最高的员工是谁?")
response2 = query_engine.query("薪资最高的员工是谁?")
print(f"回答: {response2}")
print()
# 查询3:市场部员工信息
print("查询3: 市场部的员工信息")
response3 = query_engine.query("市场部的员工信息")
print(f"回答: {response3}")
print()
# 查询4:公司的平均薪资是多少
print("查询4: 公司的平均薪资是多少?")
response4 = query_engine.query("公司的平均薪资是多少?")
print(f"回答: {response4}")
print()
# 打印演示完成提示
print("\n✅ 演示完成!")
# 捕获异常并打印错误信息
except Exception as e:
print(f"❌ 数据库操作出错: {e}")
print("请检查数据库连接和查询语句")
# 程序入口点,判断是否为主模块
if __name__ == "__main__":
# 调用主函数
main()
1.2.2.2 mysql #
uv add mysqlclient# 导入os模块,用于读取环境变量
import os
# 从llama_index.core模块导入download_loader(虽然本代码未用到)
from llama_index.core import download_loader
# 从llama_index.readers.database模块导入DatabaseReader类
from llama_index.readers.database import DatabaseReader
# 创建数据库读取器实例,使用环境变量配置数据库连接参数
reader = DatabaseReader(
# 获取数据库类型,默认为mysql
scheme=os.getenv("DB_SCHEME", "mysql"),
# 获取数据库主机地址,默认为localhost
host=os.getenv("DB_HOST", "localhost"),
# 获取数据库端口,默认为3306
port=os.getenv("DB_PORT", "3306"),
# 获取数据库用户名,默认为root
user=os.getenv("DB_USER", "root"),
# 获取数据库密码,默认为123456
password=os.getenv("DB_PASS", "123456"),
# 获取数据库名称,默认为jobs
dbname=os.getenv("DB_NAME", "jobs"),
)
# 定义SQL查询语句,查询users表的所有数据
query = "SELECT * FROM users"
# 执行SQL查询,并将结果加载为文档对象列表
documents = reader.load_data(query=query)
# 打印从数据库加载的文档数量
print(f"从数据库加载了 {len(documents)} 个文档")
# 遍历前3个文档,打印每个文档的前100个字符内容
for i, doc in enumerate(documents[:3]):
print(f"文档 {i+1}: {doc.text[:100]}...")
有数百种连接器可供使用于 LlamaHub!
1.2.3 直接创建Documents #
除了使用加载器,您也可以直接创建 Document 对象:
# 从llama_index.core模块导入Document类
from llama_index.core import Document
# 创建一个Document对象,text参数指定文档的文本内容
doc = Document(text="这是一个示例文档的文本内容。")
# 创建一个带有元数据的Document对象
# text参数指定文档内容,metadata参数为字典类型,包含文档的属性信息
doc_with_metadata = Document(
text="这是另一个示例文档。",
metadata={"source": "手动创建", "category": "示例", "created_date": "2024-01-01"},
)
# 打印第一个文档的文本内容
print(f"文档1: {doc.text}")
# 打印第二个文档的文本内容
print(f"文档2: {doc_with_metadata.text}")
# 打印第二个文档的元数据信息
print(f"文档2元数据: {doc_with_metadata.metadata}")
2. 转换 #
数据加载完成后,在存入存储系统前还需进行加工和转换。这些转换包括分块处理、提取元数据以及为每个数据块生成嵌入向量。这些步骤对于确保数据能被有效检索并被LLM高效使用至关重要。
转换的输入/输出是 Node 对象(一个 Document 是 Node 类的一个子类)。转换操作还可以堆叠和重新排序。
我们提供用于文档转换的高级和低级API。
2.1 高级转换API #
索引具有 .from_documents() 方法,该方法接受一个Document对象数组,并能正确解析和分块处理它们。但有时您可能希望对文档的拆分方式拥有更大的控制权。
# 导入VectorStoreIndex类,用于创建向量存储索引
from llama_index.core import VectorStoreIndex
# 导入Document类,用于构建文档对象
from llama_index.core import Document
# 创建一个包含多个文档的列表,每个文档都用Document类封装
documents = [
# 第一个文档,内容为人工智能的简介
Document(
text="这是一个关于人工智能的文档。人工智能是计算机科学的一个分支,致力于创建能够执行通常需要人类智能的任务的系统。"
),
# 第二个文档,内容为机器学习的简介
Document(
text="机器学习是人工智能的一个子领域,它使计算机能够在没有明确编程的情况下学习和改进。"
),
# 第三个文档,内容为深度学习的简介
Document(
text="深度学习是机器学习的一个分支,使用多层神经网络来模拟人脑的学习过程。"
),
]
# 使用documents列表创建向量存储索引
# 该方法会自动将文档分块并生成向量嵌入
vector_index = VectorStoreIndex.from_documents(documents)
# 基于向量索引创建查询引擎
# 查询引擎可以用自然语言进行检索
query_engine = vector_index.as_query_engine()
# 使用查询引擎进行一次自然语言查询
response = query_engine.query("请介绍一下这些文档的内容")
# 输出查询结果
print(response)
在底层实现中,这会将您的Document拆分为Node对象。这些Node与Documents类似(包含文本和元数据),但与父级Document存在关联关系。
如果您想自定义核心组件,比如文本分割器,通过这个抽象层您可以传入一个自定义的 transformations 列表或设置全局的 Settings:
# 导入SimpleDirectoryReader类,用于从目录中读取文档
from llama_index.core import SimpleDirectoryReader
# 导入IngestionPipeline类,用于构建数据摄取管道
from llama_index.core.ingestion import IngestionPipeline
# 导入TokenTextSplitter类,用于将文本分割成小块
from llama_index.core.node_parser import TokenTextSplitter
# 导入TitleExtractor和KeywordExtractor类,用于提取标题和关键词
from llama_index.core.extractors import TitleExtractor, KeywordExtractor
# 导入OpenAIEmbedding类,用于生成文本嵌入向量
from llama_index.embeddings.openai import OpenAIEmbedding
# 使用SimpleDirectoryReader从指定目录加载文档
documents = SimpleDirectoryReader("./data").load_data()
# 创建数据摄取管道,组合多个转换步骤
pipeline = IngestionPipeline(
transformations=[
# 使用TokenTextSplitter将文本分割为长度为512、重叠10的块
TokenTextSplitter(chunk_size=512, chunk_overlap=10),
# 提取每个节点的标题
TitleExtractor(),
# 提取每个节点的关键词
KeywordExtractor(),
# 生成每个节点的嵌入向量
OpenAIEmbedding(),
]
)
# 运行数据摄取管道,将文档转换为节点
nodes = pipeline.run(documents=documents)
# 打印生成的节点数量
print(f"生成了 {len(nodes)} 个节点")
# 遍历所有节点,打印节点的部分内容、标题、关键词和嵌入维度
for i, node in enumerate(nodes):
print(f"\n节点 {i+1}:")
print(f" 文本内容: {node.text[:150]}...")
print(f" 标题: {node.metadata.get('document_title', '无标题')}")
print(f" 关键词: {node.metadata.get('excerpt_keywords', '无关键词')}")
print(
f" 嵌入维度: {len(node.embedding) if hasattr(node, 'embedding') else '无嵌入'}"
)
print("-" * 80)2.2 低级转换API #
您也可以明确地定义这些步骤。您可以通过两种方式实现:将我们的转换模块(如文本分割器、元数据提取器等)作为独立组件使用,或者在我们的声明式框架中组合它们,即转换管道接口。
2.2.1 将您的文档拆分为Nodes #
处理文档的关键步骤是将它们分割成"块"/Node对象。核心思想是将数据分解为便于检索和输入LLM的小块片段。
LlamaIndex支持广泛的文本分割器,从基于段落/句子/标记的分割器到如HTML、JSON等基于文件的分割器。
这些可以单独使用或作为数据摄取管道的一部分。
# 导入必要的类
from llama_index.core import SimpleDirectoryReader
from llama_index.core.ingestion import IngestionPipeline
from llama_index.core.node_parser import TokenTextSplitter
# 加载文档
documents = SimpleDirectoryReader("./data").load_data()
# 创建数据摄取管道
# IngestionPipeline允许您组合多个转换步骤
pipeline = IngestionPipeline(
transformations=[
TokenTextSplitter(chunk_size=512, chunk_overlap=10), # 文本分割器
# 可以添加更多转换器,如元数据提取器、嵌入生成器等
]
)
# 运行管道,将文档转换为节点
nodes = pipeline.run(documents=documents)
# 打印节点信息
print(f"生成了 {len(nodes)} 个节点")
for i, node in enumerate(nodes[:3]):
print(f"节点 {i+1}: {node.text[:100]}...")2.2.2 添加元数据 #
您还可以选择为您的文档和节点添加元数据。这一操作可以手动完成,也可以通过自动元数据提取器。
以下是关于:
的详细指南。
# 导入Document类
from llama_index.core import Document
# 创建带有元数据的文档
# metadata参数可以包含任何键值对,用于描述文档的属性
document = Document(
text="这是一个包含丰富元数据的示例文档。",
metadata={
"filename": "example.txt", # 文件名
"category": "技术文档", # 文档类别
"author": "张三", # 作者
"created_date": "2024-01-01", # 创建日期
"tags": ["AI", "机器学习", "文档"], # 标签列表
"version": "1.0", # 版本号
"language": "zh-CN" # 语言
}
)
# 打印文档内容和元数据
print("文档内容:")
print(document.text)
print("\n文档元数据:")
for key, value in document.metadata.items():
print(f" {key}: {value}")2.2.3 添加Embeddings #
要将节点插入向量索引,它必须包含一个embedding。请参阅我们的数据摄取管道或者我们的嵌入指南了解更多详情。
# 导入llama_index核心模块中的向量存储索引、设置项和简单目录读取器
from llama_index.core import VectorStoreIndex, Settings, SimpleDirectoryReader
# 导入OpenAI的嵌入模型
from llama_index.embeddings.openai import OpenAIEmbedding
# 读取指定目录下的数据文件,加载为文档对象
documents = SimpleDirectoryReader("./data").load_data()
# 设置全局嵌入模型为OpenAIEmbedding
Settings.embed_model = OpenAIEmbedding()
# 基于加载的文档创建向量存储索引
index = VectorStoreIndex.from_documents(documents)
# 通过索引对象创建查询引擎
query_engine = index.as_query_engine()
# 使用查询引擎对文档进行内容总结查询
response = query_engine.query("请总结一下这些文档的主要内容")
# 输出查询结果
print(response)
2.2.4 直接创建并传递Nodes #
如果您愿意,可以直接创建节点并将节点列表直接传递给索引器:
# 导入TextNode类,用于表示文本节点
from llama_index.core.schema import TextNode
# 导入VectorStoreIndex类,用于创建向量存储索引
from llama_index.core import VectorStoreIndex
# 创建第一个文本节点,包含文本内容和唯一标识符
node1 = TextNode(
text="这是第一个文本块的内容。它包含了一些重要的信息。", id_="node_001"
)
# 创建第二个文本节点,包含文本内容和唯一标识符
node2 = TextNode(text="这是第二个文本块的内容。它包含了更多相关信息。", id_="node_002")
# 创建第三个文本节点,包含文本内容、唯一标识符和元数据信息
node3 = TextNode(
text="这是第三个文本块的内容。",
id_="node_003",
metadata={"source": "手动创建", "category": "示例", "importance": "高"},
)
# 通过节点列表直接创建向量存储索引
index = VectorStoreIndex([node1, node2, node3])
# 通过索引对象创建查询引擎
query_engine = index.as_query_engine()
# 使用查询引擎进行测试查询
response = query_engine.query("这些节点包含了什么信息?")
# 打印查询结果
print(response)
# 打印索引中包含的节点数量
print(f"\n索引包含 {len([node1, node2, node3])} 个节点:")
# 遍历每个节点并打印其前50个字符内容
for i, node in enumerate([node1, node2, node3], 1):
print(f"节点 {i}: {node.text[:50]}...")
3. Document VS Node #
3.1 基本概念 #
Document(文档):
- 是LlamaIndex中表示原始数据的基本单位
- 通常对应一个完整的文件或数据源
- 包含文本内容和元数据
- 是数据加载阶段的产物
Node(节点):
- 是Document经过处理后的小块数据
- 是索引和检索的基本单位
- 也包含文本内容和元数据
- 是数据转换阶段的产物
3.2 继承关系 #
3.2.1 Document和TextNode #
# 从llama_index.core模块导入Document类
from llama_index.core import Document
# 从llama_index.core.schema模块导入TextNode和BaseNode类
from llama_index.core.schema import TextNode, BaseNode
# 判断Document是否是TextNode的子类,并输出结果
print(f"Document是否是TextNode的子类: {issubclass(Document, TextNode)}")
# 判断TextNode是否是Document的子类,并输出结果
print(f"TextNode是否是Document的子类: {issubclass(TextNode, Document)}")
# 判断Document是否继承自BaseNode,并输出结果
print(f"Document是否是BaseNode的子类: {issubclass(Document, BaseNode)}")
# 判断TextNode是否继承自BaseNode,并输出结果
print(f"TextNode是否是BaseNode的子类: {issubclass(TextNode, BaseNode)}")
# 创建一个Document对象,包含文本内容和元数据
doc = Document(
text="这是一个完整的文档内容。它可能很长,包含很多信息。",
metadata={"source": "example.txt", "author": "张三"},
)
# 输出Document对象的文本内容
print(f"\n文档文本: {doc.text}")
# 输出Document对象的元数据
print(f"文档元数据: {doc.metadata}")
# 输出Document对象的节点ID
print(f"文档ID: {doc.node_id}")
# 创建一个TextNode对象用于对比,包含文本内容和元数据
node = TextNode(
text="这是一个文本节点。",
metadata={"source": "manual", "type": "example"},
)
# 输出TextNode对象的文本内容
print(f"\n节点文本: {node.text}")
# 输出TextNode对象的元数据
print(f"节点元数据: {node.metadata}")
# 输出TextNode对象的节点ID
print(f"节点ID: {node.node_id}")3.2.2 类层次结构图 #
Document 的继承链:
Document→Node→BaseNode→BaseComponent→BaseModel→object
TextNode 的继承链:
TextNode→BaseNode→BaseComponent→BaseModel→object
BaseNode (抽象基类)
├── Node (中间类)
│ └── Document (文档类)
└── TextNode (文本节点类)3.2.3 实际用途 #
- Document:当你从文件读取内容时使用
- TextNode:当文档被分割成小块时使用
- 它们都实现了
BaseNode的接口,所以可以互换使用
3.3 转换过程 #
Document到Node的转换是LlamaIndex数据处理的核心步骤:
# 导入Document类,用于表示文档对象
from llama_index.core import Document
# 导入SentenceSplitter类,用于将文档按句子分割成节点
from llama_index.core.node_parser import SentenceSplitter
# 创建一个示例文档对象,包含文本内容和元数据信息
original_document = Document(
text="人工智能是计算机科学的一个分支。它致力于创建能够执行通常需要人类智能的任务的系统。机器学习是人工智能的一个子领域。深度学习是机器学习的一个分支。",
metadata={
"source": "ai_introduction.txt",
"category": "技术文档",
"author": "专家",
},
)
# 打印原始文档的标题
print("原始文档:")
# 打印文档的文本内容
print(f"内容: {original_document.text}")
# 打印文档的元数据信息
print(f"元数据: {original_document.metadata}")
# 打印文档内容的长度(字符数)
print(f"长度: {len(original_document.text)} 字符")
# 打印空行用于分隔
print()
# 创建一个句子分割器,设置每个分块最大长度为50,重叠部分为5
text_splitter = SentenceSplitter(chunk_size=50, chunk_overlap=5)
# 使用分割器将文档分割成若干节点
nodes = text_splitter.get_nodes_from_documents([original_document])
# 打印分割后节点的数量
print(f"🔪 分割后生成了 {len(nodes)} 个节点:")
# 打印分隔线
print("-" * 50)
# 遍历每个节点,显示其详细信息
for i, node in enumerate(nodes, 1):
# 打印节点编号
print(f"节点 {i}:")
# 打印节点的文本内容
print(f" 内容: {node.text}")
# 打印节点内容的长度(字符数)
print(f" 长度: {len(node.text)} 字符")
# 打印节点的元数据信息
print(f" 元数据: {node.metadata}")
# 打印节点的唯一ID
print(f" 节点ID: {node.node_id}")
# 打印空行用于分隔
print()
3.4 关系特点 #
3.4.1 一对多关系 #
# 导入Document类,用于创建文档对象
from llama_index.core import Document
# 导入TokenTextSplitter类,用于将文档按token分割为多个节点
from llama_index.core.node_parser import TokenTextSplitter
# 创建一个长文档,内容为"123456"重复10次,附带元数据
long_document = Document(text="123456" * 10, metadata={"123": "456"}) # 重复10次
# 打印原始文档的长度(字符数)
print(f"原始文档长度: {len(long_document.text)} 字符")
# 打印原始文档的全部内容
print(f"原始文档内容: {long_document.text}")
# 打印原始文档的元数据信息
print(f"元数据: {long_document.metadata}")
# 创建一个token分割器,设置分块大小为10,重叠为0
token_splitter = TokenTextSplitter(chunk_size=10, chunk_overlap=0) #chunk_overlap=3
# 使用分割器将长文档分割为多个节点
nodes = token_splitter.get_nodes_from_documents([long_document])
# 打印分割过程分析的标题
print(f"\n=== 分割过程分析 ===")
# 打印分块大小的设置
print(f"设置的分块大小: {token_splitter.chunk_size} tokens")
# 打印重叠大小的设置
print(f"设置的重叠大小: {token_splitter.chunk_overlap} tokens")
# 打印分割后节点的数量
print(f"分割后节点数量: {len(nodes)}")
# 打印各节点详细分析的标题
print(f"\n=== 各节点详细分析 ===")
# 遍历每个节点,输出其内容、长度和元数据
for i, node in enumerate(nodes):
# 打印当前节点编号
print(f"节点 {i+1}:")
# 打印当前节点的内容
print(f" 内容: '{node.text}'")
# 打印当前节点的内容长度
print(f" 长度: {len(node.text)} 字符")
# 打印当前节点的元数据
print(f" 元数据: {node.metadata}")
# 打印统计信息的标题
print(f"\n=== 统计信息 ===")
# 计算所有节点内容的总长度
total_length = sum(len(node.text) for node in nodes)
# 计算每个节点内容的平均长度
avg_length = total_length / len(nodes)
# 打印所有节点内容的总长度
print(f"所有节点总长度: {total_length} 字符")
# 打印每个节点内容的平均长度
print(f"平均每个节点长度: {avg_length:.0f} 字符")
# 将所有节点的内容拼接起来
combined_text = "".join(node.text for node in nodes)
# 打印验证结果的标题
print(f"\n=== 验证结果 ===")
# 打印拼接后内容的总长度
print(f"节点内容组合后长度: {len(combined_text)} 字符")
# 判断拼接后内容是否与原始文档内容一致
print(f"内容是否一致: {combined_text == long_document.text}")
# 打印拼接后的全部内容
print(f"组合后内容: {combined_text}")
3.4.2 元数据继承 #
# 导入Document类,用于创建带有元数据的文档对象
from llama_index.core import Document
# 导入SentenceSplitter类,用于将文档按句子分割为节点
from llama_index.core.node_parser import SentenceSplitter
# 创建一个包含丰富元数据的文档对象
document_with_metadata = Document(
text="这是文档内容。包含重要信息。", # 文档的正文内容
metadata={ # 文档的元数据信息
"source": "important_doc.txt", # 来源文件名
"category": "重要文档", # 文档类别
"priority": "高", # 优先级
"created_date": "2024-01-01", # 创建日期
"author": "张三", # 作者
},
)
# 创建句子分割器实例
splitter = SentenceSplitter()
# 使用分割器将文档分割成节点,每个节点会继承文档的元数据
nodes = splitter.get_nodes_from_documents([document_with_metadata])
# 打印元数据继承演示标题
print("元数据继承演示:")
# 打印原始文档的元数据
print(f"原始文档元数据: {document_with_metadata.metadata}")
print()
# 遍历所有分割得到的节点
for i, node in enumerate(nodes, 1):
# 打印每个节点的元数据
print(f"节点 {i} 元数据: {node.metadata}")
# 遍历原始文档的每一项元数据,验证节点是否继承
for key, value in document_with_metadata.metadata.items():
assert node.metadata[key] == value, f"元数据 {key} 不匹配" # 若不匹配则抛出异常
# 打印继承验证通过的信息
print("✅ 元数据继承正确")
print()
3.5 实际应用场景 #
3.5.1 文档检索优化 #
# 导入Document和VectorStoreIndex类,用于文档对象和向量索引
from llama_index.core import Document, VectorStoreIndex
# 导入SentenceSplitter类,用于将文档按句子分割为节点
from llama_index.core.node_parser import SentenceSplitter
# 创建一个包含三篇示例文档的列表,每篇文档都带有元数据
documents = [
Document(
# 文档正文内容,介绍人工智能
text="人工智能(AI)是计算机科学的一个分支,致力于创建能够执行通常需要人类智能的任务的系统。",
# 文档元数据,包含主题和难度
metadata={"topic": "AI基础", "difficulty": "初级"},
),
Document(
# 文档正文内容,介绍机器学习
text="机器学习是人工智能的一个子领域,它使计算机能够在没有明确编程的情况下学习和改进。",
# 文档元数据,包含主题和难度
metadata={"topic": "机器学习", "difficulty": "中级"},
),
Document(
# 文档正文内容,介绍深度学习
text="深度学习是机器学习的一个分支,使用多层神经网络来模拟人脑的学习过程。",
# 文档元数据,包含主题和难度
metadata={"topic": "深度学习", "difficulty": "高级"},
),
]
# 打印演示标题
print("🔍 文档检索优化演示:")
# 打印分隔线
print("=" * 50)
# 方法1:直接用文档对象创建向量索引
print("方法1: 直接使用文档")
# 从文档列表创建向量索引
index1 = VectorStoreIndex.from_documents(documents)
# 获取查询引擎
query_engine1 = index1.as_query_engine()
# 对索引进行查询,问题为“什么是机器学习?”
response1 = query_engine1.query("什么是机器学习?")
# 打印查询结果
print(f"回答: {response1}")
# 打印空行分隔
print()
# 方法2:先将文档分割成节点,再创建向量索引
print("方法2: 先分割成节点")
# 创建句子分割器,设置分块大小为50,重叠为5
splitter = SentenceSplitter(chunk_size=50, chunk_overlap=5)
# 使用分割器将文档分割为节点
nodes = splitter.get_nodes_from_documents(documents)
# 用分割后的节点创建向量索引
index2 = VectorStoreIndex(nodes)
# 获取查询引擎
query_engine2 = index2.as_query_engine()
# 对索引进行查询,问题为“什么是机器学习?”
response2 = query_engine2.query("什么是机器学习?")
# 打印查询结果
print(f"回答: {response2}")
3.5.2 粒度控制 #
- 方法1:每个文档作为一个整体进行向量化,检索时匹配整个文档
- 方法2:将文档分割成更小的语义单元(chunk_size=50字符),检索时匹配更精确的片段
3.5.3 语义精确性 #
# 原始文档
"机器学习是人工智能的一个子领域,它使计算机能够在没有明确编程的情况下学习和改进。"
# 分割后可能变成多个节点:
节点1: "机器学习是人工智能的一个子领域"
节点2: "它使计算机能够在没有明确编程的情况下学习和改进"分割后的优势:
- 更小的向量维度,计算效率更高
- 更精确的语义匹配
- 减少噪声信息干扰
3.5.4 实际效果对比 #
| 特性 | 方法1(直接文档) | 方法2(分割节点) |
|---|---|---|
| 检索粒度 | 粗粒度(整文档) | 细粒度(语义片段) |
| 匹配精度 | 较低 | 较高 |
| 计算效率 | 中等 | 较高 |
| 上下文保持 | 完整 | 通过重叠保持 |
3.6 总结 #
Document和Node的关系可以概括为:
- 继承关系:Document是Node的子类,具有Node的所有功能
- 转换关系:Document通过分割器转换为多个Node
- 一对多关系:一个Document可以产生多个Node
- 元数据继承:Node会继承Document的元数据
- 用途不同:
- Document:数据加载阶段的基本单位
- Node:索引和检索阶段的基本单位
这种设计使得LlamaIndex能够:
- 灵活处理不同大小的文档
- 优化检索精度和效率
- 保持数据的完整性和关联性
- 支持复杂的文档处理流程
4. 总结 #
数据加载和转换是构建RAG应用的基础步骤。通过LlamaIndex提供的各种工具,您可以:
- 灵活加载数据:从文件系统、数据库、API等多种数据源加载数据
- 智能转换数据:将原始数据转换为适合LLM处理的结构化格式
- 自定义处理流程:根据具体需求调整数据处理的各个环节
- 高效存储索引:将处理后的数据存储到向量索引中,便于后续检索
掌握这些技术将帮助您构建更强大、更智能的AI应用程序。