调整搜索速度

编辑

为文件系统缓存分配内存

编辑

Elasticsearch 严重依赖文件系统缓存来提高搜索速度。一般来说,你应该确保至少有一半的可用内存用于文件系统缓存,以便 Elasticsearch 可以将索引的热点区域保存在物理内存中。

在 Linux 上使用适度的预读值以避免页面缓存颠簸

编辑

搜索可能会导致大量的随机读取 I/O。当底层块设备具有较高的预读值时,可能会进行大量不必要的读取 I/O,尤其是在使用内存映射访问文件时(请参阅存储类型)。

大多数 Linux 发行版对单个普通设备使用合理的预读值 128KiB,但是,当使用软件 RAID、LVM 或 dm-crypt 时,生成的块设备(支持 Elasticsearch path.data)可能会最终得到一个非常大的预读值(在几个 MiB 的范围内)。这通常会导致严重的页面(文件系统)缓存颠簸,从而对搜索(或更新)性能产生不利影响。

你可以使用 lsblk -o NAME,RA,MOUNTPOINT,TYPE,SIZEKiB 为单位检查当前值。查阅你的发行版的文档,了解如何更改此值(例如,使用 udev 规则以在重启后保持不变,或通过 blockdev --setra 作为临时设置)。我们建议预读值为 128KiB

blockdev 期望的值为 512 字节的扇区,而 lsblk 报告的值为 KiB。例如,要临时将 /dev/nvme0n1 的预读值设置为 128KiB,请指定 blockdev --setra 256 /dev/nvme0n1

使用更快的硬件

编辑

如果你的搜索受 I/O 限制,请考虑增加文件系统缓存的大小(见上文)或使用更快的存储。每次搜索都涉及跨多个文件的顺序读取和随机读取的混合,并且每个分片上可能同时运行多个搜索,因此 SSD 驱动器往往比机械磁盘表现更好。

如果你的搜索受 CPU 限制,请考虑使用更多数量的更快 CPU。

本地存储与远程存储

编辑

直接连接的(本地)存储通常比远程存储性能更好,因为它更容易配置好并且避免了通信开销。

一些远程存储的性能非常差,尤其是在 Elasticsearch 施加的那种负载下。但是,通过仔细的调整,有时也可以使用远程存储来实现可接受的性能。在提交到特定的存储架构之前,请使用实际的工作负载对你的系统进行基准测试,以确定任何调整参数的效果。如果无法达到你期望的性能,请与你的存储系统的供应商合作以找出问题。

文档建模

编辑

应该对文档进行建模,以使搜索时的操作尽可能便宜。

特别是,应避免连接。nested 会使查询速度降低数倍,而父子关系会使查询速度降低数百倍。因此,如果可以通过取消规范化文档在没有连接的情况下回答相同的问题,则可以期望显着的加速。

尽可能搜索较少的字段

编辑

query_stringmulti_match 查询的目标字段越多,速度就越慢。一种提高多个字段搜索速度的常见技术是在索引时将其值复制到单个字段中,然后在搜索时使用此字段。这可以使用映射的copy-to指令自动完成,而无需更改文档的来源。这是一个包含电影的索引示例,该索引通过将两个值都索引到 name_and_plot 字段中来优化在电影的名称和情节上搜索的查询。

resp = client.indices.create(
    index="movies",
    mappings={
        "properties": {
            "name_and_plot": {
                "type": "text"
            },
            "name": {
                "type": "text",
                "copy_to": "name_and_plot"
            },
            "plot": {
                "type": "text",
                "copy_to": "name_and_plot"
            }
        }
    },
)
print(resp)
response = client.indices.create(
  index: 'movies',
  body: {
    mappings: {
      properties: {
        name_and_plot: {
          type: 'text'
        },
        name: {
          type: 'text',
          copy_to: 'name_and_plot'
        },
        plot: {
          type: 'text',
          copy_to: 'name_and_plot'
        }
      }
    }
  }
)
puts response
const response = await client.indices.create({
  index: "movies",
  mappings: {
    properties: {
      name_and_plot: {
        type: "text",
      },
      name: {
        type: "text",
        copy_to: "name_and_plot",
      },
      plot: {
        type: "text",
        copy_to: "name_and_plot",
      },
    },
  },
});
console.log(response);
PUT movies
{
  "mappings": {
    "properties": {
      "name_and_plot": {
        "type": "text"
      },
      "name": {
        "type": "text",
        "copy_to": "name_and_plot"
      },
      "plot": {
        "type": "text",
        "copy_to": "name_and_plot"
      }
    }
  }
}

