EZLippi-浮生志

ElasticSearch Java客户端介绍

这篇文章主要介绍ElasticSearch客户端,包括Transport客户端、Jest客户端和Spring Data ElasticSearch,首先来看几个基本概念。

ElasticSearch

官方的介绍:

ElasticSearch是一个分布式的、基于Json的检索引擎,具有水平扩展、高可靠性和易于管理等优点。

为了更好的理解ealsticsearch的作用,我们可以看下Github的搜索页面:

我们在输入框输入一个单词,会列出许多查询结果,搜索引擎和数据库的区别就是相关性,
我们可以看到elasticsearch的项目排在第一位,很有可能别人在搜索这个关键词时想要访问这个项目。对于不同的应用影响排名的因素可能各不一样。

如果你想建立一个像这样的搜索引擎,首先你的安装elasticsearch,elasticsearch很容易安装,你只需要先安装好Java虚拟机就可以了。

安装和使用ElasticSearch

ElasticSearch默认绑定9200端口,你可以通过http://localhost:9200来访问,你也可以用命令行客户端来执行HTTP请求,这里我用curl来执行

1
2
3
4
5
6
wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-5.0.0.zip
unzip elasticsearch-5.0.0.zip

elasticsearch-5.0.0/bin/elasticsearch

curl -XGET "http://localhost:9200"

你会收到一个Json Document响应,这个响应包含ElasticSearch的安装信息,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"name" : "LI8ZN-t",
"cluster_name" : "elasticsearch",
"cluster_uuid" : "UvbMAoJ8TieUqugCGw7Xrw",
"version" : {
"number" : "5.0.0",
"build_hash" : "253032b",
"build_date" : "2016-10-26T04:37:51.531Z",
"build_snapshot" : false,
"lucene_version" : "6.2.0"
},
"tagline" : "You Know, for Search"
}

最重要的信息就是服务器已经启动成功,还包括ElasticSearch和Lucene的版本信息,Lucene是整个搜索功能的核心库。

现在我们开始给ElasticSearch发送一个Json文档来存储数据,这时候我们要用POST请求,这里我就以一个食物搜索系统为例来讲解怎么添加数据到索引。

1
2
3
4
5
6
7
8
9
curl -XPOST "http://localhost:9200/food/dish" -d'
{
"food": "Hainanese Chicken Rice",
"tags": ["chicken", "rice"],
"favorite": {
"location": "Tian Tian",
"price": 5.00
}
}'

我们还是用之前的端口,这时候我们加了两个字段在URL后面,food和dish,第一个是索引(index)的名称,这是所有文档的集合,第二个是类型(type),相当于关系型数据库里的表。

Dish用文档来建模,Elasticsearch支持多种数据类型,比如string,boolean,数值类型numerics以及嵌套的数据类型,比如上面的favorite。

接下来我们再通过一个POST请求添加一个文档:

1
2
3
4
5
6
curl -XPOST "http://localhost:9200/food/dish" -d'
{
"food": "Ayam Penyet",
"tags": ["chicken", "indonesian"],
"spicy": true
}'

这一次文档的结构有啥不同,它没有包含faorite元素,但添加了另一个spicy属性。同个类型的文档可能差异很大。

建立索引之后,我们就可以来搜索关键字了。我们可以在链接的后面加一个_search,然后添加一个查询参数:

1
curl -XGET "http://localhost:9200/food/dish/_search?q=chicken"

这个请求在dish类型里搜索包含chicken字段的文档,结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{"took":57,"timed_out":false,"_shards":{"total":5,"successful":5,"failed":0},"hits":{"total":2,"max_score":0.3666863,"hits":[{"_index":"food","_type":"dish","_id":"AVq9cnkMZAUVR2HS07Sa","_score":0.3666863,"_source":
{
"food": "Hainanese Chicken Rice",
"tags": ["chicken", "rice"],
"favorite": {
"location": "Tian Tian",
"price": 5.00
}
}},{"_index":"food","_type":"dish","_id":"AVq9cqoiZAUVR2HS07Sb","_score":0.2876821,"_source":
{
"food": "Ayam Penyet",
"tags": ["chicken", "indonesian"],
"spicy": true
}}]}}

搜索结果包含了找到文档的数量,最重要的属性是hits数组,这里面包含索引到的source原始数据。Elasticsearch提供基于JSON结构的查询DSL,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
curl -XPOST "http://localhost:9200/food/dish/_search" -d'
{
"query": {
"bool": {
"must": {
"match": {
"_all": "rice"
}
},
"filter": {
"term": {
"tags.keyword": "chicken"
}
}
}
}
}'

