Elasticsearch
本文最后更新于298 天前,其中的信息可能已经过时,如有错误请发送邮件到big_fw@foxmail.com

Elasticsearch是一个分布式、高扩展、高实时的搜索与数据分析引擎,广泛用于大规模的数据存储和快速检索。

倒排索引

正排索引

根据id精确匹配时,可以走索引(B+树),查询效率较高。而当搜索条件为模糊匹配时,由于索引无法生效,导致从索引查询退化为全表扫描,效率很差。

因此,正向索引适合于根据索引字段的精确搜索,不适合基于部分词条的模糊匹配。

倒排索引

倒排索引中有两个非常重要的概念:

  • 文档(Document):用来搜索的数据,其中的每一条数据就是一个文档。例如一个网页、一个商品信息
  • 词条(Term):对文档数据或用户搜索数据,利用某种算法分词,得到的具备含义的词语就是词条。例如:我是中国人,就可以分为:我、是、中国人、中国、国人这样的几个词条

1)用户输入条件"华为手机"进行搜索

2)对用户输入条件分词,得到词条:华为手机

3)拿着词条在倒排索引中查找(由于词条有索引,查询效率很高),即可得到包含词条的文档id:1、2、3

4)拿着文档id到正向索引中查找具体文档即可(由于id也有索引,查询效率也很高)

elasticsearch是面向文档(Document) 存储的,可以是数据库中的一条商品数据,一个订单信息。文档数据会被序列化为json格式后存储在elasticsearch中。原本数据库中的一行数据就是ES中的一个JSON文档

而数据库中每行数据都包含很多列,这些列就转换为JSON文档中的字段(Field)

因此,我们要将类型相同的文档集中在一起管理,称为索引(Index)。我们可以把索引当做是数据库中的表。

数据库的表会有约束信息,用来定义表的结构、字段的名称、类型等信息。因此,索引库中就有映射(mapping),是索引中文档的字段约束信息,类似表的结构约束。

MySQLElasticsearch说明
TableIndex索引(index),就是文档的集合,类似数据库的表(table)
RowDocument文档(Document),就是一条条的数据,类似数据库中的行(Row),文档都是JSON格式
ColumnField字段(Field),就是JSON文档中的字段,类似数据库中的列(Column)
SchemaMappingMapping(映射)是索引中文档的约束,例如字段类型约束。类似数据库的表结构(Schema)
SQLDSLDSL是elasticsearch提供的JSON风格的请求语句,用来操作elasticsearch,实现CRUD
  • 对安全性要求较高的写操作,使用mysql实现
  • 对查询性能要求较高的搜索需求,使用elasticsearch实现
  • 两者再基于某种方式,实现数据的同步,保证一致性

IK分词器

分词器的作用是什么?

  • 创建倒排索引时,对文档分词
  • 用户搜索时,对输入的内容分词

IK分词器有几种模式?

  • ik_smart:智能切分,粗粒度
  • ik_max_word:最细切分,细粒度

IK分词器如何拓展词条?如何停用词条?

  • 利用config目录的IkAnalyzer.cfg.xml文件添加拓展词典和停用词典
  • 在词典中添加拓展词条或者停用词条

索引库操作

Mapping映射属性

Mapping是对索引库中文档的约束,常见的Mapping属性包括:

  • type:字段数据类型,常见的简单类型有:
  • 字符串:text(可分词的文本)、keyword(精确值,例如:品牌、国家、ip地址)
  • 数值:longintegershortbytedoublefloat
  • 布尔:boolean
  • 日期:date
  • 对象:object
  • index:是否创建索引,默认为true
  • analyzer:使用哪种分词器
  • properties:该字段的子字段

索引库CRUD

PUT /索引库名称
{
  "mappings": {
    "properties": {
      "字段名":{
        "type": "text",
        "analyzer": "ik_smart"
      },
      "字段名2":{
        "type": "keyword",
        "index": "false"
      },
      "字段名3":{
        "properties": {
          "子字段": {
            "type": "keyword"
          }
        }
      },
      // ...略
    }
  }
}

GET /索引库名

向索引库中添加新字段,或者更新索引库的基础属性

PUT /索引库名/_mapping
{
  "properties": {
    "新字段名":{
      "type": "integer"
    }
  }
}

DELETE /索引库名

文档操作

新增文档