预索引数据

编辑

你应该利用查询中的模式来优化数据的索引方式。例如,如果你的所有文档都有一个 price 字段,并且大多数查询在固定的范围列表上运行 range 聚合,则可以通过将范围预索引到索引中并使用 terms 聚合来加快此聚合的速度。

例如,如果文档看起来像

resp = client.index(
    index="index",
    id="1",
    document={
        "designation": "spoon",
        "price": 13
    },
)
print(resp)
response = client.index(
  index: 'index',
  id: 1,
  body: {
    designation: 'spoon',
    price: 13
  }
)
puts response
const response = await client.index({
  index: "index",
  id: 1,
  document: {
    designation: "spoon",
    price: 13,
  },
});
console.log(response);
PUT index/_doc/1
{
  "designation": "spoon",
  "price": 13
}

并且搜索请求看起来像

resp = client.search(
    index="index",
    aggs={
        "price_ranges": {
            "range": {
                "field": "price",
                "ranges": [
                    {
                        "to": 10
                    },
                    {
                        "from": 10,
                        "to": 100
                    },
                    {
                        "from": 100
                    }
                ]
            }
        }
    },
)
print(resp)
response = client.search(
  index: 'index',
  body: {
    aggregations: {
      price_ranges: {
        range: {
          field: 'price',
          ranges: [
            {
              to: 10
            },
            {
              from: 10,
              to: 100
            },
            {
              from: 100
            }
          ]
        }
      }
    }
  }
)
puts response
const response = await client.search({
  index: "index",
  aggs: {
    price_ranges: {
      range: {
        field: "price",
        ranges: [
          {
            to: 10,
          },
          {
            from: 10,
            to: 100,
          },
          {
            from: 100,
          },
        ],
      },
    },
  },
});
console.log(response);
GET index/_search
{
  "aggs": {
    "price_ranges": {
      "range": {
        "field": "price",
        "ranges": [
          { "to": 10 },
          { "from": 10, "to": 100 },
          { "from": 100 }
        ]
      }
    }
  }
}

那么可以在索引时通过 price_range 字段来丰富文档,该字段应映射为 keyword

resp = client.indices.create(
    index="index",
    mappings={
        "properties": {
            "price_range": {
                "type": "keyword"
            }
        }
    },
)
print(resp)

resp1 = client.index(
    index="index",
    id="1",
    document={
        "designation": "spoon",
        "price": 13,
        "price_range": "10-100"
    },
)
print(resp1)
response = client.indices.create(
  index: 'index',
  body: {
    mappings: {
      properties: {
        price_range: {
          type: 'keyword'
        }
      }
    }
  }
)
puts response

response = client.index(
  index: 'index',
  id: 1,
  body: {
    designation: 'spoon',
    price: 13,
    price_range: '10-100'
  }
)
puts response
const response = await client.indices.create({
  index: "index",
  mappings: {
    properties: {
      price_range: {
        type: "keyword",
      },
    },
  },
});
console.log(response);

const response1 = await client.index({
  index: "index",
  id: 1,
  document: {
    designation: "spoon",
    price: 13,
    price_range: "10-100",
  },
});
console.log(response1);
PUT index
{
  "mappings": {
    "properties": {
      "price_range": {
        "type": "keyword"
      }
    }
  }
}

PUT index/_doc/1
{
  "designation": "spoon",
  "price": 13,
  "price_range": "10-100"
}

然后,搜索请求可以聚合这个新字段,而不是在 price 字段上运行 range 聚合。

resp = client.search(
    index="index",
    aggs={
        "price_ranges": {
            "terms": {
                "field": "price_range"
            }
        }
    },
)
print(resp)
response = client.search(
  index: 'index',
  body: {
    aggregations: {
      price_ranges: {
        terms: {
          field: 'price_range'
        }
      }
    }
  }
)
puts response
const response = await client.search({
  index: "index",
  aggs: {
    price_ranges: {
      terms: {
        field: "price_range",
      },
    },
  },
});
console.log(response);
GET index/_search
{
  "aggs": {
    "price_ranges": {
      "terms": {
        "field": "price_range"
      }
    }
  }
}

