备案添加网站,网站建设费专用票,c2c电子商务的特点,wnmp 搭建WordPress1. DSL查询文档 elasticsearch的查询依然是基于JSON风格的DSL来实现的。
1.1. DSL查询分类
Elasticsearch提供了基于JSON的DSL#xff08;Domain Specific Language#xff09;来定义查询。常见的查询类型包括#xff1a;
查询所有#xff1a;查询出所有数据#xff0c…1. DSL查询文档 elasticsearch的查询依然是基于JSON风格的DSL来实现的。
1.1. DSL查询分类
Elasticsearch提供了基于JSON的DSLDomain Specific Language来定义查询。常见的查询类型包括
查询所有查询出所有数据一般测试用。例如match_all全文检索full text查询利用分词器对用户输入内容分词然后去倒排索引库中匹配。例如 match_querymulti_match_query 精确查询根据精确词条值查找数据一般是查找keyword、数值、日期、boolean等类型字段。例如 idsrangeterm 地理geo查询根据经纬度查询。例如 geo_distancegeo_bounding_box 复合compound查询复合查询可以将上述各种查询条件组合起来合并查询条件。例如 boolfunction_score
查询语法基本一致
GET /indexName/_search
{query: {查询类型: {查询条件: 条件值}}
}我们以查询所有为例其中
查询类型为match_all没有查询条件
// 查询所有
GET /indexName/_search
{query: {match_all: {}}
}其它查询无非就是查询类型、查询条件的变化。
1.2. 全文检索查询
1.2.1. 使用场景
全文检索查询的基本流程如下
对用户搜索的内容做分词得到词条根据词条去倒排索引库中匹配得到文档id根据文档id找到文档返回给用户
比较常用的场景包括
商城的输入框搜索百度输入框搜索 因为是拿着词条去匹配因此参与搜索的字段也必须是可分词的text类型的字段。
1.2.2. 基本语法
常见的全文检索查询包括
match查询单字段查询multi_match查询多字段查询任意一个字段符合条件就算符合查询条件
match查询语法如下
GET /indexName/_search
{query: {match: {FIELD: TEXT}}
}mulit_match语法如下
GET /indexName/_search
{query: {multi_match: {query: TEXT,fields: [FIELD1, FIELD12]}}
}1.2.3. 示例
match查询示例 multi_match示例 可以看到两种查询结果是一样的为什么
因为我们将brand、name、business值都利用copy_to复制到了all字段中。因此你根据三个字段搜索和根据all字段搜索效果当然一样了。
但是搜索字段越多对查询性能影响越大因此建议采用copy_to然后单字段查询的方式。
1.2.4. 总结
match和multi_match的区别是什么
match根据一个字段查询multi_match根据多个字段查询参与查询字段越多查询性能越差
1.3. 精准查询
精确查询一般是查找keyword、数值、日期、boolean等类型字段。所以不会对搜索条件分词。常见的有
term根据词条精确值查询range根据值的范围查询
1.3.1. term查询
因为精确查询的字段搜是不分词的字段因此查询的条件也必须是不分词的词条。查询时用户输入的内容跟自动值完全匹配时才认为符合条件。如果用户输入的内容过多反而搜索不到数据。
语法说明
// term查询
GET /indexName/_search
{query: {term: {FIELD: {value: VALUE}}}
}示例 当我搜索的是精确词条时能正确查询出结果 但是当我搜索的内容不是词条而是多个词语形成的短语时反而搜索不到 1.3.2. range查询
范围查询一般应用在对数值类型做范围过滤的时候。比如做价格范围过滤。
基本语法
// range查询
GET /indexName/_search
{query: {range: {FIELD: {gte: 10, // 这里的gte代表大于等于gt则代表大于lte: 20 // lte代表小于等于lt则代表小于}}}
}示例 1.3.3. 总结
精确查询常见的有哪些
term查询根据词条精确匹配一般搜索keyword类型、数值类型、布尔类型、日期类型字段range查询根据数值范围查询可以是数值、日期的范围
1.4. 地理坐标查询
所谓的地理坐标查询其实就是根据经纬度查询官方文档文档
常见的使用场景包括
携程搜索我附近的酒店滴滴搜索我附近的出租车微信搜索我附近的人
附近的酒店 附近的车 1.4.1. 矩形范围查询
矩形范围查询也就是geo_bounding_box查询查询坐标落在某个矩形范围的所有文档 查询时需要指定矩形的左上、右下两个点的坐标然后画出一个矩形落在该矩形内的都是符合条件的点。
语法如下
// geo_bounding_box查询
GET /indexName/_search
{query: {geo_bounding_box: {FIELD: {top_left: { // 左上点lat: 31.1,lon: 121.5},bottom_right: { // 右下点lat: 30.9,lon: 121.7}}}}这种并不符合“附近的人”这样的需求所以我们就不做了。
1.4.2. 附近查询
附近查询也叫做距离查询geo_distance查询到指定中心点小于某个距离值的所有文档。
换句话来说在地图上找一个点作为圆心以指定距离为半径画一个圆落在圆内的坐标都算符合条件 语法说明
// geo_distance 查询
GET /indexName/_search
{query: {geo_distance: {distance: 15km, // 半径FIELD: 31.21,121.5 // 圆心}}
}示例
我们先搜索陆家嘴附近15km的酒店 可以发现共有47家酒店
然后把距离缩短到3km 可以发现搜索到的酒店数量减少到了5家。
1.5. 复合查询
复合compound查询复合查询可以将其它简单查询组合起来实现更复杂的搜索逻辑。常见的有两种
function score算分函数查询可以控制文档相关性算分控制文档排名bool query布尔查询利用逻辑关系组合多个其它的查询实现复杂搜索
1.5.1. 相关性算分
当我们利用match查询时文档结果会根据与搜索词条的关联度打分_score返回结果时按照分值降序排列。
例如我们搜索 “虹桥如家”结果如下
[{_score : 17.850193,_source : {name : 虹桥如家酒店真不错,}},{_score : 12.259849,_source : {name : 外滩如家酒店真不错,}},{_score : 11.91091,_source : {name : 迪士尼如家酒店真不错,}}
]在elasticsearch中早期使用的打分算法是TF-IDF算法公式如下 在后来的5.1版本升级中elasticsearch将算法改进为BM25算法公式如下 TF-IDF算法有一各缺陷就是词条频率越高文档得分也会越高单个词条对文档影响较大。而BM25则会让单个词条的算分有一个上限曲线更加平滑 小结elasticsearch会根据词条和文档的相关度做打分算法由两种
TF-IDF算法BM25算法elasticsearch5.1版本后采用的算法
1.5.2. 算分函数查询
根据相关度打分是比较合理的需求但合理的不一定是产品经理需要的。
以百度为例你搜索的结果中并不是相关度越高排名越靠前而是谁掏的钱多排名就越靠前。如图 要想认为控制相关性算分就需要利用elasticsearch中的function score 查询了。
1语法说明 function score查询中包含四部分内容
原始查询条件query部分基于这个条件搜索文档并且基于BM25算法给文档打分原始算分query score过滤条件filter部分符合该条件的文档才会重新算分算分函数符合filter条件的文档要根据这个函数做运算得到的函数算分function score有四种函数 weight函数结果是常量field_value_factor以文档中的某个字段值作为函数结果random_score以随机数作为函数结果script_score自定义算分函数算法 运算模式算分函数的结果、原始查询的相关性算分两者之间的运算方式包括 multiply相乘replace用function score替换query score其它例如sum、avg、max、min
function score的运行流程如下
1根据原始条件查询搜索文档并且计算相关性算分称为原始算分query score2根据过滤条件过滤文档3符合过滤条件的文档基于算分函数运算得到函数算分function score4将原始算分query score和函数算分function score基于运算模式做运算得到最终结果作为相关性算分。
因此其中的关键点是
过滤条件决定哪些文档的算分被修改算分函数决定函数算分的算法运算模式决定最终算分结果
2示例
需求给“如家”这个品牌的酒店排名靠前一些
翻译一下这个需求转换为之前说的四个要点
原始条件不确定可以任意变化过滤条件brand “如家”算分函数可以简单粗暴直接给固定的算分结果weight运算模式比如求和
因此最终的DSL语句如下
GET /hotel/_search
{query: {function_score: {query: { ... }, // 原始查询可以是任意条件functions: [ // 算分函数{filter: { // 满足的条件品牌必须是如家term: {brand: 如家}},weight: 2 // 算分权重为2}],boost_mode: sum // 加权模式求和}}
}测试在末添加算分函数时“如家”得分如下 添加了算分函数后“如家”得分就提升了 3小结
function score query定义的三要素是什么
过滤条件哪些文档要加分算分函数如何计算function score加权方式function score 与 query score如何运算
1.5.3. 布尔查询
布尔查询是一个或多个查询子句的组合每一个子句就是一个子查询。子查询的组合方式有
must必须匹配每个子查询类似“与”should选择性匹配子查询类似“或”must_not必须不匹配不参与算分类似“非”filter必须匹配不参与算分
比如在搜索酒店时除了关键字搜索外我们还可能根据品牌、价格、城市等字段做过滤 每一个不同的字段其查询的条件、方式都不一样必须是多个不同的查询而要组合这些查询就必须用bool查询了。
需要注意的是搜索时参与打分的字段越多查询的性能也越差。因此这种多条件查询时建议这样做
搜索框的关键字搜索是全文检索查询使用must查询参与算分其它过滤条件采用filter查询。不参与算分
1语法示例
GET /hotel/_search
{query: {bool: {must: [{term: {city: 上海 }}],should: [{term: {brand: 皇冠假日 }},{term: {brand: 华美达 }}],must_not: [{ range: { price: { lte: 500 } }}],filter: [{ range: {score: { gte: 45 } }}]}}
}2示例
需求搜索名字包含“如家”价格不高于400在坐标31.21,121.5周围10km范围内的酒店。
分析
名称搜索属于全文检索查询应该参与算分。放到must中价格不高于400用range查询属于过滤条件不参与算分。放到must_not中周围10km范围内用geo_distance查询属于过滤条件不参与算分。放到filter中 3小结
bool查询有几种逻辑关系
must必须匹配的条件可以理解为“与”should选择性匹配的条件可以理解为“或”must_not必须不匹配的条件不参与打分filter必须匹配的条件不参与打分
2. 搜索结果处理 搜索的结果可以按照用户指定的方式去处理或展示。
2.1. 排序
elasticsearch默认是根据相关度算分_score来排序但是也支持自定义方式搜索结果排序。可以排序字段类型有keyword类型、数值类型、地理坐标类型、日期类型等。
2.1.1. 普通字段排序
keyword、数值、日期类型排序的语法基本一致。
语法
GET /indexName/_search
{query: {match_all: {}},sort: [{FIELD: desc // 排序字段、排序方式ASC、DESC}]
}排序条件是一个数组也就是可以写多个排序条件。按照声明的顺序当第一个条件相等时再按照第二个条件排序以此类推
示例 需求描述酒店数据按照用户评价score降序排序评价相同的按照价格price升序排序 2.1.2. 地理坐标排序
地理坐标排序略有不同。
语法
GET /indexName/_search
{query: {match_all: {}},sort: [{geo_distance: {FIELD: 纬度经度, // 文档中geo_point类型的字段名、目标坐标点order: asc, // 排序方式unit: km // 排序的距离单位}}]
}这个查询的含义是
指定一个坐标作为目标点计算每一个文档中指定字段必须是geo_point类型的坐标到目标点的距离是多少根据距离排序
示例 需求描述实现对酒店数据按照到你的位置坐标的距离升序排序 2.2. 分页
elasticsearch默认情况下只返回top10的数据。而如果要查询更多数据就需要修改分页参数了。elasticsearch中通过修改from、size参数来控制要返回的分页结果
from从第几个文档开始size总共查询几个文档
2.2.1. 基本分页
语法
GET /indexName/_search
{query: {match_all: {}},from: 0, // 分页开始的位置默认为0size: 10, // 期望获取的文档总数sort: [{price: asc}]
}2.2.2. 深度分页
现在要查询990~1000的数据查询逻辑要这么写
GET /indexName/_search
{query: {match_all: {}},from: 990, // 分页开始的位置默认为0size: 10, // 期望获取的文档总数sort: [{price: asc}]
}这里是查询990开始的数据也就是 第990~第1000条 数据。
不过elasticsearch内部分页时必须先查询 0~1000条然后截取其中的990 ~ 1000的这10条 查询TOP1000如果es是单点模式这并无太大影响。
但是elasticsearch将来一定是集群例如我集群有5个节点我要查询TOP1000的数据并不是每个节点查询200条就可以了。
因为节点A的TOP200在另一个节点可能排到10000名以外了。
因此要想获取整个集群的TOP1000必须先查询出每个节点的TOP1000汇总结果后重新排名重新截取TOP1000。 那如果我要查询9900~10000的数据呢是不是要先查询TOP10000呢那每个节点都要查询10000条汇总到内存中
当查询分页深度较大时汇总数据过多对内存和CPU会产生非常大的压力因此elasticsearch会禁止from size 超过10000的请求。
针对深度分页ES提供了两种解决方案官方文档
search after分页时需要排序原理是从上一次的排序值开始查询下一页数据。官方推荐使用的方式。scroll原理将排序后的文档id形成快照保存在内存。官方已经不推荐使用。
2.2.3. 小结
分页查询的常见实现方案以及优缺点
from size 优点支持随机翻页缺点深度分页问题默认查询上限from size是10000场景百度、京东、谷歌、淘宝这样的随机翻页搜索 after search 优点没有查询上限单次查询的size不超过10000缺点只能向后逐页查询不支持随机翻页场景没有随机翻页需求的搜索例如手机向下滚动翻页 scroll 优点没有查询上限单次查询的size不超过10000缺点会有额外内存消耗并且搜索结果是非实时的场景海量数据的获取和迁移。从ES7.1开始不推荐建议用 after search方案。
2.3. 高亮
2.3.1. 高亮原理
什么是高亮显示呢
我们在百度京东搜索时关键字会变成红色比较醒目这叫高亮显示 高亮显示的实现分为两步
1给文档中的所有关键字都添加一个标签例如em标签2页面给em标签编写CSS样式
2.3.2. 实现高亮
语法
GET /indexName/_search
{query: {match: {FIELD: TEXT // 查询条件高亮一定要使用全文检索查询}},highlight: {fields: { // 指定要高亮的字段FIELD: {pre_tags: em, // 用来标记高亮字段的前置标签post_tags: /em // 用来标记高亮字段的后置标签}}}
}注意
高亮是对关键字高亮因此搜索条件必须带有关键字而不能是范围这样的查询。默认情况下高亮的字段必须与搜索指定的字段一致否则无法高亮如果要对非搜索字段高亮则需要添加一个属性required_field_matchfalse
示例 2.4. 总结
查询的DSL是一个大的JSON对象包含下列属性
query查询条件from和size分页条件sort排序条件highlight高亮条件
示例 3. RestClient查询文档 文档的查询同样适用RestHighLevelClient对象基本步骤包括
1准备Request对象2准备请求参数3发起请求4解析响应
3.1. 快速入门
以match_all查询为例
3.1.1. 发起查询请求 代码解读
1创建SearchRequest对象指定索引库名2利用request.source()构建DSLDSL中可以包含查询、分页、排序、高亮等 query()代表查询条件利用QueryBuilders.matchAllQuery()构建一个match_all查询的DSL 3利用client.search()发送请求得到响应
这里关键的API有两个一个是request.source()其中包含了查询、排序、分页、高亮等所有功能 另一个是QueryBuilders其中包含match、term、function_score、bool等各种查询 3.1.2. 解析响应
响应结果的解析 elasticsearch返回的结果是一个JSON字符串结构包含
hits命中的结果 total总条数其中的value是具体的总条数值max_score所有结果中得分最高的文档的相关性算分hits搜索结果的文档数组其中的每个文档都是一个json对象 _source文档中的原始数据也是json对象
因此我们解析响应结果就是逐层解析JSON字符串流程如下
SearchHits通过response.getHits()获取就是JSON中的最外层的hits代表命中的结果 SearchHits#getTotalHits().value获取总条数信息 SearchHits#getHits()获取SearchHit数组也就是文档数组 SearchHit#getSourceAsString()获取文档结果中的_source也就是原始的json文档数据
3.1.3. 完整代码
Test
void testMatchAll() throws IOException {// 1. 准备Request对象SearchRequest request new SearchRequest(hotel);// 2. 准备DSLrequest.source();// 3. 发送请求SearchResponse response client.search(request, RequestOptions.DEFAULT);// 4. 解析响应handleResponse(response);
}private void handleResponse(SearchResponse response) {// 解析结果SearchHits searchHits response.getHits();// 4.1 获取总条数assert searchHits.getTotalHits() ! null;long total searchHits.getTotalHits().value;System.out.println(共搜索到 total 条数据);// 4.2 文档数组SearchHit[] hits searchHits.getHits();// 4.3 遍历for (SearchHit hit : hits) {// 获取文档sourceString json hit.getSourceAsString();// 反序列化HotelDoc hotelDoc JSON.parseObject(json, HotelDoc.class);System.out.println(hotelDoc);}
}3.1.4. 小结
查询的基本步骤是 创建SearchRequest对象 准备Request.source()也就是DSL。 ① QueryBuilders来构建查询条件 ② 传入Request.source() 的 query() 方法 发送请求得到结果 解析结果参考JSON结果从外到内逐层解析
3.2. match查询
全文检索的match和multi_match查询与match_all的API基本一致。差别是查询条件也就是query的部分。 因此Java代码上的差异主要是request.source().query()中的参数了。同样是利用QueryBuilders提供的方法 而结果解析代码则完全一致可以抽取并共享。
完整代码
Test
void testMatch() throws IOException {// 1. 准备Request对象SearchRequest request new SearchRequest(hotel);// 2. 准备DSLrequest.source().query(QueryBuilders.matchQuery(all, 如家));// 3. 发送请求SearchResponse response client.search(request, RequestOptions.DEFAULT);// 4. 解析响应handleResponse(response);
}3.3. 精确查询
精确查询主要是两者
term词条精确匹配range范围查询
与之前的查询相比差异同样在查询条件其他都一样。 查询条件构造的API如下 Test
void testMatch() throws IOException {// 1. 准备Request对象SearchRequest request new SearchRequest(hotel);// 2. 准备DSLrequest.source().query(QueryBuilders.termQuery(city, 上海));// 3. 发送请求SearchResponse response client.search(request, RequestOptions.DEFAULT);// 4. 解析响应handleResponse(response);
}3.4. 布尔查询
布尔查询是用must、must_not、filter等方式组合其他查询代码示例如下 可以看到API与其它查询的差别同样是在查询条件的构建QueryBuilders结果解析等其他代码完全不变。
Test
void testBool() throws IOException {// 1. 准备Request对象SearchRequest request new SearchRequest(hotel);// 2. 准备DSLBoolQueryBuilder boolQuery QueryBuilders.boolQuery();boolQuery.must(QueryBuilders.termQuery(city, 上海));boolQuery.filter(QueryBuilders.rangeQuery(price).gte(100).lte(200));request.source().query(boolQuery);// 3. 发送请求SearchResponse response client.search(request, RequestOptions.DEFAULT);// 4. 解析响应handleResponse(response);
}3.5. 排序、分页
搜索结果的排序和分页是与query同级的参数因此同样是使用request.source()来设置。 Test
void testPageAndSort() throws IOException {// 页码、大小int page 2, size 5;// 1. 准备Request对象SearchRequest request new SearchRequest(hotel);// 2. 准备DSLrequest.source().query(QueryBuilders.matchAllQuery());request.source().sort(price, SortOrder.ASC);request.source().from((page - 1) * size).size(size);// 3. 发送请求SearchResponse response client.search(request, RequestOptions.DEFAULT);// 4. 解析响应handleResponse(response);
}3.6. 高亮
高亮的代码与之前代码差异较大有两点
查询到DSL其中除了查询条件还需要添加高亮条件同样是与query同级结果解析结果除了要解析_source文档数据还要解析高亮结果
3.6.1. 高亮请求构建 上述代码省略了查询条件部分但是大家不要忘了高亮查询必须使用全文检索查询并且要有搜索关键字将来才可以对关键字高亮。
Test
void testHighlight() throws IOException {// 1. 准备Request对象SearchRequest request new SearchRequest(hotel);// 2. 准备DSLrequest.source().query(QueryBuilders.matchQuery(all, 如家));request.source().highlighter(new HighlightBuilder().field(name).requireFieldMatch(false));// 3. 发送请求SearchResponse response client.search(request, RequestOptions.DEFAULT);// 4. 解析响应handleResponse(response);
}private void handleResponse(SearchResponse response) {// 解析结果SearchHits searchHits response.getHits();// 4.1 获取总条数assert searchHits.getTotalHits() ! null;long total searchHits.getTotalHits().value;System.out.println(共搜索到 total 条数据);// 4.2 文档数组SearchHit[] hits searchHits.getHits();// 4.3 遍历for (SearchHit hit : hits) {// 获取文档sourceString json hit.getSourceAsString();// 反序列化HotelDoc hotelDoc JSON.parseObject(json, HotelDoc.class);System.out.println(hotelDoc);// 获取高亮部分MapString, HighlightField highlightFields hit.getHighlightFields();HighlightField nameHighlight highlightFields.get(name);if (nameHighlight ! null) {// 返回一个包含一个Text对象的数组这个Text对象的字符串表示是emsample/emText[] fragments nameHighlight.fragments();StringBuilder sb new StringBuilder();for (Text fragment : fragments) {sb.append(fragment.string());}System.out.println(高亮部分: sb.toString());}}
}3.6.2. 高亮结果解析
4. 黑马旅游案例 4.1. 酒店搜索和分页
案例需求实现黑马旅游的酒店搜索功能完成关键字搜索和分页
4.1.1. 需求分析
在项目的首页有一个搜索框和分页按钮 前端请求参数 由此可以知道我们这个请求的信息如下
请求方式POST请求路径/hotel/list请求参数JSON对象包含个字段 key搜索关键字page页码size每页大小sortBy排序目前暂不实现 返回值分页查询需要返回分页结果PageResult包含两个属性 total总条数ListHotelDoc当前页的数据
因此实现的步骤如下
定义实体类接收请求参数的JSON对象编写controller接收页面的请求编写业务实现利用RestHighLevelClient实现搜索、分页
4.1.2. 定义实体类
实体类有两个一个是前端的请求参数实体一个是服务端应该返回的响应结果实体。
1请求参数 请求请求的json结构如下
{key: 搜索关键字,page: 1,size: 3,sortBy: default
}因此我们在cn.fg.hotel.pojo包下定义一个实体类
package cn.fg.hotel.pojo;import lombok.Data;Data
public class RequestParams {private String key;private Integer page;private Integer size;private String sortBy;
}2返回值 分页查询需要返回分页结果PageResult包含两个属性
total总条数ListHotelDoc当前页的数据
因此我们在cn.fg.hotel.pojo中定义返回结果
package cn.fg.hotel.pojo;import lombok.Data;import java.util.List;Data
public class PageResult {private Long total;private ListHotelDoc hotelDocList;public PageResult () {}public PageResult(Long total, ListHotelDoc hotelDocList) {this.total total;this.hotelDocList hotelDocList;}
}4.1.3. 定义controller
定义一个HotelController声明查询接口满足下列要求
请求方式POST请求路径/hotel/list请求参数对象类型为RequestParam返回值PageResult包含两个属性 Long total总条数ListHotelDoc hotelDocList酒店数据
因此我们在cn.fg.hotel.controller中定义HotelController
package cn.fg.hotel.controller;import cn.fg.hotel.pojo.PageResult;
import cn.fg.hotel.pojo.RequestParams;
import cn.fg.hotel.service.HotelService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;RestController
RequestMapping(/hotel)
public class HotelController {Autowiredprivate HotelService hotelService;// 搜索酒店数据PostMapping(/list)public PageResult searchHotel throws IOException(RequestBody RequestParams params) {return hotelService.searchHotel(params);}
}4.1.4. 实现搜索业务
我们在controller调用了HotelService并没有实现该方法因此下面我们就在HotelService中定义方法并且去实现业务逻辑。
1在cn.fg.hotel.service中的HotelService接口中定义一个方法
package cn.fg.hotel.service;import cn.fg.hotel.pojo.Hotel;
import cn.fg.hotel.pojo.PageResult;
import cn.fg.hotel.pojo.RequestParams;
import com.baomidou.mybatisplus.extension.service.IService;public interface HotelService extends IServiceHotel {/*** 根据关键字搜索酒店信息* param params* return*/PageResult searchHotel(RequestParams params);
}2实现搜索业务肯定离不开RestHighLevelClient我们需要把它注册到Spring中作为一个Bean。在cn.fg.hotel中的HotelDemoApplication中声明这个Bean
package cn.fg.hotel;import org.apache.http.HttpHost;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;SpringBootApplication
public class HotelDemoApplication {public static void main(String[] args) {SpringApplication.run(HotelDemoApplication.class, args);}Beanpublic RestHighLevelClient client() {return new RestHighLevelClient(RestClient.builder(HttpHost.create(http://192.168.116.100:9200)));}
}3在cn.fg.hotel.service.impl中的HotelService中实现searchHotel方法
package cn.fg.hotel.service.impl;import cn.fg.hotel.mapper.HotelMapper;
import cn.fg.hotel.pojo.Hotel;
import cn.fg.hotel.pojo.HotelDoc;
import cn.fg.hotel.pojo.PageResult;
import cn.fg.hotel.pojo.RequestParams;
import cn.fg.hotel.service.HotelService;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;import java.io.IOException;
import java.util.ArrayList;Service
public class HotelServiceImpl extends ServiceImplHotelMapper, Hotel implements HotelService {Autowiredprivate RestHighLevelClient client;Overridepublic PageResult searchHotel(RequestParams params) throws IOException {// 1. 准备RequestSearchRequest request new SearchRequest(hotel);// 2. 准备DSLString key params.getKey();if (key null || key.isEmpty()) {request.source().query(QueryBuilders.matchAllQuery());} else {request.source().query(QueryBuilders.matchQuery(all, key));}// 分页int page params.getPage();int size params.getSize();request.source().from((page - 1) * size).size(size);// 3. 发送请求SearchResponse response client.search(request, RequestOptions.DEFAULT);// 4. 解析结果return handleResponse(response);}private PageResult handleResponse(SearchResponse response) {SearchHits searchHits response.getHits();// 获取总条数assert searchHits.getTotalHits() ! null;long total searchHits.getTotalHits().value;// 文档数组SearchHit[] hits searchHits.getHits();// 遍历ArrayListHotelDoc hotelDocList new ArrayList();for (SearchHit hit : hits) {// 获取文档sourceString json hit.getSourceAsString();// 反序列化HotelDoc hotelDoc JSON.parseObject(json, HotelDoc.class);// 放入集合hotelDocList.add(hotelDoc);}return new PageResult(total, hotelDocList);}
}4.2. 酒店结果过滤
需求添加品牌、城市、星级、价格等过滤功能
4.2.1. 需求分析
在页面搜索框下面会有些过滤项 传递的参数如图 包含的过滤条件有
brand品牌值city城市minPrice~maxPrice价格范围starName星级
我们需要做
修改请求参数的对象RequestParams接收上述参数修改业务逻辑在搜索条件之外添加一些过滤条件
4.2.2. 修改实体类
修改在cn.fg.hotel.pojo包下的实体类RequestParams
package cn.fg.hotel.pojo;import lombok.Data;Data
public class RequestParams {private String key;private Integer page;private Integer size;private String sortBy;// 新增的过滤参数private String city;private String brand;private String starName;private Integer minPrice;private Integer maxPrice;
}4.2.3. 修改搜索业务
在HotelService的search方法中只有一个地方需要修改request.source().query(…)其中的查询条件。 在之前的业务中只有match查询根据关键字搜索现在要添加条件过滤包括
品牌过滤是keyword类型用term查询星级过滤是keyword类型用term查询价格过滤是数值类型用range查询城市过滤是keyword类型用term查询
多个查询条件组合肯定是boolean查询来组合
关键字搜索放到must中参与算分其他过滤条件放到filter中不参与算分
因为条件构建的逻辑比较复杂先封装一个函数 buildBasicQuery函数的代码如下
private void buildBasicQuery(RequestParams params, SearchRequest request) {// 1. 构建BooleanQueryBoolQueryBuilder boolQuery QueryBuilders.boolQuery();// 2. 关键字搜索String key params.getKey();if (key null || key.isEmpty()) {boolQuery.must(QueryBuilders.matchAllQuery());} else {boolQuery.must(QueryBuilders.matchQuery(all, key));}// 3. 过滤条件// 城市条件if (params.getCity() ! null !params.getCity().isEmpty()) {boolQuery.filter(QueryBuilders.termQuery(city, params.getCity()));}// 品牌条件if (params.getBrand() ! null !params.getBrand().isEmpty()) {boolQuery.filter(QueryBuilders.termQuery(brand, params.getBrand()));}// 星级条件if (params.getStarName() ! null !params.getStarName().isEmpty()) {boolQuery.filter(QueryBuilders.termQuery(starName, params.getStarName()));}// 价格区间if (params.getMinPrice() ! null params.getMaxPrice() ! null) {boolQuery.filter(QueryBuilders.rangeQuery(price).gte(params.getMinPrice()).lte(params.getMaxPrice()));}// 放入sourcerequest.source().query(boolQuery);
}4.3. 周边的酒店搜索
需求我附近的酒店
4.3.1. 需求分析
在酒店列表页的右侧有一个小地图点击地图的定位按钮地图会找到你所在的位置 并且在前端会发起查询请求将你的坐标发送到服务端 我们要做的事情就是基于这个location坐标然后按照距离对周围酒店排序。实现思路如下
修改RequestParams参数接收location字段修改search方法业务逻辑如果location有值添加根据geo_distance排序的功能
4.3.2. 修改实体类
修改RequestParams
package cn.fg.hotel.pojo;import lombok.Data;Data
public class RequestParams {private String key;private Integer page;private Integer size;private String sortBy;// 新增的过滤参数private String city;private String brand;private String starName;private Integer minPrice;private Integer maxPrice;// 地理坐标private String location;
}4.3.3. 距离排序API
我们以前学习过排序功能包括两种
普通字段排序地理坐标排序
我们只讲了普通字段排序对应的java写法。地理坐标排序只学过DSL语法如下
GET /indexName/_search
{query: {match_all: {}},sort: [{price: asc },{_geo_distance : {FIELD : 纬度经度,order : asc,unit : km}}]
}对应的java代码示例 4.3.4. 添加距离排序
在cn.fg.hotel.service.impl的HotelService的searchHotel方法中添加一个排序功能 4.3.5. 排序距离显示
重启服务后测试我的酒店功能 发现确实可以实现对我附近酒店的排序不过并没有看到酒店到底距离我多远这该怎么办
排序完成后页面还要获取我附近每个酒店的具体距离值这个值在响应结果中是独立的 因此我们在结果解析阶段除了解析source部分以外还要得到sort部分也就是排序的距离然后放到响应结果中。
我们要做两件事
修改HotelDoc添加排序距离字段用于页面显示修改HotelService类中的handleResponse方法添加对sort值的获取
1修改HotelDoc类添加距离字段 2修改HotelService中的handleResponse方法 重启后测试发现页面能成功显示距离了 4.4. 酒店竞价排名
需求让指定的酒店在搜索结果中排名置顶
4.4.1. 需求分析
要让指定酒店在搜索结果中排名置顶效果如图 页面会给指定的酒店添加广告标记。
那怎样才能让指定的酒店排名置顶呢
我们之前学习过的function_score查询可以影响算分算分高了自然排名也就高了。而function_score包含3个要素
过滤条件哪些文档要加分算分函数如何计算function score加权方式function score 与 query score如何运算
这里的需求是让指定酒店排名靠前。因此我们需要给这些酒店添加一个标记这样在过滤条件中就可以根据这个标记来判断是否要提高算分。
比如我们给酒店添加一个字段isADBoolean类型
true是广告false不是广告
这样function_score包含3个要素就很好确定了
过滤条件判断isAD 是否为true算分函数我们可以用最简单暴力的weight固定加权值加权方式可以用默认的相乘大大提高算分
因此业务的实现步骤包括
给HotelDoc类添加isAD字段Boolean类型挑选几个你喜欢的酒店给它的文档数据添加isAD字段值为true修改search方法添加function score功能给isAD值为true的酒店增加权重
4.4.2. 修改HotelDoc实体类
给cn.fg.hotel.pojo包下的HotelDoc类添加isAD字段 4.4.3. 添加广告标记
接下来我们挑几个酒店添加isAD字段设置为true
POST /hotel/_update/2056126831
{doc: {isAD: true}
}
POST /hotel/_update/1989806195
{doc: {isAD: true}
}
POST /hotel/_update/2056105938
{doc: {isAD: true}
}4.4.4. 添加算分函数查询
接下来我们就要修改查询条件了。之前是用的boolean 查询现在要改成function_socre查询。
function_score查询结构如下 对应的API 我们可以将之前写的boolean查询作为原始查询条件放到query中接下来就是添加过滤条件、算分函数、加权模式了。所以原来的代码依然可以沿用。
修改cn.fg.hotel.service.impl包下的HotelService类中的buildBasicQuery方法添加算分函数查询