分布式搜索引擎Elasticsearch(二)

发布于:2024-12-06 ⋅ 阅读:(174) ⋅ 点赞:(0)

一、DSL查询文档

Elasticsearch提供了基于JSON的DSL(Domain Specific  Language)来定义查询。常见的查询类型包括:

1. DSL 查询分类概述

查询所有:查询出所有数据,一般测试用。例如:match_all

全文检索(full text)查询:利用分词器对用户输入内容分词,然后去倒排索引库中匹配。例如:

match_query、multi_match_query

精确查询:根据精确词条值查找数据,一般是查找keyword、数值、日期、boolean等类型字段。例如:ids、range、term

地理(geo)查询:根据经纬度查询。例如:geo_distance、geo_bounding_box

复合(compound)查询:复合查询可以将上述各种查询条件组合起来,合并查询条件。例如:bool、function_score

基本语法

GET /索引库名/_search

{

  "query"{

    "查询类型"{

      "查询条件""条件值"

    }

  }

}

例如:查询所有 

// 查询所有
GET /indexName/_search
{
  "query": {
    "match_all": {
    }
  }
}

2. 全文检索查询

全文检索查询,会对用户输入内容分词,常用于搜索框搜索:

1. match查询:全文检索查询的一种,会对用户输入内容分词,然后去倒排索引库检索,根据一个字段查询,语法:

GET   /索引名/_search

{

  "query"{

    "match"{

      "FIELD""TEXT"

    }

  }

}

如:条件查询 酒店吗为”7天酒店“ 的数据

GET /hotel/_search
{
  "query": {
    "match": {
      "all": "7天酒店"
    }
  }
}

2. multi_match:与match查询类似,只不过允许同时查询多个字段,根据多个字段查询,参与查询字段越多,查询性能越差。语法:

GET   /索引名/_search

{

  "query": {

    "multi_match": {

      "query""TEXT",

      "fields": ["FIELD1"" FIELD12"]

    }

  }

}

如:满足"brand", "name", "business" 任意字cha

GET /hotel/_search
{
  "query": {
    "multi_match": {
      "query": "7天酒店",
      "fields": ["brand", "name", "business"]
    }
  }
}

3. 精准查询

精确查询一般是查找keyword、数值、日期、boolean等类型字段。所以不会对搜索条件分词。常见的有:

  • term:根据词条精确值查询
  • range:根据值的范围查询

 term 查询:根据词条精确匹配,一般搜索keyword类型、数值类型、布尔类型、日期类型字段

// term查询

GET  /索引名/_search

{

  "query"{

    "term"{

      "FIELD"{

        "value""VALUE"

      }

    }

  }

}

搜索城市为”上海“ 的酒店

GET /hotel/_search
{
  "query": {
    "term": {
      "city": {
        "value": "上海"
      }
    }
  }
}

range 查询:根据数值范围查询,可以是数值、日期的范围

// range查询

GET /索引名/_search

{

  "query"{

    "range"{

      "FIELD": {

        "gte"10,

        "lte"20

      }

    }

  }

}

搜索价格区间大于等于100且小于等于300的酒店

GET /hotel/_search
{
  "query": {
    "range": {
      "price": {
        "gte": "100",    #gte大于等于, gt大于
        "lte": "300"    # lte 小于等于, lt 小于
      }
    }
  }
}

4. 地理坐标查询

根据经纬度查询。常见的使用场景包括:搜索附近的人,附近的酒店等。

1.  geo_bounding_box:查询左上角和右下角相交形成的矩形范围内的所有文档

// geo_bounding_box查询

GET /索引名/_search

{

  "query": {

    "geo_bounding_box": {

      "FIELD": {

        "top_left": {

          "lat"31.1,

          "lon"121.5

        },

        "bottom_right": {

          "lat"30.9,

          "lon"121.7

        }

      }

    }

  }

}

2.  geo_distance:查询指定中心点及半径所形成的圆的所有文档

// geo_distance 查询

GET  /索引名/_search

{

  "query": {

    "geo_distance": {

      "distance""15km",

      "FIELD""31.21,121.5"

    }

  }

}

5. 复合查询