考虑将标识符映射为 keyword

编辑

并非所有数字数据都应映射为数字字段数据类型。Elasticsearch 优化了integerlong 等数字字段,以进行range 查询。但是,keyword 字段更适合 term 和其他词项级别查询。

标识符(例如 ISBN 或产品 ID)很少在 range 查询中使用。但是,它们通常使用词项级别查询来检索。

如果满足以下条件,请考虑将数字标识符映射为 keyword

  • 你不打算使用 range 查询来搜索标识符数据。
  • 快速检索很重要。keyword 字段上的 term 查询搜索通常比数字字段上的 term 搜索更快。

如果你不确定使用哪个,可以使用多字段将数据映射为 keyword _和_数字数据类型。

避免使用脚本

编辑

如果可能,请避免使用基于脚本的排序、聚合中的脚本以及 script_score 查询。请参阅 脚本、缓存和搜索速度

搜索舍入日期

编辑

在日期字段上使用 now 的查询通常是不可缓存的,因为匹配的范围一直在变化。但是,切换到舍入日期通常在用户体验方面是可以接受的,并且具有更好地利用查询缓存的好处。

例如,下面的查询

resp = client.index(
    index="index",
    id="1",
    document={
        "my_date": "2016-05-11T16:30:55.328Z"
    },
)
print(resp)

resp1 = client.search(
    index="index",
    query={
        "constant_score": {
            "filter": {
                "range": {
                    "my_date": {
                        "gte": "now-1h",
                        "lte": "now"
                    }
                }
            }
        }
    },
)
print(resp1)
response = client.index(
  index: 'index',
  id: 1,
  body: {
    my_date: '2016-05-11T16:30:55.328Z'
  }
)
puts response

response = client.search(
  index: 'index',
  body: {
    query: {
      constant_score: {
        filter: {
          range: {
            my_date: {
              gte: 'now-1h',
              lte: 'now'
            }
          }
        }
      }
    }
  }
)
puts response
const response = await client.index({
  index: "index",
  id: 1,
  document: {
    my_date: "2016-05-11T16:30:55.328Z",
  },
});
console.log(response);

const response1 = await client.search({
  index: "index",
  query: {
    constant_score: {
      filter: {
        range: {
          my_date: {
            gte: "now-1h",
            lte: "now",
          },
        },
      },
    },
  },
});
console.log(response1);
PUT index/_doc/1
{
  "my_date": "2016-05-11T16:30:55.328Z"
}

GET index/_search
{
  "query": {
    "constant_score": {
      "filter": {
        "range": {
          "my_date": {
            "gte": "now-1h",
            "lte": "now"
          }
        }
      }
    }
  }
}

可以用以下查询替换

resp = client.search(
    index="index",
    query={
        "constant_score": {
            "filter": {
                "range": {
                    "my_date": {
                        "gte": "now-1h/m",
                        "lte": "now/m"
                    }
                }
            }
        }
    },
)
print(resp)
response = client.search(
  index: 'index',
  body: {
    query: {
      constant_score: {
        filter: {
          range: {
            my_date: {
              gte: 'now-1h/m',
              lte: 'now/m'
            }
          }
        }
      }
    }
  }
)
puts response
const response = await client.search({
  index: "index",
  query: {
    constant_score: {
      filter: {
        range: {
          my_date: {
            gte: "now-1h/m",
            lte: "now/m",
          },
        },
      },
    },
  },
});
console.log(response);
GET index/_search
{
  "query": {
    "constant_score": {
      "filter": {
        "range": {
          "my_date": {
            "gte": "now-1h/m",
            "lte": "now/m"
          }
        }
      }
    }
  }
}

在这种情况下,我们舍入到分钟,因此如果当前时间是 16:31:29,则范围查询将匹配 my_date 字段的值在 15:31:0016:31:59 之间的所有内容。如果多个用户在同一分钟内运行包含此范围的查询,则查询缓存可以帮助加快速度。用于舍入的时间间隔越长,查询缓存可以提供的帮助就越大,但请注意,过于激进的舍入也可能会损害用户体验。

可能很想将范围拆分为一个大的可缓存部分和一个较小的不可缓存部分,以便能够利用查询缓存,如下所示

