Elasticsearch:从 Java High Level Rest Client 切换到新的 Java API Client

news2025/3/14 1:55:37

作者:David Pilato

我经常在讨论中看到与 Java API 客户端使用相关的问题。 为此,我在 2019 年启动了一个 GitHub 存储库,以提供一些实际有效的代码示例并回答社区提出的问题。

从那时起,高级 Rest 客户端 (High Level Rest Cliet - HLRC) 已被弃用,并且新的 Java API 客户端已发布。

为了继续回答问题,我最近需要将存储库升级到这个新客户端。 尽管它在幕后使用相同的低级 Rest 客户端,并且已经提供了升级文档,但升级它并不是一件小事。

我发现分享为此必须执行的所有步骤很有趣。 如果你 “只是寻找” 进行升级的拉取请求,请查看:

  • 切换到新的 Java API 客户端
  • 升级到 Elastic 8.7.1

这篇博文将详细介绍你在这些拉取请求中可以看到的一些主要步骤。

Java 高级 Rest 客户端 (High Level Rest Client)

我们从一个具有以下特征的项目开始:

  • Maven 项目(但这也可以应用于 Gradle 项目)
  • 使用以下任一方法运行 Elasticsearch 7.17.16:
  • Docker compose
  • 测试容器
  • 使用高级 Rest 客户端 7.17.16

注意:大家在这里对于 7.17.16 的选择可能有点糊涂。我们需要记住的一点是上一个大的版本的最后一个版本和下一个大的版本的第一个是兼容的。

客户端依赖项是:

<!-- Elasticsearch HLRC -->
<dependency>
  <groupId>org.elasticsearch.client</groupId>
  <artifactId>elasticsearch-rest-high-level-client</artifactId>
  <version>7.17.16</version>
</dependency>

<!-- Jackson for json serialization/deserialization -->
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-core</artifactId>
    <version>2.15.0</version>
</dependency>
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.15.0</version>
</dependency>

我们正在检查本地 Elasticsearch 实例是否正在 http://localhost:9200 上运行。 如果没有,我们将启动测试容器:

@BeforeAll
static void startOptionallyTestcontainers() {
  client = getClient("http://localhost:9200");
  if (client == null) {
    container = new ElasticsearchContainer(
        DockerImageName.parse("docker.elastic.co/elasticsearch/elasticsearch")
                .withTag("7.17.16"))
        .withPassword("changeme");
    container.start();
    client = getClient(container.getHttpHostAddress());
    assumeNotNull(client);
  }
}

为了构建客户端(RestHighLevelClient),我们使用:

static private RestHighLevelClient getClient(String elasticsearchServiceAddress) {
  try {
    final CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
    credentialsProvider.setCredentials(AuthScope.ANY,
        new UsernamePasswordCredentials("elastic", "changeme"));

    // Create the low-level client
    RestClientBuilder restClient = RestClient.builder(HttpHost.create(elasticsearchServiceAddress))
        .setHttpClientConfigCallback(hcb -> hcb.setDefaultCredentialsProvider(credentialsProvider));

    // Create the high-level client
    RestHighLevelClient client = new RestHighLevelClient(restClient);

    MainResponse info = client.info(RequestOptions.DEFAULT);
    logger.info("Connected to a cluster running version {} at {}.", info.getVersion().getNumber(), elasticsearchServiceAddress);
    return client;
  } catch (Exception e) {
    logger.info("No cluster is running yet at {}.", elasticsearchServiceAddress);
    return null;
  }
}

你可能已经注意到,我们正在尝试调用 GET / 端点来确保客户端在开始测试之前确实已连接:

MainResponse info = client.info(RequestOptions.DEFAULT);

然后,我们可以开始运行测试,如下例所示:

@Test
void searchData() throws IOException {
  try {
    // Delete the index if exist
    client.indices().delete(new DeleteIndexRequest("search-data"), RequestOptions.DEFAULT);
  } catch (ElasticsearchStatusException ignored) { }
  // Index a document
  client.index(new IndexRequest("search-data").id("1").source("{\"foo\":\"bar\"}", XContentType.JSON), RequestOptions.DEFAULT);
  // Refresh the index
  client.indices().refresh(new RefreshRequest("search-data"), RequestOptions.DEFAULT);
  // Search for documents
  SearchResponse response = client.search(new SearchRequest("search-data").source(
      new SearchSourceBuilder().query(
          QueryBuilders.matchQuery("foo", "bar")
      )
  ), RequestOptions.DEFAULT);
  logger.info("response.getHits().totalHits = {}", response.getHits().getTotalHits().value);
}

所以如果我们想将此代码升级到 8.11.3,我们需要:

  • 使用相同的 7.17.16 版本将代码升级到新的 Elasticsearch Java API Client
  • 将服务器和客户端都升级到 8.11.3

另一个非常好的策略是先升级服务器,然后升级客户端。 它要求你设置 HLRC 的兼容模式:

RestHighLevelClient esClient = new RestHighLevelClientBuilder(restClient)
    .setApiCompatibilityMode(true)
    .build()

我选择分两步进行,以便我们更好地控制升级过程并避免混合问题。 升级的第一步是最大的一步。 第二个要轻得多,主要区别在于 Elasticsearch 现在默认受到保护(密码和 SSL 自签名证书)。

在本文的其余部分中,我有时会将 “old” Java 代码作为注释,以便你可以轻松比较最重要部分的更改。

新的 Elasticsearch Java API 客户端

所以我们需要修改 pom.xml:

<!-- Elasticsearch HLRC -->
<dependency>
  <groupId>org.elasticsearch.client</groupId>
  <artifactId>elasticsearch-rest-high-level-client</artifactId>
  <version>7.17.16</version>
</dependency>

到:

<!-- Elasticsearch Java API Client -->
<dependency>
  <groupId>co.elastic.clients</groupId>
  <artifactId>elasticsearch-java</artifactId>
  <version>7.17.16</version>
</dependency>

容易,对吧?

嗯,没那么容易。 。 。 因为现在我们的项目不再编译了。 因此,让我们进行必要的调整。 首先,我们需要更改创建 ElasticsearchClient 而不是 RestHighLevelClient 的方式:

static private ElasticsearchClient getClient(String elasticsearchServiceAddress) {
  try {
    final CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
    credentialsProvider.setCredentials(AuthScope.ANY,
        new UsernamePasswordCredentials("elastic", "changeme"));

    // Create the low-level client
    // Before:
    // RestClientBuilder restClient = RestClient.builder(HttpHost.create(elasticsearchServiceAddress))
    //     .setHttpClientConfigCallback(hcb -> hcb.setDefaultCredentialsProvider(credentialsProvider));
    // After:
    RestClient restClient = RestClient.builder(HttpHost.create(elasticsearchServiceAddress))
        .setHttpClientConfigCallback(hcb -> hcb.setDefaultCredentialsProvider(credentialsProvider))
        .build();

    // Create the transport with a Jackson mapper
    ElasticsearchTransport transport = new RestClientTransport(restClient, new JacksonJsonpMapper());

    // And create the API client
    // Before:
    // RestHighLevelClient client = new RestHighLevelClient(restClient);
    // After:
    ElasticsearchClient client = new ElasticsearchClient(transport);

    // Before:
    // MainResponse info = client.info(RequestOptions.DEFAULT);
    // After:
    InfoResponse info = client.info();

    // Before:
    // logger.info("Connected to a cluster running version {} at {}.", info.getVersion().getNumber(), elasticsearchServiceAddress);
    // After:
    logger.info("Connected to a cluster running version {} at {}.", info.version().number(), elasticsearchServiceAddress);
    return client;
  } catch (Exception e) {
    logger.info("No cluster is running yet at {}.", elasticsearchServiceAddress);
    return null;
  }
}

主要的变化是我们现在在 RestClient(低级别)和 ElasticsearchClient 之间有一个 ElasticsearchTransport 类。 此类负责 JSON 编码和解码。 这包括应用程序类的序列化和反序列化,以前必须手动完成,现在由 JacksonJsonpMapper 处理。

