赞
踩
官网:https://www.elastic.co/cn/elasticsearch/
官方文档:https://www.elastic.co/guide/en/elasticsearch/reference/index.html
elastic:富有弹性的
search:搜索
一般简称为ES,和redis/mysql一样,不仅服务于java,其他语言同样可以使用
功能也是类似数据库的软件,能高效的从大量数据中,搜索匹配指定关键词的内容
ES本质就是一个java开发的应用程序
预设了增删改查的接口,访问ES部署服务端的接口地址(URL),即可进行操作
ES底层使用了java一套名为Lucene的API,此API提供了全文搜索引擎核心操作的接口
ES是在Lucene的基础上进行的完善,实现了开箱即用的搜索引擎软件
- Solr(也是java实现,基本淘汰)
- MongoDB(目前还很火,偏大数据方向)
在传统的关系型数据库中,只要进行模糊查询,查询效率都极为低下,尤其使用前模糊查询
这个时候可以使用ES,来执行查询操作
ES进行优化后,从同样的数据量里进行相同条件的模糊查询,效率能够提高100倍以上!
直接运行es安装包下,bin目录的elasticsearch.bat文件即可
默认端口号为9200,出现如下信息,表示启动成功
.....省略一堆.....
[2022-08-25T15:56:15,379][INFO ][o.e.n.Node ] [DESKTOP-K8VRATH] initialized
[2022-08-25T15:56:15,380][INFO ][o.e.n.Node ] [DESKTOP-K8VRATH] starting ...
[2022-08-25T15:56:15,702][INFO ][o.e.t.TransportService ] [DESKTOP-K8VRATH] publish_address {127.0.0.1:9300}, bound_addresses {127.0.0.1:9300}, {[::1]:9300}
[2022-08-25T15:56:15,912][WARN ][o.e.b.BootstrapChecks ] [DESKTOP-K8VRATH] the default discovery settings are unsuitable for production use; at least one of [discovery.seed_hosts, discovery.seed_providers, cluster.initial_master_nodes] must be configured
[2022-08-25T15:56:15,913][INFO ][o.e.c.c.Coordinator ] [DESKTOP-K8VRATH] cluster UUID [mhLGCp_LQXeLG0dGQZIlfw]
[2022-08-25T15:56:15,920][INFO ][o.e.c.c.ClusterBootstrapService] [DESKTOP-K8VRATH] no discovery configuration found, will perform best-effort cluster bootstrapping after [3s] unless existing master is discovered
[2022-08-25T15:56:16,026][INFO ][o.e.c.s.MasterService ] [DESKTOP-K8VRATH] elected-as-master ([1] nodes joined)[{DESKTOP-K8VRATH}{thQ9zjPJQ22JWYhAjrTE-g}{WG2oeMRHTj-gy8tbJ_taZg}{127.0.0.1}{127.0.0.1:9300}{dilm}{ml.machine_memory=34203144192, xpack.installed=true, ml.max_open_jobs=20} elect leader, _BECOME_MASTER_TASK_, _FINISH_ELECTION_], term: 2, version: 20, delta: master node changed {previous [], current [{DESKTOP-K8VRATH}{thQ9zjPJQ22JWYhAjrTE-g}{WG2oeMRHTj-gy8tbJ_taZg}{127.0.0.1}{127.0.0.1:9300}{dilm}{ml.machine_memory=34203144192, xpack.installed=true, ml.max_open_jobs=20}]}
[2022-08-25T15:56:16,062][INFO ][o.e.c.s.ClusterApplierService] [DESKTOP-K8VRATH] master node changed {previous [], current [{DESKTOP-K8VRATH}{thQ9zjPJQ22JWYhAjrTE-g}{WG2oeMRHTj-gy8tbJ_taZg}{127.0.0.1}{127.0.0.1:9300}{dilm}{ml.machine_memory=34203144192, xpack.installed=true, ml.max_open_jobs=20}]}, term: 2, version: 20, reason: Publication{term=2, version=20}
[2022-08-25T15:56:16,205][INFO ][o.e.l.LicenseService ] [DESKTOP-K8VRATH] license [db3b7d4b-9389-48d2-be41-5040d00898d1] mode [basic] - valid
[2022-08-25T15:56:16,206][INFO ][o.e.x.s.s.SecurityStatusChangeListener] [DESKTOP-K8VRATH] Active license is now [BASIC]; Security is disabled
[2022-08-25T15:56:16,212][INFO ][o.e.g.GatewayService ] [DESKTOP-K8VRATH] recovered [0] indices into cluster_state
[2022-08-25T15:56:16,213][INFO ][o.e.h.AbstractHttpServerTransport] [DESKTOP-K8VRATH] publish_address {127.0.0.1:9200}, bound_addresses {127.0.0.1:9200}, {[::1]:9200}
[2022-08-25T15:56:16,213][INFO ][o.e.n.Node ] [DESKTOP-K8VRATH] started
此时,使用浏览器访问 http://localhost:9200/
会显示如下信息
{ "name": "DESKTOP-K8VRATH", "cluster_name": "elasticsearch", "cluster_uuid": "mhLGCp_LQXeLG0dGQZIlfw", "version": { "number": "7.6.2", "build_flavor": "default", "build_type": "zip", "build_hash": "ef48eb35cf30adf4db14086e8aabd07ef6fb113f", "build_date": "2020-03-26T06:34:37.794943Z", "build_snapshot": false, "lucene_version": "8.4.0", "minimum_wire_compatibility_version": "6.8.0", "minimum_index_compatibility_version": "6.0.0-beta1" }, "tagline": "You Know, for Search" }
要想使用ES提高模糊查询效率,需要将数据库内需要查询的数据复制到ES中,
在新增数据到ES的过程中,ES可以对指定的列进行分词索引,保存到索引库中,形成倒排索引结构
如上图:左侧为数据库数据,右侧为ES内保存的数据,数字为id,这样进行模糊查询的时候通过ES直接搜索右侧数据,得到id即可快速查询到数据库内对应的数据
ES对中文支持不好,所以需要安装别的分词器插件来实现中文分词,此处用的是IK分词器
下载IK后,解压出来的文件+文件夹共8个
在ES安装目录下,plugins文件夹下,新建一个ik文件夹
然后将上图的文件全部粘贴进去
然后重启ES,应该可以看到此信息
[2022-08-25T16:50:37,209][INFO ][o.e.p.PluginsService ] [DESKTOP-K8VRATH] loaded plugin [analysis-ik]
IK内置了2个分词器
此时访问ES分词检测,analyzer使用ik_xxx即可使用中文词库进行分词
POST http://localhost:9200/_analyze
Content-Type: application/json
{
"text": "北京冬季奥林匹克运动会完美闭幕",
"analyzer": "ik_smart"
}
{ "tokens": [ { "token": "北京", "start_offset": 0, "end_offset": 2, "type": "CN_WORD", "position": 0 }, { "token": "冬季", "start_offset": 2, "end_offset": 4, "type": "CN_WORD", "position": 1 }, { "token": "奥林匹克运动会", "start_offset": 4, "end_offset": 11, "type": "CN_WORD", "position": 2 }, { "token": "完美", "start_offset": 11, "end_offset": 13, "type": "CN_WORD", "position": 3 }, { "token": "闭幕", "start_offset": 13, "end_offset": 15, "type": "CN_WORD", "position": 4 } ] }
POST http://localhost:9200/_analyze
Content-Type: application/json
{
"text": "北京冬季奥林匹克运动会完美闭幕",
"analyzer": "ik_max_word"
}
{ "tokens": [ { "token": "北京", "start_offset": 0, "end_offset": 2, "type": "CN_WORD", "position": 0 }, { "token": "冬季", "start_offset": 2, "end_offset": 4, "type": "CN_WORD", "position": 1 }, { "token": "奥林匹克运动会", "start_offset": 4, "end_offset": 11, "type": "CN_WORD", "position": 2 }, { "token": "奥林匹克", "start_offset": 4, "end_offset": 8, "type": "CN_WORD", "position": 3 }, { "token": "奥林匹", "start_offset": 4, "end_offset": 7, "type": "CN_WORD", "position": 4 }, { "token": "克", "start_offset": 7, "end_offset": 8, "type": "CN_CHAR", "position": 5 }, { "token": "运动会", "start_offset": 8, "end_offset": 11, "type": "CN_WORD", "position": 6 }, { "token": "运动", "start_offset": 8, "end_offset": 10, "type": "CN_WORD", "position": 7 }, { "token": "会", "start_offset": 10, "end_offset": 11, "type": "CN_CHAR", "position": 8 }, { "token": "完美", "start_offset": 11, "end_offset": 13, "type": "CN_WORD", "position": 9 }, { "token": "闭幕", "start_offset": 13, "end_offset": 15, "type": "CN_WORD", "position": 10 } ] }
ES中的index(索引)可以简单理解为常规数据库中的表
如下图,有2个index,分别为users,questions
ES中的数据搜索与操作
### 测试ES运行是否正常 GET http://localhost:9200 ### 测试ES的分词功能 POST http://localhost:9200/_analyze Content-Type: application/json { "text": "北京冬季奥林匹克运动会完美闭幕", "analyzer": "ik_max_word" } ### 创建 index PUT http://localhost:9200/questions ### 删除一个Index DELETE http://localhost:9200/questions ### 设置index中的文档属性采用ik分词 POST http://localhost:9200/questions/_mapping Content-Type: application/json { "properties": { "title": { "type": "text", "analyzer": "ik_max_word", "search_analyzer": "ik_max_word" }, "content": { "type": "text", "analyzer": "ik_max_word", "search_analyzer": "ik_max_word" } } } ### questions 中添加文档1 POST http://localhost:9200/questions/_create/1 Content-Type: application/json { "id":1, "title":"Java基本数据类型有哪些", "content":"面时候为啥要问基本类型这么简单问题呀,我们要如何回答呢?" } ### questions 中添加文档2 POST http://localhost:9200/questions/_create/2 Content-Type: application/json { "id":2, "title":"int类型的范围", "content":"为啥要了解int类型的范围呢?" } ### questions 中添加文档3 POST http://localhost:9200/questions/_create/3 Content-Type: application/json { "id":3, "title":"常用集合类有哪些", "content":"为啥企业经常问集合呀?该如何回复呢" } ### questions 中添加文档4 POST http://localhost:9200/questions/_create/4 Content-Type: application/json { "id":4, "title":"线程的run方法和start方法有啥区别", "content":"run方法可以执行线程的计算过程, start也可以执行线程的计算过程,用途一样么?" } ### 更新questions索引中的文档 POST http://localhost:9200/questions/_doc/4/_update Content-Type: application/json { "doc": { "title": "Java线程的run方法和start方法有啥区别" } } ### 删除questions中的一个文档 DELETE http://localhost:9200/questions/_doc/2 ### 查询数据 GET http://localhost:9200/questions/_doc/4 ### 搜索 ES POST http://localhost:9200/questions/_search Content-Type: application/json { "query": { "match": {"title": "类型" } } } ### 多字段搜索 should 为或的意思,包含任意一个即可,修改为 must 即为必须同时包含 POST http://localhost:9200/questions/_search Content-Type: application/json { "query": { "bool": { "should": [ { "match": { "title": "java类型" }}, { "match": { "content": "java类型"}} ] } } }
在ES的原生状态下,在Java中,访问ES需要使用socket/http client去处理,但是过于繁琐
所以我们直接使用Spring Data框架来简化操作
Spring Data是Spring提供的一套连接各种第三方数据源的框架集
支持了各种三方数据源,如JDBC,JPA,Redis,MongoDB等.
官网:https://spring.io/projects/spring-data
<!--Elasticsearch依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
spring:
elasticsearch:
rest:
# 设置ES服务端的地址与端口
uris: http://localhost:9200
logging:
level:
# SpringDataElasticsearch内部有个专门输出状态的类,设置一下debug,方便查看
org.elasticsearch.client.RestClient: debug
package cn.tedu.search.pojo.po; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import org.springframework.data.annotation.Id; import org.springframework.data.elasticsearch.annotations.Document; import org.springframework.data.elasticsearch.annotations.Field; import org.springframework.data.elasticsearch.annotations.FieldType; import java.io.Serializable; @Data @Builder @NoArgsConstructor @AllArgsConstructor //此注解是SpringDataElasticsearch标记实体类的注解,indexName指定了ES中的索引名称 //如索引不存在,会自动创建,因为该注解的createIndex属性,默认为true; //详细信息,请查看注解源代码 @Document(indexName = "items") public class Item implements Serializable { //表示当前字段为ES的主键 @Id private Long id; /** * 标题 */ //标记title属性的类型与支持分词以及相关分词器 @Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_max_word") private String title; /** * 类别 */ //类别不需要分词,所以不设置analyzer与searchAnalyzer属性 //并且设置type = FieldType.Keyword //在ES中,Keyword是不需要分词的字符串 @Field(type = FieldType.Keyword) private String category; /** * 品牌 */ @Field(type = FieldType.Keyword) private String brand; /** * 单价 */ @Field(type = FieldType.Double) private Double price; /** * 图片路径 */ //因为图片路径是不会用来搜索的,所以设置index=false //数据也是会存储在ES中,只是不会用来当成搜索的条件 @Field(type = FieldType.Keyword,index = false) private String imgPath; }
在Spring家族框架中,规范表明,接口包应名为repository
接口的名为XXXRepository,类似Mybatis中的mapper,此处为ItemRepository
请注意,SpringData可以根据方法名称自动生成对应的实现,但是方法名称必须按照规范来
复杂的查询语句,推荐自己写原生的代码,具体参考最下面的测试类
简单的查询语句,基本语法为 findBy + 属性 + 关键词 + 连接符,示例如下
关键词 | 命名规则 | 解释 | 方法命名示例 |
---|---|---|---|
and | findByField1AndField2 | 根据Field1与Field2查询 | findByTitleAndBrand |
or | findByField1OrField2 | 根据Field1或Field2查询 | findByTitleOrBrand |
is | findByField | 根据Field获得数据 | findByTitle |
not | findByFieldNot | 根据Field获得补集 | findByTitleNot |
between | findByFieldBetween | 根据Field的范围查询 | findByPriceBetween |
package cn.tedu.search.pojo.po.repository; import cn.tedu.search.pojo.po.Item; import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; import org.springframework.stereotype.Repository; import java.util.List; /** * @version 0.0.1 * @Author: LianZuoYang * @Create: 2022-08-26 11:32 */ @Repository public interface ItemRepository extends //继承此接口,并设置泛型为Item(实体类),Long(实体类@id注解的主键类型) //继承后,当前接口就会被识别为连接ES的持久层 //SpringData会为他自动生成基本的增删改查方法 ElasticsearchRepository<Item,Long> { //⽅法命名规则查询的基本语法 findBy + 属性 + 关键词 + 连接符(属性 + 关键词 + 连接符) //根据 标题查询 List<Item> findByTitle(String title); //根据 标题以及品牌查询 List<Item> findByTitleAndBrand(String title,String brand); //根据品牌查询 List<Item> findByBrand(String brand); //根据品牌查询第一个 Optional<Item> findTopByBrand(String brand); //根据 标题或品牌查询 List<Item> findByTitleOrBrand(String title,String brand); //另外一种方法声明的写法 //查询多个结果 List<Item> queryItemsByTitleMatches(String title); //只查询一个 Optional<Item> queryTopByTitleMatches(String title); //多条件查询 List<Item> queryItemsByTitleMatchesAndBrandMatches(String title, String brand); //多条件查询 List<Item> queryItemsByTitleMatchesOrBrandMatches(String title, String brand); //查询多个结果并按照价格排序(默认按照分数/匹配度排) List<Item> queryItemsByTitleMatchesOrderByPriceDesc(String title); }
package cn.tedu.search; import cn.tedu.search.pojo.po.Item; import cn.tedu.search.pojo.po.repository.ItemRepository; import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.index.query.QueryBuilders; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.data.domain.PageRequest; import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate; import org.springframework.data.elasticsearch.core.SearchHit; import org.springframework.data.elasticsearch.core.SearchHits; import org.springframework.data.elasticsearch.core.query.NativeSearchQuery; import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Optional; /** * @version 0.0.1 * @Author: LianZuoYang * @Create: 2022-08-26 10:35 */ @SpringBootTest public class ESTests { //装配ItemRepository(SpringData自动生成的) @Autowired private ItemRepository itemRepository; //如果希望使用原生查询,那么让Spring帮你装配ElasticsearchRestTemplate这个对象就可以了 //老版本的ElasticsearchTemplate已经被弃用了 @Autowired private ElasticsearchRestTemplate restTemplate; @Test void save() { //测试插入数据 Item item = Item.builder() .id(1L) .title("雷蛇 Razer 炼狱蝰蛇标准版 黑色新版 人体工程学 侧键 6400DPI 电竞游戏 有线鼠标") .category("鼠标") .brand("雷蛇") .price(89.00) .imgPath("/1.jpg") .build(); itemRepository.save(item); System.out.println("插入完毕"); } @Test void saveAll() { //测试批量插入数据 Item item1 = Item.builder() .id(2L) .title("戴尔(DELL)MS116 鼠标有线 商务办公经典对称 有线鼠标 USB接口 即插即用 鼠标 (黑色)") .category("鼠标") .brand("戴尔") .price(22.9) .imgPath("/2.jpg") .build(); Item item2 = Item.builder() .id(3L) .title("罗技(G)G502 HERO主宰者有线鼠标 游戏鼠标 HERO引擎 RGB鼠标 电竞鼠标 25600DPI") .category("鼠标") .brand("罗技") .price(249.0) .imgPath("/3.jpg") .build(); Item item3 = Item.builder() .id(4L) .title("罗技(Logitech)M185鼠标 无线鼠标 办公鼠标 对称鼠标 黑色灰边 带无线2.4G接收器") .category("鼠标") .brand("罗技") .price(49.0) .imgPath("/4.jpg") .build(); List<Item> list = new ArrayList<>(); list.add(item1); list.add(item2); list.add(item3); itemRepository.saveAll(list); System.out.println("批量插入完毕"); } @Test void count() { long count = itemRepository.count(); System.out.println("总记录数量:" + count); } @Test void findById() { //根据ID获取一个数据 Optional<Item> optional = itemRepository.findById(1L); if (optional.isPresent()) { Item item = optional.get(); System.out.println(item); } else { System.out.println("此ID不存在"); } } @Test void findByTitle() { //根据条件查询 List<Item> items = itemRepository.findByTitle("USB"); System.out.println("共计搜索到数据条数:" + items.size()); for (Item item : items) { System.out.println(item); } } @Test void findByBrand() { //根据条件查询 List<Item> items = itemRepository.findByBrand("雷蛇"); System.out.println("共计搜索到数据条数:" + items.size()); for (Item item : items) { System.out.println(item); } items = itemRepository.findByBrand("罗技"); System.out.println("共计搜索到数据条数:" + items.size()); for (Item item : items) { System.out.println(item); } } @Test void findTopByBrand(){ Optional<Item> optional = itemRepository.findTopByBrand("罗技"); if(optional.isPresent()){ System.out.println(optional.get()); }else { System.out.println("未查询到数据"); } } @Test void findByTitleAndBrand() { //根据条件查询 List<Item> items = itemRepository.findByTitleAndBrand("鼠标", "雷蛇"); System.out.println("共计搜索到数据条数:" + items.size()); for (Item item : items) { System.out.println(item); } } @Test void findByTitleOrBrand() { //根据条件查询 List<Item> items = itemRepository.findByTitleOrBrand("DPI", "罗技"); System.out.println("查询结果数量:" + items.size()); for (Item item : items) { System.out.println(item); } } @Test void testNativeSearchQuery() { //使用原生查询,而不使用接口去声明 //复杂的查询条件嵌套的,使用QueryBuilders.boolQuery()去处理 //下面的代码的查询结果与上面的findByTitleOrBrand()的结果是一致的, //title包含[DPI]或brand为[罗技] NativeSearchQuery nativeSearchQuery = new NativeSearchQueryBuilder() .withQuery( QueryBuilders.boolQuery() .should(QueryBuilders.queryStringQuery("DPI").defaultField("title")) .should(QueryBuilders.queryStringQuery("罗技").defaultField("brand")) ) .withPageable(PageRequest.of(0, 15))//第一页,15个结果 .build(); //获取查询结果 SearchHits<Item> hits = restTemplate.search(nativeSearchQuery, Item.class); //枚举结果 System.out.println("查询结果数量:" + hits.getTotalHits()); for (SearchHit<Item> hit : hits) { Item item = hit.getContent(); System.out.println(item); } } @Test void testNativeSearchQuery2() { //使用原生查询,演示复杂的嵌套 //(title包含[DPI]且包含[标准版]) 或 brand为[罗技] //转换为sql的话,类似如下语句 //select * from items where (title like '%DPI%' AND title like '%标准版%') or brand = '罗技' NativeSearchQuery nativeSearchQuery = new NativeSearchQueryBuilder() .withQuery( QueryBuilders.boolQuery() .should(QueryBuilders.boolQuery() .must(QueryBuilders.queryStringQuery("DPI").defaultField("title")) .must(QueryBuilders.queryStringQuery("标准版").defaultField("title")) ) .should(QueryBuilders.queryStringQuery("罗技").defaultField("brand")) ) .withPageable(PageRequest.of(0, 15))//第一页,15个结果 .build(); //获取查询结果 SearchHits<Item> hits = restTemplate.search(nativeSearchQuery, Item.class); //枚举结果 System.out.println("查询结果数量:" + hits.getTotalHits()); for (SearchHit<Item> hit : hits) { Item item = hit.getContent(); System.out.println(item); } } //另外一种写法的测试 @Test void queryItem(){ Optional<Item> optional = itemRepository.queryTopByTitleMatches("鼠标"); if(optional.isPresent()){ System.out.println(optional.get()); }else { System.out.println("未查询到数据"); } } @Test void queryItems(){ List<Item> items = itemRepository.queryItemsByTitleMatches("鼠标"); System.out.println("查询到的数量:"+items.size()); for (Item item : items) { System.out.println(item); } } @Test void queryItems2(){ List<Item> items = itemRepository.queryItemsByTitleMatchesAndBrandMatches("DPI","罗技"); System.out.println("查询到的数量:"+items.size()); for (Item item : items) { System.out.println(item); } } @Test void queryItems3(){ List<Item> items = itemRepository.queryItemsByTitleMatchesOrBrandMatches("DPI","罗技"); System.out.println("查询到的数量:"+items.size()); for (Item item : items) { System.out.println(item); } } }
参考CrudRepository接口,里面声明了一些基础的方法,就不在这里演示了,请自行查看!
此处提供一个ES 7.6.2的压缩包,已集成ik,下载后可直接运行使用
https://download.csdn.net/download/lianzuo123/86500446
达内:lzy
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。