1 Elasticsearch安装与运行

  • 安装

进入官网下载并解压即可

  • 运行

    1. sh /Users/cuiyichen/SpringCloudComponents/elasticsearch-7.16.2/bin/elasticsearch
    • 运行后,9300端口为Elasticsearch集群间组件的通信端口,9200端口为浏览器访问的http协议RESTful端口。
    • 可以访问地址http://localhost:9200来测试Elasticsearch是否启动成功,成功将返回如下结果
      {
      "name" : "cuiyichendeMacBook-Pro.local",
      "cluster_name" : "elasticsearch",
      "cluster_uuid" : "OebuWdYpRp2UPvKh73jy4Q",
      "version" : {
      "number" : "7.16.2",
      "build_flavor" : "default",
      "build_type" : "tar",
      "build_hash" : "2b937c44140b6559905130a8650c64dbd0879cfb",
      "build_date" : "2021-12-18T19:42:46.604893745Z",
      "build_snapshot" : false,
      "lucene_version" : "8.10.1",
      "minimum_wire_compatibility_version" : "6.8.0",
      "minimum_index_compatibility_version" : "6.0.0-beta1"
      },
      "tagline" : "You Know, for Search"
      }
      

      2 Elasticsearch基本操作

      1 索引操作

      1 创建索引

  • 创建索引,对比MySQL就是创建表

  • 在ES地址后直接追加的路径即为索引名,当以PUT方式请求时就是新建索引

    localhost:9200/shopping
    
    • 上述请求即为新建一个名为shopping的索引

2 查看索引

1 查看单个索引

  • 在ES地址后直接追加的路径即为索引名,当以GET方式请求时就是查看指定索引

    localhost:9200/shopping
    
    • 上述请求的返回值如下
      {
      "shopping": {
         "aliases": {},
         "mappings": {},
         "settings": {
             "index": {
                 "routing": {
                     "allocation": {
                         "include": {
                             "_tier_preference": "data_content"
                         }
                     }
                 },
                 "number_of_shards": "1",
                 "provided_name": "shopping",
                 "creation_date": "1640247662704",
                 "number_of_replicas": "1",
                 "uuid": "hzkgH--QQq2VbL5I0k9TpQ",
                 "version": {
                     "created": "7160299"
                 }
             }
         }
      }
      }
      

2 查看所有索引

  • 查看所有索引的请求路径为/_cat/indices,请求方式为GET

    localhost:9200/_cat/indices?v
    
    • 追加参数v用于显示表头
    • 上述请求的返回值如下
      health status index            uuid                   pri rep docs.count docs.deleted store.size pri.store.size
      green  open   .geoip_databases 4stg-s4XQ7yW1GDwtsDnlw   1   0         43            0     40.8mb         40.8mb
      yellow open   shopping         hzkgH--QQq2VbL5I0k9TpQ   1   1          0            0       226b           226b
      

3 删除索引

  • 删除索引和新建索引以及查看单个索引一样,不同的是请求方式为DELETE
    localhost:9200/shopping
    

2 文档操作

1 创建文档

  • 索引已经建好了,现在需要向索引中添加文档。这里的文档可以类比为MySQL中的行

  • 添加文档的请求方式为POST,请求路径为_doc,表示创建的是文档数据,当指定的索引不存在时将自动创建索引,http请求格式如下

    localhost:9200/shopping/_doc
    
    • Request body如下

      {
      "title": "小米手机",
      "category": "手机",
      "image": "http//qiniuyu.xxx",
      "price": 3999.00
      }
      
    • Response body如下

      {
      "_index": "shopping",
      "_type": "_doc",
      "_id": "tI6H5n0Bdgb4LMWkNTNi",
      "_version": 1,
      "result": "created",
      "_shards": {
         "total": 2,
         "successful": 1,
         "failed": 0
      },
      "_seq_no": 0,
      "_primary_term": 1
      }
      
      • 其中_id是ES随机生成的文档主键id,该id唯一性标识文档,也可以用于正排索引来查询文档
      • 如果不希望使用ES随机生成的id,则可以在_doc路径后追加指定的id值

2 修改文档

1 覆盖

  • 覆盖文档操作和创建文档操作的请求路径都是_doc,只是查询方式需要变为PUT然后在_doc路径后追加文档id,还需要在Request body中添加新的文档数据
    localhost:9200/shopping/_doc/tI6H5n0Bdgb4LMWkNTNi
    