resp = client.search(
    index="index",
    query={
        "constant_score": {
            "filter": {
                "bool": {
                    "should": [
                        {
                            "range": {
                                "my_date": {
                                    "gte": "now-1h",
                                    "lte": "now-1h/m"
                                }
                            }
                        },
                        {
                            "range": {
                                "my_date": {
                                    "gt": "now-1h/m",
                                    "lt": "now/m"
                                }
                            }
                        },
                        {
                            "range": {
                                "my_date": {
                                    "gte": "now/m",
                                    "lte": "now"
                                }
                            }
                        }
                    ]
                }
            }
        }
    },
)
print(resp)
response = client.search(
  index: 'index',
  body: {
    query: {
      constant_score: {
        filter: {
          bool: {
            should: [
              {
                range: {
                  my_date: {
                    gte: 'now-1h',
                    lte: 'now-1h/m'
                  }
                }
              },
              {
                range: {
                  my_date: {
                    gt: 'now-1h/m',
                    lt: 'now/m'
                  }
                }
              },
              {
                range: {
                  my_date: {
                    gte: 'now/m',
                    lte: 'now'
                  }
                }
              }
            ]
          }
        }
      }
    }
  }
)
puts response
const response = await client.search({
  index: "index",
  query: {
    constant_score: {
      filter: {
        bool: {
          should: [
            {
              range: {
                my_date: {
                  gte: "now-1h",
                  lte: "now-1h/m",
                },
              },
            },
            {
              range: {
                my_date: {
                  gt: "now-1h/m",
                  lt: "now/m",
                },
              },
            },
            {
              range: {
                my_date: {
                  gte: "now/m",
                  lte: "now",
                },
              },
            },
          ],
        },
      },
    },
  },
});
console.log(response);
GET index/_search
{
  "query": {
    "constant_score": {
      "filter": {
        "bool": {
          "should": [
            {
              "range": {
                "my_date": {
                  "gte": "now-1h",
                  "lte": "now-1h/m"
                }
              }
            },
            {
              "range": {
                "my_date": {
                  "gt": "now-1h/m",
                  "lt": "now/m"
                }
              }
            },
            {
              "range": {
                "my_date": {
                  "gte": "now/m",
                  "lte": "now"
                }
              }
            }
          ]
        }
      }
    }
  }
}

但是,这种做法在某些情况下可能会使查询运行速度变慢,因为 bool 查询引入的开销可能会抵消更好地利用查询缓存所带来的节省。

强制合并只读索引

编辑

只读索引可以从合并到单个段中获益。通常,基于时间的索引就是这种情况:只有当前时间范围的索引会获得新文档,而较旧的索引是只读的。已强制合并为单个段的分片可以使用更简单、更高效的数据结构来执行搜索。

不要强制合并你仍在写入或将来会再次写入的索引。相反,依靠自动后台合并过程在需要时执行合并,以保持索引平稳运行。如果你继续写入强制合并的索引,则其性能可能会变得更差。

预热全局序号

编辑

全局序号是一种用于优化聚合性能的数据结构。它们以延迟方式计算并作为 字段数据缓存 的一部分存储在 JVM 堆中。对于大量用于存储桶聚合的字段,你可以告诉 Elasticsearch 在收到请求之前构造并缓存全局序号。应该谨慎地执行此操作,因为它会增加堆使用率,并可能使 刷新花费更长的时间。可以通过设置 eager global ordinals 映射参数来动态更新现有映射上的选项

resp = client.indices.create(
    index="index",
    mappings={
        "properties": {
            "foo": {
                "type": "keyword",
                "eager_global_ordinals": True
            }
        }
    },
)
print(resp)
response = client.indices.create(
  index: 'index',
  body: {
    mappings: {
      properties: {
        foo: {
          type: 'keyword',
          eager_global_ordinals: true
        }
      }
    }
  }
)
puts response
const response = await client.indices.create({
  index: "index",
  mappings: {
    properties: {
      foo: {
        type: "keyword",
        eager_global_ordinals: true,
      },
    },
  },
});
console.log(response);
PUT index
{
  "mappings": {
    "properties": {
      "foo": {
        "type": "keyword",
        "eager_global_ordinals": true
      }
    }
  }
}

预热文件系统缓存

编辑

如果运行 Elasticsearch 的机器重启,文件系统缓存将为空,因此需要一些时间,操作系统才能将索引的热点区域加载到内存中,以便搜索操作能够快速执行。您可以使用 index.store.preload 设置,显式告知操作系统应根据文件扩展名,预先加载哪些文件到内存中。

