OpenSearch 入门

OpenSearch 简介

在开源技术领域,OpenSearch 是一个不可忽视的强大工具。它不仅是一个分布式的搜索和分析引擎,还基于广泛使用的 Apache Lucene 搜索库构建,为开发人员提供了高度的灵活性和可扩展性。无论你是在寻找一种解决方案,用于实现应用程序中的全文搜索,还是希望在系统中添加实时监控和日志分析功能,OpenSearch 都能够出色地完成这些任务。

与传统的关系型数据库相比,OpenSearch 在全文搜索能力上有着明显的优势。它专为那些需要强大搜索功能的应用程序设计,优化了快速且灵活的全文搜索,使其在处理复杂数据集时依然能够保持高效的性能。同时,OpenSearch 支持分布式架构,可以在多个节点和集群上无缝运行,这使得它非常适合处理大规模数据和高并发的应用场景。

OpenSearch 与 Elasticsearch 的对比

开发人员通常重视开源软件所带来的自由和灵活性,而当 Elastic 公司决定将 Elasticsearch 和 Kibana 转换为 Elastic 许可证时,这一自由受到了限制。为了响应这一变化,OpenSearch 项目诞生了。它是从 Elasticsearch 的最后一个开源版本(7.10.2)分支出来的,并且自 2021 年 7 月起在 Apache 许可证 2.0 版 (ALv2) 下发布,继续保留了开源软件的自由特性。

OpenSearch 的开发和维护由一个多元化的社区进行,而 Elasticsearch 主要由 Elastic 公司负责,尽管也有社区的贡献。OpenSearch 的目标是保持与 Elasticsearch 的兼容性,这意味着为 Elasticsearch 开发的应用程序可以在 OpenSearch 上以最少的修改运行。两者在功能上非常相似,都支持分布式搜索、分析以及大规模数据的处理。然而,OpenSearch 的开源性质使其在社区支持和扩展性方面更具优势。

OpenSearch 核心概念:必须了解的基础

为了能够高效地使用 OpenSearch,你需要熟悉它的一些核心术语和概念。这些术语不仅定义了 OpenSearch 的操作方式,还能帮助你更好地理解其强大的功能。

  • 索引 (Index):索引在 OpenSearch 中类似于关系数据库中的表。它是具有相似结构的文档的集合,所有相关的文档都存储在同一个索引中。
  • 文档 (Document):文档是 OpenSearch 中的基本数据单元,通常以 JSON 格式表示。每个文档代表索引中的一条数据记录。
  • 映射 (Mapping):映射定义了文档中每个字段的数据类型,例如字符串、数字、日期等。这有助于 OpenSearch 正确地理解和处理数据,以便更高效地进行索引和搜索。
  • 节点 (Node):节点是 OpenSearch 在物理或虚拟机上运行的单个实例。一个集群通常由多个节点组成,这些节点共同协作来存储数据并处理搜索请求。
  • 分片 (Shard):为了提高系统的性能和可扩展性,OpenSearch 将每个索引分成多个分片。每个分片都是一个独立的索引,可以托管在不同的节点上,这样就能实现数据的水平扩展和负载均衡。
  • 副本 (Replica):副本是分片的副本,用于提供数据的高可用性和容错能力。通过将数据复制到多个节点,系统能够在某个节点故障时继续运行。
  • 集群 (Cluster):集群是多个节点的集合,这些节点共同存储整个数据集,并在所有节点上提供联合索引和搜索功能。
  • 查询 (Query):查询是从索引中请求信息的操作。在 OpenSearch 中,查询可以是简单的关键词搜索,也可以是复杂的聚合查询,涉及各种参数和脚本。

OpenSearch 数据架构:深入理解倒排索引

在 OpenSearch 中,数据是通过倒排索引进行组织的。这种索引结构对于实现高效的全文搜索至关重要。倒排索引将术语(例如单词或短语)映射到它们在文档中的位置。每当文档中包含某个术语时,倒排索引就会记录下该术语的所有出现位置。

为了更直观地理解这一点,以下是两个示例文档及其倒排索引的创建过程:

  1. "OpenSearch is a powerful search engine. OpenSearch is used for data indexing."
  2. "OpenSearch is an open-source search engine. It's widely used for various purposes."

根据这些文档,生成的倒排索引可能如下所示:

术语 文档 ID (位置)
opensearch 1 (1, 7), 2 (1)
is 1 (2, 8), 2 (2)
a 1 (3)
powerful 1 (4)
search 1 (5)
engine 1 (6), 2 (4)
used 1 (9), 2 (6)
for 1 (10), 2 (5)
data 1 (11)
indexing 1 (12)
open-source 2 (3)
It's 2 (7)
widely 2 (8)
various 2 (9)
purposes 2 (10)