上面这个JSON是在搜索包含rice字段以及tags字段里有chicken的文档,elasticsearch 5.0提供了.keyword字段来精确查询。

到目前为止我们只访问了单个ElasticSearch实例,ElasticSearch具有水平伸缩的特性,我们可以添加更多的node,我们仍然可以连接第一个node,它可以将请求转发到集群中的其他节点。

ElasticSearch客户端介绍

Transport客户端

Transport客户端从ElasticSearch第一个版本就有了,也是使用最广泛的客户端,使用它需要在你的构建工具中添加依赖,我这里用的Gradle:

1
2
3
4
5
dependencies {
compile group: 'org.elasticsearch.client',
name: 'transport',
version: '5.0.0'
}

通过客户端你可以使用ElasticSearch的所有功能,你可以通过Settings对象来初始化一个TransportClient实例,你可以绑定多个节点的地址:

1
2
3
4
5
6
TransportAddress address =
new InetSocketTransportAddress(
InetAddress.getByName("localhost"), 9300);

Client client = new PreBuiltTransportClient(Settings.EMPTY)
addTransportAddress(address);

查询接口

对于之前的那条查询:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
curl -XPOST "http://localhost:9200/food/dish/_search" -d'
{
"query": {
"bool": {
"must": {
"match": {
"_all": "rice"
}
},
"filter": {
"term": {
"tags.keyword": "chicken"
}
}
}
}
}'

我们将它转换成Java代码:

1
2
3
4
5
6
7
8
9
10
11
12
SearchResponse searchResponse = client
.prepareSearch("food")
.setQuery(
boolQuery().
must(matchQuery("_all", "rice")).
filter(termQuery("tags.keyword", "chicken")))
.execute().actionGet();

assertEquals(1, searchResponse.getHits().getTotalHits());

SearchHit hit = searchResponse.getHits().getAt(0);
String food = hit.getSource().get("food").toString();

我这里调用prepareSearch来请求SearchSourceBuilder,然后我们可以通过静态帮助方法来设置一个查询,上面这个例子用的是bool查询,这个查询的must节点有个match查询,filter节点有个term查询。

调用execute方法会返回一个future对象,actionGet是个阻塞调用,SearchResponse的结果和我们用HTTP访问的结果是等价的。

添加索引接口

我们可以用不同的方法来添加索引,其中之一就是使用jsonBuilder来创建一个Json表达式:

1
2
3
4
5
6
7
8
9
XContentBuilder builder = jsonBuilder()
.startObject()
.field("food", "Roti Prata")
.array("tags", new String [] {"curry"})
.startObject("favorite")
.field("location", "Tiong Bahru")
.field("price", 2.00)
.endObject()
.endObject();

上面的表达式使用不同的method来创建JSON文档,这个文档可以作为IndexRequest的source:

1
2
3
4
IndexResponse resp = client.prepareIndex("food","dish")
.setSource(builder)
.execute()
.actionGet();

除了使用jsonBuilder我们还可以用其他的选项,如下所示:

对于比较简单的数据结构你可以使用Map来构造数据,可以结合序列化插件入jackson来完成对象的序列化。

从前面的例子我们可以看到Transport客户端接收一个或多个节点的地址,你可能注意到这里用的是9300而不是9200端口,因为Transport客户端不使用HTTP来通信,它内部通过传输层协议来通信。

前面只连接了一个节点,一旦这个节点挂了我们就不能访问数据了,如果你需要高可用性你可以启用sniffing选项来允许你的客户端连接集群中的其他节点,只需要把client.transport.sniff选项为true即可。

1
2
3
4
5
6
7
8
9
10
TransportAddress address =
new InetSocketTransportAddress(
InetAddress.getByName("localhost"), 9300);

Settings settings = Settings.builder()
.put("client.transport.sniff", true)
.build();

Client client = new PreBuiltTransportClient(settings)
addTransportAddress(address);

更多关于sniffing特性可以参考elasticsearch的官方文档:https://www.elastic.co/guide/en/elasticsearch/client/java-api/current/transport-client.html

Jest客户端

jest客户端能够发送请求给ElasticSearch,首先添加依赖:

1
2
3
4
5
dependencies {
compile group: 'io.searchbox',
name: 'jest',
version: '2.0.0'
}

我们可以用过工厂方法来创建一个JestClient:

1
2
3
4
5
6
7
JestClientFactory factory = new JestClientFactory();
factory.setHttpClientConfig(new HttpClientConfig
.Builder("http://localhost:9200")
.multiThreaded(true)
.build());

