针对不同的数据类型,ES提供了很多搜索匹配功能:

  • 完全匹配的term搜索
  • 按照范围匹配的range搜索
  • 分词匹配的match搜索
  • 前缀匹配的suggest搜索

查询所有文档

在关系型数据库中,当需要查询所有文档的数据时,对应的SQL语句为select * form table_name。在ES中使用ES的match_all查询可以完成类似的功能。使用match_all查询文档时,ES不对文档进行打分计算,默认情况下给每个文档赋予1.0的得分。用户可以通过boost参数设定该分值。以下示例使用match_all查询所有文档,并设定所有文档的分值为2.0:

GET /hotel/_search
{
  "_source": ["title","city"],
  "query": {
    "match_all": {
      "boost": 2 //设定所有文档的分值为2
    }
  }
}

结果返回:

{
  "took" : 3,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 4,
      "relation" : "eq"
    },
    "max_score" : 2.0,
    "hits" : [
      {
        "_index" : "hotel",
        "_type" : "_doc",
        "_id" : "001",
        "_score" : 2.0,
        "_source" : {
          "city" : "深圳",
          "title" : "java旅馆"
        }
      },
      {
        "_index" : "hotel",
        "_type" : "_doc",
        "_id" : "002",
        "_score" : 2.0,
        "_source" : {
          "city" : "北京",
          "title" : "python旅馆"
        }
      },
      {
        "_index" : "hotel",
        "_type" : "_doc",
        "_id" : "003",
        "_score" : 2.0,
        "_source" : {
          "city" : "上海",
          "title" : "go旅馆"
        }
      },
      {
        "_index" : "hotel",
        "_type" : "_doc",
        "_id" : "004",
        "_score" : 2.0,
        "_source" : {
          "city" : "广州",
          "title" : "C++旅馆"
        }
      }
    ]
  }
}

通过返回数据集可以看到,ES返回了所有的文档,并且所有文档的得分都为2.0。

        在Java客户端中进行查询时,可以调用QueryBuilders.matchAllQuery()方法新建一个match_all查询,并且通过boost()方法设置boost值。构建完term查询后,调用searchSource Builder.query()方法设置查询条件。以下为在Java客户端中使用match_all查询的示例:

    @Test
    public void testQueryAll() throws IOException {
        RestHighLevelClient client = new RestHighLevelClient(RestClient.builder(Arrays.stream("127.0.0.1:9200".split(","))
                .map(host->{
                    String[] split = host.split(":");
                    String hostName = split[0];
                    int port = Integer.parseInt(split[1]);
                    return new HttpHost(hostName,port,HttpHost.DEFAULT_SCHEME_NAME);
                }).filter(Objects::nonNull).toArray(HttpHost[]::new)));
        SearchRequest request = new SearchRequest("hotel");
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        MatchAllQueryBuilder builder = QueryBuilders.matchAllQuery().boost(2.0f);
        sourceBuilder.query(builder);
        request.source(sourceBuilder);
        SearchResponse searchResponse = client.search(request, RequestOptions.DEFAULT);
        for (SearchHit hit:searchResponse.getHits()) {
            System.out.println(hit.getSourceAsString()+"  "+hit.getScore());
        }
    }

term级别查询

term查询是结构化精准查询的主要查询方式,用于查询待查字段和查询值是否完全匹配,其请求形式如下:

POST /${index_name}/_search
{
  "query": {
    "term": {
      "FIELD": {
        "value": "VALUE"
      }
    }
  }
}

        其中,FIELD和VALUE分别代表字段名称和查询值,FIELD的数据类型可以是数值型、布尔型、日期型、数组型及关键字等

以下示例是搜索住宿价格为500元的旅馆,price字段为数值型数据:

POST /hotel/_search
{
  "query": {
    "term": {
      "price": {
        "value": "500"
      }
    }
  }
}

以下示例是搜索城市为北京的旅馆,city字段为关键字类型数据:       

POST /hotel/_search
{
  "query": {
    "term": {
      "city": {
        "value": "北京"
      }
    }
  }
}

以下示例是搜索没有满房的旅馆,full_room(满房状态)字段为布尔型数据:

POST /hotel/_search
{
  "query": {
    "term": {
      "full_room": {
        "value": "false"
      }
    }
  }
}

对于日期型的字段查询,需要按照该字段在mappings中定义的格式进行查询。如果create_time字段使用默认的格式,那么下面的请求是错误的:[此处格式是正确的]

POST /hotel/_search
{
  "query": {
    "term": {
      "create_time": {
        "value": "2022-08-05 00:00:00"
      }
    }
  }
}

        在Java客户端中进行查询时,可以调用QueryBuilders.termQuery()方法新建一个term查询。termQuery()方法传入不同的参数即可生成不同数据类型的term查询,可以传入boolean、double、float、int、long和String等类型的参数,但是没有日期类型的参数,具体可查看QueryBuilders中的方法:

 那么如何构建日期类型的term查询呢?可以使用日期形式字符串类型的term查询来解决。以下为使用日期类型的字符串参数构建的term查询:

    @Test
    public void testTermQuery() throws IOException {
        RestHighLevelClient client = new RestHighLevelClient(RestClient.builder(Arrays.stream("127.0.0.1:9200".split(","))
                .map(host->{
                    String[] split = host.split(":");
                    String hostName = split[0];
                    int port = Integer.parseInt(split[1]);
                    return new HttpHost(hostName,port,HttpHost.DEFAULT_SCHEME_NAME);
                }).filter(Objects::nonNull).toArray(HttpHost[]::new)));
        SearchRequest searchRequest = new SearchRequest("hotel");
        TermQueryBuilder queryBuilder = QueryBuilders.termQuery("create_time","2022-08-05 00:00:00");
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        SearchSourceBuilder query = sourceBuilder.query(queryBuilder);
        searchRequest.source(query);
        SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
        for (SearchHit hit:searchResponse.getHits()) {
            System.out.println(hit.getSourceAsString()+"  "+hit.getScore());
        }
    }

其他类型的term查询,如boolean、double、float、int和long等比较简单,可以自行练习

terms查询

terms查询是term查询的扩展形式,用于查询一个或多个值与待查字段是否完全匹配,其请求形式如下:

GET /${index_name}/_search
{
  "query": {
    "terms": {
      "FIELD": [
        "VALUE1",
        "VALUE2"
      ]
    }
  }
}

其中,FIELD代表待查字段名,VALUE1和VALUE2代表多个查询值,FIELD的数据类型可以是数值型、布尔型、日期型、数组型及关键字等。

以下是搜索城市为“北京”或者“深圳”的旅馆示例:

GET /hotel/_search
{
  "query": {
    "terms": {
      "city": [
        "北京",
        "深圳"
      ]
    }
  }
}

