Calendar 第一天

文本分块策略


到目前为止,我们已经讨论了点——它们的构成,以及Qdrant如何使用余弦相似度、点积或欧几里得距离等距离度量来比较它们以进行近似最近邻搜索。

但在我们给Qdrant一些有意义的东西去比较之前,这一切都无关紧要。这把我们带到了系统的真正起点。

免责声明:在本节中,我们专注于文本分块。尽管其他类型的数据(图像、视频、音频和代码)也可以分块,但我们将介绍文本分块的基础知识,因为它是最流行的数据类型。

真正的开始:数据结构

我们的管道从数据以及我们如何表示我们想要搜索的内容开始。实际上,这意味着要考虑我们正在存储的数据的结构。大多数真实世界的数据都是混乱的

  • 文本文档很长
  • 产品描述长度不一
  • 用户配置文件具有嵌套属性

我们需要一种方法将这些数据分解成可管理的块。

数据预处理,特别是分块和嵌入,定义了Qdrant处理的数据。

从原始文本到可搜索文本

整个文档的问题

将整个文档存储为单个向量通常是无效的,因为嵌入模型在有限的上下文窗口中运行。

每个模型都有一次可以处理的最大标记数。例如,许多流行的sentence-transformer模型的限制是512个标记,而OpenAI的text-embedding-3-small的限制是8,191个标记。如果文档超过了这个最大标记数,超出限制的信息就会被简单地丢弃,导致大量数据丢失。

但即使文档符合限制,将大段多主题文本嵌入到单个向量中也可能稀释其含义。模型会创建所有内容的“语义平均值”,使得特定查询难以找到精确匹配。

这就是分块的用武之地。目标是让块

  1. 足够小,以便嵌入模型能有效处理而不会被截断。
  2. 足够大,以包含有意义的、连贯的上下文。

通过将文档分解为聚焦的块,每个块都有自己的向量,准确地表示一个特定的想法。这使得搜索更加精确。

示例:考虑一个多页文档,例如Qdrant Collection Configuration Guide of day 7,涵盖从HNSW到分片和量化的所有内容。

如果用户问:“m参数的作用是什么?”

不分块

  • 指南太长,被模型截断,可能完全丢失有关m参数的部分。
  • 即使它适合,生成的向量也是所有主题的嘈杂平均值,因此不太可能针对如此具体的查询检索到。

使用适当分块

  • 指南被分成以主题为重点的块(例如,一个用于HNSW参数的块)。
  • 有关m参数的块获得了其自己的精确向量。
  • Qdrant轻松检索此特定块,提供清晰相关的答案。

为什么分块如此重要

您不再将文档视为整体块,而是将它们分解成段落、标题和子部分。每个块都有自己的向量,与特定的想法或主题相关联。您可以向每个块添加元数据,例如章节标题、页码、原始源文档和标签。

这使得

  • 过滤检索 - “只显示此部分的结果”
  • 上下文感知片段 - 对特定查询的精确答案
  • 高效处理 - 不会在不相关内容上浪费标记

分块策略:形状很重要

您如何分块会影响您的嵌入捕捉到的内容、您的检索器可以提取的内容以及您的LLM可以推理的内容。没有一刀切的方法。让我们探讨一下这些选项

1. 固定大小分块

方法:为每个块定义一个标记数(例如,200),并带有一个小的重叠缓冲区以保留上下文。

示例文本:“HNSW算法构建了一个多层图,其中每个节点代表一个向量。该算法首先将向量插入到底层,然后根据概率选择性地将一些向量提升到更高层。这创建了快捷方式,允许在搜索操作期间更快地遍历。”



固定大小的块(每个10个单词,任意中断)

块1:“HNSW算法构建了一个多层图,其中每个”

块2:“节点代表一个向量。该算法首先通过插入向量”

块3:“进入底层,然后选择性地将一些提升到”

块4:“更高层,基于概率。这创建了允许的快捷方式”

块5:“在搜索操作期间更快地遍历。”

请注意,每个块(除了最后一个)都正好有10个单词,但这会任意地打破句子。

优点

  • 易于实现
  • 一致的块大小
  • 可预测的处理

缺点

  • 忽略自然语言边界
  • 可能会在句中或思想中分裂
  • 无语义感知

最适合:缺少一致格式的文档、初始原型制作

def fixed_size_chunk(text, chunk_size=200, overlap=50):
    words = text.split()
    chunks = []
    for i in range(0, len(words), chunk_size - overlap):
        chunk = " ".join(words[i:i + chunk_size])
        chunks.append(chunk)
    return chunks

2. 基于句子的分块

方法:使用分词器将文档分解为句子,然后将句子分组到指定词数下的块中。