另请注意,请求选项是在客户端上设置的。 我们不再需要通过任何 API 来传递 RequestOptions.DEFAULT,这里是 info API (GET /):

InfoResponse info = client.info();

“getters” 也被简化了很多。 因此,我们现在调用 info.version().number(),而不是调用 info.getVersion().getNumber()。 不再需要获取前缀!

使用新客户端

让我们将之前看到的 searchData() 方法切换到新客户端。 现在删除索引是:

try {
    // Before:
    // client.indices().delete(new DeleteIndexRequest("search-data"), RequestOptions.DEFAULT);
    // After:
    client.indices().delete(dir -> dir.index("search-data"));
} catch (/* ElasticsearchStatusException */ ElasticsearchException ignored) { }

在这段代码中我们可以看到什么?

  • 我们现在大量使用 lambda 表达式,它采用构建器对象作为参数。 它需要在思想上切换到这种新的设计模式。 但这实际上是超级智能的,就像你的 IDE 一样,你只需要自动完成即可准确查看选项,而无需导入任何类或只需提前知道类名称。 经过一些练习,它成为使用客户端的超级优雅的方式。 如果你更喜欢使用构建器对象,它们仍然可用,因为这是这些 lambda 表达式在幕后使用的内容。 但是,它使代码变得更加冗长,因此你确实应该尝试使用 lambda。
  • dir 在这里是删除索引请求构建器。 我们只需要使用 index(“search-data”) 定义我们想要删除的索引。
  • ElasticsearchStatusException 更改为 ElasticsearchException。

要索引单个 JSON 文档,我们现在执行以下操作:

// Before:
// client.index(new IndexRequest("search-data").id("1").source("{\"foo\":\"bar\"}", XContentType.JSON), RequestOptions.DEFAULT);
// After:
client.index(ir -> ir.index("search-data").id("1").withJson(new StringReader("{\"foo\":\"bar\"}")));

与我们之前看到的一样,lambdas (ir) 的使用正在帮助我们创建索引请求。 这里我们只需要定义索引名称 (index("search-data")) 和 id (id("1")) 并使用 withJson(new StringReader("{\"foo\":\ "bar\"}"))。

refresh API 调用现在非常简单:

// Before:
// client.indices().refresh(new RefreshRequest("search-data"), RequestOptions.DEFAULT);
// After:
client.indices().refresh(rr -> rr.index("search-data"));

搜索是另一场野兽。 一开始看起来很复杂,但通过一些实践你会发现生成代码是多么容易:

// Before:
// SearchResponse response = client.search(new SearchRequest("search-data").source(
//   new SearchSourceBuilder().query(
//     QueryBuilders.matchQuery("foo", "bar")
//   )
// ), RequestOptions.DEFAULT);
// After:
SearchResponse<Void> response = client.search(sr -> sr
    .index("search-data")
    .query(q -> q
      .match(mq -> mq
        .field("foo")
          .query("bar"))),
  Void.class);

lambda 表达式参数是构建器:

  • sr 是 SearchRequest 构建器。
  • q 是查询构建器。
  • mq 是 MatchQuery 构建器。

如果你仔细查看代码,你可能会发现它非常接近我们所知的 json 搜索请求:

{
  "query": {
    "match": {
      "foo": {
        "query": "bar"
      }
    }
  }
}

这实际上是我一步步编码的方式:

client.search(sr -> sr, Void.class);

我将 Void 定义为我想要返回的 bean,这意味着我不关心解码 _source JSON 字段,因为我只想访问响应对象。

然后我想定义要在其中搜索数据的索引:

client.search(sr -> sr.index("search-data"), Void.class);

因为我想提供一个查询,所以我基本上是这样写的:

client.search(sr -> sr
    .index("search-data")
    .query(q -> q),
  Void.class);

我的 IDE 现在可以帮助我找到我想要使用的查询:

我想使用匹配查询:

client.search(sr -> sr
    .index("search-data")
    .query(q -> q
      .match(mq -> mq)),
  Void.class);