复合查询可以将其它简单查询组合起来,实现更复杂的搜索逻辑,例如:

fuction score:算分函数查询,可以控制文档相关性算分,控制文档排名。例如百度竞价

相关性算分

当我们利用match查询时,文档结果会根据与搜索词条的关联度打分(_score),返回结果时按照分值降序排列。相关性算分两种方式:

  • 1. TF-IDF:在elasticsearch5.0之前,会随着词频增加而越来越大
  • 2. BM25:在elasticsearch5.0之后,会随着词频增加而增大,但增长曲线会趋于水平

5.1 function score query

使用 function score query,可以修改文档的相关性算分(query score),根据新得到的算分排序。 

案例给“如家”这个品牌的酒店排名靠前一些

解析:function score需要的三要素 

1. 过滤条件:哪些文档需要算分加权?  答: 品牌为如家的酒店

2. 算分函数:算分函数是什么?   答:weight就可以

3. 加权方式:加权模式是什么?   答:求和

GET /hotel/_search
{
  "query":{
  "function_score": {
    "query": {
      "match": {
        "all": "外滩"
      }
    },
    "functions": [   #算分函数
      { 
        "filter": {   # 满足的条件,品牌必须是如家
          "term": {
            "brand": "如家"
          }
        },
        "weight": 2     #权重为2
      }
    ],
    "boost_mode": "sum"
    }  
  }
}

 5.2 Boolean query

布尔查询是一个或多个查询子句的组合。子查询的组合方式有:

must:必须匹配每个子查询,类似“与”

should:选择性匹配子查询,类似“或”

must_not:必须不匹配,不参与算分,类似“非”

filter:必须匹配,不参与算分

需求搜索名字包含“如家”,价格不高于400,在坐标31.21,121.5周围10km范围内的酒店

GET /hotel/_search
{
 "query": {
   "bool": {
     "must": [
       {
         "match": {
           "name": "如家"
         }
       }
     ],
     "must_not": [
       {
         "range": {
           "price": {
             "gt": 400
           }
         }
       }
     ],
     "filter": [
       {
         "geo_distance": {
           "distance": "10km",
           "location": {
             "lat": 31.21,
             "lon": 121.5
           }
         }
       }
     ]
   }
 } 
}

二、搜索结果处理

1. 排序

elasticsearch支持对搜索结果排序,默认是根据相关度算分(_score)来排序。可以排序字段类型有:keyword类型、数值类型、地理坐标类型、日期类型等。

GET   /索引名/_search

{

  "query"{

    "match_all"{ }

  },

  "sort"[

    {

      "FIELD""desc"  // 排序字段和排序方式ASCDESC

    }

  ]

}

案例对酒店数据按照用户评价降序排序,评价相同的按照价格升序排序

# sort 排序
GET /hotel/_search
{
  "query": {
    "match_all": {}
  },
  "sort": [
    {
      "score": "desc"
    },
    {
      "price": "asc"
    }
  ]
}

案例2实现对酒店数据按照到你的位置坐标的距离升序排序

获取经纬度的方式获取鼠标点击经纬度-地图属性-示例中心-JS API 2.0 示例 | 高德地图API

GET /hotel/_search
{
  "query": {
    "match_all": {}
  },
  "sort": [
    {
      "_geo_distance": {
        "location": {
          "lat": 31.064661,
          "lon": 121.621245
        },
        "order": "asc",
        "unit": "km"
      }
    }
  ]
}

结果:

2. 分页

elasticsearch 默认情况下值返回前10条数据,如果需要查询更多数据就需要修改分页参数了。

elasticsearch 中通过from, size参数来控制要返回分页结果。

GET /hotel/_search
{
  "query": {
    "match_all": {}
  },
  "sort": [
    {
      "price": "asc"
    }
  ],
  "from": 0,  // 分页开始的位置,默认为0
  "size": 10   // 每页显示的条数
}

通过from, size 有个缺点,就是把所有数据都会查出来,然后通过截取的方式获取分页数据。对于海量数据,这种分页方式效率低下。也就是说,ES是分布式的,所以会面临深度分页问题。例如按price排序后,获取from = 990,size =10的数据:

  1. 首先在每个数据分片上都排序并查询前1000条文档。
  2. 然后将所有节点的结果聚合,在内存中重新排序选出前1000条文档
  3. 最后从这1000条中,选取从990开始的10条文档