示例文本:“HNSW算法构建了一个多层图。每个节点代表集合中的一个向量。该算法在层之间创建快捷方式以加快搜索速度。这种分层结构可以实现高效的近似最近邻查询。”



基于句子的块(尊重句子边界)

块1:“HNSW算法构建了一个多层图。每个节点代表集合中的一个向量。”

块2:“该算法在层之间创建快捷方式以加快搜索速度。这种分层结构可以实现高效的近似最近邻查询。”

每个块都包含完整的句子,保留了逻辑流。这种方法使结构整洁并保持了完整的思想,尽管块大小会有所不同。

实施

# ! pip install nltk
from nltk.tokenize import sent_tokenize


def sentence_chunk(text, max_words=150):
    sentences = sent_tokenize(text)
    chunks, buffer, length = [], [], 0

    for sent in sentences:
        count = len(sent.split())
        if length + count > max_words:
            chunks.append(" ".join(buffer))
            buffer, length = [], 0
        buffer.append(sent)
        length += count

    if buffer:
        chunks.append(" ".join(buffer))
    return chunks

优点

  • 保留完整思想
  • 自然语言边界
  • 良好的语义连贯性

缺点

  • 不规则的块长度
  • 句子大小差异很大
  • 可能不尊重主题边界

最适合:RAG系统、问答应用程序、通用文本处理

3. 基于段落的分块

方法:在段落中断处分割,利用现有文档结构。

带段落的示例文本

段落1:“HNSW(Hierarchical Navigable Small World)是一种基于图的近似最近邻搜索算法。它构建了一个多层结构,其中每一层都包含数据点的子集。”
段落2:“该算法通过在每层中创建相邻点之间的连接来工作。更高层具有更少的点但更长的连接,从而在搜索操作期间创建更快的遍历快捷方式。”
段落3:“搜索时,HNSW从顶层开始逐渐向下移动,使用快捷方式快速导航到目标区域,然后在底层执行更详细的搜索。”

每个块对应一个完整的段落——一个思想趋于连贯的自然边界。这种方法尊重作者预期的组织,并将相关概念保持在一起。

实施

def paragraph_chunk(text):
    return [p.strip() for p in text.split("\n\n") if p.strip()]

优点

  • 与自然主题边界对齐
  • 默认情况下语义丰富
  • 尊重作者的组织

缺点

  • 不可预测的大小(单行到整页)
  • 可能需要标记限制或回退分割
  • 取决于干净的文档结构

最适合:文章、博客、文档、书籍、电子邮件

4. 滑动窗口分块

方法:创建重叠的块以保持上下文连续性。

示例文本:“HNSW构建了一个多层图,其中每个节点代表一个向量。该算法首先将向量插入到底层,然后根据概率选择性地将一些向量提升到更高层。这创建了快捷方式,允许在搜索操作期间更快地遍历。”



滑动窗口(每个块10个单词,重叠4个单词)

块1:“HNSW构建了一个多层图,其中每个节点代表一个”
块2:“其中每个节点代表一个” “向量。该算法首先通过插入向量”
块3:“首先通过插入向量” “进入底层,然后选择性地提升”
块4:“然后选择性地提升” “一些到更高层,基于概率。这”

滑动窗口分块创建了大小一致的重叠片段。每个块都保持完全相同的字数(10个单词)和一致的重叠(4个单词),确保信息在边界上连续,同时保持统一的块大小。

实施

def sliding_window(text, window=200, stride=100):
    words = text.split()
    chunks = []
    for i in range(0, len(words) - window + 1, stride):
        chunk = " ".join(words[i:i + window])
        chunks.append(chunk)
    return chunks

优点

  • 在边界处保持上下文
  • 更高的召回潜力
  • 减少信息丢失

缺点

  • 存储冗余(通常20-50%的开销)
  • 增加处理成本
  • 可能会返回重复信息

最适合:错过信息代价高昂的关键应用程序、重排系统

5. 递归分块

方法:当数据不遵循可预测结构时,使用分隔符的回退层次结构。

递归拆分使用分隔符的回退层次结构。您首先尝试在大块上拆分——例如标题或段落分隔符。如果一个块仍然太长,它会回退到较小的分隔符,如行或句子。如果仍然不适合,它会以单词或字符作为最后手段继续。

示例混乱文本
"# HNSW 概述\n\nHNSW算法构建了一个多层图。\n每个节点代表集合中的一个向量。\n\n该算法在层之间创建快捷方式以加快搜索速度。这种分层结构可以实现高效的近似最近邻查询。\n\n## 性能优势\nHNSW提供了对数搜索复杂度。"



递归分块(首先尝试段落分隔符,然后是句子,然后是单词)

