文本分块策略
到目前为止,我们已经讨论了点——它们的构成,以及Qdrant如何使用余弦相似度、点积或欧几里得距离等距离度量来比较它们以进行近似最近邻搜索。
但在我们给Qdrant一些有意义的东西去比较之前,这一切都无关紧要。这把我们带到了系统的真正起点。
免责声明:在本节中,我们专注于文本分块。尽管其他类型的数据(图像、视频、音频和代码)也可以分块,但我们将介绍文本分块的基础知识,因为它是最流行的数据类型。
真正的开始:数据结构
我们的管道从数据以及我们如何表示我们想要搜索的内容开始。实际上,这意味着要考虑我们正在存储的数据的结构。大多数真实世界的数据都是混乱的
- 文本文档很长
- 产品描述长度不一
- 用户配置文件具有嵌套属性
我们需要一种方法将这些数据分解成可管理的块。
数据预处理,特别是分块和嵌入,定义了Qdrant处理的数据。
从原始文本到可搜索文本
整个文档的问题
将整个文档存储为单个向量通常是无效的,因为嵌入模型在有限的上下文窗口中运行。
每个模型都有一次可以处理的最大标记数。例如,许多流行的sentence-transformer模型的限制是512个标记,而OpenAI的text-embedding-3-small的限制是8,191个标记。如果文档超过了这个最大标记数,超出限制的信息就会被简单地丢弃,导致大量数据丢失。
但即使文档符合限制,将大段多主题文本嵌入到单个向量中也可能稀释其含义。模型会创建所有内容的“语义平均值”,使得特定查询难以找到精确匹配。
这就是分块的用武之地。目标是让块
- 足够小,以便嵌入模型能有效处理而不会被截断。
- 足够大,以包含有意义的、连贯的上下文。
通过将文档分解为聚焦的块,每个块都有自己的向量,准确地表示一个特定的想法。这使得搜索更加精确。
示例:考虑一个多页文档,例如Qdrant Collection Configuration Guide of day 7,涵盖从HNSW到分片和量化的所有内容。
如果用户问:“m参数的作用是什么?”
不分块
- 指南太长,被模型截断,可能完全丢失有关
m参数的部分。 - 即使它适合,生成的向量也是所有主题的嘈杂平均值,因此不太可能针对如此具体的查询检索到。
使用适当分块
- 指南被分成以主题为重点的块(例如,一个用于HNSW参数的块)。
- 有关
m参数的块获得了其自己的精确向量。 - Qdrant轻松检索此特定块,提供清晰相关的答案。
为什么分块如此重要
您不再将文档视为整体块,而是将它们分解成段落、标题和子部分。每个块都有自己的向量,与特定的想法或主题相关联。您可以向每个块添加元数据,例如章节标题、页码、原始源文档和标签。
这使得
- 过滤检索 - “只显示此部分的结果”
- 上下文感知片段 - 对特定查询的精确答案
- 高效处理 - 不会在不相关内容上浪费标记
分块策略:形状很重要
您如何分块会影响您的嵌入捕捉到的内容、您的检索器可以提取的内容以及您的LLM可以推理的内容。没有一刀切的方法。让我们探讨一下这些选项
1. 固定大小分块
方法:为每个块定义一个标记数(例如,200),并带有一个小的重叠缓冲区以保留上下文。
固定大小的块(每个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. 基于句子的分块
方法:使用分词器将文档分解为句子,然后将句子分组到指定词数下的块中。
基于句子的块(尊重句子边界)
块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. 基于段落的分块
方法:在段落中断处分割,利用现有文档结构。
每个块对应一个完整的段落——一个思想趋于连贯的自然边界。这种方法尊重作者预期的组织,并将相关概念保持在一起。
实施
def paragraph_chunk(text):
return [p.strip() for p in text.split("\n\n") if p.strip()]
优点
- 与自然主题边界对齐
- 默认情况下语义丰富
- 尊重作者的组织
缺点
- 不可预测的大小(单行到整页)
- 可能需要标记限制或回退分割
- 取决于干净的文档结构
最适合:文章、博客、文档、书籍、电子邮件
4. 滑动窗口分块
方法:创建重叠的块以保持上下文连续性。
滑动窗口(每个块10个单词,重叠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提供了对数搜索复杂度。"
递归分块(首先尝试段落分隔符,然后是句子,然后是单词)
递归分块试图保留结构。它首先使用段落(由\n\n分隔)。如果这些太长,它会回退到句子。如果这些仍然溢出,它会在单词边界处切割。这种回退行为有助于即使结构不一致也能保持数据可用。
层次结构
- 大块(标题
\n\n,段落分隔符) - 中块(行
\n,句子.) - 小块(空格
,字符)
实施
# ! 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以进行快速相似性搜索。机器学习模型为文本数据生成嵌入。这些嵌入在高维空间中捕获语义含义。"
语义感知块(在含义边界处检测到分割)
语义分块不关心句子数量或固定标记限制。它寻找含义中的自然边界。如果一个定义需要多个句子,它会将它们保持在一起。这提供了更好的检索,因为块包含完整、连贯的概念。
流程
- 嵌入句子或小片段
- 计算连续片段之间的相似度
- 识别相似度下降的主题转换
- 在连贯性边界处分割
实施
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%重叠:信息丢失代价高昂的高召回场景
- 无重叠:当存储/计算成本是主要考虑因素时
关键要点
- 分块策略直接影响搜索质量 - 根据您的文本数据和用例进行选择
- 更小、更集中的块提供比整个文档嵌入更精确的结果
- 元数据对于过滤、分组和结果呈现至关重要
- 不同的策略有不同的权衡 - 实验以找到最有效的方法
- 考虑计算成本 - 语义文本分块功能强大但成本高昂
- 重叠有助于保留上下文 但增加了存储要求
下一步
既然您了解了如何构建和准备文本数据,让我们将这些概念付诸实践。在下一节中,我们将构建一个完整的电影推荐系统,演示分块、嵌入和元数据的作用。
请记住:Qdrant不假设您的数据含义。它比较向量并返回最接近的。但它看到的内容——结构、语义、上下文——完全取决于您。