在Java客户端中对应terms查询的类为TermsQuery,该类的实例通过QueryBuilders.termsQuery()生成。在QueryBuilders.termsQuery()方法中,第一个参数为字段名称,第二个参数可以是一个集合类型,也可以是一个单独类型,当为单独类型时,该参数为可变长度参数。QueryBuilders.termsQuery()方法列表如下图所示:

以下是使用terms查询城市为“北京”或“深圳”的文档: 

    @Test
    public void testTermsQuery() throws IOException {
        RestHighLevelClient restHighLevelClient = new RestHighLevelClient(RestClient.builder(Arrays.stream("127.0.0.1:9200".split(","))
                .map(host->{
                    String[] split = host.split(":");
                    String hostName = split[0];
                    int port = Integer.parseInt(split[1]);
                    return new HttpHost(hostName,port,HttpHost.DEFAULT_SCHEME_NAME);
                }).filter(Objects::nonNull).toArray(HttpHost[]::new)));
        SearchRequest searchRequest = new SearchRequest("hotel");
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        TermsQueryBuilder termsQueryBuilder = QueryBuilders.termsQuery("city", "深圳", "北京");
        sourceBuilder.query(termsQueryBuilder);
        searchRequest.source(sourceBuilder);
        SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
        for (SearchHit hit:searchResponse.getHits()) {
            System.out.println(hit.getSourceAsString()+"  "+hit.getScore());
        }
    }

range查询

range查询用于范围查询,一般是对数值型和日期型数据的查询。使用range进行范围查询时,用户可以按照需求中是否包含边界数值进行选项设置,可供组合的选项如下:

  • gt:大于;
  • lt:小于;
  • gte:大于或等于;
  • lte:小于或等于。

以下是数值类型的查询示例,查询住宿价格在300~500(包含边界值)元的旅馆:

GET /hotel/_search
{
  "query": {
    "range": {
      "price": {
        "gte": 300,
        "lte": 500
      }
    }
  }
}

使用range查询时,查询值必须符合该字段在mappings中设置的规范。例如,在hotel索引中,price字段是double类型,则range应该使用数值型或者数值类型的字符串形式,不能使用其他形式。以下示例将导致ES返回错误:

GET /hotel/_search
{
  "query": {
    "range": {
      "price": {
        "gte": "aaa"
      }
    }
  }
}

执行上述DSL后,ES返回的信息如下:

{
  "error" : {
    "root_cause" : [
      {
        "type" : "query_shard_exception",
        "reason" : "failed to create query: For input string: \"aaa\"",
        "index_uuid" : "PhJjf84EQJuHAzq7nGs17w",
        "index" : "hotel"
      }
    ],
    "type" : "search_phase_execution_exception",
    "reason" : "all shards failed",
    "phase" : "query",
    "grouped" : true,
    "failed_shards" : [
      {
        "shard" : 0,
        "index" : "hotel",
        "node" : "tiANekxXS_GtirH4DamrFA",
        "reason" : {
          "type" : "query_shard_exception",
          "reason" : "failed to create query: For input string: \"aaa\"",
          "index_uuid" : "PhJjf84EQJuHAzq7nGs17w",
          "index" : "hotel",
          "caused_by" : {
            "type" : "number_format_exception",
            "reason" : "For input string: \"aaa\""
          }
        }
      }
    ]
  },
  "status" : 400
}

和term查询类似,查询日期型的字段时,需要遵循该字段在mappings中定义的格式进行查询。例如,create_time使用的是默认格式,并且统一采用的是“yyyyMMddHHmmss”格式,则range查询应该使用如下方式:

GET /hotel/_search
{
  "query": {
    "range": {
      "create_time": {
        "gte": "2022-08-01 00:00:00",
        "lte": "2022-08-08 00:00:00"
      }
    }
  }
}

在Java客户端上构建range请求是使用QueryBuilders.rangeQuery()方法实现的,该方法的参数为字段名称,然后再调用相应的方法即可构建相应的查询范围。可以调用gt()、lt()、gte()和lte()等方法分别实现大于、小于、大于等于、小于等于等查询范围。在使用时,可以直接连着使用“.”操作符,这样不用拆分语句,也比较容易理解。以下为在Java中使用range查询的示例程序:

    @Test
    public void testRangeQuery() throws IOException {
        RestHighLevelClient restHighLevelClient = new RestHighLevelClient(RestClient.builder(Arrays.stream("127.0.0.1:9200".split(","))
                .map(host->{
                    String[] split = host.split(":");
                    String hostName = split[0];
                    int port = Integer.parseInt(split[1]);
                    return new HttpHost(hostName,port,HttpHost.DEFAULT_SCHEME_NAME);
                }).filter(Objects::nonNull).toArray(HttpHost[]::new)));
        SearchRequest request = new SearchRequest("hotel");
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        QueryBuilder query = QueryBuilders.rangeQuery("create_time").gte("2022-08-01 00:00:00")
                .lte("2022-08-08 00:00:00");
        sourceBuilder.query(query);
        request.source(sourceBuilder);
        SearchResponse searchResponse = restHighLevelClient.search(request, RequestOptions.DEFAULT);
        for (SearchHit hit:searchResponse.getHits()) {
            System.out.println(hit.getSourceAsString()+"  "+hit.getScore());
        }
    }

exists查询

在某些场景下,我们希望找到某个字段不为空的文档,则可以用exists搜索。字段不为空的条件有:

  • 值存在且不是null;
  • 值不是空数组;
  • 值是数组,但不是[null]。

其DSL语法如下:

GET /${index_name}/_search
{
  "query": {
    "exists": {
      "field": "field_name"
    }
  }
}

在Java客户端上构建exists查询时,使用SearchSourceBuilder.existsQuery(String name)来构建,传递的name参数是目标字段名称。以下是使用Java客户端构建exists查询的示例:

   @Test
    public void testExsitQuery() throws IOException {
        RestHighLevelClient restHighLevelClient = new RestHighLevelClient(RestClient.builder(Arrays.stream("127.0.0.1:9200".split(","))
                .map(host->{
                    String[] split = host.split(":");
                    String hostName = split[0];
                    int port = Integer.parseInt(split[1]);
                    return new HttpHost(hostName,port,HttpHost.DEFAULT_SCHEME_NAME);
                }).filter(Objects::nonNull).toArray(HttpHost[]::new)));
        SearchRequest request = new SearchRequest("hotel");
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        sourceBuilder.query(QueryBuilders.existsQuery("tag"));
        SearchResponse searchResponse = restHighLevelClient.search(request, RequestOptions.DEFAULT);
        for (SearchHit hit:searchResponse.getHits()) {
            System.out.println(hit.getSourceAsString()+"  "+hit.getScore());
        }
    }

