学习目标
- 能够使用function_score修改文档得分
- 能够说出深度分页的方案
- 能够使用Java Client实现高亮显示
- 能够使用Java Client测试地理坐标查询
- 能够使用Java Client测试聚合查询
- 能够说出实现同义词的方案
- 能够说出自动补全方案
- 能够完善商城项目商品搜索功能
- 能够实现商城项目自动补全功能
1 搜索高级
1.1. 修改文档得分
1.1.1 function_score
当我们利用match查询时,文档结果会根据与搜索词条的关联度打分(_score),返回结果时按照分值降序排列。例如,我们搜索 "手机",结果如下:
_score:文档与用户搜索的关键字的相关度得分
在实际业务需求中,常常会有竞价排名的功能。不是相关度越高排名越靠前,而是掏的钱多的排名靠前。
例如在百度中搜索Java培训,排名靠前的就是广告推广:
要想人为控制相关性算分,就需要利用elasticsearch中的function score 查询了。
一个例子:给小米这个品牌的手机算分提高十倍,分析如下:
- 过滤条件:品牌必须为小米
- 算分函数:常量weight,值为10
- 算分模式:相乘multiply
GET /items/_search { "query": { "function_score": { "query": { "match": { "name":"手机" } }, "functions": [ { "filter": { "term": { "brand": "小米" } }, "weight": 10 } ], "boost_mode": "multiply" } }, "from": 0, "size": 10 }
从结果可以看到小米手机排在前边,因为对品牌为“小米”的文档得分在原有分值基础上乘以10,分值越大越排在前边。function score的运行流程如下:
- 1)根据原始条件查询搜索文档,并且计算相关性算分,称为原始算分(query score)
- 2)根据过滤条件,过滤文档
- 3)符合过滤条件的文档,基于算分函数运算,得到函数算分(function score)
- 4)将原始算分(query score)和函数算分(function score)基于运算模式做运算,得到最终结果,作为相关性算分。
因此,其中的关键点是:
- 过滤条件:决定哪些文档的算分被修改
- 算分函数:决定函数算分的算法
- 运算模式:决定最终算分结果
本例中function_score的语法如下:
function score 查询中包含四部分内容:
- 原始查询条件:query部分,基于这个条件搜索文档,并且基于BM25算法给文档打分,原始算分(query score)
- 过滤条件:filter部分,符合该条件的文档才会重新算分
- 算分函数:符合filter条件的文档要根据这个函数做运算,得到的函数算分(function score),有四种函数
- weight:函数结果是常量
- field_value_factor:以文档中的某个字段值作为函数结果,适用于那些需要根据某个数值字段来影响文档排序的情况,例如根据产品的价格、文章的阅读量或者用户的活跃度来对结果进行评分。
- random_score:以随机数作为函数结果,用于测试或某些特定的用例,比如创建一个随机排序的效果
- script_score:自定义算分函数算法,允许你在查询时动态地编写脚本来计算每个文档的分数
- boost_mode运算模式:决定了如何将评分函数的结果与基础查询得分相结合,包括:
- multiply:评分函数的结果与基础查询的得分相乘。这是默认行为,适用于希望评分函数增强或减弱基础查询得分的情况。
- replace:评分函数的结果将完全替换基础查询的得分。这意味着最终得分将完全基于评分函数的结果,而不考虑基础查询的原始得分。
sum:评分函数的结果与基础查询的得分相加。这使得评分函数的结果直接增加到基础查询得分上,适合于希望累加评分因素的情况。avg:评分函数的结果与基础查询的得分取平均值。这种方式适用于希望平衡基础查询得分与评分函数得分的情况。
算分函数的其它选项,比如:field_value_factor、script_score大家使用AI自学。
示例2:将下边的查询改为算分函数查询,品牌为“爱氏晨曦”排在前边
GET /items/_search { "from" : 0, "query" : { "multi_match" : { "fields" : [ "name", "category" ], "query" : "脱脂牛奶" } }, "size" : 5 }
1.1.2 Java Client
使用Java Client实现算分函数查询
@Test void testFunctionScoreQuery() throws Exception { //构建请求 SearchRequest.Builder builder = new SearchRequest.Builder(); //设置索引 builder.index("items"); //设置查询条件 SearchRequest.Builder searchRequestBuilder = builder.query(q -> q .functionScore(f -> f .query(q1 -> q1.match(m -> m.field("name").query("手机"))) .functions(fn -> fn.filter(f1 -> f1 .term(t -> t.field("brand").value("小米"))) .weight(10d) ) .boostMode(FunctionBoostMode.Multiply)) ).from(0).size(10); SearchRequest build = searchRequestBuilder.build(); //执行请求 SearchResponse<ItemDoc> searchResponse = esClient.search(build, ItemDoc.class); //解析结果 handleResponse(searchResponse); }
1.2. 深度分页
1.2.1 深度分页问题
前边我们学习分页查询是通过修改from、size参数控制返回的分页结果,类似于mysql中的limit ?, ?
elasticsearch的数据一般会采用分片存储,也就是把一个索引中的数据分成N份,存储到不同节点上。这种存储方式比较有利于数据扩展,但给分页带来了一些麻烦。
比如一个索引库中有100000条数据,分别存储到4个分片,每个分片25000条数据。现在每页查询10条,查询第100页。那么分页查询的条件如下:
GET /items/_search { "from": 990, // 从第990条开始查询 "size": 10, // 每页查询10条 "sort": [ { "price": "asc" } ] }
从语句来分析,要查询第990~1000名的数据。
从实现思路来分析,是将所有数据排序,找出前1000名,截取其中的990~1000的部分。但问题来了,我们如何才能找到所有数据中的前1000名呢?
要知道每一片的数据都不一样,第1片上的第900~1000,在另1个节点上并不一定依然是900~1000名。所以我们只能在每一个分片上都找出排名前1000的数据,然后汇总到一起,重新排序,才能找出整个索引库中真正的前1000名,此时截取990~1000的数据即可。如图:
试想一下,假如我们现在要查询的是第1000页数据呢,是不是要找第9990~10000的数据,那岂不是需要把每个分片中的前10000名数据都查询出来,汇总在一起,在内存中排序?如果查询的分页深度更深呢,需要一次检索的数据岂不是更多?
由此可知,当查询分页深度较大时,汇总数据过多,对内存和CPU会产生非常大的压力,特别是在 from 值非常大的情况下。这是因为Elasticsearch需要先跳过前面的所有文档才能获取到所需的文档,这可能导致大量的磁盘I/O操作和CPU使用率。
因此elasticsearch会限制from+ size 请求:
size参数的最大值:- 默认情况下,
size参数的最大值被限制为 10,000。这意味着每次查询最多只能返回 10,000 条记录。 from参数的最大值:- 默认情况下,
from参数的最大值也被限制为 10,000。这意味着您不能请求超过第 10,000 条记录之后的数据。
这意味着,理论上,您可以查询的最大页数是 10,000 / size。例如,如果 size 设置为 100,则最多可以查询 100 页。
1.1.2 search after
针对深度分页,elasticsearch提供了两种解决方案:
search after:分页时需要排序,原理是从上一次的排序值开始查询下一页数据。官方推荐使用。scroll滚动查询:原理将排序后的文档id形成快照,保存下来,基于快照做分页。官方已不推荐使用。
详情见文档:https://www.elastic.co/guide/en/elasticsearch/reference/7.17/paginate-search-results.html
search after举例:查询第一页:
查询第二页:
如何使用search_after实现降序排序呢,排序字段是ID?
第一页的search_after值就需要设置一个最大值
GET /items/_search { "query": { "bool": {} }, "sort": [ { "id": { "order": "desc" } } ], "size": 10, "search_after":[999999999999] }
下边使用Java client实现深度分页,请大家自行测试。
@Test void testSearchAfter() throws IOException { // 1.创建Request SearchRequest.Builder builder = new SearchRequest.Builder(); builder.index("items"); builder.query(q -> q .bool(b -> b)) .sort(s1 -> s1 .field(f -> f.field("id").order(SortOrder.Asc))) .searchAfter("0") .size(1); SearchRequest request = builder.build(); SearchResponse<ItemDoc> response = esClient.search(request, ItemDoc.class); // 解析响应 handleResponse(response); }
总结:
大多数情况下,我们采用普通分页就可以了。查看百度、京东等网站,会发现其分页都有限制。例如百度最多支持77页,每页不足20条。京东最多100页,每页最多60条。
因此,一般我们采用限制分页深度的方式即可。
1.3.地理坐标查询
1.3.1 介绍
所谓的地理坐标查询,其实就是根据经纬度查询,官方文档
常见的使用场景包括:
- 携程:搜索我附近的酒店
- 滴滴:搜索我附近的出租车
- 微信:搜索我附近的人
找附近的酒店:
打车:
1.3.2. 矩形范围查询
1.3.2.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.3.2.2 向索引添加坐标值
下边进行测试:
- 首先向索引添加坐标值
添加映射:
PUT /items/_mapping { "properties": { "location": { "type": "geo_point" } } }
设置坐标值:
首先找到坐标值,我们使用高德地图取坐标点。访问:https://lbs.amap.com/tools/picker
输入关键,查找坐标
纬度:40.06,经度:116.34
纬度的有效范围是从 -90 到 +90 度,而经度的有效范围是从 -180 到 +180 度。
执行更新语句,更新坐标值:
POST /items/_update/584391 { "doc": { "location": { "lat": 40.06, // 纬度 "lon": 116.34 // 经度 } } }
1.3.2.3 地理坐标搜索测试
- 地理坐标搜索
找到左上和右下的坐标。
GET /items/_search { "query": { "geo_bounding_box": { "location": { "top_left": { "lat": 40.08, "lon": 116.32 }, "bottom_right": { "lat": 40.04, "lon": 116.36 } } } } }
结果:
1.3.3 附近搜索
1.3.3.1 介绍
附近查询,也叫做距离查询(geo_distance):查询到指定中心点小于某个距离值的所有文档。
换句话来说,在地图上找一个点作为圆心,以指定距离为半径,画一个圆,落在圆内的坐标都算符合条件:
1.3.3.2 语法
语法说明:
// geo_distance 查询 GET /indexName/_search { "query": { "geo_distance": { "distance": "15km", // 半径 "FIELD": "31.21,121.5" // 圆心 } } }
1.3.3.3 测试
执行下边语句进行测试:
GET /items/_search { "query": { "geo_distance": { "distance": "15km", "location": "40.061034,116.345999" } } }
1.3.4. Java Client
GeoPoint 类型是Spring data elasticsearch中的一个类,需要将它的依赖添加到pom.xml中
<!--加入spring data elasticsearch--> <dependency> <groupId>org.springframework.data</groupId> <artifactId>spring-data-elasticsearch</artifactId> </dependency>
在ItemDoc类中添加地理坐标字段
@ApiModelProperty("地理坐标") private GeoPoint location;
对照下边的dsl语句结合AI编写Java Client程序:
dsl:
GET /items/_search { "query": { "geo_bounding_box": { "location": { "top_left": { "lat": 40.08, "lon": 116.32 }, "bottom_right": { "lat": 40.04, "lon": 116.36 } } } } }
Java 程序:
@Test void testGeo() throws Exception { //构建请求 SearchRequest.Builder builder = new SearchRequest.Builder(); builder.index("items"); builder.query(q -> q.geoBoundingBox(g -> g .field("location") .boundingBox(b -> b .tlbr(tlbr->tlbr .topLeft(t -> t.latlon(l -> l.lat(40.08).lon(116.32))) .bottomRight(b1 -> b1.latlon(l -> l.lat(40.04).lon(116.36))) )))); SearchRequest build = builder.build(); //执行请求 SearchResponse<ItemDoc> searchResponse = esClient.search(build, ItemDoc.class); //解析结果 handleResponse(searchResponse); }
2 nested类型(了解)
2.1 介绍
Elasticsearch 中的 nested 类型允许你在文档内存储复杂的数据结构,比如一个用户可能有多个地址,或者一个博客文章可能有多个标签等。nested 类型可以让你索引这些复杂数据,并且允许你对嵌套的数据进行查询。
2.2 添加nested文档
下边向商品映射中添加nested类型的字段attr_list
attr_list表示商品属性,attr_list有两个字段:name、value
PUT /items/_mapping { "properties":{ "attr_list":{ "type":"nested", "properties":{ "name":{ "type":"keyword" }, "value":{ "type":"keyword" } } } } }
商品索引完整的映射如下,先删除原来的索引现创建新索引。
PUT /items { "mappings" : { "properties" : { "brand" : { "type" : "keyword" }, "category" : { "type" : "keyword" }, "commentCount" : { "type" : "integer", "index" : false }, "id" : { "type" : "keyword" }, "image" : { "type" : "keyword", "index" : false }, "isAD" : { "type" : "boolean" }, "name" : { "type" : "text", "analyzer" : "ik_max_word", "search_analyzer" : "ik_smart" }, "price" : { "type" : "integer" }, "sold" : { "type" : "integer" }, "stock" : { "type" : "integer" }, "updateTime" : { "type" : "date" }, "location" : { "type" : "geo_point" }, "attr_list":{ "type":"nested", "properties":{ "name":{ "type":"keyword" }, "value":{ "type":"keyword" } } } } } }
在ItemDoc中添加商品属性字段
package com.hmall.item.domain.po; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Data; import java.time.LocalDateTime; import java.util.List; @Data @ApiModel(description = "索引实体") public class ItemDoc { @ApiModelProperty("商品id") private String id; @ApiModelProperty("商品名称") private String name; @ApiModelProperty("价格(分)") private Integer price; @ApiModelProperty("库存") private Integer stock; @ApiModelProperty("商品图片") private String image; @ApiModelProperty("类目名称") private String category; @ApiModelProperty("品牌名称") private String brand; @ApiModelProperty("销量") private Integer sold; @ApiModelProperty("评论数") private Integer commentCount; @ApiModelProperty("是否是推广广告,true/false") private Boolean isAD; @ApiModelProperty("更新时间") private LocalDateTime updateTime; //位置 @ApiModelProperty("商品位置") private GeoPoint location; //商品属性 @ApiModelProperty("商品规格") private List<Spec> attr_list; @Data public static class Spec { private String name; private String value; } }
向索引添加文档,可以添加单个文档也可以批量添加文档,添加文档时指定商品属性。
@Test void testAddDocument2() throws Exception { //商品id Long id = 317578L; //根据id查询商品 Item item = itemService.getById(id); //转为ItemDoc ItemDoc itemDoc = BeanUtils.copyBean(item, ItemDoc.class); ItemDoc.Spec spec_1 = new ItemDoc.Spec(); spec_1.setName("大小"); spec_1.setValue("60*40"); //再设置一个新规格 ItemDoc.Spec spec_2 = new ItemDoc.Spec(); spec_2.setName("颜色"); spec_2.setValue("白色"); itemDoc.setAttr_list(List.of(spec_1, spec_2)); //使用esClient添加文档 IndexResponse response = esClient.index(i -> i .index("items") .id(id.toString()) .document(itemDoc)); // 打印结果 String s = response.result().jsonValue(); log.info("添加文档结果:{}", s); }
查询文档:GET /items/_doc/317578
{ "_index" : "items", "_type" : "_doc", "_id" : "317578", "_version" : 1, "_seq_no" : 0, "_primary_term" : 1, "found" : true, "_source" : { "id" : "317578", "name" : "RIMOWA 21寸托运箱拉杆箱 SALSA AIR系列果绿色 820.70.36.4", "price" : 28900, "stock" : 9985, "image" : "https://m.360buyimg.com/mobilecms/s720x720_jfs/t6934/364/1195375010/84676/e9f2c55f/597ece38N0ddcbc77.jpg!q70.jpg.webp", "category" : "拉杆箱", "brand" : "RIMOWA", "sold" : 0, "commentCount" : 0, "isAD" : false, "updateTime" : [ 2024, 10, 25, 17, 52, 8 ], "attr_list" : [ { "name" : "大小", "value" : "60*40" }, { "name" : "颜色", "value" : "白色" } ] } }
2.3 搜索nested
查询商品颜色是白色的商品。
GET /items/_search { "query": { "nested": { "path": "attr_list", "query": { "bool": { "must": [ { "term": { "attr_list.name": { "value": "颜色" } } }, { "term": { "attr_list.value": { "value": "白色" } } } ] } } } } }
"nested":这是一个嵌套查询,用于查询嵌套对象。它允许你在嵌套对象中执行更复杂的查询。
"path": "attr_list":指定了要查询哪个嵌套对象字段。在这个例子中,嵌套对象的字段名是attr_list
2.4 聚合nested
先按商品属性名称聚合,再按属性值聚合。
GET /items/_search { "size": 0, "aggs": { "attr_aggs": { "nested": { "path": "attr_list" }, "aggs": { "attr_name_aggs": { "terms": { "field": "attr_list.name", "size": 10 }, "aggs": { "attr_value_aggs": { "terms": { "field": "attr_list.value", "size": 10 } } } } } } } }
结果:
{ "took" : 3, "timed_out" : false, "_shards" : { "total" : 1, "successful" : 1, "skipped" : 0, "failed" : 0 }, "hits" : { "total" : { "value" : 200, "relation" : "eq" }, "max_score" : null, "hits" : [ ] }, "aggregations" : { "attr_aggs" : { "doc_count" : 400, "attr_name_aggs" : { "doc_count_error_upper_bound" : 0, "sum_other_doc_count" : 0, "buckets" : [ { "key" : "大小", "doc_count" : 200, "attr_value_aggs" : { "doc_count_error_upper_bound" : 0, "sum_other_doc_count" : 0, "buckets" : [ { "key" : "120*80", "doc_count" : 100 }, { "key" : "60*40", "doc_count" : 100 } ] } }, { "key" : "颜色", "doc_count" : 200, "attr_value_aggs" : { "doc_count_error_upper_bound" : 0, "sum_other_doc_count" : 0, "buckets" : [ { "key" : "白色", "doc_count" : 100 }, { "key" : "黑色", "doc_count" : 100 } ] } } ] } } } }
2.5 Java Client
2.5.1 nested查询
根据下边的DSL编写java代码:
DSL:
GET /items/_search { "query": { "nested": { "path": "attr_list", "query": { "bool": { "must": [ { "term": { "attr_list.name": { "value": "颜色" } } }, { "term": { "attr_list.value": { "value": "白色" } } } ] } } } } }
Java Client:
@Test void testNested() throws Exception { SearchRequest.Builder builder = new SearchRequest.Builder(); builder.index("items"); builder.query(q -> q.nested(n -> n .path("attr_list") .query(q1 -> q1 .bool(b -> b .must(a -> a .term(t -> t.field("attr_list.name").value("颜色"))) .must(a1 -> a1 .term(t -> t.field("attr_list.value").value("白色"))))))); SearchRequest build = builder.build(); SearchResponse<ItemDoc> response = esClient.search(build, ItemDoc.class); //解析结果 List<Hit<ItemDoc>> hits = response.hits().hits(); hits.forEach(hit -> { ItemDoc source = hit.source(); log.info("查询结果:{}", source); }); }
2.5.2 nested聚合
下边是对nested进行聚合:
DSL:
GET /items/_search { "size": 0, "aggs": { "attr_aggs": { "nested": { "path": "attr_list" }, "aggs": { "attr_name_aggs": { "terms": { "field": "attr_list.name", "size": 10 }, "aggs": { "attr_value_aggs": { "terms": { "field": "attr_list.value", "size": 10 } } } } } } } }
Java Client:
@Test void testNestedAggs() throws Exception { SearchRequest.Builder builder = new SearchRequest.Builder(); builder.index("items2"); builder.size(0); builder.aggregations("attr_aggs", a -> a .nested(n -> n.path("attr_list")) .aggregations("attr_name_aggs", a1 -> a1 .terms(t -> t .field("attr_list.name") .size(10)) .aggregations("attr_value_aggs", a2 -> a2 .terms(t -> t .field("attr_list.value") ) ) ) ); SearchRequest build = builder.build(); SearchResponse<ItemDoc> response = esClient.search(build, ItemDoc.class); Map<String, Aggregate> aggregations = response.aggregations(); Aggregate attrAggs = aggregations.get("attr_aggs"); //解析结果 NestedAggregate nested = attrAggs.nested(); Map<String, Aggregate> attrNameAggs = nested.aggregations(); Aggregate aggregate = attrNameAggs.get("attr_name_aggs"); aggregate.sterms().buckets().array().forEach(bucket -> { String key = bucket.key().stringValue(); Long docCount = bucket.docCount(); log.info("属性名:{},属性值数量:{}", key, docCount); Map<String, Aggregate> aggregations1 = bucket.aggregations(); Aggregate attrValueAggs = aggregations1.get("attr_value_aggs"); attrValueAggs.sterms().buckets().array() .forEach(bucket1 -> { String key1 = bucket1.key().stringValue(); Long docCount1 = bucket1.docCount(); log.info("属性值:{},属性值数量:{}", key1, docCount1); }); }); }
3. 同义词(了解)
3.1 设置同义词
搜索中同义词的需求:在搜索时输入一个关键字,包含关键字同义词的文档应该也可以搜索出来。
比如:输入“电脑”,会搜索出包含“计算机”的文档,输入“黑马”搜索出“黑马程序员”、“传智播客”的文章。
elasticsearch 的同义词有如下两种形式:
- 单向同义词:
heima,黑马 => 黑马程序员,黑马、传智播客
箭头左侧的词都会映射成箭头右侧的词。
输入箭头左侧的词可以搜索出箭头右侧的词。
- 双向同义词:
马铃薯, 土豆, potato
双向同义词可以互相映射。
输入“土豆”可以搜索出“potato”,输入“potato”可以搜索出“土豆”
怎么设置同义词?
首先在同义词加到synonyms.txt文件中,synonyms.txt文件路径:/data/soft/es7.17.7/xzb/config
我们在synonyms.txt中加入:
中国,中华人民共和国,china heima,黑马=>黑马程序员,黑马、传智播客
3.2 定义同义词分词器
在设置索引映射时自定义同义词分词器my_synonyms_analyzer,并且用于“title”字段的搜索。举例:
PUT /test_index { "settings": { "analysis": { "filter": { "my_synonym_filter": { "type": "synonym", "updateable": true, "synonyms_path": "synonyms.txt" } }, "analyzer": { "my_synonyms_analyzer": { "tokenizer": "ik_smart", "filter": [ "my_synonym_filter" ] } } } }, "mappings": { "properties": { "title": { "type": "text", "analyzer": "ik_max_word", "search_analyzer": "my_synonyms_analyzer" } } } }
my_synonym_filter是自定义的词汇过滤器,
my_synonyms_analyzer是自定义的分析器, my_synonyms_analyzer包含并引用了my_synonym_filter
updateable指示能否动态更新, 必须为true才能动态更新同义词
synonyms_path指示同义词文件的位置
my_synonyms_analyzer分析器里用ik_smart的分词器, my_synonyms_analyzer的分词流程是原始文本先经过ik_smart分词的结果再用my_synonym_filter处理。
mappings.properties.title.search_analyzer指示title字段在搜索时使用my_synonyms_analyzer分析器。
3.3 测试
首先向test_index索引中添加数据
POST /_bulk {"index": {"_index":"test_index", "_id": "5"}} {"title": "china你好"} {"index": {"_index":"test_index", "_id": "4"}} {"title": "中国你好"} {"index": {"_index":"test_index", "_id": "6"}} {"title": "中华人民共和国你好"} {"index": {"_index":"test_index", "_id": "7"}} {"title": "China你好"} {"index": {"_index":"test_index", "_id": "8"}} {"title": "这是一匹黑马"} {"index": {"_index":"test_index", "_id": "9"}} {"title": "黑马是中国良心培训机构"} {"index": {"_index":"test_index", "_id": "10"}} {"title": "黑马程序员是中国良心培训机构"} {"index": {"_index":"test_index", "_id": "11"}} {"title": "传智播客一所IT培训机构"}
搜索关键字“china”
GET /test_index/_search { "query": { "match": { "title": "china" } } }
结果中包括:中国,中华人民共和国,china词。
输入“黑马”会搜索出包括:黑马程序员,黑马、传智播客 词的文章 。
输入“传智播客”只能搜索出包含“传智播客”的文章 。
如果要实现输入“传智播客”搜索出“黑马程序员”的文章 怎么实现?
在同义词文件中加入:传智播客,黑马程序员
执行:POST /test_index/_reload_search_analyzers 使用同义词生效。
再次测试:
GET /test_index/_search { "query": { "match": { "title": "传智播客" } } }
输出结果:
{ "took" : 0, "timed_out" : false, "_shards" : { "total" : 1, "successful" : 1, "skipped" : 0, "failed" : 0 }, "hits" : { "total" : { "value" : 2, "relation" : "eq" }, "max_score" : 1.6655793, "hits" : [ { "_index" : "test_index", "_type" : "_doc", "_id" : "11", "_score" : 1.6655793, "_source" : { "title" : "传智播客一所IT培训机构" } }, { "_index" : "test_index", "_type" : "_doc", "_id" : "10", "_score" : 1.3592656, "_source" : { "title" : "黑马程序员是中国良心培训机构" } } ] } }
3.4 思考
如果要实现搜索itheima将黑马程序员相关的记录搜索出来怎么做?
自己在同义词配置文件配置同义词进行测试。
搜索“手机壳”不要搜索出手机的记录?
4 自动补全(掌握)
4.1 介绍
当用户在搜索框输入字符时提示出与该字符有关的搜索项,这个效果就是自动补全,如下图输入拼音可以补全拼音对应的中文:
输入中文也可以自动补全:
Elasticsearch如何实现自动补全?
要实现上述自动补全的需求需要完成两个功能:
- 拼音搜索
- 前缀搜索
4.2. 拼音分词器
4.2.1 安装拼音分词器
4.2.1.1 安装
自动补全如何实现根据拼音字母来补全?
效果如下图:
输入拼音"dian"提示“电动车”、“电风扇”等,根据搜索的原理,输入的关键字去匹配索引中的词,所以索引中必须有“dian”这个词条,而"dian"拼音来源于中文汉字,所以首先要解决的是中文在分词后将每个词条生成拼音,也就是在索引的词条中既有中文汉字又有拼音,这样就可以实现输入拼音可以搜索出匹配的词条。
实现拼音搜索就要用到拼音分词器。
与IK分词器一样,拼音分词器也有插件,在GitHub上有elasticsearch的拼音分词插件。
地址:https://github.com/medcl/elasticsearch-analysis-pinyin
找到与Elasticsearch版本一致的插件下载包。
可自行下载也可从课程资料目录获取
安装方式与IK分词器一样,分三步:
①解压elasticsearch-analysis-pinyin-7.17.7.zip
②上传到虚拟机中elasticsearch的plugin目录
③重启elasticsearch
④测试