POST /索引库名/_doc/文档id
{
    "字段1": "值1",
    "字段2": "值2",
    "字段3": {
        "子属性1": "值3",
        "子属性2": "值4"
    },
}

查询文档

GET /{索引库名称}/_doc/{id}

删除文档

DELETE /{索引库名}/_doc/id值

修改文档

  • 全量修改:直接覆盖原来的文档
  • 局部修改:修改文档中的部分字段

注意:如果根据id删除时,id不存在,第二步的新增也会执行,也就从修改变成了新增操作了。

PUT /{索引库名}/_doc/文档id
{
    "字段1": "值1",
    "字段2": "值2",
    // ... 略
}

POST /{索引库名}/_update/文档id
{
    "doc": {
         "字段名": "新的值",
    }
}

批处理

POST _bulk
{ "index" : { "_index" : "test", "_id" : "1" } }
{ "field1" : "value1" }
{ "delete" : { "_index" : "test", "_id" : "2" } }
{ "create" : { "_index" : "test", "_id" : "3" } }
{ "field1" : "value3" }
{ "update" : {"_id" : "1", "_index" : "test"} }
{ "doc" : {"field2" : "value2"} }

其中:

  • * _index:指定索引库名
  • _id指定要操作的文档id
  • { "field1" : "value1" }:则是要新增的文档内容
  • delete代表删除操作
  • _index:指定索引库名
  • _id指定要操作的文档id
  • update代表更新操作
  • _index:指定索引库名
  • _id指定要操作的文档id
  • { "doc" : {"field2" : "value2"} }:要更新的文档字段

RestAPI

初始化RestClient

<dependency>
    <groupId>org.elasticsearch.client</groupId>
    <artifactId>elasticsearch-rest-high-level-client</artifactId>
</dependency>

@BeforeEach修饰在方法上,在每一个测试方法(所有@Test、@RepeatedTest、@ParameterizedTest或者@TestFactory注解的方法)之前执行一次。

RestHighLevelClient client = new RestHighLevelClient(RestClient.builder(
        HttpHost.create("http://192.168.101.120:9200")
));

  private RestHighLevelClient client;
    @BeforeEach
    void setUp() {
        this.client = new RestHighLevelClient(RestClient.builder(
                HttpHost.create("http://192.168.150.101:9200")
        ));
    }
    @Test
    void testConnect() {
        System.out.println(client);
    }
    @AfterEach
    void tearDown() throws IOException {
        this.client.close();
    }

批量导入文档

void testBulk() throws IOException {
    // 1.创建Request
    BulkRequest request = new BulkRequest();
    // 2.准备请求参数
    request.add(new IndexRequest("items").id("1").source("json doc1", XContentType.JSON));
    request.add(new IndexRequest("items").id("2").source("json doc2", XContentType.JSON));
    // 3.发送请求
    client.bulk(request, RequestOptions.DEFAULT);
}

DSL查询

  • 叶子查询(Leaf query clauses):一般是在特定的字段里查询特定值,属于简单查询,很少单独使用。
  • 复合查询(Compound query clauses):以逻辑方式组合多个叶子查询或者更改叶子查询的行为方式。 GET /{索引库名}/_search
    {
    “query”: {
    “查询类型”: {
    // .. 查询条件
    }
    “match_all”: { } }
    }

叶子查询

  • 全文检索查询(Full Text Queries):利用分词器对用户输入搜索条件先分词,得到词条,然后再利用倒排索引搜索词条。例如:
  • match
  • multi_match GET /{索引库名}/_search
    {
    “query”: {
    “match”: {
    “字段名”: “搜索条件”
    }
    }
    }
    GET /{索引库名}/_search
    {
    “query”: {
    “multi_match”: {
    “query”: “搜索条件”,
    “fields”: [“字段1”, “字段2”]
    }
    }
    }
  • 精确查询(Term-level queries):不对用户输入搜索条件分词,根据字段内容精确值匹配。但只能查找keyword、数值、日期、boolean类型的字段。例如:
  • ids
  • term
  • range GET /{索引库名}/_search
    {
    “query”: {
    “term”: {
    “字段名”: {
    “value”: “搜索条件”
    }
    }
    }
    }
    GET /{索引库名}/_search
    {
    “query”: {
    “range”: {
    “字段名”: {
    “gte”: {最小值},
    “lte”: {最大值}
    }
    }
    }
    }
  • 地理坐标查询: 用于搜索地理位置,搜索方式很多,例如:
  • geo_bounding_box:按矩形搜索
  • geo_distance:按点和半径搜索