如果搜索页数过深,或者结果集(from + size)越大,对内存和CPU的消耗也越高。因此ES设定结果集查询的上限是10000

针对深度分页,ES提供了两种解决方案,官方文档

  • search after:分页时需要排序,原理是从上一次的排序值开始,查询下一页数据。官方推荐使用的方式。
  • scroll:原理将排序数据形成快照,保存在内存。官方已经不推荐使用。

结果处理集分页总结

from + size

•优点:支持随机翻页
•缺点:深度分页问题,默认查询上限(from + size)是10000
•场景:百度、京东、谷歌、淘宝这样的随机翻页搜索

after search

•优点:没有查询上限(单次查询的size不超过10000)
•缺点:只能向后逐页查询,不支持随机翻页
•场景:没有随机翻页需求的搜索,例如手机向下滚动翻页

scroll

•优点:没有查询上限(单次查询的size不超过10000)
•缺点:会有额外内存消耗,并且搜索结果是非实时的
•场景:海量数据的获取和迁移。从ES7.1开始不推荐,建议用 after search方案。

3. 高亮

高亮就是在搜索结果中把搜索关键字突出显示。

高亮原理解析:

  1. 将搜索关键字用标签标记起来。
  2. 在页面中给标签添加css样式

 

基本语法 

GET /索引库名/_search

{

  "query": {

    "match": {

      "FIELD""TEXT"

    }

  },

  "highlight": {

    "fields": { // 指定要高亮的字段

      "FIELD": {

        "pre_tags""<em>",  // 用来标记高亮字段的前置标签

        "post_tags""</em>" // 用来标记高亮字段的后置标签

      }

    }

  }

}

# 高亮显示,默认情况,ES搜索字段必须和高亮字段一致
GET /hotel/_search
{
  "query": {
    "match": {
      "all": "如家"
    }
  },
  "highlight": {
    "fields": {
      "name": {
        "require_field_match": "false"     #是否需要字段匹配,默认true,关闭后,   ES搜索字段可和高亮字段不一致 
      }
    }
  }
}

三、RestClient查询文档

1. 快速入门

通过match_all来演示下基本的API,先看请求DSL的组织:

通过match_all来演示下基本的API,解析结果:

详细代码

public class HotelSearchTest {
    private RestHighLevelClient client;

    // 客户端初始化
    @BeforeEach
    void setUp() {
        this.client = new RestHighLevelClient(RestClient.builder(
                HttpHost.create("http://192.168.30.130:9200")  // 服务器IP + ES 端口
        ));
    }

   @Test
   void testMatchAll() throws IOException {
        // 1. 准备Request
       SearchRequest request = new SearchRequest("hotel");
       // 2. 准备 DSL
       request.source()
               .query(QueryBuilders.matchAllQuery());
       // 3. 发送请求
       SearchResponse response = client.search(request, RequestOptions.DEFAULT);

       //4. 解析响应
       SearchHits hits = response.getHits();
       // 4.1 获取总条数
       long total = hits.getTotalHits().value;
       System.out.println("共搜索到" + total + "条数据");
       //4.2 文档数组
       SearchHit[] hits1 = hits.getHits();
       // 4.3 遍历
       for (SearchHit hit : hits1) {
           //获取文档source
           String json = hit.getSourceAsString();
           // 反序列化
           HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
           System.out.println("hotelDoc = " + hotelDoc);
       }
       System.out.println(response);
   }


    @AfterEach
    void tearDown() throws IOException {
        this.client.close();
    }

}

RestAPI中其中构建DSL是通过HighLevelRestClient中的resource()来实现的,其中包含了查询、排序、分页、高亮等所有功能。

 RestAPI中其中构建查询条件的核心部分是由一个名为QueryBuilders的工具类提供的,其中包含了各种查询方法:

2. match 查询

全文检索的matchmulti_match查询与match_allAPI基本一致。差别是查询条件,也就是query的部分。同样是利用QueryBuilders提供的方法:

// 单字段查询
QueryBuilders.matchQuery("all", "如家");
// 多字段查询
QueryBuilders.multiMatchQuery("如家", "name", "business");

// 查询所有
QueryBuilders.matchAllQuery();

match 查询

GET /hotel/_search

{

  "query": {

    "match": {

      "all""如家"

    }

  }

}

 match_all 查询

GET /hotel/_search

{

  "query": {

    "match_all": {}

  }

}

 @Test
    void testMatch() throws IOException {
        // 1. 准备Request
        SearchRequest request = new SearchRequest("hotel");
        // 2. 准备 DSL
        request.source()
                .query(QueryBuilders.matchQuery("all", "如家"));
        // 3. 发送请求
        SearchResponse response = client.search(request, RequestOptions.DEFAULT);

        //4. 解析响应
        SearchHits hits = response.getHits();
        // 4.1 获取总条数
        long total = hits.getTotalHits().value;
        System.out.println("共搜索到" + total + "条数据");
        //4.2 文档数组
        SearchHit[] hits1 = hits.getHits();
        // 4.3 遍历
        for (SearchHit hit : hits1) {
            //获取文档source
            String json = hit.getSourceAsString();
            // 反序列化
            HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
            System.out.println("hotelDoc = " + hotelDoc);
        }
        System.out.println(response);
    }

 

multi_match  查询

GET /hotel/_search

{

  "query": {

    "multi_match": {

      "query""如家",

      "fields": ["brand""name"]

    }

  }

}

@Test
    void testMultiMatch() throws IOException {
        // 1. 准备Request
        SearchRequest request = new SearchRequest("hotel");
        // 2. 准备 DSL
        request.source()
                .query(QueryBuilders.multiMatchQuery("如家", "name", "business"));
        // 3. 发送请求
        SearchResponse response = client.search(request, RequestOptions.DEFAULT);

        //4. 解析响应
        SearchHits hits = response.getHits();
        // 4.1 获取总条数
        long total = hits.getTotalHits().value;
        System.out.println("共搜索到" + total + "条数据");
        //4.2 文档数组
        SearchHit[] hits1 = hits.getHits();
        // 4.3 遍历
        for (SearchHit hit : hits1) {
            //获取文档source
            String json = hit.getSourceAsString();
            // 反序列化
            HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
            System.out.println("hotelDoc = " + hotelDoc);
        }
        System.out.println(response);
    }

 

3. 精确查询

精确查询常见的有term查询和range查询,同样利用QueryBuilders实现

// 词条查询
QueryBuilders.termQuery("city", "杭州");
// 范围查询
QueryBuilders.rangeQuery("price").gte(100).lte(150);

term 查询

GET /hotel/_search

{

  "query": {

    "term": {

      "city""杭州"

    }

  }

}

range 查询

GET /hotel/_search

{

  "query": {

    "range": {

      "price": { "gte"100"lte"150 }

    }

  }

}

@Test
    void testBool() throws IOException {
        // 1. 准备Request
        SearchRequest request = new SearchRequest("hotel");
        //2. 准备DSL
        //2.1 准备BooleanQuery
        BoolQueryBuilder booledQuery = QueryBuilders.boolQuery();
        //2.2 添加 term
        booledQuery.must(QueryBuilders.termQuery("city", "上海"));
        //2.3 添加range
        booledQuery.filter(QueryBuilders.rangeQuery("price").lte(250));
        request.source().query(booledQuery);
        // 3. 发送请求
        SearchResponse response = client.search(request, RequestOptions.DEFAULT);

        //4. 解析响应
        SearchHits hits = response.getHits();
        // 4.1 获取总条数
        long total = hits.getTotalHits().value;
        System.out.println("共搜索到" + total + "条数据");
        //4.2 文档数组
        SearchHit[] hits1 = hits.getHits();
        // 4.3 遍历
        for (SearchHit hit : hits1) {
            //获取文档source
            String json = hit.getSourceAsString();
            // 反序列化
            HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
            System.out.println("hotelDoc = " + hotelDoc);
        }
        System.out.println(response);
    }

 

4. 复合查询

复合查询——boolean query