2 局部更新

  • 局部更新使用的路径是_update,并且更新的值需要放在doc中

    localhost:9200/shopping/_update/tI6H5n0Bdgb4LMWkNTNi
    
    • Request body
      {
      "doc": {
         "title": "黑莓手机"
      }
      }
      

3 查询文档

1 查询单个文档

  • 查询单个文档需要通过文档的id(即正排索引)来查询,在_doc路径后追加id并采用GET方式发起请求

    localhost:9200/shopping/_doc/tI6H5n0Bdgb4LMWkNTNi
    
    • Response body如下
      {
      "_index": "shopping",
      "_type": "_doc",
      "_id": "tI6H5n0Bdgb4LMWkNTNi",
      "_version": 1,
      "_seq_no": 0,
      "_primary_term": 1,
      "found": true,
      "_source": {
         "title": "小米手机",
         "category": "手机",
         "image": "http//qiniuyu.xxx",
         "price": 3999.00
      }
      }
      

2 查询全部文档

  • 查询某个索引的全部文档则需要使用_search来代替_doc,同样需要使用GET方式
    localhost:9200/shopping/_search
    

3 条件查询

  • 条件查询的请求方式和请求路径与查询全部文档相同,不同的是需要在body中添加查询条件

如以下查询条件

{
    "query":{
        "match":{
            "title":"小米黑"
        }
    }
}
  • 上述查询的返回结果如下
    {
    "took": 4,
    "timed_out": false,
    "_shards": {
       "total": 1,
       "successful": 1,
       "skipped": 0,
       "failed": 0
    },
    "hits": {
       "total": {
           "value": 3,
           "relation": "eq"
       },
       "max_score": 1.7509375,
       "hits": [
           {
               "_index": "shopping",
               "_type": "_doc",
               "_id": "tY6i5n0Bdgb4LMWkNTPQ",
               "_score": 1.7509375,
               "_source": {
                   "title": "小米手机",
                   "category": "手机",
                   "image": "http//qiniuyu.xxx",
                   "price": 3999.00
               }
           },
           {
               "_index": "shopping",
               "_type": "_doc",
               "_id": "to6i5n0Bdgb4LMWkOTOK",
               "_score": 1.7509375,
               "_source": {
                   "title": "小米手机",
                   "category": "手机",
                   "image": "http//qiniuyu.xxx",
                   "price": 3999.00
               }
           },
           {
               "_index": "shopping",
               "_type": "_doc",
               "_id": "tI6H5n0Bdgb4LMWkNTNi",
               "_score": 0.87546873,
               "_source": {
                   "title": "黑莓手机",
                   "category": "手机",
                   "image": "http//qiniuyu.xxx",
                   "price": 9999.0
               }
           }
       ]
    }
    }
    
  • match匹配 - 对查询条件进行分词

    • 从上述查询中可以发现,虽然查询条件是“小米黑”,但是title中包含“小米”和“黑”都判断为匹配成功
    • 这是因为以match方式进行匹配时,匹配条件将被执行分词(对于汉字来说一个字就是一个词),底层进行匹配时将使用多个关键字分别进行倒排索引匹配
    • 在返回的一个个查询结果中还包含一个score属性,该属性表示与查询条件的关联性得分
  • term匹配 - 不对查询条件进行分词

    • 该方式的匹配则不进行分词,直接使用查询条件作为关键词来执行匹配
    • 由于索引分词时默认的粒度为一个单词,因此term匹配的查询条件不能包括多个单词,否则查询一定失败,除非改动分词粒度

3 创建映射

  • 采用PUT方式,在索引名后追加_mapping路径,并在body中添加映射设置

    • 请求格式

      localhost:9200/shopping/_mapping
      
    • Request body

      {
      "properties": {
         "title": {
             "type": "keyword",
             "index": true
         },
         "category": {
             "type": "text",
             "index": true
         },
         "image": {
             "type": "text",
             "index": false
         },
         "price": {
             "type": "double",
             "index": true
         }
      }
      }
      

3 Java操作Elasticsearch

1 maven依赖

<dependency>
    <groupId>org.elasticsearch</groupId>
    <artifactId>elasticsearch</artifactId>
    <version>7.6.2</version>