当你在特定字段上执行搜索查询时,OpenSearch 只会搜索与该字段对应的倒排索引,从而实现更快、更精确的搜索结果。这种设计特别适合处理大规模数据和需要高效全文搜索的应用场景。

数据处理与转换:如何优化 OpenSearch 的使用

在 OpenSearch 中,文档索引和查询时,默认使用的是标准分析器(Standard Analyzer)。这个分析器会在文本中找到单词的边界,并将文本分割成独立的术语。然后,它会执行以下转换步骤:

  • 将文本标记为独立的术语:例如,将句子拆分为单词或短语。
  • 将术语转换为小写:确保搜索不区分大小写。
  • 删除标点符号:移除所有不影响搜索的符号,以提高搜索效率。
  • 删除停用词:移除常见但意义不大的词语,如“the”、“is”等。

此外,开发人员还可以使用其他类型的分析器,如简单分析器、空白分析器、模式分析器等,这些工具能够在将数据保存到倒排索引之前进行不同的预处理操作,以支持更复杂的搜索需求。

在 OpenSearch 中,字符串数据可以被映射为关键字(Keyword)或文本(Text)类型。关键字类型的字段不会被分析或标记化,而是将整个值作为一个术语进行存储,非常适合用于处理 ID、类别等字段。而文本类型的字段会被分析和标记化,非常适合全文搜索的需求。

实例演示

我们来看以下两个示例文档:

  1. "University of Texas at Austin"
  2. "Texas city"

当字符串数据映射为文本数据类型时,它们会被标记化,每个标记都会单独存储在倒排索引中。例如:

文档 ID 文档值
1 at
1 austin
2 city
1 of
1,2 texas
1 university

然而,当字符串数据映射为关键字数据类型时,标记化不会发生,数据将以原样存储:

文档 ID 文档值
1 University of Texas at Austin
2 Texas city

当你希望基于完整的关键词或类别对数据进行聚合时,选择合适的数据类型映射至关重要。如果使用文本类型,术语“Texas city”会被拆分为“Texas”和“city”两个独立的标记,这可能会导致在查询时失去它们之间的关系。而使用关键字类型可以保证查询的准确性。

高性能系统设计:使用 OpenSearch 的策略和建议

当设计基于 OpenSearch 的高性能系统时,你需要权衡多个因素,包括性能、吞吐量、可扩展性、代码的复杂性和可维护性。以下是一些有助于你设计出平衡系统的高级策略,这些策略能够帮助你构建一个既高效又易于维护的系统。

1. 获取准确的规模估算

设计的第一步是估算系统的规模、数据的类型和数量,以及这些数据的存储方式。准确的估算可以帮助你选择合适的实例类型,配置适当的存储和 CPU 资源,避免资源的过度配置或不足配置。

2. 计算存储需求

为了准确计算存储需求,你需要估算文档的大小和索引中将创建的文档数量。此外,数据的保留期也是一个重要因素。例如,如果你的数据保留期为一年,那么你需要估算一年内创建的文档数量和总存储需求。如果典型文档大小为 1 KB,并且预计一年内会创建 5000 万个文档,那么数据总量为 50 GB。

不过,仅仅计算数据存储需求是不够的。你还需要考虑 OpenSearch 的索引开销和副本数量。每个副本都是完整的索引副本,因此需要相同数量的磁盘空间。默认情况下,每个 OpenSearch 索引有一个副本。

可以使用以下公式来计算存储需求:

最低存储需求 = 原始数据大小 * (1 + 副本数量) * 1.45

3. 选择分片数量

OpenSearch 中,每个索引默认分为五个主分片和一个副本。当索引创建后,你可以增加副本分片,但不能增加主分片数量,因为更改主分片数量需要重新分配已有数据,这个过程复杂且资源密集。

选择适当的分片大小非常重要。如果分片太大,OpenSearch 在从故障中恢复时会变得困难。通常情况下,读密集型工作负载的分片大小建议在 10-30 GiB 之间,写密集型工作负载的分片大小建议在 30-50 GiB 之间。因此,你需要根据数据量、写入负载、硬件资源和可扩展性需求等因素谨慎规划分片数量。

估算主分片数量的公式如下:

主分片数量 ≈ (源数据 + 预留空间) * (1 + 索引开销) / 期望分片大小

4. 实现批量写入