布尔查询

        复合搜索,顾名思义是一种在一个搜索语句中包含一种或多种搜索子句的搜索。

        布尔查询是常用的复合查询,它把多个子查询组合成一个布尔表达式,这些子查询之间的逻辑关系是“与”,即所有子查询的结果都为true时布尔查询的结果才为真。布尔查询还可以按照各个子查询的具体匹配程度对文档进行打分计算。

布尔查询支持的子查询有四种,各子查询的名称和功能如下表所示:

子查询名称功能
must必须匹配该查询条件
should可以匹配该查询条件
must not必须不匹配该查询条件
filter必须匹配过滤条件,不进行打分计算

1、must查询

        当查询中包含must查询时,相当于逻辑查询中的“与”查询。命中的文档必须匹配该子查询的结果,并且ES会将该子查询与文档的匹配程度值加入总得分里。must搜索包含一个数组,可以把其他的term级别的查询及布尔查询放入其中。

比如,使用must查询城市为北京并且价格在350~400元的旅馆:

GET /hotel/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "term": {
            "city": {
              "value": "北京"
            }
          }
        },
        {
          "range": {
            "price": {
              "gte": 350,
              "lte": 400
            }
          }
        }
      ]
    }
  }
}

在Java客户端上构建must搜索时,可以使用QueryBuilders.boolQuery().must()进行构建,上面的range查询例子改写成Java客户端请求的形式为:

    @Test
    public void testMustQuery() throws IOException {
        RestHighLevelClient restHighLevelClient = new RestHighLevelClient(RestClient.builder(Arrays.stream("127.0.0.1:9200".split(","))
                .map(host->{
                    String[] split = host.split(":");
                    String hostName = split[0];
                    int port = Integer.parseInt(split[1]);
                    return new HttpHost(hostName,port,HttpHost.DEFAULT_SCHEME_NAME);
                }).filter(Objects::nonNull).toArray(HttpHost[]::new)));
        SearchRequest request = new SearchRequest("hotel");
        BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder();
        TermQueryBuilder termQueryBuilder = QueryBuilders.termQuery("city","北京");
        RangeQueryBuilder rangeQueryBuilder = QueryBuilders.rangeQuery("price").gte(350).lte(400);
        boolQueryBuilder.must(termQueryBuilder).must(rangeQueryBuilder);
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        sourceBuilder.query(boolQueryBuilder);
        request.source(sourceBuilder);
        SearchResponse searchResponse = restHighLevelClient.search(request, RequestOptions.DEFAULT);
        for (SearchHit hit:searchResponse.getHits()) {
            System.out.println(hit.getSourceAsString()+"  "+hit.getScore());
        }
    }

2、should查询

当查询中包含should查询时,表示当前查询为“或”查询。命中的文档可以匹配该查询中的一个或多个子查询的结果,并且ES会将该查询与文档的匹配程度加入总得分里。should查询包含一个数组,可以把其他的term级别的查询及布尔查询放入其中。

以下示例使用should查询城市为北京或者深圳的旅馆:

GET /hotel/_search
{
  "query": {
    "bool": {
      "should": [
        {
          "term": {
            "city": {
              "value": "北京"
            }
          }
        },
        {
          "term": {
            "city": {
              "value": "深圳"
            }
          }
        }
      ]
    }
  }
}

在Java客户端上构建should搜索时,可以使用QueryBuilders.boolQuery().should()进行构建,上面的例子改写成Java客户端请求的形式为:

    @Test
    public void testShouldQuery() throws IOException {
        RestHighLevelClient restHighLevelClient = new RestHighLevelClient(RestClient.builder(Arrays.stream("127.0.0.1:9200".split(","))
                .map(host->{
                    String[] split = host.split(":");
                    String hostName = split[0];
                    int port = Integer.parseInt(split[1]);
                    return new HttpHost(hostName,port,HttpHost.DEFAULT_SCHEME_NAME);
                }).filter(Objects::nonNull).toArray(HttpHost[]::new)));
        SearchRequest request = new SearchRequest("hotel");
        BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder();
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        TermQueryBuilder termQueryBuilder = QueryBuilders.termQuery("city", "北京");
        TermQueryBuilder termQueryBuilder1 = QueryBuilders.termQuery("city", "深圳");
        sourceBuilder.query(boolQueryBuilder.should(termQueryBuilder).should(termQueryBuilder1));
        SearchResponse searchResponse = restHighLevelClient.search(request.source(sourceBuilder), RequestOptions.DEFAULT);
        for (SearchHit hit:searchResponse.getHits()) {
            System.out.println(hit.getSourceAsString()+"  "+hit.getScore());
        }
    }

3、must not查询

当查询中包含must not查询时,表示当前查询为“非”查询。命中的文档不能匹配该查询中的一个或多个子查询的结果,ES会将该查询与文档的匹配程度加入总得分里。must not查询包含一个数组,可以把其他term级别的查询及布尔查询放入其中。

以下示例中使用must not查询城市不是北京也不是深圳的旅馆:

GET /hotel/_search
{
  "query": {
    "bool": {
      "must_not": [
        {
          "term": {
            "city": {
              "value": "北京"
            }
          }
        },
        {
          "term": {
            "city": {
              "value": "深圳"
            }
          }
        }
      ]
    }
  }
}

在Java客户端上构建must_not搜索时,可以使用QueryBuilders.boolQuery().mustNot()方法进行构建,上面的例子改写成Java客户端请求的形式为:

    @Test
    public void testMustNotQuery() throws IOException {
        RestHighLevelClient restHighLevelClient = new RestHighLevelClient(RestClient.builder(Arrays.stream("127.0.0.1:9200".split(","))
                .map(host->{
                    String[] split = host.split(":");
                    String hostName = split[0];
                    int port = Integer.parseInt(split[1]);
                    return new HttpHost(hostName,port,HttpHost.DEFAULT_SCHEME_NAME);
                }).filter(Objects::nonNull).toArray(HttpHost[]::new)));
        SearchRequest request = new SearchRequest("hotel");
        BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder();
        request.source(new SearchSourceBuilder().query(boolQueryBuilder.mustNot(QueryBuilders.termQuery("city", "北京"))
                .mustNot(QueryBuilders.termQuery("city", "深圳"))));
        SearchResponse searchResponse = restHighLevelClient.search(request, RequestOptions.DEFAULT);
        for (SearchHit hit:searchResponse.getHits()) {
            System.out.println(hit.getSourceAsString()+"  "+hit.getScore());
        }
    }

4、filter查询

filter查询即过滤查询,该查询是布尔查询里非常独特的一种查询。其他布尔查询关注的是查询条件和文档的匹配程度,并按照匹配程度进行打分;而filter查询关注的是查询条件和文档是否匹配,不进行相关的打分计算,但是会对部分匹配结果进行缓存。