</dependency>

<!-- elasticsearch 的客户端 -->
<dependency>
    <groupId>org.elasticsearch.client</groupId>
    <artifactId>elasticsearch-rest-high-level-client</artifactId>
    <version>7.6.2</version>
</dependency>

2 索引操作

@SpringBootTest
@Slf4j
class IndicesTest {

    @Autowired
    RestHighLevelClient esClient;

    @Test
    void indexCreate() throws IOException {
        // 创建索引的请求对象
        CreateIndexRequest request = new CreateIndexRequest("student");
        // 发送请求,接收响应
        CreateIndexResponse response = esClient.indices().create(request, RequestOptions.DEFAULT);

        log.info("响应状态:{}", response.isAcknowledged());
    }

    @Test
    void indexGet() throws IOException {
        GetIndexRequest request = new GetIndexRequest("shopping");
        GetIndexResponse response = esClient.indices().get(request, RequestOptions.DEFAULT);
        log.info("settings: {}", response.getSettings());
    }

    @Test
    void indexDelete() throws IOException {
        DeleteIndexRequest request = new DeleteIndexRequest("student");
        AcknowledgedResponse response = esClient.indices().delete(request, RequestOptions.DEFAULT);
        log.info("响应状态: {}", response.isAcknowledged());
    }
}

3 文档操作

@SpringBootTest
@Slf4j
class DocTest {

    @Autowired
    RestHighLevelClient esClient;

    /**
     * 插入单个文档
     */
    @Test
    void docInsert() throws IOException {
        // 要插入到索引中的数据对象
        Student student = new Student("许晴", "女", 53);
        // 将对象转为json格式
        ObjectMapper mapper = new ObjectMapper();
        String studentJson = mapper.writeValueAsString(student);

        IndexRequest request = new IndexRequest();
        // 设置索引名
        request.index("student");
        // 设置文档id
        request.id("1009");
        // 设置数据源
        request.source(studentJson, XContentType.JSON);

        IndexResponse response = esClient.index(request, RequestOptions.DEFAULT);
        log.info("请求结果: {}", response.getResult());
    }

    /**
     * 批量插入文档
     */
    @Test
    void docBulkInsert() throws Exception {
        ObjectMapper mapper = new ObjectMapper();
        Student student1 = new Student("崔伟", "男", 52);
        Student student2 = new Student("崔红", "女", 54);
        Student student3 = new Student("艾雪珂", "女", 23);

        BulkRequest request = new BulkRequest();
        request.add(new IndexRequest().index("student").id("1006")
                .source(mapper.writeValueAsString(student1), XContentType.JSON));
        request.add(new IndexRequest().index("student").id("1007")
                .source(mapper.writeValueAsString(student2), XContentType.JSON));
        request.add(new IndexRequest().index("student").id("1008")
                .source(mapper.writeValueAsString(student3), XContentType.JSON));

        BulkResponse response = esClient.bulk(request, RequestOptions.DEFAULT);
        log.info("失败信息: {}", Arrays.toString(Arrays.stream(response.getItems()).map(o -> o.getFailureMessage()).toArray(String[]::new)));
    }

    /**
     * 根据文档id获取文档
     */
    @Test
    void docGet() throws IOException {

        GetRequest request = new GetRequest();
        request.index("student");
        request.id("1001");

        GetResponse response = esClient.get(request, RequestOptions.DEFAULT);
        log.info("文档详情: {}", response.getSourceAsString());
    }

    /**
     * 全量搜索
     */
    @Test
    void docSearchAll() throws IOException {
        SearchRequest request = new SearchRequest();
        request.indices("shopping", "student");

        // A query that matches on all documents.
        QueryBuilder query = QueryBuilders.matchAllQuery();
        request.source(new SearchSourceBuilder().query(query));

        SearchResponse response = esClient.search(request, RequestOptions.DEFAULT);
        SearchHits hits = response.getHits();
        log.info("命中数量: {}", hits.getTotalHits());
        for (SearchHit hit : hits) {
            log.info(hit.getSourceAsString());
        }
    }