如果在太多索引或太多文件上预先加载数据到文件系统缓存,如果文件系统缓存不够大,无法容纳所有数据,则会使搜索速度变慢。请谨慎使用。

使用索引排序加速合取查询

编辑

索引排序 对于提高合取查询的速度很有用,但会略微降低索引速度。请在 索引排序文档 中阅读更多相关信息。

使用 preference 优化缓存利用率

编辑

有多种缓存可以帮助提高搜索性能,例如 文件系统缓存请求缓存查询缓存。然而,所有这些缓存都在节点级别维护,这意味着如果您连续运行两次相同的请求,拥有 1 个或多个副本,并使用 轮询(默认路由算法),则这两个请求将转到不同的分片副本,从而阻止节点级缓存发挥作用。

由于搜索应用程序的用户通常会一个接一个地运行相似的请求,例如为了分析索引的更小子集,使用标识当前用户或会话的 preference 值可以帮助优化缓存的使用。

副本可能有助于提高吞吐量,但并非总是如此

编辑

除了提高弹性之外,副本还可以帮助提高吞吐量。例如,如果您有一个单分片索引和三个节点,您需要将副本数设置为 2,以便总共有 3 个分片副本,从而利用所有节点。

现在假设您有一个 2 分片索引和两个节点。在一种情况下,副本数为 0,这意味着每个节点持有一个分片。在第二种情况下,副本数为 1,这意味着每个节点有两个分片。哪种设置在搜索性能方面表现最佳?通常,每个节点的分片总数较少的设置性能会更好。原因是它为每个分片提供了更大的可用文件系统缓存份额,而文件系统缓存可能是 Elasticsearch 最重要的性能因素。与此同时,请注意,没有副本的设置在单个节点发生故障时容易出现故障,因此在吞吐量和可用性之间需要权衡。

那么正确的副本数是多少?如果您的集群有 num_nodes 个节点,总共num_primaries 个主分片,并且您希望最多能够应对 max_failures 个节点同时发生故障,那么对您来说,正确的副本数是 max(max_failures, ceil(num_nodes / num_primaries) - 1)

使用搜索分析器调整您的查询

编辑

Profile API 提供了关于查询和聚合的每个组件如何影响处理请求所需时间的详细信息。

Kibana 中的 搜索分析器 可以轻松导航和分析分析结果,并深入了解如何调整查询以提高性能并减少负载。

由于 Profile API 本身会给查询增加显著的开销,因此最好使用此信息来了解各种查询组件的相对成本。它不提供实际处理时间的可靠度量。

使用 index_phrases 加快短语查询

编辑

text 字段有一个 index_phrases 选项,该选项索引 2-shingles,并由查询解析器自动利用来运行没有 slop 的短语查询。如果您的用例涉及运行大量短语查询,这可以显著加快查询速度。

使用 index_prefixes 加快前缀查询

编辑

text 字段有一个 index_prefixes 选项,该选项索引所有词项的前缀,并由查询解析器自动利用来运行前缀查询。如果您的用例涉及运行大量前缀查询,这可以显著加快查询速度。

使用 constant_keyword 加快过滤速度

编辑

一个普遍的规则是,过滤器的成本主要取决于匹配文档的数量。假设您有一个包含自行车的索引。有大量的自行车,并且许多搜索都对 cycle_type: bicycle 进行过滤。这个非常常见的过滤器不幸的是也非常昂贵,因为它匹配了大多数文档。有一种简单的方法可以避免运行此过滤器:将自行车移动到它们自己的索引中,并通过搜索此索引而不是向查询添加过滤器来过滤自行车。

不幸的是,这会使客户端逻辑变得棘手,而 constant_keyword 可以提供帮助。通过在包含自行车的索引上将 cycle_type 映射为值为 bicycleconstant_keyword,客户端可以继续运行与他们在单体索引上运行的完全相同的查询,并且 Elasticsearch 将在自行车索引上正确处理,忽略 cycle_type 上的过滤器(如果值为 bicycle),否则不返回任何匹配项。

以下是映射可能的样子

resp = client.indices.create(
    index="bicycles",
    mappings={
        "properties": {
            "cycle_type": {
                "type": "constant_keyword",
                "value": "bicycle"
            },
            "name": {
                "type": "text"
            }
        }
    },
)
print(resp)

