1.4 RAG中的Schema

什么是Schema?

在计算机科学中,Schema(模式)指的是对数据结构、组织方式和约束条件的正式定义。在数据库领域,Schema定义了表的结构、字段类型、关系等。在文档处理中,Schema可以理解为对文档及其元数据结构的描述,它规定了哪些元数据字段是必需的,它们的类型、格式以及可能的取值范围。

为什么在RAG中需要统一的元数据Schema?

  1. 一致性:确保不同来源、不同类型的文档在存储和检索时具有一致的元数据字段,便于统一处理。
  2. 可预测性:系统知道每个文档块包含哪些元数据,可以基于这些字段进行过滤、排序和聚合。
  3. 数据质量:通过定义字段类型和约束,可以避免无效或错误的数据进入系统。
  4. 可维护性:当系统扩展时,统一的Schema使得新增文档类型或字段更加容易。
  5. 查询优化:向量数据库可以利用Schema中的字段类型和索引进行高效的查询。

如何定义元数据Schema?

一个完整的元数据Schema应该包括以下内容:

  • 字段名称(Field Name):元数据的键。
  • 字段类型(Field Type):如字符串、整数、浮点数、日期、布尔值等。
  • 是否必需(Required):该字段是否必须存在。
  • 默认值(Default Value):如果字段不存在,可以使用的默认值。
  • 描述(Description):字段的含义和用途。
  • 约束(Constraints):如字符串长度、数值范围、枚举值等。

RAG中元数据Schema的示例

假设我们有一个企业知识库,包含多种类型的文档(如PDF报告、Word文档、网页等)。我们可以定义以下统一的元数据Schema:

|-------------------|-------|----|-------|----------------|-------------------------------------|
| 字段名 | 类型 | 必需 | 默认值 | 描述 | 约束 |
| source | 字符串 | 是 | 无 | 文档来源的完整路径或URL | 最大长度500 |
| document_id | 字符串 | 是 | 无 | 文档的唯一标识符 | UUID格式 |
| title | 字符串 | 是 | 无 | 文档标题 | 最大长度200 |
| author | 字符串 | 否 | 未知 | 文档作者 | 最大长度100 |
| created_date | 日期 | 否 | 无 | 文档创建日期 | ISO 8601格式 |
| last_modified | 日期 | 否 | 无 | 文档最后修改日期 | ISO 8601格式 |
| document_type | 字符串 | 是 | 无 | 文档类型 | 枚举:pdf, word, excel, ppt, html, txt |
| page_number | 整数 | 否 | 无 | 页码(从1开始) | 大于等于1 |
| chunk_id | 整数 | 是 | 无 | 块在文档中的顺序索引 | 从0开始 |
| chunk_start_index | 整数 | 否 | 无 | 块在原始文档中的起始字符索引 | 大于等于0 |
| chunk_end_index | 整数 | 否 | 无 | 块在原始文档中的结束字符索引 | 大于等于0 |
| language | 字符串 | 否 | zh-CN | 文档语言 | 遵循BCP 47标准 |
| tags | 字符串列表 | 否 | 空列表 | 文档标签 | 每个标签最大长度50 |

在代码中实施Schema

我们可以在文档加载和分块后,对每个文档块进行元数据标准化,确保它们符合Schema。以下是一个示例函数:

python 复制代码
import uuid
from datetime import datetime
from typing import Dict, Any, List

def normalize_metadata(chunk, original_doc_metadata: Dict[str, Any], chunk_id: int, start_index: int, end_index: int) -> Dict[str, Any]:
    """
    根据Schema标准化元数据。
    
    Args:
        chunk: 文本块对象
        original_doc_metadata: 原始文档的元数据
        chunk_id: 块的ID
        start_index: 块在原始文档中的起始索引
        end_index: 块在原始文档中的结束索引
    
    Returns:
        标准化后的元数据字典
    """
    
    # 从原始文档元数据中提取信息,如果没有则使用默认值
    source = original_doc_metadata.get('source', 'unknown')
    title = original_doc_metadata.get('title', '无标题')
    author = original_doc_metadata.get('author', '未知作者')
    created_date = original_doc_metadata.get('created_date')
    last_modified = original_doc_metadata.get('last_modified')
    document_type = original_doc_metadata.get('document_type', 'unknown')
    page_number = original_doc_metadata.get('page', 1)  # 假设原始元数据中页码为0开始,这里转换为1开始
    language = original_doc_metadata.get('language', 'zh-CN')
    tags = original_doc_metadata.get('tags', [])
    
    # 如果created_date是字符串,尝试转换为datetime对象,然后再格式化为ISO字符串
    if created_date and isinstance(created_date, str):
        try:
            # 尝试解析常见日期格式,这里简化处理,实际可能需要更复杂的解析
            created_date = datetime.fromisoformat(created_date.replace('Z', '+00:00')).isoformat()
        except:
            created_date = datetime.now().isoformat()
    elif created_date and isinstance(created_date, datetime):
        created_date = created_date.isoformat()
    else:
        created_date = datetime.now().isoformat()
    
    # 同样处理last_modified
    if last_modified and isinstance(last_modified, str):
        try:
            last_modified = datetime.fromisoformat(last_modified.replace('Z', '+00:00')).isoformat()
        except:
            last_modified = datetime.now().isoformat()
    elif last_modified and isinstance(last_modified, datetime):
        last_modified = last_modified.isoformat()
    else:
        last_modified = datetime.now().isoformat()
    
    # 确保page_number是整数且至少为1
    try:
        page_number = int(page_number)
        if page_number < 1:
            page_number = 1
    except:
        page_number = 1
    
    # 确保tags是列表
    if not isinstance(tags, list):
        tags = [tags] if tags else []
    
    # 构建标准化元数据
    normalized_metadata = {
        'source': str(source)[:500],  # 限制长度
        'document_id': original_doc_metadata.get('document_id', str(uuid.uuid4())),
        'title': str(title)[:200],
        'author': str(author)[:100],
        'created_date': created_date,
        'last_modified': last_modified,
        'document_type': document_type,
        'page_number': page_number,
        'chunk_id': chunk_id,
        'chunk_start_index': start_index,
        'chunk_end_index': end_index,
        'language': language,
        'tags': tags[:10]  # 限制标签数量
    }
    
    return normalized_metadata