我只需定义要搜索的 field (foo) 和要应用的 query (bar):

client.search(sr -> sr
    .index("search-data")
    .query(q -> q
      .match(mq -> mq
        .field("foo")
          .query("bar"))),
  Void.class);

我现在可以要求 IDE 从结果中生成一个字段,并且我可以进行完整的调用:

SearchResponse<Void> response = client.search(sr -> sr
    .index("search-data")
    .query(q -> q
      .match(mq -> mq
        .field("foo")
          .query("bar"))),
  Void.class);

我可以从响应对象中读取 hits 总数:

// Before:
// logger.info("response.getHits().totalHits = {}", response.getHits().getTotalHits().value);
// After:
logger.info("response.hits.total.value = {}", response.hits().total().value());

Exists API

让我们看看如何将调用切换到 exists API:

// Before:
boolean exists1 = client.exists(new GetRequest("test", "1"), RequestOptions.DEFAULT);
boolean exists2 = client.exists(new GetRequest("test", "2"), RequestOptions.DEFAULT);

现在可以变成:

// After:
boolean exists1 = client.exists(gr -> gr.index("exist").id("1")).value();
boolean exists2 = client.exists(gr -> gr.index("exist").id("2")).value();

Bulk API

要批量对 Elasticsearch 进行索引/删除/更新操作,你绝对应该使用 Bulk API:

BinaryData data = BinaryData.of("{\"foo\":\"bar\"}".getBytes(StandardCharsets.UTF_8), ContentType.APPLICATION_JSON);
BulkResponse response = client.bulk(br -> {
  br.index("bulk");
  for (int i = 0; i < 1000; i++) {
    br.operations(o -> o.index(ir -> ir.document(data)));
  }
  return br;
});
logger.info("bulk executed in {} ms {} errors", response.errors() ? "with" : "without", response.ingestTook());
if (response.errors()) {
  response.items().stream().filter(p -> p.error() != null)
      .forEach(item -> logger.error("Error {} for id {}", item.error().reason(), item.id()));
}

请注意,该操作也可以是 DeleteRequest:

br.operations(o -> o.delete(dr -> dr.id("1")));

BulkProcessor 到 BulkIngester 助手

我一直很喜欢的 HLRC 功能之一是 BulkProcessor。 它就像 “一劳永逸 ”的功能,当你想要使用批量(bulk API)发送大量文档时,该功能非常有用。

正如我们之前所看到的,我们需要手动等待数组被填充,然后准备批量请求。

有了BulkProcessor,事情就容易多了。 你只需添加索引/删除/更新操作,BulkProcessor 将在你达到阈值时自动创建批量请求:

  • 文件数量
  • 全局有效载荷的大小
  • 给定的时间范围
// Before:
BulkProcessor bulkProcessor = BulkProcessor.builder(
    (request, bulkListener) -> client.bulkAsync(request, RequestOptions.DEFAULT, bulkListener),
    new BulkProcessor.Listener() {
      @Override public void beforeBulk(long executionId, BulkRequest request) {
        logger.debug("going to execute bulk of {} requests", request.numberOfActions());
      }
      @Override public void afterBulk(long executionId, BulkRequest request, BulkResponse response) { 
        logger.debug("bulk executed {} failures", response.hasFailures() ? "with" : "without");
      }
      @Override public void afterBulk(long executionId, BulkRequest request, Throwable failure) { 
        logger.warn("error while executing bulk", failure);
      }
    })
    .setBulkActions(10)
    .setBulkSize(new ByteSizeValue(1L, ByteSizeUnit.MB))
    .setFlushInterval(TimeValue.timeValueSeconds(5L))
    .build();

让我们将该部分移至新的 BulkIngester 以注入我们的 Person 对象:

// After:
BulkIngester<Person> ingester = BulkIngester.of(b -> b
  .client(client)
  .maxOperations(10_000)
  .maxSize(1_000_000)
  .flushInterval(5, TimeUnit.SECONDS));