以下为使用filter的简单的例子,请求城市为北京并且未满房的旅馆:

GET /hotel/_search
{
  "query": {
    "bool": {
      "filter": [
        {
          "term": {
            "city": "北京"
          }
        },
        {
          "term": {
            "full_room": false
          }
        }
      ]
    }
  }
}

在Java客户端上构建filter搜索时,可以使用QueryBuilders.boolQuery().filter()进行构建,上面的例子改写成Java客户端请求的形式为:

    @Test
    public void testFilterQuery() throws IOException {
        RestHighLevelClient restHighLevelClient = new RestHighLevelClient(RestClient.builder(Arrays.stream("127.0.0.1:9200".split(","))
                .map(host->{
                    String[] split = host.split(":");
                    String hostName = split[0];
                    int port = Integer.parseInt(split[1]);
                    return new HttpHost(hostName,port,HttpHost.DEFAULT_SCHEME_NAME);
                }).filter(Objects::nonNull).toArray(HttpHost[]::new)));
        SearchRequest request = new SearchRequest("hotel");
        request.source(new SearchSourceBuilder().query(new BoolQueryBuilder().filter(QueryBuilders.termQuery("city","北京"))
                .filter(QueryBuilders.termQuery("full_room",false))));
        SearchResponse searchResponse = restHighLevelClient.search(request, RequestOptions.DEFAULT);
        for (SearchHit hit:searchResponse.getHits()) {
            System.out.println(hit.getSourceAsString()+"  "+hit.getScore());
        }
    }

filter查询原理

假设当前有5个文档,ES对于city字段的倒排索引结构如下图所示:

ES对于满房字段倒排的索引结构如下图所示:

         当ES执行过滤条件时,会查询缓存中是否有city字段值为“北京”对应的bitset数据。bitset,中文为位图,它可以用非常紧凑的格式来表示给定范围内的连续数据。如果查询缓存中有对应的bitset数据,则取出备用;如果缓存中没有bitset数据,则ES在查询数据后会对查询条件进行bitset的构建并将其放入缓存中。同时,ES也会考察满房字段为false是否有对应的bitset数据。如果有,则取出备用;如果缓存中没有,ES也会进行bitset的构建。

        假设city字段值为“北京”,缓存中没有对应的bitset数据,则bitset构建的过程如下:

        首先,ES在倒排索引中查找字段city值为“北京”字符串的文档,这里为doc1和doc5。然后为所有文档构建bitset数组,数组中每个元素的值用来表示对应位置的文档是否和查询条件匹配,0表示未匹配,1表示匹配。在本例中,doc1和doc5匹配“北京”,对应位置的值为1;doc2、doc3、doc4不匹配,对应位置的值为0。最终,本例的bitset数组为[1,0,0,0,1]。之所以用bitset表示文档和query的匹配结果,是因为该结构不仅节省空间而且后续进行操作时也能节省时间。

        如果满房字段缓存中没有对应的bitset数据,ES构建满房字段为false对应bitset的过程也是类似的。如下图所示为ES构建的字段city值为“北京”和满房字段值为false时对应的bitset结构。

        接下来ES会遍历查询条件的bitset数组,按照文档命中与否进行文档过滤。当一个请求中有多个filter查询条件时,ES会构建多个bitset数组。为提升效率,ES会从最稀疏的数组开始遍历,因为遍历稀疏的数组可以过滤掉更多的文档。此时,城市为“北京”对应的bitset比满房为false的bitset更加稀疏,因此先遍历城市为“北京”的bitset,再遍历满房为false的bitset。遍历的过程中也进行了位运算,每次运算的结果都逐渐接近符合条件的结果。遍历计算完这两个bitset后,得到匹配所有过滤条件的文档,即doc1和doc5。 

        如果查询内包含filter,那么ES首先就从缓存中搜索这个filter条件是否有执行记录,是否有对应的bitset缓存可查询。如果有,则从缓存中查询;如果没有,则为filter中的每个查询项新建bitset,并且缓存该bitset,以供后续其他带有filter的查询可以先在缓存中查询。也就是说,ES对于bitset是可重用的,这种重用的机制叫作filter cache(过滤器缓存)。

        filter cache会跟踪每一个filter查询,ES筛选一部分filter查询的bitset进行缓存。首先,这些过滤条件要在最近256个查询中出现过;其次,这些过滤条件的次数必须超过某个阈值。

        另外,filter cache是有自动更新机制的,即如果有新增文档或者文档被修改过,那么filter cache对应的过滤条件中的bitset将被更新。例如城市为“北京”过滤条件对应的bitset为[1,0,0,0,1],如果文档4的城市被修改为“北京”,则“北京”过滤条件对应的bitset会被自动更新为[1,0,0,1,1]。

        filter查询带来的效益远不止这些,使用filter查询的子句是不计算分数的,这可以减少不小的时间开销。

        为提升查询效率,对于简单的term级别匹配查询,应该根据自己的实际业务场景选择合适的查询语句,需要确定这些查询项是否都需要进行打分操作,如果某些匹配条件不需要打分操作的话,那么应该把这些查询全部改成filter形式,让查询更高效。

Constant Score查询

        如果不想让检索词频率TF(Term Frequency)对搜索结果排序有影响,只想过滤某个文本字段是否包含某个词,可以使用Constant Score将查询语句包装起来。

  假设需要查询amenities字段包含关键词“停车场”的旅馆,则请求的DSL如下:

GET /hotel/_search
{
  "_source": ["amenities"],
  "query": {
    "constant_score": { //满足条件即打分为1
      "filter": {
        "match":{ //查询设施中是否包含“停车场”文档
          "value": "amenities"
        }
      }
    }
  }
}

使用Constant Score搜索时,命中的文档对应的amenities字段都包含有“停车场”一词。但是不论该词在文档中出现多少次,这些文档的得分都是一样的,值为1.0。在Constant Score搜索中,参数boost可以控制命中文档的得分,默认值为1.0。以下为更改boost参数为2.0的例子:

GET /hotel/_search
{
  "_source": ["amenities"],
  "query": {
    "constant_score": { 
      "filter": {
        "match":{ 
          "value": "amenities"
        }
      },
      "boost": 2.0
    }
  }
}

 在Java客户端上构建Constant Score搜索时,可以使用ConstantScoreQueryBuilder类的实例进行构建,上面的Constant Score查询例子改写成Java客户端请求的形式为:

    @Test
    public void testCosntantScore() throws IOException {
        RestHighLevelClient restHighLevelClient = new RestHighLevelClient(RestClient.builder(Arrays.stream("127.0.0.1:9200".split(","))
                .map(host->{
                    String[] split = host.split(":");
                    String hostName = split[0];
                    int port = Integer.parseInt(split[1]);
                    return new HttpHost(hostName,port,HttpHost.DEFAULT_SCHEME_NAME);
                }).filter(Objects::nonNull).toArray(HttpHost[]::new)));
        SearchRequest request = new SearchRequest("hotel");
        request.source(new SearchSourceBuilder().query(new ConstantScoreQueryBuilder(QueryBuilders.termQuery("amenities","停车场")).boost(2.0f)));
        SearchResponse searchResponse = restHighLevelClient.search(request, RequestOptions.DEFAULT);
        for (SearchHit hit:searchResponse.getHits()) {
            System.out.println(hit.getSourceAsString()+"  "+hit.getScore());
        }
    }

Function Score查询

        当使用ES进行搜索时,命中的文档默认按照相关度进行排序。有些场景下用户需要干预该“相关度”,此时就可以使用Function Score查询。使用时,用户必须定义一个查询以及一个或多个函数,这些函数为每个文档计算一个新分数。

下面使用一个随机函数对查询结果进行排序:

GET /hotel/_search
{
  "_source": ["title","city"]
  , "query": {
    "function_score": {
      "query": {
        "term": {
          "city": {
            "value": "北京"
          }
        }
      },
      "functions": [  //定义函数【此处为随机函数】
        {
          "random_score": {}
        }
      ],
      "score_mode": "sum" //最终分数是各个函数的加和值
    }
  }
}

        上述请求使用了Function Score查询,其中,query子句负责对文档进行匹配,本例使用了简单的term查询,functions子句负责输出对文档的排序分值,此处使用了random_score随机函数,使得每个文档的分数都是随机生成的。每次执行上述查询时生成的文档分数都不同。以下为某次搜索的结果:

{
  "took" : 29,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 1,
      "relation" : "eq"
    },
    "max_score" : 0.8679311,
    "hits" : [
      {
        "_index" : "hotel",
        "_type" : "_doc",
        "_id" : "002",
        "_score" : 0.8679311,
        "_source" : {
          "city" : "北京",
          "title" : "python旅馆"
        }
      }
    ]
  }
}

        在Java客户端中使用Function Score进行查询时,可以调用ScoreFunctionBuilders.randomFunction()方法新建一个随机函数,然后将该随机函数的实例传给QueryBuilders.functionScoreQuery()方法生成FunctionScoreQueryBuilder的实例,最终由该实例完成搜索。以下是使用Java客户端的Function Score进行查询的例子,其排序结果和上述使用Function Score进行查询的示例相似,即文档的得分是通过随机函数生成的。

    @Test
    public void testFunctionScoreSearch() throws IOException {
        RestHighLevelClient restHighLevelClient = new RestHighLevelClient(RestClient.builder(Arrays.stream("127.0.0.1:9200".split(","))
                .map(host->{
                    String[] split = host.split(":");
                    String hostName = split[0];
                    int port = Integer.parseInt(split[1]);
                    return new HttpHost(hostName,port,HttpHost.DEFAULT_SCHEME_NAME);
                }).filter(Objects::nonNull).toArray(HttpHost[]::new)));
        SearchRequest request = new SearchRequest("hotel");
        RandomScoreFunctionBuilder randomScoreFunctionBuilder = ScoreFunctionBuilders.randomFunction();
        FunctionScoreQueryBuilder functionScoreQueryBuilder = QueryBuilders.functionScoreQuery(QueryBuilders.termQuery("city", "北京")
                , randomScoreFunctionBuilder).boostMode(CombineFunction.SUM);
        SearchSourceBuilder query = new SearchSourceBuilder().query(functionScoreQueryBuilder);
        request.source(query);
        SearchResponse searchResponse = restHighLevelClient.search(request, RequestOptions.DEFAULT);
        for (SearchHit hit:searchResponse.getHits()) {
            System.out.println(hit.getSourceAsString()+"  "+hit.getScore());
        }
    }

Function Score查询提供了很多实用函数,能满足绝大多数的用户自定义打分需求。

全文搜索

        不同于结构化查询,全文搜索首先对查询词进行分析,然后根据查询词的分词结果构建查询。这里所说的全文指的是文本类型数据(text类型),默认的数据形式是人类的自然语言,如对话内容、图书名称、商品介绍和旅馆名称等。结构化搜索关注的是数据是否匹配,全文搜索关注的是匹配的程度;结构化搜索一般用于精确匹配,而全文搜索用于部分匹配.

1.match查询

        match查询是全文搜索的主要代表。对于最基本的math搜索来说,只要分词中的一个或者多个在文档中存在即可。例如搜索“北京旅馆”,查询词先被分词器切分为“北”“京”“旅”“馆”,因此,只要文档中包含这4个字中的任何一个字,都会被搜索到。

        由于在默认情况下,match查询使用的是标准分词器。该分词器比较适用于英文,如果是中文则按照字进行切分,因此默认的分词器不适合做中文搜索,因此不会被拆分为“北京”和“旅馆”。

比如以下示例:

PUT /_bulk
{"index":{"_index":"hotel","_id":"100"}}
{"title":"北京旅馆"}
{"index":{"_index":"hotel","_id":"101"}}
{"title":"北京欣欣旅馆"}
{"index":{"_index":"hotel","_id":"102"}}
{"title":"北京益田假日旅馆"}
{"index":{"_index":"hotel","_id":"103"}}
{"title":"深圳旅馆"}
{"index":{"_index":"hotel","_id":"104"}}
{"title":"深圳精选旅馆"}

搜索“北京旅馆”:

GET /hotel/_search
{
  "_source": ["title"],
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "title": "北京旅馆"
          }
        }
      ],
      "must_not": [
        {"exists": {"field": "city"}}
      ]
    }
  }
}

搜索结果如下:

{
  "took" : 5,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 5,
      "relation" : "eq"
    },
    "max_score" : 2.2961648,
    "hits" : [
      {
        "_index" : "hotel",
        "_type" : "_doc",
        "_id" : "100",
        "_score" : 2.2961648,
        "_source" : {
          "title" : "北京旅馆"
        }
      },
      {
        "_index" : "hotel",
        "_type" : "_doc",
        "_id" : "101",
        "_score" : 1.9264048,
        "_source" : {
          "title" : "北京欣欣旅馆"
        }
      },
      {
        "_index" : "hotel",
        "_type" : "_doc",
        "_id" : "102",
        "_score" : 1.6592152,
        "_source" : {
          "title" : "北京益田假日旅馆"
        }
      },
      {
        "_index" : "hotel",
        "_type" : "_doc",
        "_id" : "103",
        "_score" : 0.106962316,
        "_source" : {
          "title" : "深圳旅馆"
        }
      },
      {
        "_index" : "hotel",
        "_type" : "_doc",
        "_id" : "104",
        "_score" : 0.08973777,
        "_source" : {
          "title" : "深圳精选旅馆"
        }
      }
    ]
  }
}

        从结果中可以看到,匹配度最高的文档是“北京旅馆”,该旅馆的名称和查询词相同,得分为最高;次之的文档是"北京欣欣旅馆",因为该旅馆名称中包含“北”“京”“旅”“馆”,并且标题相对较短,所以部分匹配;再次之的文档是“北京益田假日旅馆”,虽然该旅馆名称中包含“北”“京”“旅”“馆”,但是相对于文档"北京欣欣旅馆"其标题相对较长,因此位居其后。后面两个文档只有“旅”“馆”两个字和查询词部分匹配,因此排在后面,又因为文档005比文档001较长,所以位居最后。

        假设用户搜索“北京”是想搜索名称中同时包含“北”和“京”的旅馆,而不需要命中名称中包含“北”或“京”的旅馆。显然,最后两个文档不是用户想命中的文档。match搜索可以设置operator参数,该参数决定文档按照分词后的词集合进行“与”还是“或”匹配。在默认情况下,该参数的值为“与”关系,即operator的值为or,这也解释了搜索结果中包含部分匹配的文档。如果希望各个词之间的匹配结果是“与”关系,则可以设置operator参数的值为and。

下面的请求示例设置查询词之间的匹配结果为“与”关系:

GET /hotel/_search
{
  "_source": ["title"],
  "query": {
    "match": {
      "title": {
        "query": "北京",
        "operator": "and"
      }
    }
  }
}

返回结果如下:

{
  "took" : 3,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 3,
      "relation" : "eq"
    },
    "max_score" : 2.1892025,
    "hits" : [
      {
        "_index" : "hotel",
        "_type" : "_doc",
        "_id" : "100",
        "_score" : 2.1892025,
        "_source" : {
          "title" : "北京旅馆"
        }
      },
      {
        "_index" : "hotel",
        "_type" : "_doc",
        "_id" : "101",
        "_score" : 1.8366671,
        "_source" : {
          "title" : "北京欣欣旅馆"
        }
      },
      {
        "_index" : "hotel",
        "_type" : "_doc",
        "_id" : "102",
        "_score" : 1.581924,
        "_source" : {
          "title" : "北京益田假日旅馆"
        }
      }
    ]
  }
}

        有时搜索多个关键字,关键词和文档在某一个比例上匹配即可,如果使用“与”操作过于严苛,如果使用“或”操作又过于宽松。这时可以采用minimum_should_match参数,该参数叫作最小匹配参数,其值为一个数值,意义为可以匹配上的词的个数。在一般情况下将其设置为一个百分数,因为在真实场景中并不能精确控制具体的匹配数量。以下示例设置最小匹配为80%的文档:

GET /hotel/_search
{
  "_source": ["title"],
  "query": {
    "match": {
      "title": {
        "query": "北京",
        "operator": "or",
        "minimum_should_match": "80%" //设置最小匹配度为80%
      }
    }
  }
}

        在Java客户端上可以使用QueryBuilders.matchQuery()方法构建match请求,分别给该方法传入字段名称和查询值即可进行match查询。以下代码展示了match请求的使用逻辑:

    @Test
    public void testMatchQuery() throws IOException {
        RestHighLevelClient restHighLevelClient = new RestHighLevelClient(RestClient.builder(Arrays.stream("127.0.0.1:9200".split(","))
                .map(host->{
                    String[] split = host.split(":");
                    String hostName = split[0];
                    int port = Integer.parseInt(split[1]);
                    return new HttpHost(hostName,port,HttpHost.DEFAULT_SCHEME_NAME);
                }).filter(Objects::nonNull).toArray(HttpHost[]::new)));
        SearchRequest request = new SearchRequest("hotel");
        request.source(new SearchSourceBuilder().query(QueryBuilders.matchQuery("title","北京").operator(Operator.AND)));
        SearchResponse searchResponse = restHighLevelClient.search(request, RequestOptions.DEFAULT);
        for (SearchHit hit:searchResponse.getHits()) {
            System.out.println(hit.getSourceAsString()+"  "+hit.getScore());
        }
    }

2.multi_match查询

        有时用户需要在多个字段中查询关键词,除了使用布尔查询封装多个match查询之外,可替代的方案是使用multi_match。可以在multi_match的query子句中组织数据匹配规则,并在fields子句中指定需要搜索的字段列表。

下面的示例在title和city两个字段中同时搜索“北京”关键词:

GET /hotel/_search
{
  "_source": ["title","city"],
  "query": {
    "multi_match": {
      "query": "北京",
      "fields": ["title","city"]
    }
  }
}

返回结果如下:

{
  "took" : 3,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 4,
      "relation" : "eq"
    },
    "max_score" : 2.1892025,
    "hits" : [
      {
        "_index" : "hotel",
        "_type" : "_doc",
        "_id" : "100",
        "_score" : 2.1892025,
        "_source" : {
          "title" : "北京旅馆"
        }
      },
      {
        "_index" : "hotel",
        "_type" : "_doc",
        "_id" : "101",
        "_score" : 1.8366671,
        "_source" : {
          "title" : "北京欣欣旅馆"
        }
      },
      {
        "_index" : "hotel",
        "_type" : "_doc",
        "_id" : "102",
        "_score" : 1.581924,
        "_source" : {
          "title" : "北京益田假日旅馆"
        }
      },
      {
        "_index" : "hotel",
        "_type" : "_doc",
        "_id" : "002",
        "_score" : 1.2039728,
        "_source" : {
          "city" : "北京",
          "title" : "python旅馆"
        }
      }
    ]
  }
}

根据结果可以看到,命中的文档要么在title中包含“北京关键词,要么在city字段中包含“北京”关键词。

        在Java客户端上可以使用QueryBuilders.multiMatchQuery()方法构建multi_match请求,分别给该方法传入查询值和多个字段名称即可进行multi_match查询。以下代码展示了multi_match请求的使用逻辑:

    @Test
    public void testMultiMatchQuery() throws IOException {
        RestHighLevelClient restHighLevelClient = new RestHighLevelClient(RestClient.builder(Arrays.stream("127.0.0.1:9200".split(","))
                .map(host->{
                    String[] split = host.split(":");
                    String hostName = split[0];
                    int port = Integer.parseInt(split[1]);
                    return new HttpHost(hostName,port,HttpHost.DEFAULT_SCHEME_NAME);
                }).filter(Objects::nonNull).toArray(HttpHost[]::new)));
        SearchRequest request = new SearchRequest("hotel");
        request.source(new SearchSourceBuilder().query(QueryBuilders.multiMatchQuery("北京","title","city")));
        SearchResponse searchResponse = restHighLevelClient.search(request, RequestOptions.DEFAULT);
        for (SearchHit hit:searchResponse.getHits()) {
            System.out.println(hit.getSourceAsString()+"  "+hit.getScore());
        }
    }