在分块过程中应用Schema

在分块时,我们可以调用这个函数来标准化每个块的元数据。例如:

python 复制代码
from langchain.text_splitter import RecursiveCharacterTextSplitter

text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=500,
    chunk_overlap=50,
    add_start_index=True  # 这个参数会在元数据中添加start_index
)

def split_and_normalize(documents):
    all_chunks = []
    for doc in documents:
        # 分块
        chunks = text_splitter.split_documents([doc])
        
        # 对每个块进行标准化
        for i, chunk in enumerate(chunks):
            # 获取start_index和end_index
            start_index = chunk.metadata.get('start_index', 0)
            end_index = start_index + len(chunk.page_content)
            
            # 标准化元数据
            normalized_metadata = normalize_metadata(
                chunk=chunk,
                original_doc_metadata=doc.metadata,
                chunk_id=i,
                start_index=start_index,
                end_index=end_index
            )
            
            # 更新块的元数据
            chunk.metadata = normalized_metadata
            all_chunks.append(chunk)
    
    return all_chunks

# 使用示例
# documents = loader.load()
# normalized_chunks = split_and_normalize(documents)

在向量数据库中定义Schema

当我们将文档块存入向量数据库时,也可以根据Schema来定义集合(collection)的结构。以ChromaDB为例:

python 复制代码
import chromadb

chroma_client = chromadb.PersistentClient(path="./chroma_db")

# 创建集合时,可以指定元数据字段的类型,以便进行过滤
collection = chroma_client.create_collection(
    name="documents",
    metadata={"hnsw:space": "cosine"},
    # 我们可以通过代码约束元数据字段,但ChromaDB目前不强制元数据Schema
)

# 添加文档时,确保元数据符合Schema
collection.add(
    documents=[chunk.page_content for chunk in normalized_chunks],
    metadatas=[chunk.metadata for chunk in normalized_chunks],
    ids=[f"chunk_{i}" for i in range(len(normalized_chunks))]
)

查询时利用Schema

在查询时,我们可以利用元数据字段进行过滤,例如只搜索某种类型的文档或某个作者的文档:

python 复制代码
# 查询时过滤
results = collection.query(
    query_texts=["查询内容"],
    n_results=10,
    where={"document_type": "pdf", "author": "张三"}  # 过滤条件
)

# 也可以使用范围查询
results = collection.query(
    query_texts=["查询内容"],
    n_results=10,
    where={"page_number": {"$gte": 10, "$lte": 20}}  # 页码在10到20之间
)

总结

统一的元数据Schema是构建健壮RAG系统的基石。它确保了数据的一致性,提高了系统的可维护性和查询能力。在实际项目中,Schema的设计需要根据业务需求灵活调整,并在文档处理的各个环节中严格执行。

通过定义清晰的Schema,我们可以:

  1. 规范数据摄入流程
  2. 提高检索的准确性
  3. 实现复杂的过滤和排序
  4. 便于系统监控和调试
相关推荐
爱可生开源社区1 天前
2026 年,优秀的 DBA 需要具备哪些素质?
数据库·人工智能·dba
随逸1771 天前
《从零搭建NestJS项目》
数据库·typescript
加号32 天前
windows系统下mysql多源数据库同步部署
数据库·windows·mysql
シ風箏2 天前
MySQL【部署 04】Docker部署 MySQL8.0.32 版本(网盘镜像及启动命令分享)
数据库·mysql·docker
李慕婉学姐2 天前
Springboot智慧社区系统设计与开发6n99s526(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
数据库·spring boot·后端
百锦再2 天前
Django实现接口token检测的实现方案
数据库·python·django·sqlite·flask·fastapi·pip
tryCbest2 天前
数据库SQL学习
数据库·sql
jnrjian2 天前
ORA-01017 查找机器名 用户名 以及library cache lock 参数含义
数据库·oracle
十月南城2 天前
数据湖技术对比——Iceberg、Hudi、Delta的表格格式与维护策略
大数据·数据库·数据仓库·hive·hadoop·spark
Henry Zhu1232 天前
数据库:并发控制基本概念
服务器·数据库