【翻译】Elasticsearch Java API Client 8.13.2 (第三章-API约定)

Java API 客户端使用非常一致的代码结构,使用现代代码模式,使复杂的请求更容易编写,复杂的响应更容易处理。以下部分详细解释了这些内容。

包结构和命名空间客户端

Elasticsearch API 很大,并且被组织成 功能组,如 Elasticsearch API 文档中所示。

Java API 客户端遵循以下结构:功能组称为"命名空间",每个命名空间位于 co.elastic.clients.elasticsearch 的子包中。

每个命名空间客户端都可以从顶级 Elasticsearch 客户端访问。唯一的例外是"搜索"和"文档"API,它们位于 core 子包中,可以在主 Elasticsearch 客户端对象上访问。

下面的代码片段展示了如何使用索引命名空间客户端创建索引(构建 API 对象中解释了 lambda 语法):

java 复制代码
// Create the "products" index
ElasticsearchClient client = ...
client.indices().create(c -> c.index("products"));

命名空间客户端是非常轻量级的对象,可以动态创建。

方法命名约定

Java API 客户端中的类包含两种方法和属性:

  • 属于 API 一 部分的方法和属性,例如 ElasticsearchClient.search()SearchResponse.maxScore() 。它们是使用标准 Java camelCaseNaming 约定从 Elasticsearch JSON API 中各自的名称派生出来的。
  • 属于构建 Java API 客户端的框架一部分的方法和属性,例如 Query._kind() 。这些方法和属性都带有下划线前缀,以避免与 API 名称发生任何命名冲突,并作为区分 API 和框架的简单方法。

阻塞和异步客户端

API 客户端有两种类型:阻塞式和异步式。异步客户端上的所有方法都会返回标准 CompletableFuture 。

根据您的需要,两种风格可以同时使用,共享相同的传输对象:

java 复制代码
ElasticsearchTransport transport = ...

// Synchronous blocking client
ElasticsearchClient client = new ElasticsearchClient(transport);

if (client.exists(b -> b.index("products").id("foo")).value()) {
    logger.info("product exists");
}

// Asynchronous non-blocking client
ElasticsearchAsyncClient asyncClient =
    new ElasticsearchAsyncClient(transport);

asyncClient
    .exists(b -> b.index("products").id("foo"))
    .whenComplete((response, exception) -> {
        if (exception != null) {
            logger.error("Failed to index", exception);
        } else {
            logger.info("Product exists");
        }
    });

尽管我们不会更深入地了解 Java 异步编程,但请记住处理异步任务的失败。人们很容易忽视它们并且错误被忽视。

构建API对象

生成器对象

Java API 客户端中的所有数据类型都是不可变的。对象的创建 使用 2008 年 Effect Java 中流行的构建器模式。

java 复制代码
ElasticsearchClient client = ...
CreateIndexResponse createResponse = client.indices().create(
    new CreateIndexRequest.Builder()
        .index("my-index")
        .aliases("foo",
            new Alias.Builder().isWriteIndex(true).build()
        )
        .build()
);

请注意,构建器在调用其 build() 方法后不应重复使用。

构建器 lambda 表达式

虽然这工作得很好,但必须实例化构建器类并调用 build() 方法有点冗长。因此,Java API 客户端中的每个属性设置器还接受 lambda 表达式,该表达式将新创建的构建器作为参数并返回填充的构建器。上面的代码片段也可以写成:

java 复制代码
ElasticsearchClient client = ...
CreateIndexResponse createResponse = client.indices()
    .create(createIndexBuilder -> createIndexBuilder
        .index("my-index")
        .aliases("foo", aliasBuilder -> aliasBuilder
            .isWriteIndex(true)
        )
    );

这种方法允许更简洁的代码,并且还避免导入类(甚至记住它们的名称),因为类型是从方法参数签名推断出来的。

请注意,在上面的示例中,构建器变量仅用于启动属性设置器链。因此,这些变量的名称并不重要,可以缩短以提高可读性:

java 复制代码
ElasticsearchClient client = ...
CreateIndexResponse createResponse = client.indices()
    .create(c -> c
        .index("my-index")
        .aliases("foo", a -> a
            .isWriteIndex(true)
        )
    );

构建器 lambda 对于复杂的嵌套查询特别有用,如下所示,取自间隔查询 API 文档。