复合查询

  • 第一类:基于逻辑运算组合叶子查询,实现组合条件,例如
  • bool
  • 第二类:基于某种算法修改查询时的文档相关性算分,从而改变文档排名。例如:
  • function_score
  • dis_max

bool

  • must:必须匹配每个子查询,类似“与”
  • should:选择性匹配子查询,类似“或”
  • must_not:必须不匹配,不参与算分,类似“非”
  • filter:必须匹配,不参与算分

PS:

GET /items/_search
{
  "query": {
    "bool": {
      "must": [
        {"match": {"name": "手机"}}
      ],
      "should": [
        {"term": {"brand": { "value": "vivo" }}},
        {"term": {"brand": { "value": "小米" }}}
      ],
      "must_not": [
        {"range": {"price": {"gte": 2500}}}
      ],
      "filter": [
        {"range": {"price": {"lte": 1000}}}
      ]
    }
  }
}

function_score

基本语法

function score 查询中包含四部分内容:

  • 原始查询条件:query部分,基于这个条件搜索文档,并且基于BM25算法给文档打分,原始算分(query score)
  • 过滤条件:filter部分,符合该条件的文档才会重新算分
  • 算分函数:符合filter条件的文档要根据这个函数做运算,得到的函数算分(function score),有四种函数
  • weight:函数结果是常量
  • field_value_factor:以文档中的某个字段值作为函数结果
  • random_score:以随机数作为函数结果
  • script_score:自定义算分函数算法
  • 运算模式:算分函数的结果、原始查询的相关性算分,两者之间的运算方式,包括:
  • multiply:相乘
  • replace:用function score替换query score
  • 其它,例如:sum、avg、max、min

排序

GET /indexName/_search
{
  "query": {
    "match_all": {}
  },
  "sort": [
    {
      "排序字段": "排序方式asc和desc"
      /*
      "排序字段": {
        "order": "排序方式asc和desc"
      }
      */
    }
  ]
}

分页

GET /items/_search
{
  "query": {
    "match_all": {}
  },
  "from": 0, // 分页开始的位置,默认为0
  "size": 10,  // 每页文档数量,默认10
  "sort": [
    {
      "price": "desc"
    }
  ]
}

分片存储

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

场景:数据迁移、手机滚动查询

高亮

GET /{索引库名}/_search
{
  "query": {
    "match": {
      "搜索字段": "搜索关键字"
    }
  },
  "highlight": {
    "fields": {
      "高亮字段名称": {
        "pre_tags": "<em>",
        "post_tags": "</em>"  //默认<em>
      }
    }
  }
}

注意

  • 搜索必须有查询条件,而且是全文检索类型的查询条件,例如match
  • 参与高亮的字段必须是text类型的字段
  • 默认情况下参与高亮的字段要与搜索字段一致,除非添加:required_field_match=false

RestClient实现查询

request.source().query(QueryBuilders.matchQuery("name", "脱脂牛奶"));
request.source().query(QueryBuilders.multiMatchQuery("脱脂牛奶", "name", "category"));
request.source().query(QueryBuilders.rangeQuery("price").gte(10000).lte(30000));
request.source().query(QueryBuilders.termQuery("brand", "华为"));

复合

void testBool() throws IOException {
    // 1.创建Request
    SearchRequest request = new SearchRequest("items");
    // 2.组织请求参数
    // 2.1.准备bool查询
    BoolQueryBuilder bool = QueryBuilders.boolQuery();
    // 2.2.关键字搜索
    bool.must(QueryBuilders.matchQuery("name", "脱脂牛奶"));
    // 2.3.品牌过滤
    bool.filter(QueryBuilders.termQuery("brand", "德亚"));
    // 2.4.价格过滤
    bool.filter(QueryBuilders.rangeQuery("price").lte(30000));
    request.source().query(bool);
    // 3.发送请求
    SearchResponse response = client.search(request, RequestOptions.DEFAULT);
    // 4.解析响应
    handleResponse(response);
}

排序和分页

