首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >理解Elasticsearch中的分块策略

理解Elasticsearch中的分块策略

原创
作者头像
点火三周
修改2025-09-29 21:01:11
修改2025-09-29 21:01:11
1420
举报

在设计语义搜索引擎、RAG应用程序或其他使用嵌入的系统时,有一个决定性的因素可能会影响搜索质量:如何对数据进行分块。

分块是将大型文本分解为较小的、语义上有意义的片段,以便进行单独嵌入和搜索的过程。当文档超出嵌入模型的上下文窗口,或者需要检索特定的相关片段而不是整个文档时,这一过程就显得尤为必要。传统的文本搜索是按原样索引文档,而基于向量的系统需要一种不同的方法,主要基于以下两个原因:

  1. 嵌入模型有一个令牌限制,限制了可同时处理的文本量。例如,ELSER和e5-small的令牌上限是512个。为了生成有意义的嵌入,我们需要对文本进行分割,否则超出限制的令牌中的信息将会丢失。
  2. LLM同样也有令牌限制,并且它们的注意力范围有限,因此仅发送相关的片段要比发送整个文档更为高效。分块可以产生更好的结果,并减少超过令牌限制的可能性。

请记住,您选择的嵌入模型对您的分块策略有重大影响:

  • 上下文窗口较大的模型允许使用较大的块,保留更多上下文,而较小上下文窗口的模型则需要更激进的分块。
  • 一些模型针对较短的段落进行了优化,而另一些则更适合处理段落。

在这篇博客中,我们将讨论分块的基础知识,并探讨使用分块策略如何影响Elasticsearch中的语义搜索结果。

什么是分块?

分块是将大型文档在创建嵌入和索引之前分解为较小片段的过程。与将一篇50页的文本转换为单个嵌入不同,分块将其拆分为可以单独搜索的较小逻辑单元。

分块过程如何将大文档分解为较小文档,然后创建嵌入和索引它们。
分块过程如何将大文档分解为较小文档,然后创建嵌入和索引它们。

在Elasticsearch中,semantic_text字段类型使用分块技术在自动生成嵌入前将文本分解为较小的片段。

为什么分块对搜索质量很重要?

如果我们在生成嵌入之前不应用分块,会遇到一些问题,比如:

  • LLM上下文窗口和注意力限制:LLM能处理的上下文令牌数量有限。如果我们只为小部分相关上下文提供所有文本,则更容易达到这个限制。此外,即使文本适合上下文窗口,LLM也会因为注意力问题如“中间丢失”问题而忽略大块文本中的重要信息。
  • 嵌入质量和数据丢失:从大文档生成的嵌入可能无法很好地捕捉单一概念,过于笼统且分散。较小的块会产生更集中的嵌入,更好地代表信息。另一方面,如果文本超出模型的令牌限制,信息将会丢失,因为在生成嵌入时并未考虑到这些超出部分。
  • 结果可读性:分块将产生易于人类阅读的结果。您可以仅检索相关部分,而不必阅读整个文档。

分块大小与搜索精度的关系

每个块的大小在向量搜索中形成了一个权衡:

  • 较小的块:将提供更精确的匹配,但可能缺乏足够的上下文来理解信息。
  • 较大的块:会包含更多上下文,但可能包括多个概念,导致匹配不够精确。

目标是选择一个块大小,在最小化信息丢失的同时仍然包含有用的上下文。 一般来说,可以这样考虑块的大小:

较小的块:

  • 当您需要高度具体的答案时(如查找定义、日期或特定程序)
  • 当您的文档包含许多不同的、自成一体的概念时

较大的块:

  • 当上下文对于理解至关重要时(如分析论点、叙述或复杂解释)
  • 对于其中概念相互依存且分割会导致意义丧失的文档

为了找到适合您使用场景的合适块大小,您可以使用自己的内容进行测试。监控是否得到了过多不相关的结果(块太大)或缺乏足够上下文的答案(块太小)。

分块策略

Elasticsearch允许我们选择不同的方法来分割文档。您可以使用此对比表来选择最适合您用例的策略,考虑文档结构和您使用的模型:

句子、单词、递归及无分块策略的优缺点。
句子、单词、递归及无分块策略的优缺点。

