1. 概述

本文,我们学习如何使用全文搜索引擎 - Elasticsearch。我们重点关注如何在Java中集成Elasticsearch Client,学习如何使用主要功能,如索引、删除、查询和搜索,不会详细介绍如何配置Elasticsearch,以及其底层工作原理。

2. 搭建Elasticsearch

简单起见,我们使用Docker搭建一个无需认证的Elasticsearch服务器,监听9200端口:

docker run -d --name elastic-test -p 9200:9200 -e "discovery.type=single-node" -e "xpack.security.enabled=false" docker.elastic.co/elasticsearch/elasticsearch:8.8.2

Elasticsearch 默认监听9200端口的HTTP请求。我们可以通过在浏览器中打开http://localhost:9200/来验证它是否成功启动:

{
  "name" : "739190191b07",
  "cluster_name" : "docker-cluster",
  "cluster_uuid" : "_tUFwsigQW2FKhm_9yLiFQ",
  "version" : {
    "number" : "8.7.2",
    "build_flavor" : "default",
    "build_type" : "docker",
    "build_hash" : "f229ed3f893a515d590d0f39b05f68913e2d9b53",
    "build_date" : "2023-04-27T04:33:42.127815583Z",
    "build_snapshot" : false,
    "lucene_version" : "9.6.0",
    "minimum_wire_compatibility_version" : "7.17.0",
    "minimum_index_compatibility_version" : "7.0.0"
  },
  "tagline" : "You Know, for Search"
}

3. Maven配置

Elasticsearch 已经运行起来了。下面让我们直接转向Java Client。

我们需要在 pom.xml文件中添加ElasticsearchJackson 的依赖:

<dependency>
    <groupId>org.elasticsearch</groupId>
    <artifactId>elasticsearch</artifactId>
    <version>8.9.0</version>
</dependency>
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.16.0</version>
</dependency>

最新版本请查询Maven仓库。

4. Elasticsearch Java客户端

初始化Elasticsearch Java 客户端:

RestClient restClient = RestClient
  .builder(HttpHost.create("http://localhost:9200"))
  .build();
ElasticsearchTransport transport = new RestClientTransport(restClient, new JacksonJsonpMapper());
ElasticsearchClient client = new ElasticsearchClient(transport);

现在,我们已经准备好与Elasticsearch交互了。接下来,我们将查看如何执行最常见的操作,如将文档索引到索引中、从索引中删除文档以及在索引中搜索文档。

4.1. 创建索引

开始搜索前,我们需要插入数据到ES中,使用ElasticsearchClient.index()方法:

Person person = new Person(20, "Mark Doe", new Date(1471466076564L));
IndexResponse response = client.index(i -> i
  .index("person")
  .id(person.getFullName())
  .document(person));

在这里,我们实例化了一个简单的Java对象,然后将其保存在名为person的索引中。我们不需要将Java对象转换为JSON,因为客户端会使用JacksonJsonpMapper为我们完成这个过程。

我们可以进一步检查返回的IndexResponse,确认数据已被正确地导入到ES中:

log.info("Indexed with version: {}", response.version());
assertEquals(Result.Created, response.result());
assertEquals("person", response.index());
assertEquals("Mark Doe", response.id());

Elasticsearch中的所有数据都有一个版本。如果我们更新一个对象,它的版本将会不同。

同样,我们也可以直接将JSON字符串发送到ES,方法类似:

String jsonString = "{\"age\":10,\"dateOfBirth\":1471466076564,\"fullName\":\"John Doe\"}";
StringReader stringReader = new StringReader(jsonString);
IndexResponse response = client.index(i -> i
  .index("person")
  .id("John Doe")
  .withJson(stringReader));

我们需要将JSON字符串转换为StringReaderInputStream对象,以便与.withJson()方法一起使用。

4.2. 搜索

现在索引中已有数据,我们可以开始搜索了:

String searchText = "John";
SearchResponse<Person> searchResponse = client.search(s -> s
  .index("person")
  .query(q -> q
    .match(t -> t
      .field("fullName")
      .query(searchText))), Person.class);

List<Hit<Person>> hits = searchResponse.hits().hits();
assertEquals(1, hits.size());
assertEquals("John Doe", hits.get(0).source().getFullName());

.search()方法的结果是SearchResponse,其中包含Hits我们可以通过首先从SearchResponse对象中获取HitsMetadata,然后再次调用.hits()方法来获取所有匹配搜索请求的Person对象列表。

我们可以通过添加额外参数来增强请求,以自定义查询连接器:

SearchResponse<Person> searchResponse = client.search(s -> s
  .index("person")
  .query(q -> q
    .match(t -> t
      .field("fullName").query(searchText)))
  .query(q -> q
    .range(range -> range
      .field("age").from("1").to("10"))),Person.class);

4.3. 通过ID检索和删除单条记录

使用.get()方法,根据ID检索文档:

String documentId = "John Doe";
GetResponse<Person> getResponse = client.get(s -> s
  .index("person")
  .id(documentId), Person.class);
Person source = getResponse.source();
assertEquals("John Doe", source.getFullName());

使用.delete()删除数据:

String documentId = "Mark Doe";
DeleteResponse response = client.delete(i -> i
  .index("person")
  .id(documentId));
assertEquals(Result.Deleted, response.result());
assertEquals("Mark Doe", response.id());

5. 复杂搜索查询示例

Elasticsearch Java客户端库非常灵活,提供了多种查询构建器。当我们使用.search()方法搜索文档时,可以使用RangeQuery来匹配字段值在特定范围内的文档:

Query ageQuery = RangeQuery.of(r -> r.field("age").from("5").to("15"))._toQuery();
SearchResponse<Person> response1 = client.search(s -> s.query(q -> q.bool(b -> b
  .must(ageQuery))), Person.class);
response1.hits().hits().forEach(hit -> log.info("Response 1: {}", hit.source()));

.matchQuery()方法返回所有包含提供的字段值的文档:

Query fullNameQuery = MatchQuery.of(m -> m.field("fullName").query("John"))._toQuery();
SearchResponse<Person> response2 = client.search(s -> s.query(q -> q.bool(b -> b
  .must(fullNameQuery))), Person.class);
response2.hits().hits().forEach(hit -> log.info("Response 2: {}", hit.source()));

我们还可以在查询中使用正则表达式和通配符:

Query doeContainsQuery = SimpleQueryStringQuery.of(q -> q.query("*Doe"))._toQuery();
SearchResponse<Person> response3 = client.search(s -> s.query(q -> q.bool(b -> b
  .must(doeContainsQuery))), Person.class);
response3.hits().hits().forEach(hit -> log.info("Response 3: {}", hit.source()));

尽管我们可以在查询中使用通配符和正则表达式,但我们必须考虑每个请求的性能和内存消耗。 如果大量使用通配符,响应时间可能会变差。

此外,如果你更熟悉Lucene查询语法,可以使用SimpleQueryStringQuery构建器来自定义搜索查询:

Query simpleStringQuery = SimpleQueryStringQuery.of(q -> q.query("+John -Doe OR Janette"))._toQuery();
SearchResponse<Person> response4 = client.search(s -> s.query(q -> q.bool(b -> b
  .must(simpleStringQuery))), Person.class);
response4.hits().hits().forEach(hit -> log.info("Response 4: {}", hit.source()));

此外,我们可以使用Lucene的查询解析器语法来构建简单但强大的查询。例如,这里有一些基本运算符,可以与ANDORNOT运算符一起使用,构建搜索查询:

  • 必须运算符(*):要求文档的某个字段中包含指定的文本。
  • 排除运算符(-):排除所有包含声明在(-)符号后面的关键词的文档。

6. 总结

在这篇文章中,我们了解了如何使用Elasticsearch的Java API执行全文搜索引擎相关的标准功能。

你可以查看本文提供的示例代码在GitHub上