    /**
     * 分页全量搜索
     */
    @Test
    void docPageSearchAll() throws IOException {
        SearchRequest request = new SearchRequest();
        request.indices("shopping", "student");

        QueryBuilder query = QueryBuilders.matchAllQuery();
        // 这里进行了一个分页
        request.source(new SearchSourceBuilder().query(query).from(0).size(2));

        SearchResponse response = esClient.search(request, RequestOptions.DEFAULT);
        SearchHits hits = response.getHits();
        log.info("命中数量: {}", hits.getTotalHits());
        for (SearchHit hit : hits) {
            log.info(hit.getSourceAsString());
        }
    }

    /**
     * 全量搜索,但是是搜索结果只要name
     */
    @Test
    void docSearchAllNames() throws IOException {
        SearchRequest request = new SearchRequest();
        request.indices("shopping", "student");

        QueryBuilder query = QueryBuilders.matchAllQuery();
        // 这里进行了include和exclude设置
        String[] includes = {"name"};
        String[] excludes = {};
        request.source(new SearchSourceBuilder().query(query).fetchSource(includes, excludes));

        SearchResponse response = esClient.search(request, RequestOptions.DEFAULT);
        SearchHits hits = response.getHits();
        log.info("命中数量: {}", hits.getTotalHits());
        for (SearchHit hit : hits) {
            log.info(hit.getSourceAsString());
        }
    }

    /**
     * match搜索/分词搜索
     */
    @Test
    void docSearchByMatch() throws IOException {
        SearchRequest request = new SearchRequest();
        request.indices("student");

        // Creates a match query with type "BOOLEAN" for the provided field name and text
        QueryBuilder query = QueryBuilders.matchQuery("name", "崔奕宸");
        request.source(new SearchSourceBuilder().query(query));

        SearchResponse response = esClient.search(request, RequestOptions.DEFAULT);
        SearchHits hits = response.getHits();
        log.info("命中数量: {}", hits.getTotalHits());
        for (SearchHit hit : hits) {
            log.info(hit.getSourceAsString());
        }
    }

    /**
     * term搜索/不分词搜索
     * (如果term值为多个单词则搜索失败,因为termQuery不会分词,而且索引默认分词粒度为一个单词)
     */
    @Test
    void docSearchByTerm() throws IOException {
        SearchRequest request = new SearchRequest();
        request.indices("student");

        // A Query that matches documents containing a term.
        QueryBuilder query = QueryBuilders.termQuery("name", "宸");
        request.source(new SearchSourceBuilder().query(query));

        SearchResponse response = esClient.search(request, RequestOptions.DEFAULT);
        SearchHits hits = response.getHits();
        log.info("命中数量: {}", hits.getTotalHits());
        for (SearchHit hit : hits) {
            log.info(hit.getSourceAsString());
        }
    }

    /**
     * 范围查询
     */
    @Test
    void docRangeSearch() throws IOException {
        SearchRequest request = new SearchRequest();
        request.indices("student");

        // A Query that matches documents within an range of terms.
        RangeQueryBuilder rangeQuery = QueryBuilders.rangeQuery("age");
        // 大于等于 greater than or equal, gte
        rangeQuery.gte(20);
        // 小于等于 lower than or equal, lte
        rangeQuery.lte(40);
        request.source(new SearchSourceBuilder().query(rangeQuery));

        SearchResponse response = esClient.search(request, RequestOptions.DEFAULT);
        SearchHits hits = response.getHits();
        log.info("命中数量: {}", hits.getTotalHits());
        for (SearchHit hit : hits) {
            log.info(hit.getSourceAsString());
        }
    }

    /**
     * 组合查询,可以组合多种match搜索、term搜索等
     */
    @Test
    void docSearchByCondition() throws IOException {
        SearchRequest request = new SearchRequest();
        request.indices("student");

        // A Query that matches documents matching boolean combinations of other queries.
        BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
        boolQuery.must(QueryBuilders.matchQuery("name", "崔陈邱艾常许"));
        boolQuery.must(QueryBuilders.termQuery("sex", "女"));
        boolQuery.must(QueryBuilders.rangeQuery("age").lte(30));
        boolQuery.mustNot(QueryBuilders.termQuery("name", "红"));
        request.source(new SearchSourceBuilder().query(boolQuery));

        SearchResponse response = esClient.search(request, RequestOptions.DEFAULT);
        SearchHits hits = response.getHits();
        log.info("命中数量: {}", hits.getTotalHits());
        for (SearchHit hit : hits) {
            log.info(hit.getSourceAsString());
        }
    }
}