赞
踩
Elasticsearch与Solr类似,同样是一个基于Lucene的开源的分布式搜索引擎,隐藏了Lucene的复杂性,为开发者提供了一套简单易用的RESTful API。当年由于Lucene的Java API比较难用,于是Shay Banon就开发 出一个叫作Compass的框架来对Lucene进行封装。Compass框架用起来十分方便,后来发现在2009年之后,Compass项目就不更新了。因为Shay Banon用Elasticsearch取代了Compass。由于Compass只是一个Java框架,所以必须掌握Java编程才能使用Compass;而Elasticsearch则是一个独立应用,它提供了RESTful的操作接口,因此不管用什么编程语言, 即使不会编程,也可使用Elasticsearch,只要会用Postman或curl发送请求即可。Elasticsearch可以说是目前最先进、高性能、全功能的搜索引擎,是目前全文搜索引擎的首选。
Elasticsearch不仅仅是全文搜索引擎,它更是一个分布式的实时文档存储服务,能够轻松实现上百个服务节点的扩展,支持PB级别的结构化或者非结构化数据。
- 搜索领域:如百度、谷歌等搜索企业。
- 门户网站:访问统计、文章点赞、留言评论等。
- 广告推广:记录用户行为数据、消费趋势、特定群体进行定制推广等。
- 信息采集:记录应用的埋点数据、访问日志数据等,方便大数据进行分析。
倒排索引可以用一个 Map来简单描述这个结构。这个 Map 的 Key 的即是分词后的单词,这里的单词称为 Term,这一系列的 Term 组成了倒排索引的第一个部分 —— Term Dictionary (索引表,可简称为 Dictionary)。
倒排索引的另一部分为 Postings List(记录表),也对应上述 Map 结构的 Value 部分集合。记录表由所有的 Term 对应的数据(Postings) 组成,它不仅仅为文档 id 信息,可能包含以下信息:
- 文档 id(DocId, Document Id),包含单词的所有文档唯一 id,用于去正排索引中查询原始数据。
- 词频(TF,Term Frequency),记录 Term 在每篇文档中出现的次数,用于后续相关性算分。
- 位置(Position),记录 Term 在每篇文档中的分词位置(多个),用于做词语搜索(Phrase Query)。
- 偏移(Offset),记录 Term 在每篇文档的开始和结束位置,用于高亮显示等。
(1)登录官网(地址)下载Elasticsearch,JDK版本对照关系(地址),不能使用root账户。
https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-7.17.7-linux-x86_64.tar.gz
(2)下载后得到Elasticsearch 7.17.0
➢ bin:该目录下包含Elasticsearch的各种工具命令。 ➢ config:该目录下包含Elasticsearch的各种配置文件,尤其是elasticsearch.yml和jvm.options两个配置文件很重要,其中elasticsearch.yml用于配置Elasticsearch,jvm.options用于配置JVM的堆内存,垃圾回收机制等选项。 ➢ jdk:该目录下包含一份最新的JDK。 ➢ lib:该目录下保存Elasticsearch的核心JAR包及依赖的第三方JAR包。 ➢ logs:日志目录。 ➢ plugins:Elasticsearch的插件目录。(3)修改sysctl.conf
在/etc/sysctl.conf文件最后添加一行 vm.max_map_count = 262144 即可永久修改(4)修改config/elasticsearch.yml文件
#cluster.name: my-application #配置集群名 node.name: node-1 #配置节点名 network.host: 0.0.0.0 #配置Elasticsearch绑定的IP地址,外网访问 http.port: 9200 #监听端口 cluster.initial_master_nodes: ["node-1"]Elasticsearch的集群配置非常简单,在同一个局域网内的多个节点(多个Elasticsearch服务器)上只要指定了相同的 cluster.name,它们都会自动加入同一个集群。因此,一个节点只要设置了cluster.name就能加入集群,成为集群的一部分。
(5)启动Elasticsearch:不能使用root账户。
启动elasticearch,如果需要在后台运行的话加上 -d ./elasticsearch -d访问(http://localhost:9200)发送GET请求,输出name,cluster_name就是前面配置的节点名和集群名,也可以看到Elasticsearch的版本信息。则表明Elasticsearch启动成功。
{ "name" : "VM-16-14-ubuntu", "cluster_name" : "elasticsearch", "cluster_uuid" : "_na_", "version" : { "number" : "7.17.7", "build_flavor" : "default", "build_type" : "tar", "build_hash" : "78dcaaa8cee33438b91eca7f5c7f56a70fec9e80", "build_date" : "2022-10-17T15:29:54.167373105Z", "build_snapshot" : false, "lucene_version" : "8.11.1", "minimum_wire_compatibility_version" : "6.8.0", "minimum_index_compatibility_version" : "6.0.0-beta1" }, "tagline" : "You Know, for Search" }
如果想就这样使用Elasticsearch,当然也是可以的,但很明显安全性不够,下面为Elasticsearch启用SSL支持,以及配置用户名,密码。
(1)修改config目录下的elasticsearch.yml文件,添加内容。
# ---------------------------------- Security ---------------------------------- xpack.security.enabled: true(2)启动Elasticsearch,设置密码。
./elasticsearch-setup-passwords interactive
Elasticsearch内置了用于不同目的几个用户,故此处要依次为每个用户设置密码,每个密码都要设置两次。
- elastic:超级用户。
- kibana:Kibana通过该用户连接Elasticsearch。
- logstash_system:Logstash将监控信息存储到Elasticsearch中时使用该用户。
- beats_system:Beats在Elasticsearch中存储监视信息时使用该用户。
- apm_system:APM服务器在Elasticsearch中存储监视信息时使用该用户。
- remote_monitoring_user:Metricbeat用户在Elasticsearch中收集和存储监视信息时使用该用户。
【Elastisearch、RDBMS、Solr基本对应关系】
- 索引:索引是含义相同的属性文档的集合,是Elasticsearch的一个逻辑存储,可以理解为关系型数据库中的数据库。Elasticsearch可以把索引数据存放到一台服务器上,也可以分片后存到多台服务器上,每个索引有一个或多个分片,每个分片可以有多个副本。
- 类型:Elastic 6.x版只允许每个索引包含一个类型,7.x版将会彻底移除类型。
- 文档:文档是可以被索引的基本数据单位,存储在Elasticsearch中的主要实体叫文档,可以理解为关系型数据库中表的一行记录。每个文档由多个字段构成,Elasticsearch是一个非结构化的数据库,每个文档可以有不同的字段,并且有一个唯一的标识符。
RDBMS Elasticsearch Solr database Index Core(Collection) Table Type Type row(行) Document(文档) Document(文档) column(列) Field(字段) Field(字段)
(1)添加Index
curl -k -u elastic:123456 -X PUT http://localhost:9200/test
PS:不允许使用大写
(2)查看Index
curl -k -u elastic:123456 http://localhost:9200/_cat/indices
结果以点 (.) 开头的Index是Elasticsearch内置的Index。
(3)删除Index
curl -k -u elastic:123456 -X DELETE http://localhost:9200/test
(4)替换默认分词器:Elasticsearch 内置了一些分词器,但这些分词器对中文支持并不 好,这里可选择使用 IK 分词器来处理中文分词(地址),注意版本对应关系。
elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.16.3/elasticsearch-analysis-ik-7.16.3.zip
安装完成后,IK分词器会被自动安装到Elasticsearch的plugins目录下,还会在config目录下创建一个analysis-ik子目录,用于保存IK分词器的配置文件。为Elasticsearch安装任何插件后都需要重启Elasticsearch服务器来加载IK分词器。然后在当前目录下(命令行提示符“>”前的路径)下创建一个配置文件:
{ "settings": { "analysis": { "analyzer": { "default": { "tokenizer": "ik_max_word" } } } } }上面文件指定为Index设置默认的中文分词器:ik_max_word,该分词器由IK分词器提供,它还提供了一个名为“ik_smart”的中文分词器。
curl -k -u elastic:123456 -X PUT http://localhost:9200/test -d @test.json -H "Content-Type:application/json" -H:设置Content-Type请求头的值为“application/json”; -d:用于读取配置文件的内容作为请求数据。
- 在命令行所在的当前路径下定义一个test.json文件,该JSON文件指定使用ik_max_word分词器,text属性指定要测试分 词的文本内容。
{ "analyzer": "ik_max_word", "text": "Elasticsearch与Solr类似,同样是一个基于Lucene的开源的分布式搜索引擎。" }
- 运行如下命令
curl -k -u elastic:123456 -X POST http://localhost:9200/test/_analyze?pretty=true -d @test.json -H "Content-Type:application/json" { "tokens" : [ { "token" : "elasticsearch", "start_offset" : 0, "end_offset" : 13, "type" : "ENGLISH", "position" : 0 }, { "token" : "与", "start_offset" : 13, "end_offset" : 14, "type" : "CN_CHAR", "position" : 1 }, ...... ] }每个词都被称作一个token,每个token都对应如下属性:
- start_offset:起始位置。
- end_offset:结束位置。
- type:类型。
- position:词的位置。
(1)添加文档:在命令行所在的当前路径下定义一个book.json文件,内容如下:
{ "name": "呐喊", "description": "生动地塑造了狂人、孔乙己、阿Q等一批不朽的艺术形象,深刻反映了19世纪末到20世纪20年代间中国社会生活的现状,有力揭露和鞭挞了封建旧恶势力,表达了作者渴望变革,为时代呐喊,希望唤醒国民的思想。", "price": 35 }
curl -k -u elastic:123456 -X POST http://localhost:9200/test/book/1 -d @book.json -H "Content-Type:application/json" {"_index":"test","_type":"book","_id":"1","_version":1,"result":"created","_shards":{"total":2,"successful":1,"failed":0},"_seq_no":0,"_primary_term":1} book:就是type 1:就是被添加文档的ID,这个ID其实是字符串,因此也可指定为“abc”(2)查看Index下所有文档:命令中的pretty=true是一个很常见的参数,用于让Elasticsearch生成格式良好的响应。从该命令可以看出,查看Index下的所有文档,只要在该Index后添加“_search”即可。
curl -k -u elastic:123456 http://localhost:9200/test/_search?pretty=true { "took" : 717, "timed_out" : false, "_shards" : { "total" : 1, "successful" : 1, "skipped" : 0, "failed" : 0 }, "hits" : { "total" : { "value" : 1, "relation" : "eq" }, "max_score" : 1.0, "hits" : [ { "_index" : "test", "_type" : "book", "_id" : "1", "_score" : 1.0, "_source" : { "name" : "呐喊", "description" : "生动地塑造了狂人、孔乙己、阿Q等一批不朽的艺术形象,深刻反映了19世纪末到20世纪20年代间中国社会生活的现状,有力揭露和鞭挞了封建旧恶势力,表达了作者渴望变革,为时代呐喊,希望唤醒国民的思想。", "price" : 35 } } ] } }(3)查看Index下指定ID的文档:
curl -k -u elastic:123456 http://localhost:9200/test/book/1?pretty=true { "_index" : "test", "_type" : "book", "_id" : "1", "_version" : 1, "_seq_no" : 0, "_primary_term" : 1, "found" : true, "_source" : { "name" : "呐喊", "description" : "生动地塑造了狂人、孔乙己、阿Q等一批不朽的艺术形象,深刻反映了19世纪末到20世纪20年代间中国社会生活的现状,有力揭露和鞭挞了封建旧恶势力,表达了作者渴望变革,为时代呐喊,希望唤醒国民的思想。", "price" : 35 } }(4)删除指定ID的文档:
curl -k -u elastic:123456 -X DELETE http://localhost:9200/test/book/1 {"_index":"test","_type":"book","_id":"1","_version":2,"result":"deleted","_shards":{"total":2,"successful":1,"failed":0},"_seq_no":1,"_primary_term":1}(5)全文检索:执行全文检索同样是向 Index 后加“_search”的 URL 地址发送请求,只不过需要添加 JSON格式的请求数据而已。
{ "query": { "match": { "description": "孔乙己" } } }Elasticsearch自己的查询语法(地址),它要求查询参数满足JSON格式,其中query属性的值才是实际的查询参数:
- match 表明使用普通关键词查询
- regexp 表示正则表达式查询
- fuzzy表示模糊查询
- prefix 表示前缀查询
- wildcard表示通配符查询
- range表示范围查询
- query_string定义查询字符串
curl -k -u elastic:123456 http://localhost:9200/test/_search?pretty=true -d @search.json -H "Content-Type:application/json"
(6)根据查询删除:如果要根据查询条件来删除文档,只要向Index后加“_delete_by_que ry”的URL地址发送POST请求即可。
curl -k -u elastic:123456 -X POST http://localhost:9200/test/_delete_by_query?pretty=true -d @search.json -H "Content-Type:application/json"
如果打算使用 Elasticsearch 自带的 RestClient 来操作 Elasticsearch,甚至不需要添加 spring-boot-starter-data-elasticsearch依赖,则直接使用最基本的spring-boot-starter依赖和Elasticsearch提供的RestClient依赖即可。
Elasticsearch官方提供的RestClient分为两种:
- 高级RestClient(推荐):开发者面向Index,文档等高层次的API编程,因此更加简单,方便。通常建议以高级 RestClient 为主,只有当高级 RestClient 实在搞不定时,才考虑使用低级RestClient。
- 低级RestClient:开发者直接面向底层 RESTful 接口编程,发送最原始的请求参数,Elasticsearch 服务器也返回最原始的响应,这种方式需要开发者自行处理请求,响应的序列化和反序列化,相当麻烦,但灵活性最好。
Spring Boot只要检测到类路径下有 elasticsearch-rest-high-level-client 依赖(无须使用Spring Data Elasticsearch),Spring Boot 就会在容器中创建一个自动配置的 RestHighLevelClient,它就是Elasticsearch的高级RestClient。如果想使用低级RestClient,只要调用它的 getLowLevelClient() 方法即可返回 ResLowLevelClient,它就是Elasticsearch的低级RestClient。
如果需要对 RestClient 进行定制,则可在容器中部署一个或多个 RestClientBuilderCustomizerBean,该 Bean 的 customize() 方法即可对 RestClientBuilder、HttpAsyncClientBuilder、RequestConfig.Builder进行定制, 这些定制最终将作用于Elasticsearch的RestClient。
当容器中有了自动配置的RestHighLevelClient之后,容器可通过依赖注入将它注入其他任何组件(主要是DAO组件),接下来该组件可通过它的如下方法来操作Elasticsearch索引库:
- count(CountRequest countRequest,RequestOptions options): 查询符合条件的文档数量。
- countAsync(CountRequest countRequest,RequestOptions option s,ActionListener<CountResponse> listener):以异步方式查询符合条件的文档数量,其中 listener 参数负责处理异步查询的结果。
- delete(DeleteRequest deleteRequest,RequestOptions options): 根据ID删除文档。
- deleteAsync(DeleteRequest deleteRequest,RequestOptions option s,ActionListener<DeleteResponse> listener):以异步方式根据ID删除文档,其中listener参数负责处理异步删除的结果。
- deleteByQuery(DeleteByQueryRequest deleteByQueryRequest,RequestOptions options):删除符合查询条件的文档。
- deleteByQueryAsync(DeleteByQueryRequest deleteByQueryRequest,RequestOptions options,ActionListener<BulkByScrollResponse> listener):以异步方式删除符合查询条件的文档,其中listener参数负责处理异步删除的结果。
- exists(GetRequest getRequest,RequestOptions options):判断指定ID对应的文档是否存在。
- existsAsync(GetRequest getRequest,RequestOptions options,ActionListener<Boolean> listener):以异步方式判断指定ID对应的文档是否存在。
- get(GetRequest getRequest,RequestOptions options):根据ID 获取文档。
- getAsync(GetRequest getRequest,RequestOptions options,ActionListener<GetResponse> listener):以异步方式根据ID获取文档。
- index(IndexRequest indexRequest,RequestOptions options):创建索引或文档。
- indexAsync(IndexRequest indexRequest,RequestOptions options,ActionListener<IndexResponse> listener):以异步方式创建索引或文档。
- mget(MultiGetRequest multiGetRequest,RequestOptions option s):根据多个ID获取多个文档。
- mgetAsync(MultiGetRequest multiGetRequest,RequestOptions options,ActionListener<MultiGetResponse> listener):以异步方式根据多个ID获取多个文档。
- msearch(MultiSearchRequest multiSearchRequest,RequestOptions options):根据多个查询条件返回文档。
- msearchAsync(MultiSearchRequest multiSearchRequest,RequestOptions options,ActionListener<MultiSearchResponse> listener):以异步方式根据多个查询条件返回文档。
- search(SearchRequest searchRequest,RequestOptions option s):查询文档。
- searchAsync(SearchRequest searchRequest,RequestOptions options,ActionListener<SearchResponse> listener):以异步方式查询文档。
- update(UpdateRequest updateRequest , RequestOptions options):根据ID更新文档。
- updateAsync(UpdateRequest updateRequest,RequestOptions options,ActionListener<UpdateResponse> listener):以异步方式根据ID更新文档。
- updateByQuery(UpdateByQueryRequest updateByQueryRequest, RequestOptions options):更新符合条件的所有文档。
- updateByQueryAsync(UpdateByQueryRequest updateByQueryRequest,RequestOptions options,ActionListener<BulkByScrollResponse> listener):以异步方式更新符合条件的所有文档。
此外,它还提供了大量 xxx() 方法来返回对应的 XxxClient,如 asyncSearch() 方法返回AsyncSearchClient,cluster() 方法返回 ClusterClient,eql() 方法返回 EqlClient,indices()方法返回IndicesClient……这些XxxClient又提供了大量的方法来执行相应的操作。
【添加 elasticsearch-rest-high-level-client 依赖】
<dependency> <groupId>org.elasticsearch.client</groupId> <artifactId>elasticsearch-rest-high-level-client</artifactId> </dependency>【修改 application.properties】
# 指定Elasticsearch服务器的地址 spring.elasticsearch.rest.uris=http://127.0.0.1:9200 spring.elasticsearch.rest.read-timeout=10s # 配置用户名和密码 spring.elasticsearch.rest.username=elastic spring.elasticsearch.rest.password=123456
【创建Controller】
@RestController public class HelloController { @Autowired private RestHighLevelClient restHighClient; @RequestMapping("CreateIndex") public boolean testCreateIndex() throws IOException { // 定义创建Index的设置,和前面test.json文件的内容相同 // 设置该Index的默认分词器是ik_max_word var json = "{\n" + " \"settings\": {\n" + " \"analysis\": {\n" + " \"analyzer\": {\n" + " \"default\": {\"tokenizer\": \"ik_max_word\"}\n" + " }\n" + " }\n" + " }\n" + "}\n"; var indexRequest = new CreateIndexRequest("books").source(json, XContentType.JSON); AcknowledgedResponse resp = restHighClient.indices().create(indexRequest, RequestOptions.DEFAULT); return resp.isAcknowledged(); } @RequestMapping("DeleteIndex") public boolean testDeleteIndex(String index) throws IOException { var indexRequest = new DeleteIndexRequest(index); AcknowledgedResponse resp = restHighClient.indices().delete(indexRequest, RequestOptions.DEFAULT); return resp.isAcknowledged(); } @RequestMapping("SaveDocument") public void testSaveDocument(String index, Integer id, String name, String description, Double price) throws IOException { IndexRequest request = new IndexRequest(index).id(id + "").source("name", name, "description", description, "price", price); IndexResponse resp = restHighClient.index(request, RequestOptions.DEFAULT); System.out.println(resp); } @RequestMapping("GetDocument") public void testGetDocument(String index, Integer id) throws IOException { var request = new GetRequest(index).id(id + ""); GetResponse resp = restHighClient.get(request, RequestOptions.DEFAULT); System.out.println(resp.getSource()); } @RequestMapping("Search") public void testSearch(String index, String field, String term) throws IOException { var builder = new SearchSourceBuilder(); if (term != null && term.contains("*")) { builder.query(QueryBuilders.wildcardQuery(field, term)); } else { builder.query(QueryBuilders.matchQuery(field, term)); } var request = new SearchRequest(index) .source(builder); SearchResponse resp = restHighClient.search(request, RequestOptions.DEFAULT); SearchHits hits = resp.getHits(); hits.forEach(System.out::println); } @RequestMapping("DeleteDocument") public void testDeleteDocument(String index, Integer id) throws IOException { var request = new DeleteRequest(index) .id(id + ""); DeleteResponse resp = restHighClient.delete(request, RequestOptions.DEFAULT); System.out.println(resp.status()); } }
testSearch()方法测试全文检索功能,该方法对传入的 term 关键 词进行判断,如果该关键词包含星号( * ),就使用通配符查询(Wildc ard Query),否则就使用普通查询。
由于Elasticsearch官方并未提供反应式的RestClient,因此Spring Data Elasticsearch额外补充了一个 ReactiveElasticsearchClient,用于提供反应式 API 支持。ReactiveElasticsearchClient 相当于RestHighLevelClient的反应式版本,因此它们二者的功能基本相似。只不过在调用ReactiveElasticsearchClient的方法时无须传入RequestOptions参数,且其方法的返回值都是Flux或Mono (反应式API),因此下面程序使用了blockOptional(),toIterable() 来保证反应式API能执行完成。
ReactiveElasticsearchClient 是基于 WebFlux 的 WebClient 的,因此如果要使用反应式的RestClient,还需要添加Spring WebFlux依赖。
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-elasticsearch</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> </dependency>
# 指定Elasticsearch服务器的地址 spring.data.elasticsearch.client.reactive.endpoints=127.0.0.1:9200 spring.data.elasticsearch.client.reactive.use-ssl=false spring.elasticsearch.rest.read-timeout=10s # 配置用户名和密码 spring.elasticsearch.rest.username=elastic spring.elasticsearch.rest.password=123456当容器中有了自动配置的ReactiveElasticsearchClient之后,接下来即可将它依赖注入其他任何组件。
@RestController public class HelloController { @Autowired private ReactiveElasticsearchClient reactiveClient; @RequestMapping("CreateIndex") public boolean testCreateIndex() throws IOException { // 定义创建Index的设置,和前面test.json文件的内容相同 // 设置该Index的默认分词器是ik_max_word var json = "{\n" + " \"settings\": {\n" + " \"analysis\": {\n" + " \"analyzer\": {\n" + " \"default\": {\"tokenizer\": \"ik_max_word\"}\n" + " }\n" + " }\n" + " }\n" + "}\n"; CreateIndexRequest indexRequest = new CreateIndexRequest("books").source(json, XContentType.JSON); Mono<Boolean> resp = reactiveClient.indices().createIndex(indexRequest); return resp.blockOptional().isPresent(); } @RequestMapping("DeleteIndex") public boolean testDeleteIndex(String index) throws IOException { var indexRequest = new DeleteIndexRequest(index); Mono<Boolean> resp = reactiveClient.indices().deleteIndex(indexRequest); return resp.blockOptional().isPresent(); } @RequestMapping("SaveDocument") public void testSaveDocument(String index, Integer id, String name, String description, Double price) throws IOException { IndexRequest request = new IndexRequest(index).id(id + "").source("name", name, "description", description, "price", price); Mono<IndexResponse> resp = reactiveClient.index(request); resp.blockOptional().ifPresent(System.out::println); } @RequestMapping("GetDocument") public void testGetDocument(String index, Integer id) throws IOException { var request = new GetRequest(index).id(id + ""); Mono<GetResult> resp = reactiveClient.get(request); resp.blockOptional().ifPresent(e -> System.out.println(e.getSource())); } @RequestMapping("Search") public void testSearch(String index, String field, String term) throws IOException { var builder = new SearchSourceBuilder(); if (term != null && term.contains("*")) { builder.query(QueryBuilders.wildcardQuery(field, term)); } else { builder.query(QueryBuilders.matchQuery(field, term)); } var request = new SearchRequest(index) .source(builder); Flux<SearchHit> resp = reactiveClient.search(request); resp.toIterable().forEach(System.out::println); } @RequestMapping("DeleteDocument") public void testDeleteDocument(String index, Integer id) throws IOException { var request = new DeleteRequest(index).id(id + ""); Mono<DeleteResponse> resp = reactiveClient.delete(request); resp.blockOptional().ifPresent(e -> System.out.println(e.status())); } }
由于Spring Data是高层次的抽象,而Spring Data Elasticsearch只是属于底层的具体实现,因此Spring Data Elasticsearch也提供了与前面Spring Data完全一致的操作。
Spring Data Elasticsearch大致包括如下几方面功能:
- DAO接口只需继承CrudRepository或ReactiveCrudRepository,Spring Data Elasticsearch能为DAO组件提供实现类。
- Spring Data Elasticsearch支持方法名关键字查询,只不过Elasticsearch查询都是全文检索查询。
- Spring Data Elasticsearch同样支持DAO组件添加自定义的查询方法—通过添加额外的接口,并为额外的接口提供实现类,Spring Data Elasticsearch就能将该实现类中的方法“移植”到DAO组件中。
Spring Data Elasticsearch的 Repository 操作的数据类同样使用@Document和@Field注解修饰,其中@Document修饰的实体类被映射到文档, 使用该注解时可指定如下两个常用属性。
- indexName:指定该实体类被映射到哪个Index。
- createIndex:指定是否根据实体类创建Index。
@Field修饰的属性则被映射到索引文档的Field,使用该注解时可指定如下常用属性。
- name:指定该属性被映射到索引文档的哪个Field,如果不指定该属性,则默认基于同名映射。
- analyzer:指定该Field所使用的分词器。
- searchAnalyzer:指定对该Field执行搜索时所使用的分词器。
【Maven】
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-elasticsearch</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies>【application.properties】
# 指定Elasticsearch服务器的地址 spring.elasticsearch.rest.uris=https://127.0.0.1:9200 spring.elasticsearch.rest.read-timeout=10s # 配置用户名和密码 spring.elasticsearch.rest.username=elastic spring.elasticsearch.rest.password=123456【Entity】
(1)@Document注解会对实体中的所有属性建立索引:
- indexName = "customer":表示创建一个名为customer的索引。
- type="customer":表示在索引中创建一个名为customer的类别,而在Elasticsearch 7.x版本中取消了类别的概念。
- shards = 1:表示只使用一个分片,默认为5。
- replicas = 0:表示副本数量,默认为1,0表示不使用副本。
- refreshInterval = "-1":表示禁止索引刷新。
- createIndex = true:指定自动创建索引,Spring Data Elasticsearch可根据该实体类自动创建 Index—如果该Index不存在的话。
(2)@Id作用在成员变量,标记一个字段作为id主键。
(3)@Field作用在成员变量,标记为文档的字段,并指定字段映射属性:
- type:字段类型,取值是枚举:FieldType。
- index:是否索引,布尔类型,默认是true。
- store:是否存储,布尔类型,默认是false。
- analyzer:分词器名称是ik_max_word。
@Document(indexName = "book", createIndex = true) @Data @NoArgsConstructor @AllArgsConstructor public class Book { @Id private Long id; @Field(analyzer = "ik_max_word", searchAnalyzer = "ik_smart",type = FieldType.Text) private String bookName; @Field(analyzer = "ik_max_word", searchAnalyzer = "ik_smart",type = FieldType.Text) private String author; @Field(type = FieldType.Float) private float price; @Field(type = FieldType.Integer) private int page; @Field(analyzer = "ik_max_word", searchAnalyzer = "ik_smart",type = FieldType.Text) private String category; }【Repository】
public interface BookRepository extends ElasticsearchRepository<Book, Integer> { }【Controller】
@RestController public class BookController { @Autowired BookRepository bookRepository; @RequestMapping("/save") public Book save() { Book book = new Book(1L, "西游记", "吴承恩", 34.51F, 100, "novel"); bookRepository.save(book); return bookRepository.findById(1).orElse(null); } }
{"id":1,"bookName":"西游记","author":"吴承恩","price":34.51,"page":100,"category":"novel"}
【创建文档】创建文档就是插入一条数据到Elasticsearch,JPA已经默认实现了很多方法,调用save()方法保存数据。
@RequestMapping("/save") public String save() { for (int i = 1; i < 11; i++) { Book book = new Book(i, "西游记", "吴承恩", new Random().nextFloat() * 100, 100, "novel"); bookRepository.save(book); } return "success"; }【查询文档】使用JPA的默认查询方法或者自定义简单查询方法等都可以实现。
@RequestMapping("/findAll") public void findAll() { for (Book books : bookRepository.findAll()) { System.out.println(books.toString()); } } ================================================= Book(id=1, bookName=西游记, author=吴承恩, price=54.838203, page=100, category=novel) Book(id=2, bookName=西游记, author=吴承恩, price=40.703766, page=100, category=novel) Book(id=3, bookName=西游记, author=吴承恩, price=52.918945, page=100, category=novel) Book(id=4, bookName=西游记, author=吴承恩, price=49.116974, page=100, category=novel) Book(id=6, bookName=西游记, author=吴承恩, price=3.0150056, page=100, category=novel) Book(id=8, bookName=西游记, author=吴承恩, price=19.553465, page=100, category=novel) Book(id=5, bookName=西游记, author=吴承恩, price=41.90748, page=100, category=novel) Book(id=7, bookName=西游记, author=吴承恩, price=53.16013, page=100, category=novel) Book(id=9, bookName=西游记, author=吴承恩, price=1.6808152, page=100, category=novel) Book(id=10, bookName=西游记, author=吴承恩, price=87.849304, page=100, category=novel)【更新文档】调用save()方法对属性进行修改。
@RequestMapping("/update") public void update() { Book book = bookRepository.findById(1).orElse(null); book.setAuthor("明朝:吴承恩"); bookRepository.save(book); }【删除文档】
@RequestMapping("/delete") public void deleteById() { bookRepository.deleteById(1); } @RequestMapping("/deleteAll") public void deleteAll() { for (Book books : bookRepository.findAll()) { bookRepository.deleteById(books.getId()); } }使用Spring Boot操作Elasticsearch非常简单,通过少量代码即可实现日常大部分业务的需求。这正是Spring Data的强大之处,不用写任何语句,自动根据方法名或类的信息进行增删改查(CRUD)操作。只要定义一个接口,然后继承Repository,就能具备各种基本的增删改查功能。
【分页查询】
(1)Pageable分页:每页3条数据,查出第一页(0页起始)。
@RestController public class PageAble { @Autowired BookRepository bookRepository; @RequestMapping("/pageList") public void fetchPageCustomers() { Pageable pageable = PageRequest.of(1, 3, Sort.Direction.DESC, "price"); Page<Book> book = bookRepository.findByBookName("西游记", pageable); for (Book page : book) { System.out.println(page.toString()); } } } ==================================================== Book(id=3, bookName=西游记, author=吴承恩, price=52.918945, page=100, category=novel) Book(id=4, bookName=西游记, author=吴承恩, price=49.116974, page=100, category=novel) Book(id=5, bookName=西游记, author=吴承恩, price=41.90748, page=100, category=novel)(2)QueryBuilder分页:除了Spring Data自带的Pageable分页之外,我们也可以使用QueryBuilder来构建分页查询。(SpringBoot:2.3.7.RELEASE,高版本中ElasticsearchRepository不再继承ElasticsearchCrudRepository,search方法找不到)
@RestController public class PageAble { @Autowired BookRepository bookRepository; @RequestMapping("/pageList2") public void fetchPage2Customers() { QueryBuilder customerQuery = QueryBuilders.boolQuery().must(QueryBuilders.matchQuery("bookName", "西游记")); Page<Book> book = bookRepository.search(customerQuery, PageRequest.of(0, 3)); for (Book page : book) { System.out.println(page.toString()); } } }使用 QueryBuilder 可以构建多条件查询,再结合 PageRequest 最后使用 search() 方法完成分页查询。BoolQueryBuilder 有一些关键字和 AND、OR、NOT一一对应:
- must:AND——必须完全匹配条件。
- mustNot:NOT——关键字不匹配条件。
- should:OR——至少满足一个条件,这个文档就符合should。
【多个关键字的组合查询】
public void testBoolQuery() { NativeSearchQuery nativeSearchQuery = new NativeSearchQueryBuilder() .withQuery(QueryBuilders.boolQuery() .should(QueryBuilders.termQuery("bookName", "史")) .should(QueryBuilders.termQuery("author", "明")) ) .withSort(SortBuilders.fieldSort("id").order(SortOrder.DESC)) .withPageable(PageRequest.of(0, 50)) .build(); Iterable<Book> books = repository.search(nativeSearchQuery); for (Book b : books){ System.out.println(b); } }
【精准查询】精确查询指的是查询关键字(或者关键字分词后)必须与目标分词结果完全匹配。
(1)单条件匹配
@RestController public class PageAble { @Autowired BookRepository bookRepository; @RequestMapping("/singleFind") public void singleFind() { //不分词查询 参数1: 字段名,参数2:字段查询值,因为不分词,所以汉字只能查询一个字,英语是一个单词 QueryBuilder queryBuilder = QueryBuilders.termQuery("bookName", "西游记"); Page<Book> book = bookRepository.search(queryBuilder, PageRequest.of(0, 3)); for (Book page : book) { System.out.println(page.toString()); } //分词查询,采用默认的分词器 QueryBuilder queryBuilder2 = QueryBuilders.matchQuery("bookName", "西游记"); Page<Book> book2 = bookRepository.search(queryBuilder2, PageRequest.of(0, 3)); for (Book page : book2) { System.out.println(page.toString()); } } }使用termQuery为不分词查询,而matchQuery为分词模糊查询,并采用默认的分词器。
(2)多条件匹配
// 不分词查询,参数1:字段名,参数2:多个字段查询值,因为不分词,所以只能查询一个汉字,而英文可以查询一个单词 QueryBuilder termsQuery=QueryBuilders.termsQuery("bookName", "西","游"); // 分词查询,采用默认的分词器 QueryBuilder multiMatchQuery = QueryBuilders.multiMatchQuery("西游", "author","bookName");termsQuery和termQuery的功能类似,支持传入多个参数。multiMatchQuery多个匹配其实就是传入多个值进行匹配查询。
【模糊查询】模糊查询是指查询关键字与目标关键字进行模糊匹配。
//1.常用的字符串查询 QueryBuilders.queryStringQuery("fieldValue").field("fieldName");//左右模糊 //2.常用的用于推荐相似内容的查询 QueryBuilders.moreLikeThisQuery(new String[] {"fieldName"}).addLikeText("pipeidhua");//如果不指定filedName,则默认全部,常用在相似内容的推荐上 //3.前缀查询,如果字段没分词,就匹配整个字段前缀 QueryBuilders.prefixQuery("fieldName","fieldValue"); //4.fuzzy query:分词模糊查询,通过增加 fuzziness 模糊属性来查询,如能够匹配 hotelName 为 tel 前或后加一个字母的文档,fuzziness 的含义是检索的 term 前后增加或减少 n 个单词的匹配查询 QueryBuilders.fuzzyQuery("hotelName", "tel").fuzziness(Fuzziness.ONE); //5.wildcard query:通配符查询,支持* 任意字符串;?任意一个字符 QueryBuilders.wildcardQuery("fieldName","ctr*");//前面是fieldname,后面是带匹配字符的字符串 QueryBuilders.wildcardQuery("fieldName","c?r?");在分词的情况下,fuzzyQuery、prefixQuery、wildcardQuery不支持分词查询,即使有这种文档数据,也不一定能查询出来。
【范围查询】QueryBuilders通过rangeQuery方法实现价格、年龄等字段的范围查询。
// 价格范围查询,默认闭区间查询 QueryBuilder queryBuilder0 = QueryBuilders.rangeQuery("price").from("50").to("50"); // 价格范围查询,开区间查询 QueryBuilder queryBuilder1 = QueryBuilders.rangeQuery("price").from("40").to("60").includeUpper(false).includeLower(false);// 默认是true,也就是包含 // 价格范围查询,大于60 QueryBuilder queryBuilder2 = QueryBuilders.rangeQuery("price").gt("60"); // 价格范围查询,大于等于60 QueryBuilder queryBuilder3 = QueryBuilders.rangeQuery("price").gte("60"); // 价格范围查询,小于60 QueryBuilder queryBuilder4 = QueryBuilders.rangeQuery("price").lt("60"); // 价格范围查询,小于等于60 QueryBuilder queryBuilder5 = QueryBuilders.rangeQuery("price").lte("60");QueryBuilders提供了from、to、gt、lt等方法实现范围查询和数据比较等功能。from、to用于实现范围查询;gt、gte、lt、lte用于数字比较,实现数字、时间等类型字段的范围查询;includeUpper包含大于和小于,默认为true,也就是包含。
【聚合查询】
第一步,使用 QueryBuilder 构建查询条件:
QueryBuilder customerQuery = QueryBuilders.boolQuery() .must(QueryBuilders.matchQuery("address", "北京"));
- 第二步,使用 SumAggregationBuilder 指明需要聚合的字段:
SumAggregationBuilder sumBuilder = AggregationBuilders.sum("sumAge").field("age");
- 第三步,以前两部分的内容为参数构建成 SearchQuery:
SearchQuery searchQuery = new NativeSearchQueryBuilder() .withQuery(customerQuery) .addAggregation(sumBuilder) .build();
- 第四步,使用 Aggregations 进行查询:
Aggregations aggregations = elasticsearchTemplate.query(searchQuery, new ResultsExtractor<Aggregations>() { @Override public Aggregations extract(SearchResponse response) { return response.getAggregations(); } });
- 第五步,解析聚合查询结果:
//转换成 map 集合 Map<String, Aggregation> aggregationMap = aggregations.asMap(); //获得对应的聚合函数的聚合子类,该聚合子类也是个 map 集合,里面的 value 就是桶 Bucket,我们要获得 Bucket InternalSum sumAge = (InternalSum) aggregationMap.get("sumAge"); System.out.println("sum age is "+sumAge.getValue());
使用ElasticsearchRepository实现Elasticsearch数据的增删改查等功能。但是,如果涉及一些位置、高亮、聚合等复杂查询,可能ElasticsearchRepository就不太合适了。
- 如果Spring Boot在类加载路径下检测到Spring Data Elasticsearch,Spring Boot就会在容器中自动配置一个ElasticsearchRestTemplate(注意不是ElasticsearchTemplate,ElasticsearchTemplate在7.6.2版本已经被废除)。ElasticsearchRestTemplate底层依赖于容器中自动配置的RestHighLevelClient。
- 如果Spring Boot在类加载路径下同时检测到Spring Data Elasticsearch和Spring WebFlux,Spring Boot就会在容器中自动配置一个ReactiveElasticsearchTemplate。ReactiveElasticsearchTemplate底层依赖于容器中自动配置的 ReactiveElasticsearchClient。正如 ReactiveElasticsearchClient 是RestHighLevelClient的反应式版本,ReactiveElasticsearchTemplate则是ElasticsearchRestTemplate的反应式版本。
与RestHighLevelClient、ReactiveElasticsearchClient 相比,ElasticsearchRestTemplate、ReactiveElasticsearchTemplate 能以更加面向对象的方法来操作 Elasticsearch 索引库,这些XxxTemplate的方法操作的是实体对象,而Spring Data Elasticsearch会自动将面向实体对象的操作转化为对索引库的操作。
【创建文档】ElasticsearchRestTemplate提供了index()创建单个文档,同时提供了bulkIndex()批量创建文档。
@RestController public class EsRestTemplate { private static final String Book_INDEX = "book"; @Autowired private ElasticsearchRestTemplate elasticsearchRestTemplate; @RequestMapping("/index") public void index() { Book book = new Book(); book.setId(101); book.setBookName("雪中悍刀行"); book.setAuthor("烽火戏诸侯"); book.setCategory("网络小说"); book.setPrice(300); book.setPage(5000); IndexQuery indexQuery = new IndexQueryBuilder() .withId(book.getId() + "") .withObject(book).build(); String documentId = elasticsearchRestTemplate.index(indexQuery, IndexCoordinates.of(Book_INDEX)); System.out.println(documentId); } }首先构建IndexQuery对象,然后调用index()保存文档数据。其实最终是由ElasticsearchRestTemplate调用Elasticsearch的HTTP接口完成数据的保存。
@RestController public class EsRestTemplate { private static final String Book_INDEX = "book"; @Autowired private ElasticsearchRestTemplate elasticsearchRestTemplate; @RequestMapping("/bulkIndex") public void bulkIndex() { Book book = new Book(); book.setId(10); book.setBookName("盗墓笔记"); book.setAuthor("南派三叔"); book.setCategory("网络小说"); book.setPrice(200); book.setPage(4000); List<Book> books = new ArrayList<>(); books.add(book); List<IndexQuery> queries = books.stream() .map(book1 -> new IndexQueryBuilder().withId(book1.getId()+"").withObject(book1).build()) .collect(Collectors.toList()); List<String> documentIds = elasticsearchRestTemplate.bulkIndex(queries, IndexCoordinates.of(Book_INDEX)); for (String documentId : documentIds) { System.out.println(documentId); } } }bulkIndex与index调用方法类似,只是需要构建List<IndexQuery>参数,最后返回的也是文档的documentId。
【更新文档】 使用ElasticsearchRestTemplate更新文档也是一样的流程,首先构建UpdateQuery,然后调用update()更新文档。
@RestController public class EsRestTemplate { private static final String Book_INDEX = "book"; @Autowired private ElasticsearchRestTemplate elasticsearchRestTemplate; @RequestMapping("/update") public void update() { Book book = new Book(); book.setId(10); book.setBookName("盗墓笔记"); book.setAuthor("南派三叔"); book.setCategory("网络小说"); book.setPrice(300); book.setPage(4000); // 构造updateQuery UpdateQuery updateQuery = UpdateQuery.builder("10") // 如果不存在就新增,默认为false .withDocAsUpsert(true) .withDocument(Document.parse(JSON.toJSONString(book))) .build(); UpdateResponse response = elasticsearchRestTemplate.update(updateQuery, IndexCoordinates.of(Book_INDEX)); System.out.println(JSON.toJSONString(response)); } }需要注意的是,update()方法返回的是UpdateResponse对象。
【删除文档】
public void deleteById() { String result = elasticsearchRestTemplate.delete("5", IndexCoordinates.of(Book_INDEX)); System.out.println(result); }自定义删除条件,根据 bookName删除:
public void deleteByBookName() { NativeSearchQuery nativeSearchQuery = new NativeSearchQueryBuilder() .withQuery(QueryBuilders.termQuery("bookName", "三")) .build(); elasticsearchRestTemplate.delete(nativeSearchQuery,Book.class, IndexCoordinates.of("book")); }
【查询文档】ElasticsearchRestTemplate通过search()方法实现非常完善的文档查询功能。它的使用方式与ElasticsearchRepository的query()类似。首先构建QueryBuilder对象,然后将查询对象传入search()方法执行查询。
- NativeQuery:可以灵活地构建各种复查查询(如聚合、筛选和排序)。
- StringQuery:使用JSON字符串来构建查询条件,和Repository中的@Query注解中的JSON字符串类似。
- CriteriaQuery:通过简单地连接和组合所要搜索的文档必须满足指定的条件来生成查询,而无须了解Elasticsearch查询的语法或基础知识。
(1)NativeQuery:通过QueryBuilder构建category字段包含“历史”关键字的查询条件,然后使用NativeSearchQueryBuilder构建NativeQuery,最后执行search()方法。
public void nativeSearchQuery() { QueryBuilder queryBuilder = QueryBuilders.matchQuery("category", "历史"); Query searchQuery = new NativeSearchQueryBuilder() .withQuery(queryBuilder) .build(); SearchHits<Book> bookSearchHits = elasticsearchRestTemplate.search (searchQuery,Book.class,IndexCoordinates.of(Book_INDEX)); bookSearchHits.getSearchHits().forEach(System.out::println); }(2)StringQuery:使用StringQuery就是传入JSON查询条件,查询bookName为“史记”的数据。
public void stringQuery() { Query searchQuery = new StringQuery("{\n" + " \"match\": { \n" + " \"bookName\": { \"query\": \"史记\" } \n" + " } \n" + " }"); SearchHits<Book> bookSearchHits = elasticsearchRestTemplate.search (searchQuery,Book.class,IndexCoordinates.of(Book_INDEX)); bookSearchHits.getSearchHits().forEach(System.out::println); }(3)CriteriaQuery:CriteriaQuery通过where、is、contains、and、or等简单地连接和组合所要搜索的文档必须满足指定的条件来生成查询,而无须了解Elasticsearch查询的语法或基础知识。
public void criteriaQuery() { // 构造条件 Criteria criteria = Criteria.where(new SimpleField("bookName")) .contains("明") .or(new SimpleField("author")) .contains("明"); CriteriaQuery criteriaQuery = new CriteriaQuery(criteria); SearchHits<Book> blogSearchHits = elasticsearchRestTemplate.search (criteriaQuery, Book.class); blogSearchHits.getSearchHits().forEach(System.out::println); }
【高亮显示】ElasticsearchRestTemplate提供HighlightBuilder实现查询结果的关键字高亮显示,支持设置某些关键字高亮,可以设置n个高亮的关键字,最后的查询结果按照符合高亮条件的个数来排序,即优先展示高亮字段多的。HighlightBuilder通过preTags()和postTags()设置关键字高亮显示的效果,Field()设置高亮显示的字段。
public void highlightQuery() { QueryBuilder queryBuilder = QueryBuilders.matchQuery("category", "历史"); // 设置高亮效果 String preTag = "<font color='#dd4b39'>";//Google的色值 String postTag = "</font>"; Query searchQuery = new NativeSearchQueryBuilder() .withQuery(queryBuilder) .withHighlightFields(new HighlightBuilder.Field("category") .preTags(preTag).postTags(postTag)).build(); SearchHits<Book> bookSearchHits = elasticsearchRestTemplate.search (searchQuery,Book.class,IndexCoordinates.of(Book_INDEX)); bookSearchHits.getSearchHits().forEach(System.out::println); }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。