JestClient client = factory.getObject();

和普通的Rest客户端一样,Jest不支持生成查询,你可以使用string模板或者复用ElasticSearch builders,builder可以用来创建搜索请求:

1
2
3
4
5
6
7
8
String query = jsonStringThatMagicallyAppears;

Search search = new Search.Builder(query)
.addIndex("library")
.build();

SearchResult result = client.execute(search);
assertEquals(Integer.valueOf(1), result.getTotal());

获取查询结果:

1
2
3
4
5
6
JsonObject jsonObject = result.getJsonObject();
JsonObject hitsObj = jsonObject.getAsJsonObject("hits");
JsonArray hits = hitsObj.getAsJsonArray("hits");
JsonObject hit = hits.get(0).getAsJsonObject();

// ... more boring code

上面这个不是正常使用Jest的方法,Jest支持搜索和索引Java Bean,比如说我用下面这个Java Bean来表示Dish类:

1
2
3
4
5
6
7
8
9
10
11
public class Dish {

private String food;
private List<String> tags;
private Favorite favorite;

@JestId
private String id;

// ... getters and setters
}

可以自动将查询结果转换成Dish对象:

1
2
3
Dish dish = result.getFirstHit(Dish.class).source;

assertEquals("Roti Prata", dish.getFood());

当通过HTTP来访问ElasticSearch时Jest是个不错的选择。

Spring Data ElasticSearch

Spring Data项目提供了通用的编程模型来访问不同的数据源,吸引人的特性是Spring Data允许你使用接口来定义查询,比如比较流行的用来访问关系型数据库的Spring Data JPA以及Spring Data MongoDB。

首先添加依赖:

1
2
3
4
5
dependencies {
compile group: 'org.springframework.data',
name: 'spring-data-elasticsearch',
version: '2.0.4.RELEASE'
}

使用自定义注解来表示要索引的文档:

1
2
3
4
5
6
7
8
9
10
11
12
@Document(indexName = "spring_dish")
public class Dish {

@Id
private String id;
private String food;
private List<String> tags;
private Favorite favorite;

// more code

}

我们可以定义一个接口来访问文档,这里使用ElasticsearchCrudRepository,它提供通用的索引和查询操作:

1
2
3
4
public interface DishRepository 
extends ElasticsearchCrudRepository<Dish, String> {


}

Spring Data ElasticSearch支持XML配置:

1
2
3
4
5
6
7
8
9
<elasticsearch:transport-client id="client" />

<bean name="elasticsearchTemplate"
class="o.s.d.elasticsearch.core.ElasticsearchTemplate">

<constructor-arg name="client" ref="client"/>
</bean>

<elasticsearch:repositories
base-package="ezlippi.elasticsearch.springdata" />

transport-client用于实例化transport客户端,elasticsearchTemplate提供访问ElasticSearch的通用操作,最后repositories元素告诉Spring Data扫描继承自Spring Data接口的接口,Spring会自动为这些接口创建实例。

接下来就可以在Java代码中使用repository来执行索引操作:

1
2
3
4
5
6
7
8
9
10
11
12
Dish mie = new Dish();
mie.setId("hokkien-prawn-mie");
mie.setFood("Hokkien Prawn Mie");
mie.setTags(Arrays.asList("noodles", "prawn"));

repository.save(Arrays.asList(hokkienPrawnMie));

// one line ommited

Iterable<Dish> dishes = repository.findAll();

Dish dish = repository.findOne("hokkien-prawn-mie");

根据ID来查询文档没什么意思,你可以添加更多方法到你的接口里:

1
2
3
4
5
6
7
8
9
10
11
12
public interface DishRepository 
extends ElasticsearchCrudRepository<Dish, String> {


List<Dish> findByFood(String food);

List<Dish> findByTagsAndFavoriteLocation(String tag, String location);

List<Dish> findByFavoritePriceLessThan(Double price);

@Query("{\"query\": {\"match_all\": {}}}")
List<Dish> customFindAll();
}

大部分方法都是以findBy+属性名开头,比如findByFood会查询food属性,结构化查询也是支持的,比如上面的lessThan,上面这个接口会查询比给定价格少的dishes,最后这个查询使用不同的方式,可以使用Query注解来添加查询。

更多关于Spring Data ElasticSearch的知识可以参考Spring的文档:http://docs.spring.io/spring-data/elasticsearch/docs/current/reference/html/#project

总结

上面介绍了几种常用的ElasticSearch Java客户端,可以针对实际使用场景来选择。

🐶 您的支持将鼓励我继续创作 🐶