更具可读性,对吧? 这里的要点之一是你不再被迫提供 listener,尽管我相信这仍然是正确处理错误的良好实践。 如果你想提供一个 listener,只需执行以下操作:

// After:
BulkIngester<Person> ingester = BulkIngester.of(b -> b
  .client(client)
  .maxOperations(10_000)
  .maxSize(1_000_000)
  .flushInterval(5, TimeUnit.SECONDS))
  .listener(new BulkListener<Person>() {
      @Override public void beforeBulk(long executionId, BulkRequest request, List<Person> persons) {
          logger.debug("going to execute bulk of {} requests", request.operations().size());
      }
      @Override public void afterBulk(long executionId, BulkRequest request, List<Person> persons, BulkResponse response) {
          logger.debug("bulk executed {} errors", response.errors() ? "with" : "without");
      }
      @Override public void afterBulk(long executionId, BulkRequest request, List<Person> persons, Throwable failure) {
          logger.warn("error while executing bulk", failure);
      }
  });

每当你需要在代码中添加一些请求到 BulkProcessor 时:

// Before:
void index(Person person) {
  String json = mapper.writeValueAsString(person);
  bulkProcessor.add(new IndexRequest("bulk").source(json, XContentType.JSON));
}

现在变成了:

// After:
void index(Person person) {
  ingester.add(bo -> bo.index(io -> io
    .index("bulk")
    .document(person)));
}

如果你想发送原始 json 字符串,你应该使用 Void 类型这样做:

BulkIngester<Void> ingester = BulkIngester.of(b -> b.client(client).maxOperations(10));
void index(String json) {
  BinaryData data = BinaryData.of(json.getBytes(StandardCharsets.UTF_8), ContentType.APPLICATION_JSON);
  ingester.add(bo -> bo.index(io -> io.index("bulk").document(data)));
}

当你的应用程序退出时,你需要确保关闭 BulkProcessor,这将导致挂起的操作之前被刷新,这样你就不会丢失任何文档:

// Before:
bulkProcessor.close();

现在很容易转换成:

// After:
ingester.close();

当然,当使用 try-with-resources 模式时,你可以省略 close() 调用,因为 BulkIngester 是 AutoCloseable 的:

try (BulkIngester<Void> ingester = BulkIngester.of(b -> b
  .client(client)
  .maxOperations(10_000)
  .maxSize(1_000_000)
  .flushInterval(5, TimeUnit.SECONDS)
)) {
    BinaryData data = BinaryData.of("{\"foo\":\"bar\"}".getBytes(StandardCharsets.UTF_8), ContentType.APPLICATION_JSON);
    for (int i = 0; i < 1000; i++) {
        ingester.add(bo -> bo.index(io -> io.index("bulk").document(data)));
    }
}

好处

我们已经在 BulkIngester 部分中触及了这一点,但新 Java API 客户端添加的重要功能之一是你现在可以提供 Java Bean,而不是手动执行序列化/反序列化。 这在编码方面可以节省时间。

因此,要索引 Person 对象,我们可以这样做:

void index(Person person) {
  client.index(ir -> ir.index("person").id(person.getId()).document(person));
}

以我的愚见,力量来自于搜索。 我们现在可以直接读取我们的实体:

client.search(sr -> sr.index("search-data"), Person.class);
SearchResponse<Person> response = client.search(sr -> sr.index("search-data"), Person.class);
for (Hit<Person> hit : response.hits().hits()) {
  logger.info("Person _id = {}, id = {}, name = {}", 
    hit.id(),                 // Elasticsearch _id metadata
    hit.source().getId(),     // Person id
    hit.source().getName());  // Person name
}

这里的 source() 方法可以直接访问 Person 实例。 你不再需要自己反序列化 json _source 字段。

更多阅读:

  • Elasticsearch:使用 Low Level Java 客户端来创建连接 - Elastic Stack 8.x        

原文:Switching from the Java High Level Rest Client to the new Java API Client | Elastic Blog

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1520666.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

DP-不同的二叉搜索树