此示例还强调了深度嵌套结构中构建器参数的有用命名约定。对于具有单个参数的 lambda 表达式,Kotlin 提供隐式 it 参数,Scala 允许使用 _ 。在 Java 中,这可以通过使用下划线或单个字母前缀后跟代表深度级别的数字来近似(即 _0_1b0b1 等等)。这不仅消除了创建一次性变量名称的需要,而且还提高了代码的可读性。正确的缩进还可以使查询的结构更加突出。

java 复制代码
ElasticsearchClient client = ...
SearchResponse<SomeApplicationData> results = client
    .search(b0 -> b0
        .query(b1 -> b1
            .intervals(b2 -> b2
                .field("my_text")
                .allOf(b3 -> b3
                    .ordered(true)
                    .intervals(b4 -> b4
                        .match(b5 -> b5
                            .query("my favorite food")
                            .maxGaps(0)
                            .ordered(true)
                        )
                    )
                    .intervals(b4 -> b4
                        .anyOf(b5 -> b5
                            .intervals(b6 -> b6
                                .match(b7 -> b7
                                    .query("hot water")
                                )
                            )
                            .intervals(b6 -> b6
                                .match(b7 -> b7
                                    .query("cold porridge")
                                )
                            )
                        )
                    )
                )
            )
        ),
    SomeApplicationData.class  //搜索结果将映射到 SomeApplicationData 实例,以便应用程序随时可用。
);

Lists and maps

Additive builder setters

ListMap 类型的属性由对象构建器公开为一组重载的仅附加方法,这些方法通过附加到列表并向映射添加新条目(或替换现有的)。

对象构建器创建不可变对象,这也适用于在对象构造时变得不可变的列表和映射属性。

java 复制代码
// Prepare a list of index names
List<String> names = Arrays.asList("idx-a", "idx-b", "idx-c");

// Prepare cardinality aggregations for fields "foo" and "bar"
Map<String, Aggregation> cardinalities = new HashMap<>();
cardinalities.put("foo-count", Aggregation.of(a -> a.cardinality(c -> c.field("foo"))));
cardinalities.put("bar-count", Aggregation.of(a -> a.cardinality(c -> c.field("bar"))));

// Prepare an aggregation that computes the average of the "size" field
final Aggregation avgSize = Aggregation.of(a -> a.avg(v -> v.field("size")));

SearchRequest search = SearchRequest.of(r -> r
    // Index list:
    // - add all elements of a list
    .index(names)
    // - add a single element
    .index("idx-d")
    // - add a vararg list of elements
    .index("idx-e", "idx-f", "idx-g")

    // Sort order list: add elements defined by builder lambdas
    .sort(s -> s.field(f -> f.field("foo").order(SortOrder.Asc)))
    .sort(s -> s.field(f -> f.field("bar").order(SortOrder.Desc)))

    // Aggregation map:
    // - add all entries of an existing map
    .aggregations(cardinalities)
    // - add a key/value entry
    .aggregations("avg-size", avgSize)
    // - add a key/value defined by a builder lambda
    .aggregations("price-histogram",
        a -> a.histogram(h -> h.field("price")))
);
List and map values are never null

Elasticsearch API 有很多可选属性。对于单值属性,Java API 客户端将缺少的可选值表示为 null 。因此,应用程序必须在使用可选值之前对它们进行空检查。

然而,对于list和maps,应用程序通常只关心它们是否为空,甚至只是迭代它们的内容。使用 null 值很麻烦。为了避免这种情况,Java API 客户端集合属性永远不会 null ,并且缺少的可选集合将作为空集合返回。

如果您需要区分缺失(未定义)的可选集合和 Elasticsearch 返回的有效空集合, ApiTypeHelper 类提供了一个实用方法来区分它们:

java 复制代码
NodeStatistics stats = NodeStatistics.of(b -> b
    .total(1)
    .failed(0)
    .successful(1)
);

// The `failures` list was not provided.
// - it's not null
assertNotNull(stats.failures());
// - it's empty
assertEquals(0, stats.failures().size());
// - and if needed we can know it was actually not defined
assertFalse(ApiTypeHelper.isDefined(stats.failures()));

变体类型

Elasticsearch API 有很多变体类型:查询、聚合、字段映射、分析器等等。在如此大的集合中找到正确的类名可能具有挑战性。