void testPageAndSort() throws IOException {
    int pageNo = 1, pageSize = 5;
    // 1.创建Request
    SearchRequest request = new SearchRequest("items");
    // 2.组织请求参数
    // 2.1.搜索条件参数
    request.source().query(QueryBuilders.matchQuery("name", "脱脂牛奶"));
    // 2.2.排序参数
    request.source().sort("price", SortOrder.ASC);
    // 2.3.分页参数
    request.source().from((pageNo - 1) * pageSize).size(pageSize);
    // 3.发送请求
    SearchResponse response = client.search(request, RequestOptions.DEFAULT);
    // 4.解析响应
    handleResponse(response);
}

高亮

void testHighlight() throws IOException {
    // 1.创建Request
    SearchRequest request = new SearchRequest("items");
    // 2.组织请求参数
    // 2.1.query条件
    request.source().query(QueryBuilders.matchQuery("name", "脱脂牛奶"));
    // 2.2.高亮条件
    request.source().highlighter(
            SearchSourceBuilder.highlight()
                    .field("name")
                    .preTags("<em>")
                    .postTags("</em>")
    );
    // 3.发送请求
    SearchResponse response = client.search(request, RequestOptions.DEFAULT);
    // 4.解析响应
    handleResponse(response);
}

数据聚合

  • 桶(Bucket) 聚合:用来对文档做分组
  • TermAggregation:按照文档字段值分组,例如按照品牌值分组、按照国家分组
  • Date Histogram:按照日期阶梯分组,例如一周为一组,或者一月为一组
  • 度量( Metric ) 聚合:用以计算一些值,比如:最大值、最小值、平均值等
  • Avg:求平均值
  • Max:求最大值
  • Min:求最小值
  • Stats:同时求maxminavgsum
  • 管道(pipeline) 聚合:其它聚合的结果为基础做进一步运算

注意: 参加聚合的字段必须是keyword、日期、数值、布尔类型

DSL

Bucket聚合

GET /items/_search
{
  "size": 0,   //不包含文档
  "aggs": {    //定义聚合
    "category_agg": {    //起名
      "terms": {    //类型
        "field": "category",    //聚合字段
        "size": 20    //获取聚合结果的数量,默认20
        }
    }
  }
}

带条件聚合

GET /items/_search
{
  "query": {
    "bool": {
      "filter": [
        {
          "term": { "category": "手机"}
        },
        {
          "range": {
            "price": { "gte": 300000}
          }
        }
      ]
    }
  }, 
  "size": 0, 
  "aggs": {
    "brand_agg": {
      "terms": {"field": "brand", "size": 20}
    }
  }
}

Metric聚合

GET /items/_search
{
  "query": {
    "bool": {
      "filter": [
        {
          "term": {"category": "手机"}
        },
        {
          "range": {"price": {"gte": 300000}}
        }
      ]
    }
  }, 
  "size": 0, 
  "aggs": {
    "brand_agg": {
      "terms": {
        "field": "brand",
        "size": 20
      },
      "aggs": {
        "stats_meric": {
          "stats": { "field": "price"}
        }
      }
    }
  }
}

RestClient

    // 1.创建Request
    SearchRequest request = new SearchRequest("items");
    // 2.准备请求参数
    BoolQueryBuilder bool = QueryBuilders.boolQuery()
            .filter(QueryBuilders.termQuery("category", "手机"))
            .flter(QueryBuilders.rangeQuery("price").gte(300000));
    request.source().query(bool).size(0);
    // 3.聚合参数
    request.source().aggregation(
            AggregationBuilders.terms("brand_agg").field("brand").size(5)
    );
    // 4.发送请求
    SearchResponse response = client.search(request, RequestOptions.DEFAULT);
    // 5.解析聚合结果
    Aggregations aggregations = response.getAggregations();
    // 5.1.获取品牌聚合
    Terms brandTerms = aggregations.get("brand_agg");
    // 5.2.获取聚合中的桶
    List<? extends Terms.Bucket> buckets = brandTerms.getBuckets();
    // 5.3.遍历桶内数据
    for (Terms.Bucket bucket : buckets) {
        // 5.4.获取桶内key
        String brand = bucket.getKeyAsString();
        System.out.print("brand = " + brand);
        long count = bucket.getDocCount();
        System.out.println("; count = " + count);
    }
如果觉得本文对您有所帮助,那这将是对我最大的鼓励,期待您留下您宝贵的评论。
暂无评论

发送评论 编辑评论


				
上一篇
下一篇