现在让我们看看每种策略的实际应用:

句子分块

这种策略将文本拆分为一个或多个完整句子,以优先考虑句子层级的可读性和语义连贯性。

输入:

代码语言:javascript
复制
人工智能正在通过预测分析改变医疗保健。机器学习算法可以分析患者数据以识别风险因素。疾病的早期检测可以挽救生命并降低成本。然而,数据隐私仍然是一个关键问题。医疗服务提供者必须在创新和患者保密之间取得平衡。

结果数组(max_chunk_size: 50, sentence_overlap: 1):

代码语言:javascript
复制
[
  "人工智能正在通过预测分析改变医疗保健。机器学习算法可以分析患者数据以识别风险因素。疾病的早期检测可以挽救生命并降低成本。",
  "疾病的早期检测可以挽救生命并降低成本。然而,数据隐私仍然是一个关键问题。医疗服务提供者必须在创新和患者保密之间取得平衡。"
]

单词分块

这种策略将文本拆分为单个单词,直到达到max_chunk_size。这确保了块大小的一致性,为处理和存储提供了可预测性。缺点是可能会在多个块中拆分相关上下文。

输入:

代码语言:javascript
复制
人工智能正在通过预测分析改变医疗保健。机器学习算法可以分析患者数据以识别风险因素。疾病的早期检测可以挽救生命并降低成本。然而,数据隐私仍然是一个关键问题。医疗服务提供者必须在创新和患者保密之间取得平衡。

结果数组(max_chunk_size: 25, overlap: 5):

代码语言:javascript
复制
[
  "人工智能正在通过预测分析改变医疗保健。机器学习算法可以分析患者数据以识别风险因素。疾病的早期检测可以挽救生命",
  "挽救生命并降低成本。然而,数据隐私仍然是一个关键问题。医疗服务提供者必须在创新和患者保密之间取得平衡。"
]

递归分块

这种策略基于分隔符列表(如换行)递归地拆分文本。按顺序应用分隔符后,如果块仍然太大,则回退到句子拆分。对于格式化内容(如markdown文档),这种策略尤其有效。

输入:

代码语言:markdown
复制
# 气候变化解决方案

## 可再生能源
太阳能和风能正在与化石燃料的成本竞争。投资于清洁能源基础设施创造了就业机会。

## 碳捕获
直接空气捕获技术从大气中去除二氧化碳。这些系统需要大量的能源输入,但在大规模部署中显示出希望。

## 政策变革
政府法规可以通过碳定价和可再生能源法规加速向清洁能源的转变。

结果数组(max_chunk_size: 30, separators: "#", "\n\n"):

代码语言:markdown
复制
[
  "# 气候变化解决方案\n\n## 可再生能源\n太阳能和风能正在与化石燃料的成本竞争。投资于清洁能源基础设施创造了就业机会。",
  "## 碳捕获\n直接空气捕获技术从大气中去除二氧化碳。这些系统需要大量的能源输入,但在大规模部署中显示出希望。",
  "## 政策变革\n政府法规可以通过碳定价和可再生能源法规加速向清洁能源的转变。"
]

无分块策略

这种“策略”禁用分块,从完整文本创建嵌入。它适用于所有上下文始终需要的小型文本和文档。这一策略的潜在缺陷是,模型可能会在生成嵌入时丢弃多余的令牌。

输入:

代码语言:javascript
复制
人工智能正在通过预测分析改变医疗保健。机器学习算法可以分析患者数据以识别风险因素。疾病的早期检测可以挽救生命并降低成本。然而,数据隐私仍然是一个关键问题。医疗服务提供者必须在创新和患者保密之间取得平衡。

结果:

代码语言:javascript
复制
["人工智能正在通过预测分析改变医疗保健。机器学习算法可以分析患者数据以识别风险因素。疾病的早期检测可以挽救生命并降低成本。然而,数据隐私仍然是一个关键问题。医疗服务提供者必须在创新和患者保密之间取得平衡。"]

关键分块参数