Java API 客户端构建器使这一切变得简单:变体类型(例如 Query )的构建器具有适用于每个可用实现的方法。我们已经在上面的 intervals (一种查询)和 allOfmatchanyOf (各种查询)中看到了这一点。间隔)。

这是因为 Java API 客户端中的变体对象是"标记联合"的实现:它们包含它们所持有的变体的标识符(或标签)以及该变体的值。例如, Query 对象可以包含带有标签 intervalsIntervalsQuery 、带有标签 termTermQuery ,等等。这种方法允许编写流畅的代码,您可以让 IDE 补全功能指导您构建和导航复杂的嵌套结构:

变体构建器为每个可用的实现都提供了 setter 方法。它们使用与常规属性相同的约定,并接受构建器 lambda 表达式和变体实际类型的现成对象。这是构建术语查询的示例:

java 复制代码
Query query = new Query.Builder()
    .term(t -> t        //选择 term 变体来构建术语查询。                  
        .field("name")   //使用构建器 lambda 表达式构建术语查询。                 
        .value(v -> v.stringValue("foo"))
    )
    .build();           //构建现在包含 term 类型的 TermQuery 对象的 Query 。                  

变体对象对于每个可用的实现都有 getter 方法。这些方法检查对象是否确实拥有该类型的变体,并返回向下转换为正确类型的值。否则他们会抛出 IllegalStateException 。这种方法允许编写流畅的代码来遍历变体。

java 复制代码
assertEquals("foo", query.term().value().stringValue());

变体对象还提供有关它们当前持有的变体类型的信息:

  • 每种变体类型都有 is 方法: isTerm()isIntervals()isFuzzy() 等。
  • 使用定义所有变体类型的嵌套 Kind 枚举。

在检查其实际类型后,可以使用此信息向下导航到特定变体:

java 复制代码
if (query.isTerm()) { //测试变体是否属于特定类型。
    doSomething(query.term());
}

switch(query._kind()) { //测试更大的一组变体类型。
    case Term:
        doSomething(query.term());
        break;
    case Intervals:
        doSomething(query.intervals());
        break;
    default:
        doSomething(query._kind(), query._get()); //获取变体对象所持有的种类和值。
}
插件提供的自定义扩展

Elasticsearch 接受可以扩展多种类型的可用变体的插件。这包括查询、聚合、文本分析器和分词器、摄取处理器等。

除了内置类型之外,这些类型的 Java API 客户端类还接受 _custom 变体。这允许您通过在请求中提供任意 JSON 来使用这些插件定义的扩展,并且还可以在响应中接收插件生成的任意 JSON。

在下面的示例中,我们使用一个假设的插件,该插件添加了一个 sphere-distance 聚合,该聚合根据包含 3D 坐标的文档到参考位置的距离对其进行分组。

要创建自定义聚合,请使用 _custom() 聚合类型并提供由插件定义的标识符和参数。参数可以是任何可以序列化为 JSON 的对象或值。在下面的示例中,我们使用一个简单的maps:

java 复制代码
Map<String, Object> params = new HashMap<>(); //自定义聚合的参数。
params.put("interval", 10);
params.put("scale", "log");
params.put("origin", new Double[]{145.0, 12.5, 1649.0});

SearchRequest request = SearchRequest.of(r -> r
    .index("stars")
    .aggregations("neighbors", agg -> agg
        ._custom("sphere-distance", params) //创建一个名为 neighbors 的自定义聚合,类型为 sphere-distance 及其参数。
    )
);

自定义变体的结果以 JsonData 对象表示的原始 JSON 形式返回。然后您可以遍历 JSON 树来获取数据。由于这并不总是很方便,您还可以定义表示 JSON 数据的类并从原始 JSON 反序列化它们。

遍历 JSON 树:

java 复制代码
SearchResponse<Void> response = esClient.search(request, Void.class); //如果您只对聚合结果感兴趣,而不是搜索命中,请使用 Void (另请参阅聚合)。

JsonData neighbors = response
    .aggregations().get("neighbors")
    ._custom(); //获取 neighbors 聚合结果作为自定义 JSON 结果。

JsonArray buckets = neighbors.toJson() //遍历 JSON 树以提取结果数据。
    .asJsonObject()
    .getJsonArray("buckets");

for (JsonValue item : buckets) {
    JsonObject bucket = item.asJsonObject();
    double key = bucket.getJsonNumber("key").doubleValue();
    double docCount = bucket.getJsonNumber("doc_count").longValue();
    doSomething(key, docCount);
}