块1:# HNSW 概述
块2:HNSW算法构建了一个多层图。
块3:每个节点代表集合中的一个向量。
块4:该算法在层之间创建快捷方式以加快搜索速度。
块5:这种分层结构可以实现高效的近似最近邻查询。
块6:## 性能优势\nHNSW提供了对数搜索复杂度。

递归分块试图保留结构。它首先使用段落(由\n\n分隔)。如果这些太长,它会回退到句子。如果这些仍然溢出,它会在单词边界处切割。这种回退行为有助于即使结构不一致也能保持数据可用。

层次结构

  1. 大块(标题\n\n,段落分隔符)
  2. 中块(行\n,句子.
  3. 小块(空格,字符)

实施

# ! pip install langchain
from langchain.text_splitter import RecursiveCharacterTextSplitter

splitter = RecursiveCharacterTextSplitter(
    chunk_size=512, chunk_overlap=100, separators=["\n\n", "\n", ". ", " ", ""]
)

text = """
Hello, world! More text here. another line.
Hello, world! More text here. another line......
"""
chunks = splitter.split_text(text)

优点

  • 适应混乱或不一致的输入
  • 尽可能保持语义连贯性
  • 处理各种文档格式

缺点

  • 基于启发式,结果可能不一致
  • 逻辑复杂
  • 可能并非适用于所有内容类型

最适合:抓取的网页内容、混合格式、CMS导出

6. 语义感知分块

方法:使用嵌入来检测含义变化并在主题边界处中断。

到目前为止,所有内容都与结构有关。但结构与含义不同。语义分块使用嵌入来发现含义变化——您检测主题或语义连贯性发生变化的位置,并在那里中断。

具有主题变化的示例文本
"HNSW是一种基于图的向量搜索算法。它构建分层结构以实现高效导航。该算法使用概率在层之间提升节点。像Qdrant这样的向量数据库实现了HNSW以进行快速相似性搜索。机器学习模型为文本数据生成嵌入。这些嵌入在高维空间中捕获语义含义。"



语义感知块(在含义边界处检测到分割)

主题1 - HNSW算法:“HNSW是一种基于图的向量搜索算法。它构建分层结构以实现高效导航。该算法使用概率在层之间提升节点。”
主题2 - 向量数据库:“像Qdrant这样的向量数据库实现了HNSW以进行快速相似性搜索。”
主题3 - 机器学习:“机器学习模型为文本数据生成嵌入。这些嵌入在高维空间中捕获语义含义。”

语义分块不关心句子数量或固定标记限制。它寻找含义中的自然边界。如果一个定义需要多个句子,它会将它们保持在一起。这提供了更好的检索,因为块包含完整、连贯的概念。

流程

  1. 嵌入句子或小片段
  2. 计算连续片段之间的相似度
  3. 识别相似度下降的主题转换
  4. 在连贯性边界处分割

实施

from sentence_transformers import SentenceTransformer
import numpy as np

def semantic_chunking(text, similarity_threshold=0.5):
    model = SentenceTransformer('all-MiniLM-L6-v2')
    sentences = text.split('.')
    embeddings = model.encode(sentences)
    
    chunks = []
    current_chunk = [sentences[0]]
    
    for i in range(1, len(sentences)):
        # Calculate cosine similarity between consecutive sentences
        similarity = np.dot(embeddings[i-1], embeddings[i]) / (
            np.linalg.norm(embeddings[i-1]) * np.linalg.norm(embeddings[i])
        )
        
        if similarity < similarity_threshold:
            chunks.append('. '.join(current_chunk))
            current_chunk = [sentences[i]]
        else:
            current_chunk.append(sentences[i])
    
    chunks.append('. '.join(current_chunk))
    return chunks

权衡是计算成本。您首先嵌入整个文档只是为了决定在哪里分割它——甚至在您存储任何东西之前。它更慢且更昂贵,但每个块都包含连贯的思想。

优点

  • 高语义精度
  • 每个块都包含连贯的思想
  • 最适合复杂文档

缺点

  • 计算成本高昂(需要嵌入整个文档)
  • 需要额外的模型推理
  • 处理管道较慢

最适合:法律文件、研究论文、需要高精度的关键应用程序

文本分块策略比较

方法优点权衡最适合
固定大小简单、可预测的块忽略结构,破坏含义原始或非结构化文本
句子保留完整思想大小不一致RAG、问答系统
段落与语义单元对齐长度差异大文档、手册、教学内容
滑动窗口保持完整上下文冗余,计算量大重排,高召回检索
递归灵活,处理混乱输入启发式,有时脆弱抓取的网页内容,混合来源
语义高质量,含义感知较慢,资源密集法律、研究、关键QA

注意:有时,有必要保持文档完整。如果分块过于复杂,或者文档包含丰富的视觉内容(图表、图形等),您可以使用VLMs来嵌入整个页面。

通过元数据添加含义

块本身只是文本片段。它们没有告诉您它们来自哪里、它们属于什么,或者如何控制检索到的内容。

这就是元数据的用武之地。

在Qdrant中,这些元数据存储在payload中——一个附加到每个向量的JSON对象,它带有真实的结构。您可以使用它存储任何需要识别或组织块的信息。

基本元数据字段

{
  "document_id": "collection-config-guide",
  "document_title": "What is a Vector Database",
  "section_title": "What Is a Vector",
  "chunk_index": 7,
  "chunk_count": 15,
  "url": "https://qdrant.org.cn/documentation/concepts/collections/",
  "tags": ["qdrant", "vector search", "point", "vector", "payload"],
  "source_type": "documentation", 
  "created_at": "2025-01-15T10:00:00Z",
  "content": "There are three key elements that define a vector in vector search: the ID, the dimensions, and the payload. These components work together to represent a vector effectively within the system...",
  "word_count": 45,
  "char_count": 287
}

元数据实现的功能

免责声明:出于性能原因,可过滤字段必须使用Payload Index进行索引。

1. 过滤搜索(精确匹配) 您可以根据精确的元数据值过滤结果,这非常适合分类数据。

from qdrant_client import models

# Only show results from a specific article
filter = models.Filter(
    must=[
        models.FieldCondition(
            key="document_id", match=models.MatchValue(value="collection-config-guide")
        )
    ]
)

2. 带有文本过滤的混合搜索(全文搜索) 对于更强大的基于文本的过滤,您可以将向量搜索与传统关键词搜索结合起来。这需要在payload字段上设置全文索引

# Find vectors that also contain the keyword "HNSW" in their content
filter = models.Filter(
    must=[
        models.FieldCondition(
            key="content", # The field with the full-text index
            match=models.MatchText(text="HNSW algorithm")
        )
    ]
)

3. 分组结果

# Top result per document - get the most relevant chunk from each source
group_by = "document_id"

您可以在此处阅读更多关于分组的信息。

4. 丰富的结果显示

  • 带有来源归属的原始内容
  • 章节上下文以便更好地理解
  • 完整文档的直接链接
  • 创建时间戳用于时效性

5. 权限控制

# Filter by user permissions
filter = models.Filter(
    must=[
        models.FieldCondition(
            key="access_level", match=models.MatchValue(value="public")
        )
    ]
)

使用元数据搜索

def search_with_filters(query, document_type=None, date_range=None):
    """Search with metadata filtering"""

    # Build filter conditions
    filter_conditions = []

    if document_type:
        filter_conditions.append(
            models.FieldCondition(
                key="source_type", match=models.MatchValue(value=document_type)
            )
        )

    if date_range:
        filter_conditions.append(
            models.FieldCondition(
                key="created_at",
                range=models.Range(gte=date_range["start"], lte=date_range["end"]),
            )
        )

    # Execute search
    query_filter = models.Filter(must=filter_conditions) if filter_conditions else None

    results = client.query_points(
        collection_name="documents",
        query=generate_embedding(query),
        query_filter=query_filter,
        limit=5,
    )

    return results

性能考量

标记效率

考虑您的嵌入模型的标记限制

  • OpenAI text-embedding-3-small:最多8,191个标记
  • Sentence Transformers:因模型而异。许多经典模型(例如all-MiniLM-L6-v2)的最大长度为512个标记,但更新的模型可以处理更多。请务必查看您的特定模型的文档。
  • 始终为特殊标记和格式留出缓冲区空间

重叠建议

  • 10-20%重叠:适用于大多数应用程序的良好平衡
  • 25-50%重叠:信息丢失代价高昂的高召回场景
  • 无重叠:当存储/计算成本是主要考虑因素时

关键要点

  1. 分块策略直接影响搜索质量 - 根据您的文本数据和用例进行选择
  2. 更小、更集中的块提供比整个文档嵌入更精确的结果
  3. 元数据对于过滤、分组和结果呈现至关重要
  4. 不同的策略有不同的权衡 - 实验以找到最有效的方法
  5. 考虑计算成本 - 语义文本分块功能强大但成本高昂
  6. 重叠有助于保留上下文 但增加了存储要求

下一步

既然您了解了如何构建和准备文本数据,让我们将这些概念付诸实践。在下一节中,我们将构建一个完整的电影推荐系统,演示分块、嵌入和元数据的作用。

请记住:Qdrant不假设您的数据含义。它比较向量并返回最接近的。但它看到的内容——结构、语义、上下文——完全取决于您。