如果你的系统需要高写入吞吐量,考虑实施批量插入或更新操作。每个请求都会产生一定的网络延迟和处理开销,将多个请求批量处理可以有效降低这些成本。

OpenSearch 可以在单个批处理中处理多个文档,这样更好地利用 CPU 和内存资源,减少网络往返,从而提高系统的整体吞吐量。每个批处理的请求数量取决于 OpenSearch 集群的硬件配置。通常,可以从 1MB 的批量请求开始,逐步增加到每批 3-5MB,同时监控系统的 CPU 和内存利用率,以找到最佳配置。

5. 定义索引映射

为文档字段定义合适的数据类型可以提高索引和搜索性能。如果未定义映射,OpenSearch 会自动推断数据类型。例如,OpenSearch 会将字符串同时索引为 Keyword 和 Text 类型,这被称为动态映射。这种方式虽然灵活,但在处理大量数据时可能会引入性能开销。

通常情况下,全文搜索是寻找最相关文档的最佳选择,而关键字搜索则适用于查找确切的单词或短语。关键字字段未经过分析,整个值作为单一术语处理,适合用于 ID、类别等精确匹配的场景。文本字段则经过分析和标记化,适合用于复杂的全文搜索需求。

6. 使用动态模板

如果在系统设计阶段无法提前确定所有字段,可以使用动态模板在创建索引时帮助定义映射。例如,考虑一个用于存储各种产品信息的索引“PRODUCTS”。虽然产品名称和品牌等标准字段可以提前映射,但某些产品特性仅对特定产品有意义。

比如“storage_capacity”对智能手机有意义,但对智能灯泡就不适用。当这些特性出现时,可以根据其性质动态映射为整数或关键字。这种动态模板可以基于字段名称的正则表达式或字段路径来定义映射,确保系统的灵活性和扩展性。

7. 插入与更新策略

在 OpenSearch 中,更新操作通常比插入操作更为昂贵。当你更新文档时,系统会将旧版本标记为已删除,并创建新版本的文档,而不是直接修改现有文档。这涉及到多次 I/O 操作,因此比单次插入操作更加耗费资源。

在设计项目时,选择插入或更新策略对系统性能影响很大。例如,假设你需要维护订单从下单到交付的状态变化,可能的状态包括 ORDER_PLACED、ORDER_PACKED、OUT_FOR_DELIVERY 和 ORDER_DELIVERED。你可以选择在每次状态变化时插入一个新文档,或者更新现有文档。

每种策略都有其优缺点:

  • 仅插入策略:每次状态变化都插入一个新文档,这会导致查询变得复杂,但保留了所有历史记录,适合需要跟踪事件时间线的场景。
  • 插入与更新结合策略:在每次状态变化时更新同一个文档,简化了查询,但无法保留历史记录,适合只关心最新状态的场景。

8. 并发和一致性处理

在 OpenSearch 中,更新文档涉及读取现有文档并进行修改。这种操作在并发场景中容易导致冲突。一般来说,数据库提供乐观锁定和悲观锁定策略来解决这个问题。

  • 悲观锁定策略:在更新文档前必须先获取锁定,防止其他线程同时更新相同文档。这种策略可能导致系统并发性下降,甚至发生死锁。
  • 乐观锁定策略:不需要锁定文档,而是通过版本号来控制更新冲突。如果文档在你读取后被其他线程修改,则更新操作会失败。你可以选择放弃更新或重试。

OpenSearch 默认使用乐观锁定策略,这种方式更具扩展性,适合处理高并发场景。

9. 一致写入与读取

OpenSearch 是一个最终一致性的系统,这意味着在某些情况下,数据的最新状态可能不会立即在所有节点上可用。当你执行写入操作时,OpenSearch 会将更改记录到事务日志,并将其写入内存缓冲区。虽然数据很快就会被刷新并可用于搜索,但在分布式系统中,数据的传播需要时间。

在事件驱动系统中,例如 Kafka,你可能需要确保一致性读取。例如,一个上游应用程序向 OpenSearch 写入数据并向下游消费者发送事件,而下游消费者需要读取最新的数据来处理事件。在这种情况下,定期刷新索引可以解决这个问题。

为了确保数据的一致性,生产者可以在插入或更新文档时缝合版本信息。消费者在处理事件时可以基于版本信息验证文档的最新性,从而决定是否执行刷新操作或等待一段时间后再重试。

总结

本文深入探讨了 OpenSearch 的基础知识、架构概念以及如何设计高性能系统。通过合理的策略和最佳实践,你可以充分利用 OpenSearch 的能力,构建出高效、可靠、可扩展的系统。