resp1 = client.indices.create(
    index="other_cycles",
    mappings={
        "properties": {
            "cycle_type": {
                "type": "keyword"
            },
            "name": {
                "type": "text"
            }
        }
    },
)
print(resp1)
response = client.indices.create(
  index: 'bicycles',
  body: {
    mappings: {
      properties: {
        cycle_type: {
          type: 'constant_keyword',
          value: 'bicycle'
        },
        name: {
          type: 'text'
        }
      }
    }
  }
)
puts response

response = client.indices.create(
  index: 'other_cycles',
  body: {
    mappings: {
      properties: {
        cycle_type: {
          type: 'keyword'
        },
        name: {
          type: 'text'
        }
      }
    }
  }
)
puts response
const response = await client.indices.create({
  index: "bicycles",
  mappings: {
    properties: {
      cycle_type: {
        type: "constant_keyword",
        value: "bicycle",
      },
      name: {
        type: "text",
      },
    },
  },
});
console.log(response);

const response1 = await client.indices.create({
  index: "other_cycles",
  mappings: {
    properties: {
      cycle_type: {
        type: "keyword",
      },
      name: {
        type: "text",
      },
    },
  },
});
console.log(response1);
PUT bicycles
{
  "mappings": {
    "properties": {
      "cycle_type": {
        "type": "constant_keyword",
        "value": "bicycle"
      },
      "name": {
        "type": "text"
      }
    }
  }
}

PUT other_cycles
{
  "mappings": {
    "properties": {
      "cycle_type": {
        "type": "keyword"
      },
      "name": {
        "type": "text"
      }
    }
  }
}

我们将索引分为两部分:一部分将仅包含自行车,另一部分将包含其他自行车:单轮车、三轮车等。然后在搜索时,我们需要搜索这两个索引,但我们不需要修改查询。

resp = client.search(
    index="bicycles,other_cycles",
    query={
        "bool": {
            "must": {
                "match": {
                    "description": "dutch"
                }
            },
            "filter": {
                "term": {
                    "cycle_type": "bicycle"
                }
            }
        }
    },
)
print(resp)
response = client.search(
  index: 'bicycles,other_cycles',
  body: {
    query: {
      bool: {
        must: {
          match: {
            description: 'dutch'
          }
        },
        filter: {
          term: {
            cycle_type: 'bicycle'
          }
        }
      }
    }
  }
)
puts response
const response = await client.search({
  index: "bicycles,other_cycles",
  query: {
    bool: {
      must: {
        match: {
          description: "dutch",
        },
      },
      filter: {
        term: {
          cycle_type: "bicycle",
        },
      },
    },
  },
});
console.log(response);
GET bicycles,other_cycles/_search
{
  "query": {
    "bool": {
      "must": {
        "match": {
          "description": "dutch"
        }
      },
      "filter": {
        "term": {
          "cycle_type": "bicycle"
        }
      }
    }
  }
}

bicycles 索引上,Elasticsearch 将简单地忽略 cycle_type 过滤器,并将搜索请求重写为下面的请求

resp = client.search(
    index="bicycles,other_cycles",
    query={
        "match": {
            "description": "dutch"
        }
    },
)
print(resp)
response = client.search(
  index: 'bicycles,other_cycles',
  body: {
    query: {
      match: {
        description: 'dutch'
      }
    }
  }
)
puts response
const response = await client.search({
  index: "bicycles,other_cycles",
  query: {
    match: {
      description: "dutch",
    },
  },
});
console.log(response);
GET bicycles,other_cycles/_search
{
  "query": {
    "match": {
      "description": "dutch"
    }
  }
}

other_cycles 索引上,Elasticsearch 将快速判断出 bicycle 不存在于 cycle_type 字段的词项字典中,并返回没有匹配项的搜索响应。

这是一种通过将常见值放在专用索引中来降低查询成本的强大方法。此想法还可以跨多个字段组合:例如,如果您跟踪每辆自行车的颜色,并且您的 bicycles 索引最终拥有大多数黑色自行车,则可以将其拆分为 bicycles-blackbicycles-other-colors 索引。

此优化并非严格要求使用 constant_keyword:也可以更新客户端逻辑,以便根据过滤器将查询路由到相关索引。但是,constant_keyword 使其透明地进行,并允许将搜索请求与索引拓扑结构解耦,而只需付出极少的开销。