有效的语义搜索在很大程度上依赖于输入到嵌入模型的数据的质量和格式。原始数据,无论是从网站抓取、从文档提取还是从数据库获取,通常都无法直接用于向量化。就像生食材烹饪前需要准备一样,您的数据也需要在有效嵌入和索引前进行清洗和结构化。本节主要介绍数据准备的关键步骤,以及将大量信息分割成可管理、语义相关的块的处理方法。妥善处理这个阶段对于构建能够根据含义获取真正相关结果的搜索系统来说非常重要。数据清洗的重要性在考虑分割数据之前,我们必须确保数据是干净的。嵌入模型功能强大,但对噪声很敏感。文本中不相关的内容会稀释所得向量中捕获的语义含义,导致搜索结果的准确性降低。常见的清洗步骤包括:移除无关内容: 这包括删除HTML标签(如、)、CSS样式、JavaScript代码、网站导航栏、页眉、页脚、广告以及不构成内容核心含义的样板文本。这里通常会使用正则表达式或专门的HTML解析库(例如Python中的BeautifulSoup)。处理不同格式: 您的数据可能来自各种来源:PDF、Word文档、纯文本文件、数据库条目。在进一步处理之前,您需要一个流程将这些数据转换为统一的文本格式(通常是纯文本)。大多数编程语言中都有处理常见文档类型的库。规范化(可选): 根据嵌入模型和任务的不同,您可能会考虑文本规范化步骤,例如将文本转换为小写、删除标点符号,甚至纠正常见拼写错误。但是,请谨慎操作,因为这些步骤有时会消除重要的细节(例如,代码中的大小写敏感性)。请在您的特定应用中测试规范化的影响。为何需要分块数据的大小是为嵌入模型准备数据时的一个主要考量。当前大多数嵌入模型,特别是基于Transformer的模型,都有最大输入序列长度限制,通常以tokens(大致对应于词或子词)来衡量。例如,许多BERT变体的限制是512个tokens。尝试嵌入远超此限制的文档,将导致错误,或者更常见的是截断——模型会直接忽略超出其限制的文本,从而丢失有价值的信息。除了这个技术限制,还有重要的语义原因需要分块:嵌入的精确性: 嵌入一个非常长的文档(即使可能)通常会得到一个表示整个文本平均含义的向量。这会模糊子部分中讨论的具体细节或主题。针对某个特定点的搜索查询可能与整个文档的平均向量匹配不佳。检索粒度: 当用户提出问题时,他们通常需要一个具体的答案,而不是一个可能隐藏着答案的100页文档。分块让搜索系统能够识别并获取更小、更集中的文本段落,这些段落直接对应查询的语义含义。这会带来更好的用户体验。因此,将大型文档分解为更小、更连贯的块是构建语义搜索系统中一种普遍且必要的做法。文本分块方法分块的目的是创建足够小、适合嵌入模型,同时尽可能保留语义上下文的文本片段。没有唯一的“最佳”方法;最适合的方法取决于您的数据性质和应用程序的需求。以下是常见的分块技术:1. 固定大小分块这是最简单的方法:将文本分割成固定长度的片段,长度可以按字符或tokens衡量。描述: 每隔$N$个字符或$N$个tokens分割文档。优点: 非常容易实现。分块大小可预测。缺点: 如果按字符计数,文本很可能在句中甚至词中被截断。这种突然的分割会切断语义连接,降低所得嵌入的质量。上下文经常在边界处丢失。2. 固定大小重叠分块为了减轻固定大小分块中上下文丢失的问题,一种常见的改进方法是在连续块之间引入重叠。描述: 每隔$N$个字符/tokens分割文档,但使每个块与前一个块重叠$M$个字符/tokens。例如,块1可能是字符0-1000,块2可能是字符800-1800,块3可能是字符1600-2600,依此类推(重叠200个字符)。优点: 有助于在块边界处保留上下文。一个在块末尾被分割的句子很可能完整地包含在下一个块的开头附近。缺点: 增加了生成的总块数,从而增加了嵌入和索引的存储和计算成本。引入了数据冗余。选择合适的重叠大小需要进行实验。digraph G { rankdir=LR; node [shape=box, style=filled, fillcolor="#e9ecef", height=0.2, fontname="sans-serif", fontsize=10]; edge [arrowhead=none, penwidth=2, color="#adb5bd"]; subgraph cluster_0 { label = "固定大小 (无重叠)"; style=dashed; color="#adb5bd"; fontname="sans-serif"; fontsize=11; a1 [label="块 1"]; a2 [label="块 2"]; a3 [label="块 3"]; a1 -> a2; a2 -> a3; } subgraph cluster_1 { label = "固定大小 (有重叠)"; style=dashed; color="#adb5bd"; fontname="sans-serif"; fontsize=11; node [fillcolor="#a5d8ff"]; edge [color="#74c0fc"]; b1_1 [label="部分 1"]; b1_2 [label="重叠 A", fillcolor="#4dabf7"]; b2_1 [label="重叠 A", fillcolor="#4dabf7"]; b2_2 [label="部分 2"]; b2_3 [label="重叠 B", fillcolor="#4dabf7"]; b3_1 [label="重叠 B", fillcolor="#4dabf7"]; b3_2 [label="部分 3"]; {rank=same; b1_1; b1_2;} {rank=same; b2_1; b2_2; b2_3;} {rank=same; b3_1; b3_2;} b1_1 -> b1_2 [style=invis]; // Invisible edge for layout b2_1 -> b2_2 [style=invis]; b2_2 -> b2_3 [style=invis]; b3_1 -> b3_2 [style=invis]; edge [tailport=e, headport=w, constraint=false]; b1_2 -> b2_1; b2_3 -> b3_1; subgraph cluster_b1 { label="块 1"; color="#74c0fc"; b1_1; b1_2; } subgraph cluster_b2 { label="块 2"; color="#74c0fc"; b2_1; b2_2; b2_3; } subgraph cluster_b3 { label="块 3"; color="#74c0fc"; b3_1; b3_2; } } }可视化比较有重叠和无重叠的分块。重叠的块(底部)共享部分内容(深蓝色),以在边界处保持上下文。3. 内容感知分块 (语义分块)描述: 使用自然分隔符分割文本,例如段落(\n\n)、句子(使用NLTK或spaCy等NLP句子分词器),或由标题或其他标记指示的逻辑部分。优点: 倾向于生成语义上更连贯的块,因为它尊重作者预期的结构。不太可能不自然地分割相关联的观点。缺点: 实现起来可能更复杂,尤其是在需要NLP库进行句子边界检测时。块大小可能高度可变——有些段落或部分可能非常长,可能仍然超出模型限制,而另一些可能非常短。可能需要组合多种方法(例如,按段落分割,如果段落过长则再按句子分割)。4. 递归分块这通常是一种实用且有效的折衷办法,旨在尊重语义边界,同时将块保持在大小限制内。描述: 从一组潜在分隔符列表开始,按从最大逻辑单元到最小的顺序排列(例如,["\n\n", "\n", ". ", " ", ""])。尝试使用第一个分隔符分割文本。如果任何结果块仍然过大,则将列表中的下一个分隔符递归应用于这些过大的块。继续此过程,直到所有块都低于所需的大小限制。通常与重叠结合使用。优点: 适应文本结构。在可能的情况下,优先保持更大的语义单元(如段落)的完整性,但在需要满足大小限制时,会退回到更小的单元(句子、单词)。相对有效。缺点: 实现比固定大小分块略微复杂。分割的质量仍然取决于源文本中分隔符的一致性。选择合适的分块方法最好的分块方法取决于几个因素:数据特点: 您的文本是结构良好、段落和部分清晰(如文章或文档),还是更自由的形式(如聊天记录或转录)?是否存在非常长、不间断的段落?嵌入模型限制: 了解您选择的嵌入模型的最大序列长度。目标是使块大小远低于此限制,为模型可能添加的特殊tokens留出空间。检索目标: 您需要获取精确的句子、整个段落还是更大的部分?这会影响您的目标块大小。复杂性与性能: 较简单的方法(固定大小)实现起来更快,但相关性可能较低。更复杂的方法(递归、内容感知)需要更多工作,但可以带来更好的语义表示和搜索结果。通常会尝试不同的分块方法和参数(块大小、重叠),并使用本章后续讨论的指标评估它们对下游搜索性能的影响。{"data": [{"type": "bar", "x": ["固定 (无重叠)", "固定 (有重叠)", "段落分割", "递归"], "y": [65, 72, 78, 80], "marker": {"color": ["#ffc9c9", "#a5d8ff", "#b2f2bb", "#ffec99"], "line": {"color": "#495057", "width": 1}}, "name": "相关性评分"}], "layout": {"title": {"text": "不同分块方法下的检索相关性", "font": {"family": "sans-serif", "size": 16, "color": "#495057"}}, "xaxis": {"title": {"text": "分块方法", "font": {"family": "sans-serif", "size": 12, "color": "#495057"}}, "tickfont": {"family": "sans-serif", "size": 11, "color": "#495057"}}, "yaxis": {"title": {"text": "相关性评分 (例如,NDCG@10)", "font": {"family": "sans-serif", "size": 12, "color": "#495057"}}, "range": [0, 100], "tickfont": {"family": "sans-serif", "size": 11, "color": "#495057"}}, "margin": {"l": 60, "r": 20, "t": 40, "b": 50}, "paper_bgcolor": "#ffffff", "plot_bgcolor": "#e9ecef"}}比较显示,与简单的固定大小方法相比,更具上下文感知能力的分块方法可能会带来更好的搜索相关性评分。实际结果很大程度上取决于数据和任务。实现方面的考量库: 尽可能使用现有库。像LangChain这样的框架提供了各种TextSplitter实现(如RecursiveCharacterTextSplitter, MarkdownTextSplitter等)。NLTK和spaCy等NLP库提供句子分词功能。元数据: 这非常重要。当您创建块时,您必须在每个块的向量旁边存储元数据。最起码,这应包括原始文档的ID以及该块在该文档中的位置或标识符。其他有用的元数据可能包括标题、页码或时间戳。这使得您在搜索时可以获取该块,然后可能为用户获取完整的源文档或周边上下文。总之,准备和分块您的数据不只是一项初步工作;它是构建语义搜索系统中的一个关键设计环节。细致的清洗可以去除噪声,而有效的分块则能确保您的数据符合模型限制,并生成集中、语义有意义的向量。在这里做出的选择直接影响最终搜索结果的粒度和相关性。