// 创建布尔查询
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
// 添加must条件
boolQuery.must(QueryBuilders.termQuery("city", "杭州"));
// 添加filter条件
boolQuery.filter(QueryBuilders.rangeQuery("price").lte(250));

GET /hotel/_search

{

  "query": {

    "bool": {

      "must": [

        {

          "term": { "city""杭州" }

        }

      ],

      "filter": [

        {

          "range": {

            "price": { "lte"250 }

          }

        }

      ]

    }

  }

}

tips: 要构建查询条件,只要记住一个类:QueryBuilders 

@Test
    void testBool() throws IOException {
        // 1. 准备Request
        SearchRequest request = new SearchRequest("hotel");
        //2. 准备DSL
        //2.1 准备BooleanQuery
        BoolQueryBuilder booledQuery = QueryBuilders.boolQuery();
        //2.2 添加 term
        booledQuery.must(QueryBuilders.termQuery("city", "上海"));
        //2.3 添加range
        booledQuery.filter(QueryBuilders.rangeQuery("price").lte(250));
        request.source().query(booledQuery);
        // 3. 发送请求
        SearchResponse response = client.search(request, RequestOptions.DEFAULT);

        //4. 解析响应
        SearchHits hits = response.getHits();
        // 4.1 获取总条数
        long total = hits.getTotalHits().value;
        System.out.println("共搜索到" + total + "条数据");
        //4.2 文档数组
        SearchHit[] hits1 = hits.getHits();
        // 4.3 遍历
        for (SearchHit hit : hits1) {
            //获取文档source
            String json = hit.getSourceAsString();
            // 反序列化
            HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
            System.out.println("hotelDoc = " + hotelDoc);
        }
        System.out.println(response);
    }

5. 排序、分页

搜索结果的排序和分页是与query同级的参数,对应的API如下:

// 查询
request.source().query(QueryBuilders.matchAllQuery());

// 分页
request.source().from(0).size(5);

// 价格排序
request.source().sort("price", SortOrder.ASC);

// 距离排序
request.source().sort(SortBuilders
        .
geoDistanceSort("location", new GeoPoint("31.21, 121.5"))
        .order(SortOrder.
ASC)
        .unit(DistanceUnit.
KILOMETERS)
);

DSL 语句 

GET /indexName/_search

{

  "query": {

    "match_all": {}

  },

  "from"0,

  "size"5

  "sort": [

    {

      "FIELD""desc"  

    },

  ]

}

@Test
    void testPageAndSort() throws IOException {
        int page = 1, size = 5;
        // 1. 准备Request
        SearchRequest request = new SearchRequest("hotel");
        //2. 准备DSL
        //2.1 query
        request.source().query(QueryBuilders.matchAllQuery());
        //2.2 排序
        request.source().sort("price", SortOrder.ASC);
        //2.3 分页
        request.source().from((page - 1) * size).size(5);
        // 3. 发送请求
        SearchResponse response = client.search(request, RequestOptions.DEFAULT);

        //4. 解析响应
        SearchHits hits = response.getHits();
        // 4.1 获取总条数
        long total = hits.getTotalHits().value;
        System.out.println("共搜索到" + total + "条数据");
        //4.2 文档数组
        SearchHit[] hits1 = hits.getHits();
        // 4.3 遍历
        for (SearchHit hit : hits1) {
            //获取文档source
            String json = hit.getSourceAsString();
            // 反序列化
            HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
            System.out.println("hotelDoc = " + hotelDoc);
        }
        System.out.println(response);
    }

6. 高亮

高亮API包括请求DSL构建和结果解析两部分。我们先看请求的DSL构建:

request.source().highlighter(new HighlightBuilder()
        .field(
"name")
       
// 是否需要与查询字段匹配
       
.requireFieldMatch(false)
);

DSL 语句

GET /hotel/_search

{

  "query": {

    "match": {

      "all""如家"

    }

  },

  "highlight": {

    "fields": {

      "name": {

        "require_field_match""false"

      }

    }

  }

}

 

高亮结果解析

// 获取source
HotelDoc hotelDoc = JSON.parseObject(hit.getSourceAsString(), 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);
    }
}


网站公告

今日签到

点亮在社区的每一天
去签到