3.match_phrase查询

match_phrase用于匹配短语,与match查询不同的是,match_phrase用于搜索确切的短语或邻近的词语。假设在旅馆标题中搜索“文雅旅馆”,希望旅馆标题中的“文雅”与“旅馆”紧邻并且“文雅”在“旅馆”前面,则使用match_phrase查询的DSL如下:

GET /hotel/_search
{
  "query": {
    "match_phrase": {
      "title": "文雅旅馆"
    }
  }
}

如果需要文档“文雅XX旅馆”也命中上述查询,则可以设置match_phrase查询的slop参数,它用来调节匹配词之间的距离阈值。下面的DSL将slop设置为2:

GET /hotel/_search
{
  "query": {
    "match_phrase": {
      "title": {
        "query": "文雅旅馆",
        "slop": 2 //设置“文雅”和“旅馆”之间的最大匹配距离
      }
    }
  }
}

        在Java客户端上可以使用QueryBuilders.matchPhraseQuery()方法构建match_phrase请求,分别给该方法传入查询字段和值即可进行multi_match查询。以下代码展示了match_phrase请求的使用逻辑:

    @Test
    public void testMatchPhrase() throws IOException {
        RestHighLevelClient restHighLevelClient = new RestHighLevelClient(RestClient.builder(Arrays.stream("127.0.0.1:9200".split(","))
                .map(host->{
                    String[] split = host.split(":");
                    String hostName = split[0];
                    int port = Integer.parseInt(split[1]);
                    return new HttpHost(hostName,port,HttpHost.DEFAULT_SCHEME_NAME);
                }).filter(Objects::nonNull).toArray(HttpHost[]::new)));
        SearchRequest request = new SearchRequest("hotel");
        request.source(new SearchSourceBuilder().query(QueryBuilders.matchPhraseQuery("title","文雅旅馆").slop(2)));
        SearchResponse searchResponse = restHighLevelClient.search(request, RequestOptions.DEFAULT);
        for (SearchHit hit:searchResponse.getHits()) {
            System.out.println(hit.getSourceAsString()+"  "+hit.getScore());
        }
    }

基于地理位置查询

        随着互联网+的热门,越来越多的传统行业将全部或者部分业务转移到互联网上,其中不乏一些和地理位置强相关的行业。基于地理位置的搜索功能,大大提升了人们的生活和工作效率。例如,外出旅行时,只需要用手机打开订旅馆的应用软件,查找附近心仪的旅馆下单即可;又或者打车行业,人们不用在寒冷的户外去拦截出租车,只需要在室内打开手机里的打车App定位到当前位置,然后确定目的地,系统就可以为附近的车辆派发订单。

        ES为用户提供了基于地理位置的搜索功能。它主要支持两种类型的地理查询:一种是地理点(geo_point),即经纬度查询,另一种是地理形状查询(geo_shape),即支持点、线、圆形和多边形查询等。

        从实用性上来说,地理点(即geo_point)数据类型的使用更多一些

        对应于geo_point字段类型的查询方式有3种,分别为geo_distance查询、geo_bounding_box查询和geo_polygon。

        geo_distance查询方式需要用户指定一个坐标点,在指定距离该点的范围后,ES即可查询到相应的文档。假设北京天安门的经纬度为[116.4039,39.915143],以下为使用geo_distance查询所找到的天安门5km范围内的旅馆:

GET /hotel/_search
{
  "query": {
    "geo_distance":{
      "distance": "5km", //设置距离范围为5km
      "location":{       //设置中心点经纬度
        "lat":"39.915143",
        "lon":"116.4039"
      }
    }
  }
}

Java客户端使用QueryBuilders.geoDistanceQuery()方法构建geo_distance请求,同时可以设置基准点坐标和周边距离。以下代码展示了geo_distance请求的使用逻辑:

    @Test
    public void testDistanceQuery() throws IOException {
        RestHighLevelClient restHighLevelClient = new RestHighLevelClient(RestClient.builder(Arrays.stream("127.0.0.1:9200".split(","))
                .map(host->{
                    String[] split = host.split(":");
                    String hostName = split[0];
                    int port = Integer.parseInt(split[1]);
                    return new HttpHost(hostName,port,HttpHost.DEFAULT_SCHEME_NAME);
                }).filter(Objects::nonNull).toArray(HttpHost[]::new)));
        SearchRequest request = new SearchRequest("hotel");
        request.source(new SearchSourceBuilder().query(QueryBuilders.geoDistanceQuery("location").distance(5,DistanceUnit.KILOMETERS)
                .point(39.915143,116.4039)));
        SearchResponse searchResponse = restHighLevelClient.search(request, RequestOptions.DEFAULT);
        for (SearchHit hit:searchResponse.getHits()) {
            System.out.println(hit.getSourceAsString()+"  "+hit.getScore());
        }
    }

geo_shape查询提供的是矩形内的搜索,需要用户给出左上角的顶点地理坐标和右下角的顶点地理坐标。假设定义国贸商圈为一个矩形,其左上角顶点的经纬度为[116.457044,39.922821],右下角顶点的经纬度为[116.479466,39.907104],则在国贸商圈内搜索旅馆的DSL如下:

GET /hotel/_search
{
  "query": {
    "geo_bounding_box":{
      "location":{
        "top_left":{
          "lat":"39.922821",
          "lon":"116.457044"
        },
        "bottom_right":{
          "lat":"39.907104",
          "lon":"116.479466"
        }
      }
    }
  }
}

geo_polygon比geo_shape提供的地理范围功能更加灵活,它支持多边形内的文档搜索,使用该查询需要提供多边形所有顶点的地理坐标。假设北京地坛公园商圈的地形为三角形,该三角形的三个顶点的经纬度分别为[116.417088,39.959829]、[116.432035,39.960272]和[116.421399,39.965802],则在地坛公园商圈内搜索旅馆的DSL语句如下:

GET /hotel/_search
{
  "query": {
    "geo_polygon":{
      "location":{
        "points":[
          {
            "lat":"39.959829",
            "lon":"116.417088"
          },
          {
            "lat":"39.960272",
            "lon":"116.432035"
          },
          {
            "lat":"39.965802",
            "lon":"116.421399"
          }
        ]
      }
    }
  }
}