Elastic为分块策略提供了以下自定义参数:

  • overlap:块之间重叠的单词数。仅用于单词策略。
  • sentence_overlap:块之间重叠的句子数。仅用于句子策略。
  • separator_group:递归分块策略的预定义分隔符列表。可以是“markdown”或“plaintext”。
  • separators:递归分块策略的自定义分隔符列表。可以是普通字符串或正则表达式模式。
  • strategy:选择的策略(句子、单词、递归或无)。
  • max_chunk_size:块的最大大小,以单词计量。

在Elasticsearch中进行分块

让我们通过一个实际示例来看看在Elasticsearch中如何进行分块。

分块设置是在创建推理端点时指定的,Elasticsearch在索引文档时会自动处理分块过程。

在以下示例中,我们将创建一个text_embedding推理端点,使用句子分块策略,并检查如何在语义查询中生成和查找块:

  1. 创建推理端点:
代码语言:javascript
复制
PUT _inference/text_embedding/my-embedding-model
{
  "service": "elasticsearch",
  "service_settings": {
    "num_allocations": 1,
    "num_threads": 1,
    "model_id": ".multilingual-e5-small_linux-x86_64"
  },
  "chunking_settings": {
    "strategy": "sentence",
    "max_chunk_size": 40,
    "sentence_overlap": 0
  }
}

这里我们使用了40个单词的最大块大小,并且块之间没有重叠。句子策略将文本在自然句子边界处拆分。

确保已部署多语言e5小型模型。该模型的最大令牌窗口为512。

  1. 创建一个使用我们新推理端点的semantic_text字段的索引:
代码语言:javascript
复制
PUT chunking_test
{
  "mappings": {
    "properties": {
      "my-semantic-field": {
        "type": "semantic_text",
        "inference_id": "my-embedding-model"
      }
    }
  }
}

当您索引文档时,Elasticsearch会自动应用分块策略,并为my-semantic-field字段中的每个块创建嵌入。

  1. 将示例文档索引到我们的索引中:
代码语言:javascript
复制
PUT chunking_test/_doc/1
{
  "my-semantic-field": "制作完美咖啡需要注意几个关键因素。水温应在195-205°F之间以获得最佳提取效果。研磨大小显著影响酿造时间和风味强度。使用1:15的咖啡与水比例适用于大多数酿造方法。酿造时间因方法而异:手冲需要3-4分钟,而意式浓缩需要25-30秒。新鲜咖啡豆在两周内烘焙可以产生最佳效果。"
}

该文档将根据我们的句子策略自动拆分为多个块。

  1. 在索引上执行语义搜索:
代码语言:javascript
复制
GET /chunking_test/_search
{
  "fields": ["_inference_fields"],
  "query": {
    "semantic": {
      "field": "my-semantic-field",
      "query": "我应该酿造咖啡多长时间?"
    }
  },
  "highlight": {
    "fields": {
      "my-semantic-field": {
        "order": "score",
        "number_of_fragments": 1
      }
    }
  }
}

让我们逐步看看查询的每一部分:

代码语言:javascript
复制
"fields": ["_inference_fields"],

在这里,我们要求返回标题和推理字段;这些字段包括关于端点、模型和块的信息。

代码语言:javascript
复制
"query": {
  "semantic": {
    "field": "my-semantic-field",
    "query": "我应该酿造咖啡多长时间?"
  }
}

这是对my-semantic-field的语义查询,它自动使用我们在步骤1中创建的推理端点来搜索与用户查询中的术语匹配的内容。

代码语言:javascript
复制
"highlight": {
  "fields": {
    "my-semantic-field": {
      "order": "score",
      "number_of_fragments": 1
    }
  }
}

这部分将返回从文本生成的最高得分块。

现在的响应:

代码语言:javascript
复制
"hits": [
  {
    "_index": "chunking_test",
    "_id": "1",
    "_score": 0.9537368,
    "_source": {
      "my-semantic-field": "制作完美咖啡需要注意几个关键因素。水温应在195-205°F之间以获得最佳提取效果。研磨大小显著影响酿造时间和风味强度。使用1:15的咖啡与水比例适用于大多数酿造方法。酿造时间因方法而异:手冲需要3-4分钟,而意式浓缩需要25-30秒。新鲜咖啡豆在两周内烘焙可以产生最佳效果。",
      "_inference_fields": {
        "my-semantic-field": {
          "inference": {
            "inference_id": "my-embedding-model",
            "model_settings": {
              "service": "elasticsearch",
              "task_type": "text_embedding",
              "dimensions": 384,
              "similarity": "cosine",
              "element_type": "float"
            },
            "chunks": {
              "my-semantic-field": [
                {
                  "start_offset": 0,
                  "end_offset": 135,
                  "embeddings": [
                    -0.047878783,
                    ...
                    0.02849774
                  ]
                },
                {
                  "start_offset": 135,
                  "end_offset": 261,
                  "embeddings": [
                    -0.019347992,
                    ...
                    0.046932716
                  ]
                },
                {
                  "start_offset": 261,
                  "end_offset": 356,
                  "embeddings": [
                    -0.021673936,
                    ...
                    0.03294023
                  ]
                },
                {
                  "start_offset": 356,
                  "end_offset": 418,
                  "embeddings": [
                    0.027161874,
                    ...
                    0.033048477
                  ]
                }
              ]
            }
          }
        }
      }
    },
    "highlight": {
      "my-semantic-field": [
        "酿造时间因方法而异:手冲需要3-4分钟,而意式浓缩需要25-30秒。"
      ]
    }
  }
]

让我们分解这个响应:

原始内容:

代码语言:javascript
复制
"_source": {
  "my-semantic-field": "制作完美咖啡需要注意几个关键因素..."
}

这是索引的完整原始文本。

分块详情:

代码语言:javascript
复制
"_inference_fields": {
  "my-semantic-field": {
    "inference": {
      "inference_id": "my-embedding-model",
      "chunks": {
        "my-semantic-field": [
          {
            "start_offset": 0,
            "end_offset": 135,
            "embeddings": [
              -0.047878783,
              ...
              0.02849774
            ]
          },
          {
            "start_offset": 135,
            "end_offset": 261,
            "embeddings": [
              -0.019347992,
              ...
              0.046932716
            ]
          },
          {
            "start_offset": 261,
            "end_offset": 356,
            "embeddings": [
              -0.021673936,
              ...
              0.03294023
            ]
          },
          {
            "start_offset": 356,
            "end_offset": 418,
            "embeddings": [
              ```javascript
              0.027161874,
              ...
              0.033048477
            ]
          }
        ]
      }
    }
  }
}

这显示了:

  • start_offset/end_offset:每个块在原始文本中开始和结束的字符位置
  • embeddings:每个块的向量表示(对于e5-small模型是384维)
  • 块的数量:在这个例子中,从咖啡酿造文本中创建了4个块

最佳匹配块:

代码语言:javascript
复制
"highlight": {
  "my-semantic-field": [
    "酿造时间因方法而异:手冲需要3-4分钟,而意式浓缩需要25-30秒。"
  ]
}

高亮显示了哪个特定块在我们的“我应该酿造咖啡多长时间?”查询中得分最高,展示了语义搜索如何找到最相关的内容,即使精确的词语并不匹配。

分块策略实验

设置

我们将使用美洲一些国家的维基百科页面。这些页面包含长文本,使我们可以展示分块与无分块策略的区别。我们准备了一个仓库来获取维基百科内容,创建适当的推理端点和映射以上传数据。

  1. 克隆仓库:
代码语言:bash
复制
git clone https://github.com/Alex1795/embeddings_chunking_strategies_blog.git
cd embeddings_chunking_strategies_blog
  1. 安装所需的库:
代码语言:bash
复制
pip install -r requirements.txt
  1. 设置环境变量:
代码语言:bash
复制
export ES_HOST="your-elasticsearch-endpoint"
export ES_API_KEY="your-api-key"
  1. 运行set_up.py文件:
代码语言:bash
复制
python set_up.py

这个脚本执行以下步骤:

  • 创建两个推理端点,均使用ELSER模型生成稀疏嵌入:
    1. sentence-chunking-demo:使用句子分块策略,最大块大小为80,句子重叠为1
    2. none-chunking-demo:不使用任何分块策略
  • 创建索引countries_wiki的映射,包括两个多字段wiki_article字段:
    1. wiki_article.sentence:使用sentence-chunking-demo推理端点
    2. wiki_article.none:使用none-chunking-demo推理端点
  • 获取每个国家的维基百科文章,并将标题和内容上传到我们在countrywiki_article字段中的新索引

这可能需要几分钟;请记住,Elastic正在语义文本字段上进行分块和生成嵌入。如果一切顺利,您将看到进程正确完成:

Elasticsearch分块策略演示部分1
Elasticsearch分块策略演示部分1
Elasticsearch分块策略演示部分2
Elasticsearch分块策略演示部分2

运行语义搜索

脚本run_semantic_search.py实现了几个辅助函数,在两个语义文本字段上执行相同的语义搜索,并以表格形式打印结果以便于比较。您可以简单地执行脚本,如下所示:

代码语言:bash
复制
python run_semantic_search.py

您将看到demo_queries列表中定义的每个查询的结果(在此处添加您自己的查询以进行测试):

代码语言:javascript
复制
demo_queries = [
  "inca帝国的国家",
  "咖啡生产",
  "石油和石油出口",
  "海滩目的地",
  "曲棍球"
]

结果

让我们探索demo中的查询结果:

查询:inca帝国的国家

策略:句子分块

使用句子分块策略的查询结果。
使用句子分块策略的查询结果。

我们得到了预期的结果:秘鲁、厄瓜多尔、玻利维亚、智利、阿根廷和相关的块。例如,对于阿根廷,我们得到了一个非常具体的信息:

代码语言:javascript
复制
"在西北部的先进Diaguita定居贸易文化,约在1480年被Inca帝国征服"

正如我们所见,我们可以很容易地确定每个文档为什么出现在结果中。

查询:inca帝国的国家

策略:无分块

不使用分块策略的查询结果。
不使用分块策略的查询结果。

在这里,我们可以看到相同的前四个结果,但最后我们看到墨西哥,墨西哥在任何方面都不是Incan帝国的一部分。我们还可以看到整个文档被接收到作为一个相关块,所以我们无法真正确定这些文档为什么相关。这些结果的问题是,嵌入是从文本的前512个令牌中形成的,因此丢失了很多信息。很可能我们没有获得关于每个国家历史的信息。

查询:曲棍球

策略:句子分块

使用句子分块策略的曲棍球查询结果。
使用句子分块策略的曲棍球查询结果。

我们可以看到与曲棍球直接相关的国家,如美国和加拿大,还有阿根廷、智利和巴拿马。在相关块列中,我们看到在每篇文章中如何提到曲棍球。

查询:曲棍球

策略:无分块

不使用分块策略的曲棍球查询结果。
不使用分块策略的曲棍球查询结果。

在这些结果中,我们可以看到一些通常与曲棍球(或冬季运动)无关的国家。由于相关文本是整篇文章,我们无法真正看出为什么这些出现在结果中。此外,请注意分数比之前低得多;这表明这些可能根本不是好结果,这些只是与查询“最接近”的,但几乎没有相关性。

结论

正如我们所见,使用分块策略可以提升结果质量。没有分块,我们会遇到如下问题:

  • 不相关的结果(如Incan帝国中的墨西哥):当文本大于我们嵌入模型的令牌限制时,某些数据在生成嵌入时根本没有包括在内,我们丢失了信息。
  • 结果解释:即使结果相关,从整个文本中解释为什么一个特定文档出现比从相关块中解释要困难得多。这在验证我们的结果并与终端用户建立信任时可能是个问题。
  • 效率:如果我们要将结果发送给下游的LLM,整个文档会迅速消耗上下文窗口的空间并带来不相关的信息,导致更高的令牌成本和更差的响应质量。

总而言之,处理比模型令牌限制更长的文本时,分块策略始终是个好主意。它改善结果并降低成本,是设计AI系统时的基础环节。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 什么是分块?
  • 为什么分块对搜索质量很重要?
  • 分块大小与搜索精度的关系
  • 分块策略
    • 句子分块
    • 单词分块
    • 递归分块
    • 无分块策略
  • 关键分块参数
  • 在Elasticsearch中进行分块
  • 分块策略实验
    • 设置
    • 运行语义搜索
    • 结果
  • 结论
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档