给你一个整数 n &#xff0c;求恰由 n 个节点组成且节点值从 1 到 n 互不相同的 二叉搜索树 有多少种&#xff1f;返回满足题意的二叉搜索树的种数。 示例 1&#xff1a; 输入&#xff1a;n 3 输出&#xff1a;5示例 2&#xff1a; 输入&#xff1a;n 1 输出&#xff1a;1提…

Unity URP 如何写基础的曲面细分着色器

左边是默认Cube在网格模式下经过曲面细分的结果&#xff0c;右边是原状态。 曲面细分着色器在顶点着色器、几何着色器之后&#xff0c;像素着色器之前。 它的作用时根据配置信息生成额外的顶点以切割原本的面片。 关于这部分有一个详细的英文教程&#xff0c;感兴趣可以看一…

3.15号arm

汇编语言 1. 汇编语言的组成 汇编文件中由伪操作、伪指令、汇编指令以及代码注释这几部分组成 伪操作&#xff1a; ARM的汇编中伪操作以.为前缀&#xff0c;所有的伪操作不占用内存空间&#xff0c;编译汇编时告诉编译器怎么编译当前文件&#xff0c;主要用来修改汇编内…

如何本地部署SeaFile文件共享服务并实现无公网IP访问内网本地文件

文章目录 1. 前言2. SeaFile云盘设置2.1 Owncould的安装环境设置2.2 SeaFile下载安装2.3 SeaFile的配置 3. cpolar内网穿透3.1 Cpolar下载安装3.2 Cpolar的注册3.3 Cpolar云端设置3.4 Cpolar本地设置 4.公网访问测试5.结语 1. 前言 现在我们身边的只能设备越来越多&#xff0c…

C++语法、Linux命令查询网站

文章目录 1.cplusplus2.cppreference3.Linux命令查询网站 1.cplusplus 网址&#xff1a;https://legacy.cplusplus.com/ 2.cppreference 1.cppreference中文网站&#xff1a;https://zh.cppreference.com/w/首页 2.cppreference英文原站&#xff1a;https://en.cppreference…

最新潮乎盲盒系统源码,附搭建教程

搭建方法 宝塔创建网站&#xff0c;上传后端程序到根目录&#xff0c;在.env修改数据库账号密码 上传数据库&#xff0c;伪静态thinkphp 运行目录public PHP扩展安装下面的 禁用函数先禁用下面那个&#xff0c;就可以了 前端是uniapp 后台admin 禁用函数putenv、 扩展fileinfo…

python 如何使用 NLPchina 开源sql插件,提供代码

分享一段使用python&#xff0c;通过使用发送post请求的方式&#xff0c;来从es集群中获取数据。不用使用 elasticsearh&#xff0c;仅需要导入request和json包即可。 开源sql插件官方 文档 GitHub - NLPchina/elasticsearch-sql: Use SQL to query Elasticsearch 示例代码 调…

【AI】如何创建自己的自定义ChatGPT

如何创建自己的自定义ChatGPT 目录 如何创建自己的自定义ChatGPT大型语言模型(LLM)GPT模型ChatGPTOpenAI APILlamaIndexLangChain参考推荐超级课程: Docker快速入门到精通Kubernetes入门到大师通关课本文将记录如何使用OpenAI GPT-3.5模型、LlamaIndex和LangChain创建自己的…

蓝桥杯单片机快速开发笔记——AT24C02 E2PROM

一、原理分析 此处考点分析&#xff1a;可能会在引用iic驱动文件时需要自己在头文件定义SCL/SDA sbit sda P2^1; sbit scl P2^0; 工作原理&#xff1a;24C02是一种电可擦除可编程只读存储器&#xff0c;通过I2C总线与微处理器或控制器通信。它可以通过电子方式对存储的数据进…

LeetCode Python - 55.跳跃游戏

目录 题目答案运行结果 题目 给你一个非负整数数组 nums &#xff0c;你最初位于数组的 第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度。 判断你是否能够到达最后一个下标&#xff0c;如果可以&#xff0c;返回 true &#xff1b;否则&#xff0c;返回 fal…

VUE 运行NPM 报错:npm ERR! code CERT_HAS_EXPIRED 解决方案