使用表示自定义聚合结果的类:

java 复制代码
SearchResponse<Void> response = esClient.search(request, Void.class);

SphereDistanceAggregate neighbors = response
    .aggregations().get("neighbors")
    ._custom()
    .to(SphereDistanceAggregate.class); //将自定义 JSON 反序列化为专用的 SphereDistanceAggregate 类。

for (Bucket bucket : neighbors.buckets()) {
    doSomething(bucket.key(), bucket.docCount());
}

其中 SphereDistanceAggregate 可以定义如下:

java 复制代码
public static class SphereDistanceAggregate {
    private final List<Bucket> buckets;
    @JsonCreator
    public SphereDistanceAggregate(
        @JsonProperty("buckets") List<Bucket> buckets
    ) {
        this.buckets = buckets;
    }
    public List<Bucket> buckets() {
        return buckets;
    };
}

public static class Bucket {
    private final double key;
    private final double docCount;
    @JsonCreator
    public Bucket(
        @JsonProperty("key") double key,
        @JsonProperty("doc_count") double docCount) {
        this.key = key;
        this.docCount = docCount;
    }
    public double key() {
        return key;
    }
    public double docCount() {
        return docCount;
    }
}

对象生命周期和线程安全

Java API Client中有五种不同生命周期的对象:
Object mapper 对象映射器
无状态且线程安全,但创建成本可能很高。它通常是在应用程序启动时创建的单例并用于创建传输。

Transport 运输
线程安全,通过底层HTTP客户端持有网络资源。传输对象与 Elasticsearch 集群关联,必须显式关闭以释放底层资源(例如网络连接)。

Clients 客户
不可变、无状态和线程安全。这些是非常轻量级的对象,仅包装传输并提供 API 端点作为方法。

Builders 建设者
可变的,非线程安全的。构建器是瞬态对象,在调用 build() 后不应重用。

Requests & other API objects
请求和其他 API 对象

不可变、线程安全。如果您的应用程序反复使用相同的请求或请求的相同部分,则可以提前准备这些对象,并在具有不同传输的多个客户端的多个调用中重复使用。

从 JSON 数据创建 API 对象

使用 Elasticsearch 开发应用程序期间的常见工作流程是使用 Kibana 开发者控制台以交互方式准备和测试查询、聚合、索引映射和其他复杂的 API 调用。这会产生您可能想要在应用程序中使用的有效 JSON 片段。

由于将这些 JSON 片段转换为 Java 代码可能非常耗时且容易出错,因此 Java API 客户端中的大多数数据类都可以从 JSON 文本加载:对象构建器具有填充构建器的 withJson() 方法来自原始 JSON。这还允许您将动态加载的 JSON 与对象的编程构造结合起来。

在底层, withJson() 方法调用对象的反序列化器。因此,JSON 文本的结构和值类型对于目标数据结构必须正确。使用 withJson() 可以保持 Java API 客户端的强类型保证。

例子

从资源文件加载索引定义

考虑一个包含索引定义的资源文件 some-index.json

java 复制代码
{
  "mappings": {
    "properties": {
      "field1": { "type": "text" }
    }
  }
}

您可以根据该定义创建索引,如下所示:

java 复制代码
InputStream input = this.getClass()
    .getResourceAsStream("some-index.json"); //打开 JSON 资源文件的输入流。

CreateIndexRequest req = CreateIndexRequest.of(b -> b
    .index("some-index")
    .withJson(input) //使用资源文件内容填充索引创建请求。
);

boolean created = client.indices().create(req).acknowledged();
从 JSON 文件中提取文档

同样,您可以从数据文件中读取要存储在Elasticsearch中的文档:

java 复制代码
FileReader file = new FileReader(new File(dataDir, "document-1.json"));

IndexRequest<JsonData> req; //当对具有泛型类型参数的数据结构调用 withJson() 时,这些泛型类型将被视为 JsonData 。

req = IndexRequest.of(b -> b
    .index("some-index")
    .withJson(file)
);

client.index(req);
结合 JSON 和编程构造创建搜索请求

您可以将 withJson() 与对 setter 方法的常规调用结合起来。下面的示例从 String 加载搜索请求的查询部分,并以编程方式添加聚合。

