学这个之前先来说一下ElasticSearch也是一个数据库,我们连上mysql,redis和ElasticSearch也就三个数据库了,接下来我们对比一下这三个数据库:
Mysql:
分类: 关系型数据库
概述: mysql在存储数据时,数据和数据之间有一定的关联关系
存储介质: 存放在硬盘上
优点: 不会导致数据丢失
缺点: 执行效率低
硬盘 ---> 内存 ---> CPU
事务控制,这也是他没被替换掉的原因
redis:
分类: 非关系型数据库
概述: 数据在存储时,数据和数据之间没有关联关系
存储介质: 内存
优点: 执行效率高
缺点: 可能会导致数据丢失
ElasticSearch: 灵活的搜索
分类: 非关系型数据库
概述: ES专门用于对海量数据的搜索
存储介质: 内存
优点: 搜索效率高
数据库排行查询网址:https://db-engines.com/
搜索引擎-ElasticSearch
1、面临问题
在模糊查询中,由于索引失效,mysql的查询效率太过低下。
2、解决方案-ElasticSearch
2.1、简介
【elasticsearch】
elasticsearch是一款非常强大的开源搜索引擎,具备非常多强大功能,可以帮助我们从海量数据中快速找到需要的内容.
【ELK】
elasticsearch结合kibana、Logstash、Beats,也就是elastic stack(ELK)。被广泛应用在日志数据分析、实时监控等领域:而elasticsearch是elastic stack的核心,负责存储、搜索、分析数据。
【Lucene】
elasticsearch底层是基于lucene来实现的。 Lucene是一个Java语言的搜索引擎类库,是Apache公司的顶级项目,由DougCutting于1999年研发。 官网:https://lucene.apache.org/
2.2、优缺点
【优点】
检索数据非常快
【缺点】
处理数据少的时候就不如sql了,因为他要倒叙排序再去分词,然后再去查找,如果数据量少的话不如sql数据库
2.3、运行原理-倒排索引
【正向索引】
正向索引是最传统的,根据id索引的方式。但根据词条查询时,必须先逐条获取每个文档,然后判断文档中是否包含所需要的词条,是根据文档找词条的过程. 优点: 可以给多个字段创建索引 根据索引字段搜索、排序速度非常快 缺点: 根据非索引字段,或者索引字段中的部分词条查找时,只能全表扫描。
【倒排索引】
倒排索引则相反,是先找到用户要搜索的词条,根据词条得到保护词条的文档的id,然后根据id获取文档。是根据词条找文档的过程。 优点: 根据词条搜索、模糊搜索时,速度非常快 缺点: 只能给词条创建索引,而不是字段 无法根据字段做排序
2.4、相关名词
【索引库】
索引(Index),就是相同类型的文档的集合。类似于mysql中的表
【mapping映射】
映射(mapping),是索引中文档的字段约束信息,类似mysql表的结构约束。
【文档】
文档(Document),就是一条条的数据,类似数据库中的行(Row),文档都是JSON格式
【词条】
词条(Term),对文档数据或用户搜索数据,利用某种算法分词,得到的具备含义的词语就是词条。
2.5、Kibana
可视化工具,在浏览器输入DSL语句即可查看ES中的内容。
3、使用步骤
3.1、Kibana浏览器操作-索引库&文档
【索引库操作】
【创建索引库】
PUT /索引库名称 { "mappings": { "properties": { "字段名":{ "type": "text", "analyzer": "ik_smart" }, "字段名2":{ "type": "keyword", "index": "false" }, "字段名3":{ "properties": { "子字段": { "type": "keyword" } } } } } }
【查看索引库】
GET /索引库名
【删除索引库】
DELETE /索引库名
【修改索引库(追加)】 这个也可以修改也可以新增,如果有这个索引就是先删除再从新新增直接覆盖,如果没有那么就新增
PUT /索引库名/_mapping { "properties":{ "新字段":{ "type":"数据类型(integer)" } } }
【文档操作】
【新增文档】
POST /索引库名/_doc/文档id(不给会随机生成id) { "字段1": "值1", "字段2": "值2", "字段3": { "子属性1": "值3", "子属性2": "值4" } }
【查看文档】
GET /索引库名/_doc/文档id
【删除文档】
DELETE /索引库名/_doc/文档id
【修改文档】
【全量修改】
PUT /索引库名/_doc/文档id { "字段1": "值1", "字段2": "值2" }
【增量修改】
POST /索引库名/_update/文档id { "doc": { "字段名": "新的值" } }
3.2、java代码操作-索引库&文档
【导入依赖】
<dependency> <groupId>org.elasticsearch.client</groupId> <artifactId>elasticsearch-rest-high-level-client</artifactId> </dependency>
【与版本对应】
<properties> <java.version>1.8</java.version> <elasticsearch.version>7.12.1</elasticsearch.version> </properties>
【初始化连接&关闭连接】
private RestHighLevelClient client;
// 创建连接
@BeforeEach
void setUp() {
this.client = new RestHighLevelClient(RestClient.builder(
HttpHost.create("http://192.168.248.222:9200")
));
}
// 关闭连接
@AfterEach
void tearDown() throws IOException {
this.client.close();
}
【索引库操作】
【创建索引库】
// 创建索引库
@Test
public void createIndex() throws IOException {
// 1.创建请求语义对象
CreateIndexRequest request = new CreateIndexRequest("hotel");
// 添加 source
request.source(HotelConstants.MAPPING_TEMPLATE, XContentType.JSON);
// 2.发送请求
CreateIndexResponse response = client.indices().create(request, RequestOptions.DEFAULT);
// 3.解析响应
boolean flag = response.isAcknowledged();
// 打印结果
System.out.println(flag);
}
【删除索引库】
// 删除索引库
@Test
public void deleteIndex() throws IOException {
// 1.创建请求语义对象
DeleteIndexRequest request = new DeleteIndexRequest("hotel");
// 2.发送请求
AcknowledgedResponse response = client.indices().delete(request, RequestOptions.DEFAULT);
// 3.解析响应
boolean flag = response.isAcknowledged();
// 打印结果
System.out.println(flag);
}
【查看索引库是否存在】
// 查看索引库是否存在
@Test
public void existsIndex() throws IOException {
// 1.创建请求语义对象
GetIndexRequest request = new GetIndexRequest("hotel");
// 2.发送请求
boolean flag = client.indices().exists(request, RequestOptions.DEFAULT);
// 3.解析响应
// 打印结果
System.out.println(flag);
}
【文档操作】
【添加文档数据】
@Autowired private IHotelService hotelService; // 添加文档数据
@Test
public void add() throws IOException {
// 1.根据id获取数据
Hotel hotel = hotelService.getById(36934L);
// 2.转换成 ES 对应的数据
HotelDoc hotelDoc = new HotelDoc(hotel);
// 3.转换成JSON
String hotelDocJson = JSON.toJSONString(hotelDoc);
// 4.创建请求语义对象 这个语义对象就是相当于es中的put
// 所以如果我们es库中有这个字段的数据那么就删除再新增就完成了修改,如果没有这个数据
// 那么就会直接新增 就是全量修改
IndexRequest request = new IndexRequest("hotel");
request.id(hotel.getId() + "");
request.source(hotelDocJson, XContentType.JSON);
// 5.发送请求
IndexResponse response = client.index(request, RequestOptions.DEFAULT);
// 6.解析响应结果
DocWriteResponse.Result result = response.getResult();
System.out.println(result);
}
【查询文档数据】
// 查询文档数据
@Test
public void select() throws IOException {
// 1.创建请求语义对象
GetRequest request = new GetRequest("hotel","36934");
// 2.发送请求
GetResponse response = client.get(request, RequestOptions.DEFAULT);
// 3.解析响应结果
String json = response.getSourceAsString();
HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
System.out.println(hotelDoc);
}
【修改文档数据】
// 修改文档数据 这个修改文档是增量修改,可以根据给定字段给定值去修改
@Test
public void update() throws IOException {
// 1.创建请求语义对象
UpdateRequest request = new UpdateRequest("hotel","36934");
request.doc(
"name", "7天连锁酒店(上海宝山路地铁站店)"
);
// 2.发送请求
UpdateResponse response = client.update(request, RequestOptions.DEFAULT);
// 3.解析响应结果
DocWriteResponse.Result result = response.getResult();
System.out.println(result);
}
【删除文档数据】
// 删除文档数据
@Test
public void delete() throws IOException {
// 1.创建请求语义对象
DeleteRequest request = new DeleteRequest("hotel","36934");
// 2.发送请求
DeleteResponse response = client.delete(request, RequestOptions.DEFAULT);
// 3.解析响应结果
DocWriteResponse.Result result = response.getResult();
System.out.println(result);
}
【批量添加】
@Autowired
private IHotelService hotelService;
// 批量添加文档数据
@Test
public void add() throws IOException {
// 1.批量查询酒店数据
List<Hotel> hotels = hotelService.list();
// 2.创建请求语义对象
BulkRequest request = new BulkRequest();
for (Hotel hotel : hotels) {
// 转换格式
HotelDoc hotelDoc = new HotelDoc(hotel);
String hotelDocJson = JSON.toJSONString(hotelDoc);
IndexRequest indexRequest = new IndexRequest("hotel");
indexRequest.id(hotel.getId() + "");
indexRequest.source(hotelDocJson, XContentType.JSON);
request.add(indexRequest);
}
// 5.发送请求
BulkResponse response = client.bulk(request, RequestOptions.DEFAULT);
// 6.解析响应结果
RestStatus status = response.status();
System.out.println(status);
}
3.3、Kibana浏览器-查询&结果处理
【查询所有】
GET /索引库名/_search { "query": { "match_all": { } } }
【全文检索查询】
【多字段】
GET /索引库名/_search { "query": { "multi_match": { "query": "内容", "fields": ["字段1", "字段2"] } } }
【单字段】
GET /hotel/_search { "query": { "match": { "字段": "值" } } }
【精确查询】
【term查询】
GET /索引库名/_search { "query": { "term": { "字段": { "value": "值" } } } }
【range查询】
GET /索引库名/_search { "query": { "range": { "字段": { "gte": 10, // 这里的gte代表大于等于,gt则代表大于 "lte": 20 // lte代表小于等于,lt则代表小于 } } } }
【地理坐标查询】
【矩形范围查询】
GET /索引库名/_search { "query": { "geo_bounding_box": { "字段": { "top_left": { // 左上点 "lat": 31.1, "lon": 121.5 }, "bottom_right": { // 右下点 "lat": 30.9, "lon": 121.7 } } } } }
【附近/距离查询】
GET /索引库名/_search { "query": { "geo_distance": { "distance": "15km", // 半径 "字段": "31.21,121.5" // 圆心 } } }
【复合查询】
【算分函数查询】
GET /hotel/_search { "query": { "function_score": { "query": { // 原始查询条件 根据BM25算法进行算分 "match": { "all": "外滩" } }, "functions": [ { // 过滤条件 符合条件重新算分 "filter": { "term": { "brand": "如家" } }, //算分函数 weight是常量值 "weight": 10 } ], //加权模式 默认是multiply "boost_mode": "sum" } } }
【布尔查询】
GET /hotel/_search { "query": { "bool": { //必须匹配 "must": [ { "match": { "all": "上海" } } ], //选择匹配 "should": [ { "term": { "brand": { "value": "皇冠假日" } } }, { "term": { "brand": "华美达" } } ], //必须不匹配 不参与算分 "must_not": [ { "range": { "price": { "lte": 400 } } } ], //必须匹配 不参与算分 "filter": [ { "geo_distance": { "distance": "10km", "location": { "lat": 31.21, "lon": 121.5 } } } ] } } }
【排序】
【普通字段】
GET /索引库名/_search { "query": { "match_all": {} }, "sort": [ { "字段": "desc" // 排序字段、排序方式ASC、DESC } ] }
【地理坐标】
GET /hotel/_search { "query": { "match_all": {} }, "sort": [ { "_geo_distance": { "地理坐标相关字段": { "lat": 31.21, "lon": 121.5 }, "order":"asc", //单位 "unit":"km" } } ] }
【分页】
【基本分页】
GET /hotel/_search { "query": { "match_all": {} }, "from": 0, // 分页开始的位置,默认为0 "size": 10, // 期望获取的文档总数 "sort": [ {"price": "asc"} ] }
【深度分页】
【高亮】
GET /hotel/_search { "query": { "match": { "FIELD": "TEXT" // 查询条件,高亮一定要使用全文检索查询 } }, "highlight": { "fields": { // 指定要高亮的字段 "FIELD": { "pre_tags": "<em>", // 用来标记高亮字段的前置标签 "post_tags": "</em>", // 用来标记高亮字段的后置标签 "require_field_match": "false" } } } }
3.4、java代码操作-查询&结果处理
/** * 提示: * term和match的区别: * term匹配的是比如我们输入的是 如家 那么就根据如家去查 输入 如家aaa 那么就根据如家aaa查 * match匹配的是比如我们输入的是 如家 那么就根据如家去查 输入 如家aaa 那么就根据如家和aaa去查 具体怎么分词根据我们的分词器来规定 * 总结:区别就是一个会查询的时候会分词一个不会 */
【查询所有】
// 查询所有--match all
/**
* 查询方式:matchAll
* 查询所有
* 对应的es语句:
* GET /hotel/_search{
* "query":{
* "match_all":{}
* }
* }
*/
@Test
public void testSearch() throws IOException {
SearchRequest request = new SearchRequest("hotel");
request.source().query(QueryBuilders.matchAllQuery());
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
SearchHit[] hits = response.getHits().getHits();
for (SearchHit hit : hits) {
System.out.println("hit = " + hit);
}
}
【全文检索查询】
// 全文检索查询--match
/**
* 查询方式: match
* 查询单个字段,也是可以进行分词的这个字段,前提是这个字段支持分词
* 对应es语句:
* GET /hotel/_search
* {
* "query":{
* "match":{
* "name": "如家"
* }
* }
* }
* 如果name改成brand,并且根据 如 字查,那么肯定查不出来,因为brand是keyword,如果根据brand查询 内容是 如家 那么就可以了,因为brand整个字段是如家的是纯在的
*/
@Test
public void testMatch() throws IOException {
SearchRequest request = new SearchRequest("hotel");
request.source().query(QueryBuilders.matchQuery("name", "如家"));
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
SearchHit[] hits = response.getHits().getHits();
for (SearchHit hit : hits) {
System.out.println("hit = " + hit);
}
}
多条件查询 mutil_match
/**
* 查询方式: multi_match
* 多条件查询,相当于是sql中的and查询,这里面查的是name里面有 如 字的,因为name是可以分词的,那么就可以查询出来,
* 但是brand是不能分词的,只能查全部的,所以brand没有满足的,所以查不出来,除非brand的整个字段放进去然后再去看有没有分词name匹配上的,如果有就命中
* 对应es语句:
* GET /hotel/_search
* {
* "query": {
* "mutil_match": {
* "query": "如",
* "fields": ["name","brand"]
* }
* }
* }
*/
@Test
public void testMatch_Multiple() throws IOException {
SearchRequest request = new SearchRequest("hotel");
request.source()
.query(QueryBuilders.multiMatchQuery("如", "name", "brand"));
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
SearchHit[] hits = response.getHits().getHits();
for (SearchHit hit : hits) {
System.out.println("hit = " + hit);
}
}
【精确查询】
【term查询】
// 精确查询--term
/**
* 精准查询:term
* term匹配的是比如我们输入的是 如家 那么就根据如家去查 输入 如家aaa 那么就根据 如家aaa 查
* 语法es:
* GET /hotel/_search
* {
* "query":{
* "term": {
* "name": {
* value: ""如家""
* }
* }
* }
* }
*/
@SneakyThrows
@Test
public void testTerm() {
SearchRequest request = new SearchRequest("hotel");
request.source().query(QueryBuilders.termQuery("name", "如家"));
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
SearchHit[] hits = response.getHits().getHits();
for (SearchHit hit : hits) {
System.out.println("hit = " + hit);
}
}
【range查询】
// 范围查询--range
/**
* 范围查询:range
* 对应es语法:
* GET /hotel/_search
* {
* "query":{
* "range":{
* "price": {
* "gte": 500
* }
* }
* }
* }
*/
@Test
public void testRange() throws IOException {
SearchRequest request = new SearchRequest("hotel");
request.source().query(QueryBuilders.rangeQuery("price").gte("500").lte("800"));
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
SearchHit[] hits = response.getHits().getHits();
for (SearchHit hit : hits) {
String hotelJson = hit.getSourceAsString();
System.out.println("hotelJson = " + hotelJson);
}
}
【复合查询】
【布尔查询】
// 布尔查询--bool
/**
* bool查询
* 对应es语句:
* GET /hotel/_search
* {
* "query":{
* "bool": {
* "must":[
* {
* "match": {
* "name": "如家"
* }
* },
* {
* "match":{
* "id": "415600"
* }
* },
* {
* "range":{
* "price":{
* "lte": 415600
* }
* }
* }
* ]
* }
* }
* }
* 如果想要多个字段查询,那么就直接在语义对象上添加must就可以
*/
@Test
public void testBool() throws IOException {
SearchRequest request = new SearchRequest("hotel");
// 2.准备DSL
// 2.1.准备BooleanQuery
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
//2.2.添加must
boolQueryBuilder.must(QueryBuilders.termQuery("name", "如家"));
boolQueryBuilder.must(QueryBuilders.termQuery("id", "415600"));
//2.3.添加range
boolQueryBuilder.filter(QueryBuilders.rangeQuery("price").lte(300));
//3.发送请求
request.source().query(boolQueryBuilder);
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
SearchHit[] hits = response.getHits().getHits();
for (SearchHit hit : hits) {
System.out.println("hit = " + hit.getSourceAsString());
}
}
【排序分页】
// 排序、分页--sort from size 处于性能考虑,es规定from+size<=10000
/**
* form size 排序分页查询
* 对应es语句:
* GET /hotel/_search
* {
* "query":{
* “match_all”:{}
* },
* "sort":[
* {
* "id":{
* "order":"desc"
* }
* }
* ]
* "from": 0,
* size: 2
*
* }
*/
@Test
public void testFormAndSize() throws IOException {
//搜索结果的排序和分页是与query同级的参数,因此同样是使用request.source()来设置。
SearchRequest request = new SearchRequest("hotel");
//设置分页,搜索结果的排序和分页是与query同级的参数,因此同样是使用request.source()来设置。
request.source().from(0).size(2);
request.source().sort("id", SortOrder.DESC);
//设置查询条件 这里是查询全部
request.source().query(QueryBuilders.matchAllQuery());
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
SearchHit[] hits = response.getHits().getHits();
for (SearchHit hit : hits) {
String json = hit.getSourceAsString();
System.out.println("json = " + json);
}
}
【高亮】
// 高亮--highlights
/**
* 高亮
*/
@Test
public void testHightField() throws IOException {
//搜索结果的排序和分页是与query同级的参数,因此同样是使用request.source()来设置。
SearchRequest request = new SearchRequest("hotel");
//设置分页,搜索结果的排序和分页是与query同级的参数,因此同样是使用request.source()来设置。
request.source().from(0).size(2);
request.source().sort("id", SortOrder.DESC);
//这里可以插叙多个字段,如果是多个字段那么就应该下边getFragments()[0]设置需要的那个字段了
request.source().highlighter(new HighlightBuilder().field("name").requireFieldMatch(false));
//设置查询条件 这里是查询全部
request.source().query(QueryBuilders.matchQuery("name","如家"));
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
SearchHit[] hits = response.getHits().getHits();
for (SearchHit hit : hits) {
System.out.println("hit = " + hit.getSourceAsString());
//我们es的高亮显示是hit中是完整的,但是他高亮的地方是另外展示的
//下边的map是得到了一个高亮的集合,我们上边设置的我们我们的高亮字段只有name,那么我i们就获取
Map<String, HighlightField> highlightFields = hit.getHighlightFields();
if (!highlightFields.isEmpty()) {
String name = highlightFields.get("name").getFragments()[0].toString();
System.out.println("name = " + name);
}
}
}
【响应结果解析】
// 响应结果解析
private void handleResponse(SearchResponse response) {
// 4.解析响应结果
SearchHits searchHits = response.getHits();
// 查询总条数
long total = searchHits.getTotalHits().value;
System.out.println("查询总条数:" + total);
// 查询结果数组
SearchHit[] hits = searchHits.getHits();
// 遍历
for (SearchHit hit : hits) {
// 得到json
String hotelDocJson = hit.getSourceAsString();
// 转换成Java对象
HotelDoc hotelDoc = JSON.parseObject(hotelDocJson, HotelDoc.class);
// 高亮特殊处理
Map<String, HighlightField> highlightFields = hit.getHighlightFields();
if (!CollectionUtils.isEmpty(highlightFields)) {
// 高亮字段结果
HighlightField highlightField = highlightFields.get("name");
if (highlightField!=null) {
// 不为空时的第一个就是酒店名称
String name = highlightField.getFragments()[0].string();
hotelDoc.setName(name);
}
}
// 打印结果
System.out.println(hotelDoc);
}
}
3.5、Kibana浏览器-聚合
GET /hotel/_search { "query": {//限定聚合条件 "range": { "price": { "lte": 200 // 只对200元以下的文档聚合 } } }, "size": 0, // 设置size为0,结果中不包含文档,只包含聚合结果 "aggs": { // 定义聚合 "brandAgg": { //给聚合起个名字 "terms": { // 聚合的类型,按照品牌值聚合,所以选择term "field": "brand", // 参与聚合的字段 "order": { "_count": "asc" // 按照_count升序排列 }, "size": 20 // 希望获取的聚合结果数量 }, "aggs": { // 是brands聚合的子聚合,也就是分组后对每组分别计算 "score_stats": { // 聚合名称 "stats": { // 聚合类型,这里stats可以计算min、max、avg等 "field": "score" // 聚合字段,这里是score } } } } } }
3.6、java代码操作-聚合
【聚合查询】
// 聚合查询--aggregation
/**
* 根据品牌分桶
* 对应es语句:
* GET /hotel/_search
* {
* "aggs":{
* "brandAgg":{
* "terms":{
* "field": "brand",
* "size": 4
* }
* }
* }
*
* }
*/
@Test
public void testAgg1() throws IOException {
//创建语义对象
SearchRequest request = new SearchRequest("hotel");
//准备DEL语句,这句话是使用AggregationBuilders创建一个名称为brandAgg的桶,按照brand字段分捅,查询出来前四个
request.source().aggregation(AggregationBuilders.terms("brandAgg").field("brand").size(4));
//发送给ES请求得到响应
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
//根据响应得到桶指定brandAgg捅的信息,等号前边表示使用什么格式的查询就是用什么接收,上边是使用了terms所以这里就是用Terms接收
Terms brandAgg = response.getAggregations().get("brandAgg");
//得到里面捅的集合
List<? extends Terms.Bucket> buckets = brandAgg.getBuckets();
for (Terms.Bucket bucket : buckets) {
//得到key和doc_count
System.out.println(bucket.getKeyAsString()+" : "+bucket.getDocCount());
}
}
【响应结果解析】
// 响应结果解析
private void handleResponse(SearchResponse response) {
// 4.解析聚合响应结果
Aggregations aggregations = response.getAggregations();
// 根据名称获得聚合结果
Terms brandTerms = aggregations.get("brand_agg");
// 获取桶
List<? extends Terms.Bucket> buckets = brandTerms.getBuckets();
// 遍历
for (Terms.Bucket bucket : buckets) {
// 获取品牌信息
String brandName = bucket.getKeyAsString();
long count = bucket.getDocCount();
System.out.println(brandName+":"+count);
}
}
分桶案例代码
/**
* 显示品牌 星级 价格
*/
@Override
public Map<String, List<String>> filtersQuery(RequestParams params) throws IOException {
SearchRequest request = new SearchRequest("hotel");
//这个是让我们的数据不显示
request.source().size(0);
//判断key是不是空,如果不为空那么就添加搜索条件
if (!Strings.isNullOrEmpty(params.getKey())) {
request.source().query(QueryBuilders.boolQuery()
.must(QueryBuilders.matchQuery("all", params.getKey())));
}
//判断价格的范围是不是空,如果不为空那么就添加搜索条件
if (!Strings.isNullOrEmpty(params.getMinPrice()+"") && !Strings.isNullOrEmpty(params.getMaxPrice()+"")) {
request.source().query(QueryBuilders.boolQuery()
.must(QueryBuilders.rangeQuery("price").gte(params.getMinPrice()).lte(params.getMaxPrice())));
}
//添加聚合函数,一共分了三个捅
request.source()
//设置捅名为cityAdds ,分桶的字段按照city字段来分桶
.aggregation(AggregationBuilders.terms("cityAdds").field("city"))
//设置捅名为brandAggs ,分桶的字段按照brand字段来分桶
.aggregation(AggregationBuilders.terms("brandAggs").field("brand"))
//设置捅名为starAgg ,分桶的字段按照starName字段来分桶
.aggregation(AggregationBuilders.terms("starAgg").field("starName"));
//发送请求给es
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
//获取捅的数据集合
List<String> cityAdds = getAggByName(response, "cityAdds");
List<String> brandAggs = getAggByName(response, "brandAggs");
List<String> starAgg = getAggByName(response, "starAgg");
HashMap<String, List<String>> map = new HashMap<>();
map.put("brand", brandAggs);
map.put("city", cityAdds);
map.put("starName", starAgg);
//返回给前端
return map;
}
上边分桶案例解析代码:
/**
* 提取方法,将响应结果的分桶提取出来key
*/
public List<String> getAggByName(SearchResponse response, String aggName) {
//创建集合用来收集key
List<String> keyList = new ArrayList<>();
//得到整个捅的对象
Aggregations aggregations = response.getAggregations();
//得带指定名称的捅
Terms aggTerms = aggregations.get(aggName);
List<? extends Terms.Bucket> buckets = aggTerms.getBuckets();
buckets.forEach(bucket -> keyList.add(bucket.getKeyAsString()));
return keyList;
}
使用拼音分词和自动补齐
在我们的settings设置的时候我们具体的有些设置太多了,接下来是官方的一个图片,有一些配置可以参考一下:
使用拼音分词器的话需要我们安装拼音分词器,具体怎么安装 可以百度
例如使用后代码例子为:
POST /_analyze
{
"text": "如家酒店还不错",
"analyzer": "pinyin"
}
自定义分词器
默认的拼音分词器会将每个汉字单独分为拼音,而我们希望的是每个词条形成一组拼音,需要对拼音分词器做个性化定制,形成自定义分词器。
elasticsearch中分词器(analyzer)的组成包含三部分:
character filters:在tokenizer之前对文本进行处理。例如删除字符、替换字符
tokenizer:将文本按照一定的规则切割成词条(term)。例如keyword,就是不分词;还有ik_smart
tokenizer filter:将tokenizer输出的词条做进一步处理。例如大小写转换、同义词处理、拼音处理等
如上图,也就是说我们会先过滤,将一些符号什么的替换,然后再对过滤后的内容进行分词,分完词以后再对我们分词后的词语进行拼音分词,这样我们就可以得到中文的分词和拼音的分词
但是如果我们在使用的时候在搜索的时候用拼音+中文分词,那么就会出现如下一个例子:
当我们文档中有 文档id为1:name=“狮子” 文档2的id为2:name=“虱子”
当我们搜索时搜索 name=“狮子” 这时候我们的分词就是 狮子 sz shizi 当通过狮子查的时候查出来了文档1,但是接下来通过sz和shizi查那么就是文档2也就满足了,这样就导致了我们命名查询的狮子但是虱子也被查出来了。这是不好的,那么我们就设置当我们查询的时候就不使用拼音分词器,直接使用 ik 分词器,这样在输入狮子那么就没用拼音分词了,当我们输入的拼音,那么直接根据我们输入的拼音去匹配拼音分词
如何使用拼音分词器?
①下载pinyin分词器
②解压并放到elasticsearch的plugin目录
③重启即可
如何自定义分词器?
①创建索引库时,在settings中配置,可以包含三部分
②character filter
③tokenizer
④filter
拼音分词器注意事项?
为了避免搜索到同音字,搜索时不要使用拼音分词器
自动补全
elasticsearch提供了Completion Suggester查询来实现自动补全功能。这个查询会匹配以用户输入内容开头的词条并返回。为了提高补全查询的效率,对于文档中字段的类型有一些约束:
-
参与补全查询的字段必须是completion类型。
-
字段的内容一般是用来补全的多个词条形成的数组。
completion: 被其修饰的字段可以自动补全
自动补全示例:
# 自动补全查询
GET /test/_search
{
"suggest": {
"title_suggest": {
"text": "n", // 关键字
"completion": {
"field": "title", // 补全查询的字段
"skip_duplicates": true, // 跳过重复的
"size": 10 // 获取前10条结果
}
}
}
}
学习案例Java自动补全代码示例:
/**
* 通过搜索得到联想次词
*
* @param key 搜索框的内容
* @return
*/
@Override
public List<String> getSuggest(String key) {
//设置我们的自动补全函数名称
String suggestName = "getSuggest";
SearchRequest request = new SearchRequest("hotel");
//这个请求的条件,把我们的自动补全字段添加上,然后是前缀为传过来的key的就查出来
request.source().suggest(new SuggestBuilder().addSuggestion(
suggestName, SuggestBuilders.completionSuggestion("suggestion")
.prefix(key)
.skipDuplicates(true)
.size(10)));
//发送请求
SearchResponse response = null;
try {
response = client.search(request, RequestOptions.DEFAULT);
} catch (IOException e) {
e.printStackTrace();
}
//拆关键集合用来收集并且传给前端
List<String> result = new ArrayList();
if (!Objects.isNull(response)) {
//解析获取数据 suggestName是我们的函数名称在上边写了 String suggestName = "getSuggest";
CompletionSuggestion suggestion = response.getSuggest().getSuggestion(suggestName);
List<CompletionSuggestion.Entry.Option> options = suggestion.getOptions();
result = options.stream().map(value -> value.getText().string()).collect(Collectors.toList());
}
return result;
}
对应上边Java的ES的代码如下:
GET /hotel/_search
{
"suggest": {
"sugg_test": {
"text": "ru",
"completion": {
"field": "suggestion"
}
}
}
}
结果:
{
"took" : 1,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 0,
"relation" : "eq"
},
"max_score" : null,
"hits" : [ ]
},
"suggest" : {
"sugg_test" : [
{
"text" : "ru",
"offset" : 0,
"length" : 2,
"options" : [
{
"text" : "如家",
"_index" : "hotel",
"_type" : "_doc",
"_id" : "415600",
"_score" : 1.0,
"_source" : {
"address" : "三间房乡褡裢坡村青年沟西侧558号",
"brand" : "如家",
"business" : "传媒大学/管庄",
"city" : "北京",
"id" : 415600,
"location" : "39.923212, 116.560023",
"name" : "如家酒店(北京朝阳北路传媒大学褡裢坡地铁站店)",
"pic" : "https://m.tuniucdn.com/fb3/s1/2n9c/3NezpxNZWQMdNXibwbMkQuAZjDyJ_w200_h200_c1_t0.jpg",
"price" : 259,
"score" : 47,
"starName" : "二钻",
"suggestion" : [
"如家",
"传媒大学",
"管庄"
]
}
},
{
"text" : "如家",
"_index" : "hotel",
"_type" : "_doc",
"_id" : "416121",
"_score" : 1.0,
"_source" : {
"address" : "莲花池东路120-2号6层",
"brand" : "如家",
"business" : "北京西站/丽泽商务区",
"city" : "北京",
"id" : 416121,
"location" : "39.896449, 116.317382",
"name" : "如家酒店(北京西客站北广场店)",
"pic" : "https://m.tuniucdn.com/fb3/s1/2n9c/42DTRnKbiYoiGFVzrV9ZJUxNbvRo_w200_h200_c1_t0.jpg",
"price" : 275,
"score" : 43,
"starName" : "二钻",
"suggestion" : [
"如家",
"北京西站",
"丽泽商务区"
]
}
},
{
"text" : "如家",
"_index" : "hotel",
"_type" : "_doc",
"_id" : "433576",
"_score" : 1.0,
"_source" : {
"address" : "南京东路480号保安坊内",
"brand" : "如家",
"business" : "人民广场",
"city" : "上海",
"id" : 433576,
"location" : "31.236454, 121.480948",
"name" : "如家酒店(上海南京路步行街店)",
"pic" : "https://m.tuniucdn.com/fb2/t1/G6/M00/52/BA/Cii-U13eXVaIQmdaAAWxgzdXXxEAAGRrgNIOkoABbGb143_w200_h200_c1_t0.jpg",
"price" : 379,
"score" : 44,
"starName" : "二钻",
"suggestion" : [
"如家",
"人民广场"
]
}
},
{
"text" : "如家",
"_index" : "hotel",
"_type" : "_doc",
"_id" : "434082",
"_score" : 1.0,
"_source" : {
"address" : "复兴东路260号",
"brand" : "如家",
"business" : "豫园",
"city" : "上海",
"id" : 434082,
"location" : "31.220706, 121.498769",
"name" : "如家酒店·neo(上海外滩城隍庙小南门地铁站店)",
"pic" : "https://m.tuniucdn.com/fb2/t1/G6/M00/52/B6/Cii-U13eXLGIdHFzAAIG-5cEwDEAAGRfQNNIV0AAgcT627_w200_h200_c1_t0.jpg",
"price" : 392,
"score" : 44,
"starName" : "二钻",
"suggestion" : [
"如家",
"豫园"
]
}
},
{
"text" : "如家",
"_index" : "hotel",
"_type" : "_doc",
"_id" : "441836",
"_score" : 1.0,
"_source" : {
"address" : "西坝河东里36号",
"brand" : "如家",
"business" : "国展中心",
"city" : "北京",
"id" : 441836,
"location" : "39.966238, 116.450142",
"name" : "如家酒店(北京国展三元桥店)",
"pic" : "https://m.tuniucdn.com/fb2/t1/G6/M00/52/39/Cii-TF3eRTGITp1UAAYIilRD7skAAGLngIuAnQABgii479_w200_h200_c1_t0.png",
"price" : 458,
"score" : 47,
"starName" : "二钻",
"suggestion" : [
"如家",
"国展中心"
]
}
}
]
}
]
}
}