现象 由于各种原因需要调试一下VUE代码&#xff0c;用Git拉下来运行不了&#xff08;之前是可以正常运行的&#xff09;&#xff0c;报错为&#xff1a;npm ERR! code CERT_HAS_EXPIRED........... 原因 NPM 证书签名过期了 解决方法 第一步&#xff1a;CMD 命令 查看NPM代理源…

抖音获得抖音商品详情 API 返回值说明

抖音&#xff08;Douyin&#xff09;的商品详情API返回值通常会包含有关商品的详细信息。这些信息可能包括但不限于商品ID、商品名称、商品价格、商品图片、商品描述、商品销售属性等。以下是一个简化的抖音商品详情API返回值示例和说明&#xff1a; 调用链接获取详情 item_g…

【elasticsearch实战】从零开始设计全站搜索引擎

业务需求 最近需要一个全站搜索的功能&#xff0c;我们的站点的特点是数据多源&#xff0c;即有我们本地数据库&#xff0c;也包含了第三方数据源&#xff0c;我们的数据类型除了网页&#xff0c;还包括了各种类型的文档&#xff0c;例如&#xff1a;doc、pdf、excel、ppt等格…

Hive借助java反射解决User-agent编码乱码问题

一、需求背景 在截取到浏览器user-agent&#xff0c;并想保存入数据库中&#xff0c;经查询发现展示的为编码后的结果。 现需要经过url解码过程&#xff0c;将解码后的结果保存进数据库&#xff0c;那么有几种实现方式。 二、问题解决 1、百度&#xff1a;url在线解码工具 …

Mac上使用M1或M2芯片的设备安装Node.js时遇到一些问题,比如卡顿或性能问题

对于Mac上使用M1或M2芯片的设备可能会遇到在安装Node.js时遇到一些问题&#xff0c;比如卡顿或性能问题。这可能是因为某些软件包或工具在M1或M2芯片上的兼容性不佳。为了解决这个问题&#xff0c;您可以尝试以下方法&#xff1a; 1. 使用Rosetta模式 对于一些尚未适配M1或M2…

YOLOv9|加入2023Gold YOLO中的GD机制!遥遥领先!

专栏介绍&#xff1a;YOLOv9改进系列 | 包含深度学习最新创新&#xff0c;助力高效涨点&#xff01;&#xff01;&#xff01; 一、Gold YOLO摘要 在过去的几年里&#xff0c;YOLO系列模型已经成为实时目标检测领域的领先方法。许多研究通过修改体系结构、增加数据和设计新的损…

记录dockers中Ubuntu安装python3.11

参考&#xff1a; docker-ubuntu 安装python3.8,pip3_dockerfile ubuntu22 python3.8-CSDN博客

JavaScript中的事件模型(详细案例代码)

文章目录 一、事件与事件流二、事件模型原始事件模型特性 标准事件模型特性 IE事件模型 一、事件与事件流 javascript中的事件&#xff0c;可以理解就是在HTML文档或者浏览器中发生的一种交互操作&#xff0c;使得网页具备互动性&#xff0c; 常见的有加载事件、鼠标事件、自定…

FPGA静态时序分析与约束(四)、时序约束

系列文章目录 FPGA静态时序分析与约束&#xff08;一&#xff09;、理解亚稳态 FPGA静态时序分析与约束&#xff08;二&#xff09;、时序分析 FPGA静态时序分析与约束&#xff08;三&#xff09;、读懂vivado时序报告 文章目录 系列文章目录前言一、什么是时序约束&#xff1…

【CVE-2022-47549】OPTEE之使用故障注入攻击绕过Raspberry Pi3上的TA签名验证

目录 一、问题描述 二、严重性评估 三、缓解措施 四、Patches 五、解决方案 六、参考 七、OP-TEE ID 八、报告人/单位 九、更多信息 十、时间线 一、问题描述 SEAL研究人员和工程师成功地通过利用电磁故障注入的glitch攻击&#xff0c;在树莓派3设备上攻克了签名检查…