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
文件中添加Elasticsearch和Jackson 的依赖:
<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字符串转换为StringReader
或InputStream
对象,以便与.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的查询解析器语法来构建简单但强大的查询。例如,这里有一些基本运算符,可以与AND
、OR
和NOT
运算符一起使用,构建搜索查询:
- 必须运算符(
*
):要求文档的某个字段中包含指定的文本。 - 排除运算符(
-
):排除所有包含声明在(-)
符号后面的关键词的文档。
6. 总结
在这篇文章中,我们了解了如何使用Elasticsearch的Java API执行全文搜索引擎相关的标准功能。
你可以查看本文提供的示例代码在GitHub上。