java 复制代码
Reader queryJson = new StringReader(
    "{" +
    "  \"query\": {" +
    "    \"range\": {" +
    "      \"@timestamp\": {" +
    "        \"gt\": \"now-1w\"" +
    "      }" +
    "    }" +
    "  }" +
    "}");

SearchRequest aggRequest = SearchRequest.of(b -> b
    .withJson(queryJson) //从 JSON 字符串加载查询。
    .aggregations("max-cpu", a1 -> a1   //添加聚合。
        .dateHistogram(h -> h
            .field("@timestamp")
            .calendarInterval(CalendarInterval.Hour)
        )
        .aggregations("max", a2 -> a2
            .max(m -> m.field("host.cpu.usage"))
        )
    )
    .size(0)
);

Map<String, Aggregate> aggs = client
    .search(aggRequest, Void.class) //由于这是一个聚合,我们不关心结果文档并将其目标类设置为 Void ,这意味着它们将被忽略。请注意,将 size 设置为零实际上会阻止返回任何文档。
    .aggregations();
从多个 JSON 片段创建搜索请求

withJson() 方法是部分反序列化器:从 JSON 加载的属性将设置属性值或替换以前的属性值,但不会重置 JSON 输入中未找到的其他属性。您可以使用它来组合多个 JSON 片段来构建复杂的搜索请求。在下面的示例中,我们结合了选择一些文档的查询的单独定义和对此查询结果运行的聚合。

java 复制代码
Reader queryJson = new StringReader(
    "{" +
    "  \"query\": {" +
    "    \"range\": {" +
    "      \"@timestamp\": {" +
    "        \"gt\": \"now-1w\"" +
    "      }" +
    "    }" +
    "  }," +
    "  \"size\": 100" +  //将查询返回文档的最大数量设置为 100。
    "}");

Reader aggregationJson = new StringReader(
    "{" +
    "  \"size\": 0, " +  //我们不希望聚合中有任何匹配的文档。
    "  \"aggregations\": {" +
    "    \"hours\": {" +
    "      \"date_histogram\": {" +
    "        \"field\": \"@timestamp\"," +
    "        \"interval\": \"hour\"" +
    "      }," +
    "      \"aggregations\": {" +
    "        \"max-cpu\": {" +
    "          \"max\": {" +
    "            \"field\": \"host.cpu.usage\"" +
    "          }" +
    "        }" +
    "      }" +
    "    }" +
    "  }" +
    "}");

SearchRequest aggRequest = SearchRequest.of(b -> b
    .withJson(queryJson) //加载请求的查询部分。
    .withJson(aggregationJson) //加载请求的聚合部分(覆盖查询中的 size )。
    .ignoreUnavailable(true)  //以编程方式设置的附加请求属性。
);

Map<String, Aggregate> aggs = client
    .search(aggRequest, Void.class)
    .aggregations();

请注意,当 JSON 片段具有一些通用属性时,顺序很重要:就像以编程方式设置属性值一样,为属性设置的最后一个值会覆盖前一个值。

异常

客户端方法可以抛出两种异常:

  • Elasticsearch 服务器收到但被拒绝的请求(验证错误、超出服务器内部超时等)将生成 ElasticsearchException 。此异常包含由 Elasticsearch 提供的有关错误的详细信息。
  • 未能到达服务器的请求(网络错误、服务器不可用等)将产生 TransportException 。该异常的原因是较低级别的实现抛出的异常。对于 RestClientTransport 来说,它将是包含低级 HTTP 响应的 ResponseException
相关推荐
芒果披萨13 分钟前
El表达式和JSTL
java·el
q5673152314 分钟前
在 Bash 中获取 Python 模块变量列
开发语言·python·bash
许野平39 分钟前
Rust: 利用 chrono 库实现日期和字符串互相转换
开发语言·后端·rust·字符串·转换·日期·chrono
也无晴也无风雨42 分钟前
在JS中, 0 == [0] 吗
开发语言·javascript
狂奔solar1 小时前
yelp数据集上识别潜在的热门商家
开发语言·python
duration~1 小时前
Maven随笔
java·maven
zmgst1 小时前
canal1.1.7使用canal-adapter进行mysql同步数据
java·数据库·mysql
跃ZHD1 小时前
前后端分离,Jackson,Long精度丢失
java
blammmp1 小时前
Java:数据结构-枚举
java·开发语言·数据结构
研究是为了理解1 小时前
Git Bash 常用命令
git·elasticsearch·bash