Java客户端使用QueryBuilders.geoPolygonQuery()方法构建geo_polygon请求,在构建请求之前,需要将多边形的顶点事先准备好。以下代码展示了geo_polygon请求的使用逻辑:

    @Test
    public void testGeoQuery() throws IOException {
        RestHighLevelClient restHighLevelClient = new RestHighLevelClient(RestClient.builder(Arrays.stream("127.0.0.1:9200".split(","))
                .map(host->{
                    String[] split = host.split(":");
                    String hostName = split[0];
                    int port = Integer.parseInt(split[1]);
                    return new HttpHost(hostName,port,HttpHost.DEFAULT_SCHEME_NAME);
                }).filter(Objects::nonNull).toArray(HttpHost[]::new)));
        SearchRequest request = new SearchRequest("hotel");
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        List<GeoPoint> geoPointList = new ArrayList<>();
        geoPointList.add(new GeoPoint(39.959829,116.417088));
        geoPointList.add(new GeoPoint(39.960272,116.432035));
        geoPointList.add(new GeoPoint(39.965802,116.421399));
        request.source(sourceBuilder.query(QueryBuilders.geoPolygonQuery("location",geoPointList)));
        SearchResponse searchResponse = restHighLevelClient.search(request, RequestOptions.DEFAULT);
        for (SearchHit hit:searchResponse.getHits()) {
            System.out.println(hit.getSourceAsString()+"  "+hit.getScore());
        }
    }

搜索建议

        搜索建议即在用户输入搜索关键词的过程中系统进行自动补全,用户可以根据自己的需求单击搜索建议的内容直接进行搜索。在搜索时,用户每输入一个字符,前端就需要向后端发送一次查询请求对匹配项进行查询,因此这种场景对后端响应速度的要求比较高。通过协助用户进行搜索,可以避免用户输入错误的关键词,引导用户使用更合适的关键词,提升用户的搜索体验和搜索效率。

搜索建议目前是各大搜索引擎和电商的标配服务,下图所示为在百度搜索中输入elastic时的搜索建议示例。

那么类似的功能在ES中是如何实现呢?答案就是ES的搜索建议查询。对于以上应用来说,ES中的Completion Suggester是比较合适的。为了使用Completion Suggester,其对应的字段类型需要定义为completion类型。在以下示例中定义了一个旅馆搜索建议的索引:

PUT /hotel_sug
{
  "mappings": {
    "properties": {
      "query_word":{
        "type": "completion"
      }
    }
  }
}

 现在向索引中写入一些候选数据:

POST /_bulk
{"index":{"_index":"hotel_sug","_id":"001"}}
{"query_word":"如家旅馆"}
{"index":{"_index":"hotel_sug","_id":"002"}}
{"query_word":"如家快捷旅馆"}
{"index":{"_index":"hotel_sug","_id":"003"}}
{"query_word":"如家精选旅馆"}
{"index":{"_index":"hotel_sug","_id":"004"}}
{"query_word":"汉庭假日旅馆"}

假设用户输入“如家”关键词,需要ES给出前缀为该词的旅馆查询词,DSL如下:

GET /hotel_sug/_search
{
  "suggest": {
    "hotel_zh_sug": {
      "prefix": "如家",
      "completion": {
        "field": "query_word"
      }
    }
  }
}

在上述查询中,hotel_zh_sug定义的是搜索建议的名称,prefix定义的是用户输入的关键词,completion.field定义的是搜索建议的候选集对应的字段名称。

执行上述DSL后,ES返回的结果如下:

{
  "took" : 138,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 0,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  },
  "suggest" : {
    "hotel_zh_sug" : [
      {
        "text" : "如家",
        "offset" : 0,
        "length" : 2,
        "options" : [
          {
            "text" : "如家快捷旅馆",
            "_index" : "hotel_sug",
            "_type" : "_doc",
            "_id" : "002",
            "_score" : 1.0,
            "_source" : {
              "query_word" : "如家快捷旅馆"
            }
          },
          {
            "text" : "如家旅馆",
            "_index" : "hotel_sug",
            "_type" : "_doc",
            "_id" : "001",
            "_score" : 1.0,
            "_source" : {
              "query_word" : "如家旅馆"
            }
          },
          {
            "text" : "如家精选旅馆",
            "_index" : "hotel_sug",
            "_type" : "_doc",
            "_id" : "003",
            "_score" : 1.0,
            "_source" : {
              "query_word" : "如家精选旅馆"
            }
          }
        ]
      }
    ]
  }
}

        和普通搜索不同的是,搜索建议的结果不是封装在hits中,而是单独封装在suggest中。在suggest.hotel_zh_sug.options中可以看到每一个候选集的文档信息。

        在Java客户端上可以使用SuggestBuilder方法构建搜索建议请求。使用suggestBuilder.addSuggestion()方法添加具体的搜索建议,第1个参数为自定义名称,对于第2个参数可以新建一个CompletionSuggestionBuilder实例对该方法进行传参。获取搜索建议结果时,可以通过SearchResponse.getSuggest().getSuggestion()方法获取completion类型的搜索建议结果,然后进行遍历即可。以下为Java查询搜索建议结果的使用示例:

@Test
    public void testSuggestQuery() throws IOException {
        RestHighLevelClient restHighLevelClient = new RestHighLevelClient(RestClient.builder(Arrays.stream("127.0.0.1:9200".split(","))
                .map(host->{
                    String[] split = host.split(":");
                    String hostName = split[0];
                    int port = Integer.parseInt(split[1]);
                    return new HttpHost(hostName,port,HttpHost.DEFAULT_SCHEME_NAME);
                }).filter(Objects::nonNull).toArray(HttpHost[]::new)));
        SearchRequest request = new SearchRequest("hotel_sug");
        SearchSourceBuilder sourceBuilder  = new SearchSourceBuilder();
        CompletionSuggestionBuilder suggestionBuilder = SuggestBuilders.completionSuggestion("query_word").prefix("如家");
        SuggestBuilder suggestBuilder = new SuggestBuilder();
        suggestBuilder.addSuggestion("hotel_zh_sug",suggestionBuilder);
        sourceBuilder.suggest(suggestBuilder);
        request.source(sourceBuilder);
        SearchResponse searchResponse = restHighLevelClient.search(request, RequestOptions.DEFAULT);
        CompletionSuggestion hotel_zh_sug = searchResponse.getSuggest().getSuggestion("hotel_zh_sug");
        for (CompletionSuggestion.Entry.Option hit:hotel_zh_sug.getOptions()) {
            System.out.println(hit.getText().toString());
        }
    }

        需要注意的是,ES提供的Completion Suggester功能使用的索引结构不是倒排索引,而是在内存中构建FST(Finite StateTransducers)。构建该数据结构是有比较大的内存存储成本的,因此在生产环境中向索引中添加数据时一定要关注ES节点的内存消耗,避免数据量过大造成ES节点内存耗尽从而影响集群服务。

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