ES分布式搜索引擎高级

简介: 本课程涵盖搜索高级功能,包括使用function_score修改文档得分、深度分页方案(search_after)、地理坐标查询、Java Client实现高亮与聚合查询,并深入讲解同义词处理、自动补全及nested类型的应用,助力完善商城项目搜索功能。

学习目标

  1. 能够使用function_score修改文档得分
  2. 能够说出深度分页的方案
  3. 能够使用Java Client实现高亮显示
  4. 能够使用Java Client测试地理坐标查询
  5. 能够使用Java Client测试聚合查询
  6. 能够说出实现同义词的方案
  7. 能够说出自动补全方案
  8. 能够完善商城项目商品搜索功能
  9. 能够实现商城项目自动补全功能

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 深度分页问题

前边我们学习分页查询是通过修改fromsize参数控制返回的分页结果,类似于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 请求:

  1. size 参数的最大值:
  2. 默认情况下,size 参数的最大值被限制为 10,000。这意味着每次查询最多只能返回 10,000 条记录。
  3. from 参数的最大值:
  4. 默认情况下,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 向索引添加坐标值

下边进行测试:

  1. 首先向索引添加坐标值

添加映射:

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 地理坐标搜索测试

  1. 地理坐标搜索

找到左上和右下的坐标。

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如何实现自动补全?

要实现上述自动补全的需求需要完成两个功能:

  1. 拼音搜索
  2. 前缀搜索

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

④测试

相关文章
|
12天前
|
数据采集 人工智能 安全
|
7天前
|
机器学习/深度学习 人工智能 前端开发
构建AI智能体:七十、小树成林,聚沙成塔:随机森林与大模型的协同进化
随机森林是一种基于决策树的集成学习算法,通过构建多棵决策树并结合它们的预测结果来提高准确性和稳定性。其核心思想包括两个随机性:Bootstrap采样(每棵树使用不同的训练子集)和特征随机选择(每棵树分裂时只考虑部分特征)。这种方法能有效处理大规模高维数据,避免过拟合,并评估特征重要性。随机森林的超参数如树的数量、最大深度等可通过网格搜索优化。该算法兼具强大预测能力和工程化优势,是机器学习中的常用基础模型。
344 164
|
6天前
|
机器学习/深度学习 自然语言处理 机器人
阿里云百炼大模型赋能|打造企业级电话智能体与智能呼叫中心完整方案
畅信达基于阿里云百炼大模型推出MVB2000V5智能呼叫中心方案,融合LLM与MRCP+WebSocket技术,实现语音识别率超95%、低延迟交互。通过电话智能体与座席助手协同,自动化处理80%咨询,降本增效显著,适配金融、电商、医疗等多行业场景。
345 155
|
7天前
|
编解码 人工智能 自然语言处理
⚽阿里云百炼通义万相 2.6 视频生成玩法手册
通义万相Wan 2.6是全球首个支持角色扮演的AI视频生成模型,可基于参考视频形象与音色生成多角色合拍、多镜头叙事的15秒长视频,实现声画同步、智能分镜,适用于影视创作、营销展示等场景。
581 4
|
15天前
|
SQL 自然语言处理 调度
Agent Skills 的一次工程实践
**本文采用 Agent Skills 实现整体智能体**,开发框架采用 AgentScope,模型使用 **qwen3-max**。Agent Skills 是 Anthropic 新推出的一种有别于mcp server的一种开发方式,用于为 AI **引入可共享的专业技能**。经验封装到**可发现、可复用的能力单元**中,每个技能以文件夹形式存在,包含特定任务的指导性说明(SKILL.md 文件)、脚本代码和资源等 。大模型可以根据需要动态加载这些技能,从而扩展自身的功能。目前不少国内外的一些框架也开始支持此种的开发方式,详细介绍如下。
1019 7