likes
comments
collection
share

ElasticSearch 基本使用

作者站长头像
站长
· 阅读数 11

使用 mysql 搜索时会面临的问题:

  1. 性能低下:当数据量比较小的时候,问题不大,当数据量比较大或者并发比较高时,用 mysqllike 查询,性能是比较低的
  2. 没有相关性的排名:例如像搜索引擎它们,匹配度越高的,排名越靠前,mysqllike 没有这个功能
  3. 没有全文搜索
  4. 没有分词功能

什么是全文搜索

比如说在一件商品中,很多属性都有介绍的功能,比如标题、副标题、简介、详情等,数据库在存储时,需要为他们分别建立字段,但在搜索时,只需要输入一个关键词,就想搜索到所相关的商品,在数据库中是比较难做到的

在比如日志搜索,每一种不一样的日志,都要写 sql 查询,如果后面日志发生变化,还要修改 sql

对于这种不规则的数据,任何时候都可以搜索,这就是全文搜索

ElasticSearch 就是一个全文搜索引擎,它可以帮助我们解决上面的问题

它是一个分布式可扩展的实时搜索和分析引擎,是建立在 Lucene 基础之上的,提供基于 RESTfulweb 接口

安装 ElasticSearch 和 Kibana

使用 docker 安装 ElasticSearchKibana

安装 ElasticSearch

docker run --name go-elasticsearch -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" -e ES_JAVA_OPTS="-Xms128m -Xmx256m" --network network1 --network-alias go-es -d  elasticsearch:7.10.1

访问:localhost:9200 可以看到 ElasticSearch 的信息

{
  "name": "d6c7d3b1c14b",
  "cluster_name": "docker-cluster",
  "cluster_uuid": "JqlWTdi3RaCxcJwDSnSoRg",
  "version": {
    "number": "7.10.1", // 版本号
    "build_flavor": "default",
    "build_type": "docker",
    "build_hash": "1c34507e66d7db1211f66f3513706fdf548736aa",
    "build_date": "2020-12-05T01:00:33.671820Z",
    "build_snapshot": false,
    "lucene_version": "8.7.0",
    "minimum_wire_compatibility_version": "6.8.0",
    "minimum_index_compatibility_version": "6.0.0-beta1"
  },
  "tagline": "You Know, for Search"
}

安装 kibana

kiabanaelasticsearch 的可视化工具

docker run --name go-kibana -e ELASTICSEARCH_HOSTS="http://go-elasticsearch:9200" -p 5601:5601 --network network1 --network-alias go-kibana  -d kibana:7.10.1

通过 http://localhost:5601/app/home#/ 访问 kibana

  • 在运行容器时
    • ELASTICSEARCH_HOSTS 需要配置 elasticsearch 的地址,这里使用的是 docker 容器名
    • kibana 的版本需要和 elasticsearch 的版本一致

ES 基本概念

  1. ES7.x 开始,索引 index 可以理解为数据库中的 table
  2. ES7.x 开始 type 被废弃了(现在固定值为 _doc),但还保留着,8.x 正式废弃
  3. document 可以理解为数据库中的一条记录
  4. field 可以理解为数据库中的字段
  5. mapping 可以理解为数据库的 schema

基本使用

查看当前节点所有的索引

GET _cat/indices

green open .apm-custom-link                36Wb9HwmSOuZOedi_uLlLg 1 0  0  0   208b   208b
green open .kibana_task_manager_1          TmhGqbXASUOtf4UVvTmsDQ 1 0  5 35  172kb  172kb
green open .apm-agent-configuration        R7E5SWxFTOSVDVsQAjVwnA 1 0  0  0   208b   208b
green open .kibana-event-log-7.10.1-000001 -YHTgGvvT9G6ov2SgTL1CA 1 0  3  0 16.4kb 16.4kb
green open .kibana_1                       hW6aAfKcQySo6NTxSwo_2w 1 0 24  9  2.1mb  2.1mb
  • 状态(green) - 索引的健康状态,green 表示可用,yellow 表示索引有问题,red 表示不可用
  • 是否开启(open) - 索引是否被开启使用
  • 索引名(.apm-custom-link) - 索引的名称
  • UUID(36Wb9HwmSOuZOedi_uLlLg) - 索引的唯一标识符
  • 分片数(1) - 索引的主分片数
  • 副本数(0) - 索引设置的副本分片数
  • Docs count(0) - 索引中的文档数
  • Deleted docs(0) - 索引中被标记删除的文档数
  • Store size(208b) - 索引主分片所使用的存储空间大小
  • Pri.Store size(208b) - 索引所有主分片总存储空间大小

获取某个索引

GET account
{
  "account": {
    "aliases": {},
    "mappings": {        # 对应的是数据库的表结构
      "properties": {
        "age": {
          "type": "long"
        },
        "company": {
          "properties": {
            "address": {
              "type": "text",
              "fields": {
                "keyword": {
                  "type": "keyword",
                  "ignore_above": 256
                }
              }
            },
            "name": {
              "type": "text",
              "fields": {
                "keyword": {
                  "type": "keyword",
                  "ignore_above": 256
                }
              }
            }
          }
        },
        "name": {
          "type": "text",
          "fields": {
            "keyword": {
              "type": "keyword",
              "ignore_above": 256
            }
          }
        }
      }
    },
    "settings": {
      "index": {
        "routing": {
          "allocation": {
            "include": {
              "_tier_preference": "data_content"
            }
          }
        },
        "number_of_shards": "1",
        "provided_name": "account",
        "creation_date": "1693545227630",
        "number_of_replicas": "1",
        "uuid": "y9lUDYf9Q1umYjUrnZ-mKA",
        "version": {
          "created": "7100199"
        }
      }
    }
  }
}

新建数据

使用 PUT 操作数据说明:

  • account:索引名
  • _doc 固定值
  • 1:文档的唯一标识符,用 PUT 必须指定

如果 account 不存在,会创建 account

PUT /account/_doc/1
# 请求体
{
  "name": "uccs",
  "age": 18,
  "company": [
    {
      "name": "astak",
      "address": "shanghai"
    },
    {
      "name": "astak2",
      "address": "beijing"
    }
  ]
}
# 响应体
{
  "_index": "account",  # 索引名
  "_type": "_doc",      # 固定值 _doc
  "_id": "1",           # 文档的唯一标识符,用 PUT 必须指定,用 POST ES 会自己生成
  "_version": 1,        # 版本号,第几次操作
  "result": "created",  # 如果数据不存在,这个值是 create;如果数据存在,这个值是 updated
  "_shards": {          # 分片信息
    "total": 2,
    "successful": 1,
    "failed": 0
  },
  "_seq_no": 0,         # 乐观锁
  "_primary_term": 1
}

使用 POST 操作数据说明:

  1. 新建数据时无需指定 idES 会自动生成,同时 result 的值是 created
  2. 更新数据时需要指定 id,同时 result 的值是 updated
  3. 多次运行会生成多条数据,可通过响应体中 idresult 可知
POST user/_doc
# 请求体
{
  "name": "uccs",
  "company": "imooc"
}
# 响应体
{
  "_index": "user",
  "_type": "_doc",
  "_id": "WYUzT4oBZ0WpQB8-3pDP",   # 自动生成,每次运行都不一样
  "_version": 1,
  "result": "created",             # 每次运行都是 created
  "_shards": {
    "total": 2,
    "successful": 1,
    "failed": 0
  },
  "_seq_no": 0,
  "_primary_term": 1
}

新建数据时,如果数据不存在,会创建数据;如果数据存在,会报错

POST user/_create/1
# 请求体
{
  "name": "uccs",
  "company": "imooc"
}
# 响应体
{
  "error": {
    "root_cause": [
      {
        "type": "version_conflict_engine_exception",
        "reason": "[1]: version conflict, document already exists (current version [1])",  # 错误的理由
        "index_uuid": "oAG9ecTqTB6djw11e4OJYA",
        "shard": "0",
        "index": "user"
      }
    ],
    "type": "version_conflict_engine_exception",
    "reason": "[1]: version conflict, document already exists (current version [1])",
    "index_uuid": "oAG9ecTqTB6djw11e4OJYA",
    "shard": "0",
    "index": "user"
  },
  "status": 409
}

获取数据

获取数据的方式有两种形式:

  1. 通过 id 获取数据(知道数据的 id)
  2. 搜索数据(将所有符合条件的数据都找到)

通过 id 获取数据

通过 id 获取数据,需要加上 _doc

返回的数据中,_source 是真正的数据

GET user/_doc/1
# 响应体
{
  "_index": "user",
  "_type": "_doc",
  "_id": "1",
  "_version": 1,
  "_seq_no": 2,
  "_primary_term": 1,
  "found": true,    # 是否找到数据
  "_source": {      # 真正的数据
    "name": "uccs",
    "company": "imooc"
  }
}

如果我们只想获取真正的数据,不需要其他的信息,可以使用 _source 过滤

也就是说将 _doc 替换成 _source 即可

GET user/_source/1
# 响应体
{
  "name": "uccs",
  "company": "imooc"
}

搜索数据

有两种方式可以查询:

  1. url 的形式查询,类似 query 的形式,不过查询能力有限
  2. 通过请求体的形式查询,查询能力比较强
使用 url 查询

下面的例子是个全量查询,会查询所有的索引,从响应结果中可以看到,它还查询了一些系统索引(不过现在已经说明以后会被废弃)

GET _search?q=shanghai
# 响应体
#! Deprecation: this request accesses system indices: [.apm-agent-configuration, .apm-custom-link, .kibana_1, .kibana_task_manager_1], but in a future major version, direct access to system indices will be prevented by default
{
  "took": 13,             # 查询耗时
  "timed_out": false,     # 是否超时
  "_shards": {            # 分片信息
    "total": 7,
    "successful": 7,
    "skipped": 0,
    "failed": 0
  },
  "hits": {               # 查询到的数据
    "total": {
      "value": 1,
      "relation": "eq"    # 查询的条件,eq 表示 = 的意思
    },
    "max_score": 0.3616575,   # 得分
    "hits": [                 # 搜索结果,匹配度最高的一项
      {
        "_index": "account",  # 数据所在的索引名
        "_type": "_doc",
        "_id": "1",
        "_score": 0.3616575,
        "_source": {          # 真正的数据
          "name": "uccs",
          "age": 18,
          "company": [
            {
              "name": "astak",
              "address": "shanghai"
            },
            {
              "name": "astak2",
              "address": "beijing"
            }
          ]
        }
      }
    ]
  }
}

如果只想查询某一个索引的数据,可以在 url 中指定索引名

GET account/_search?q=shanghai

它还可以做的很复杂,不过这种方式不常用,了解即可

GET /_search?pretty&q=title:azure&explain=true&from=1&size=10&sort=title:asc&fields:user,title,content
使用请求体查询

query 是查询条件,match_all 表示查询所有数据,默认返回 10 条数据

通过这种方式查询是没办法计算得分的,所有的数据都是 1.0

GET user/_search
# 请求体
{
  "query": {          # 查询条件
    "match_all": {}   # 查询所有数据
  }
}
# 响应体
{
  "took": 3,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 3,
      "relation": "eq"
    },
    "max_score": 1.0,           # match_all 是没法计算得分的,所以都是 1.0
    "hits": [
      {
        "_index": "user",
        "_type": "_doc",
        "_id": "WYUzT4oBZ0WpQB8-3pDP",
        "_score": 1.0,
        "_source": {
          "name": "uccs",
          "company": "imooc"
        }
      },
      {
        "_index": "user",
        "_type": "_doc",
        "_id": "WoU0T4oBZ0WpQB8-pZAj",
        "_score": 1.0,
        "_source": {
          "name": "uccs",
          "company": "imooc"
        }
      },
      {
        "_index": "user",
        "_type": "_doc",
        "_id": "1",
        "_score": 1.0,
        "_source": {
          "name": "uccs",
          "company": "imooc"
        }
      }
    ]
  }
}

更新数据

使用这种方式更新数据,如果数据不存在,会创建数据,如果数据存在,会更新数据

POST user/_doc/3
# 请求体
{
  "name": "astak"
}
# 响应体
{
  "_index": "user",
  "_type": "_doc",
  "_id": "3",
  "_version": 2,
  "result": "updated",  # 如果数据不存在,这个值是 create;如果数据存在,这个值是 updated
  "_shards": {
    "total": 2,
    "successful": 1,
    "failed": 0
  },
  "_seq_no": 4,
  "_primary_term": 1
}

这种问题会存在一个坑,当我向这个数据更新某个字段时,会覆盖掉其他的字段

比如 user/3 这条数据中已经存在了 name 的字段,当我在设置 age 时,会覆盖掉 name 的值

POST user/_doc/3  # 使用 PUT 也是同样的问题
# 请求体
{
  "age": 10
}
#########
GET user/_doc/3
# 响应体
{
  "_index": "user",
  "_type": "_doc",
  "_id": "3",
  "_version": 5,
  "_seq_no": 7,
  "_primary_term": 1,
  "found": true,
  "_source": {
    "age": 10
  }
}

update

如何解决这个问题呢?

需要将 _doc 换成 _update

它的写法有不一样,需要将更新的数据放到 doc

POST user/_update/3
# 请求体
{
  "doc": {
    "name": "astak"
  }
}
#######
GET user/_doc/3
# 响应体
{
  "_index": "user",
  "_type": "_doc",
  "_id": "3",
  "_version": 7,
  "_seq_no": 9,
  "_primary_term": 1,
  "found": true,
  "_source": {
    "age": 10,
    "name": "astak"
  }
}

这里要注意细节 _doc_update 的一个细节

  1. 使用 _update 更新数据时,如果每次更新的数据都一样,_version_seq_no 的值不会变化
  2. 使用 _doc 更新数据时,更新一次,_version_seq_no 的值就会加 1

删除数据

删除某条数据

DELETE user/_doc/3
# 响应体
{
  "_index": "user",
  "_type": "_doc",
  "_id": "3",
  "_version": 8,
  "result": "deleted",    # 表示数据删除成功
  "_shards": {
    "total": 2,
    "successful": 1,
    "failed": 0
  },
  "_seq_no": 10,
  "_primary_term": 1
}
########
GET user/_doc/3
# 响应体
{
  "_index": "user",
  "_type": "_doc",
  "_id": "3",
  "found": false    # 是否找到数据
}

删除整个索引

DELETE user
# 响应体
{
  "acknowledged": true      # 表示删除成功
}
#########
GET user/_doc/3
# 响应体
{
  "error": {
    "root_cause": [
      {
        "type": "index_not_found_exception",
        "reason": "no such index [user]",
        "resource.type": "index_expression",
        "resource.id": "user",
        "index_uuid": "_na_",
        "index": "user"
      }
    ],
    "type": "index_not_found_exception",
    "reason": "no such index [user]",
    "resource.type": "index_expression",
    "resource.id": "user",
    "index_uuid": "_na_",
    "index": "user"
  },
  "status": 404
}

批量操作

批量插入,使用 POST_bulk 是固定值

每条数据有两行:

  • 第一行表示操作类型,index 表示插入,delete 表示删除,create 表示创建,update 表示更新
    • _index:在哪个索引上操作
    • _id:数据的唯一标识符
  • 第二行表示数据
  • 这里要注意:
    • delete 操作没有第二行数据
    • update 操作的第二行数据是 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"} }
POST _bulk
# 请求体
{ "index": { "_index": "user", "_id": "1" } }
{ "age": 20 }
{ "delete": { "_index": "user", "_id": "3" } }   # 删除不存在的数据,不影响其他数据
# 响应体
{
  "took": 186,
  "errors": false,
  "items": [             # 操作成功后数据在这里显示
    {
      "index": {
        "_index": "user",
        "_type": "_doc",
        "_id": "1",
        "_version": 1,
        "result": "created",
        "_shards": {
          "total": 2,
          "successful": 1,
          "failed": 0
        },
        "_seq_no": 0,
        "_primary_term": 1,
        "status": 201
      }
    },
    {
      "delete": {
        "_index": "user",
        "_type": "_doc",
        "_id": "3",
        "_version": 1,
        "result": "not_found",    # 操作失败
        "_shards": {
          "total": 2,
          "successful": 1,
          "failed": 0
        },
        "_seq_no": 2,
        "_primary_term": 1,
        "status": 404
      }
    }
  ]
}

批量查询

GET _mget
# 请求体
{
  "docs": [
    { "_index": "user", "_id": "1" },    # user 索引, id 为 1 的数据
    { "_index": "account", "_id": "1" }  # account 索引,id 为 1 的数据
  ]
}
# 响应体
{
  "docs": [
    {
      "_index": "user",
      "_type": "_doc",
      "_id": "1",
      "_version": 1,
      "_seq_no": 0,
      "_primary_term": 1,
      "found": true,
      "_source": {
        ...
      }
    },
    {
      "_index": "account",
      "_type": "_doc",
      "_id": "1",
      "_version": 2,
      "_seq_no": 1,
      "_primary_term": 1,
      "found": true,
      "_source": {
        ....
      }
    }
  ]
}

分页查询

分页查询可以使用 fromsize:

  • from:第几页,默认是 0
  • size:多少条,,默认是 10

需要注意的是,fromsize 适用于数据量比较小的情况,在数据量大的情况的下,优先使用 scroll

GET user/_search
# 请求体
{
  "query": {
    "match_all": {}
  },
  "from": 10,
  "size": 20
}

条件查询

match 按条件查询:

  • key: value 的形式,搜索 user 索引 address 字段中所有包含 street 的数据
  • value 大小写不敏感
GET user/_search
# 请求体
{
  "query": {
    "match": {
      "address": "Street"   # 大小写不敏感
    }
  }
}

倒排索引(分词)

ElasticSearch 中的倒排索引是指:

  • 在写入数据时,ES 会对数据进行分词,然后将分词后的数据存储到倒排索引中
  • 在搜索时,ES 也会对搜索的数据进行分词,然后去倒排索引中查找,最后将结果返回

具体如下图所示:

ElasticSearch 基本使用

671 Bristol Street789 Madison Street 举例:

  • 写入数据时,会进行分词,分词后的数据存储到倒排索引中
    • 671 Bristol Street 分词后的数据是 671bristolstreet
    • 789 Madison Street 分词后的数据是 789sadisonstreet
    • 分词后会在 doc 中保存相关信息,比如:id,位置,出现次数等(图中只列举了 id
  • 查询数据时,也会进行分词,分词后去倒排索引中查找,最后将结果返回
    • 比如搜索 Madison Street,分词后的数据是 madisonstreet
    • 然后去倒排索引中查找
      • madison 找的到,id13
      • street 找的到,id613
    • 最后将 id613 的数据返回

例如下面的例子,我们可以看到:

  • 搜索出来了 3 条结果,因为这三条结果满足了分词 streetmadison 的条件
  • 每条结果都有得分,匹配度越高的得分越高,排名越靠前
GET user/_search
# 请求体
{
  "query": {
    "match": {
      "address": "Madison Street"
    }
  }
}
# 响应体
{
  "took": 1,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 386,
      "relation": "eq"
    },
    "max_score": 6.9447823,
    "hits": [
      {
        "_index": "user",
        "_type": "_doc",
        "_id": "13",
        "_score": 6.9447823,                # 匹配度最高
        "_source": {
          "address": "789 Madison Street"
        }
      },
      {
        "_index": "user",
        "_type": "_doc",
        "_id": "956",
        "_score": 5.990829,
        "_source": {
          "address": "490 Madison Place"
        }
      },
      {
        "_index": "user",
        "_type": "_doc",
        "_id": "6",
        "_score": 0.95395315,
        "_source": {
          "address": "671 Bristol Street"
        }
      }
    ]
  }
}

match_phrase 查询

match 查询是模糊匹配,会对输入的数据进行分词,只要字段中包含分词,就都会被查询出来:

使用 Madison Street 去查询 Madison walk on the street 也会被匹配出来

但这并不是我们想要的结果

我们想要精确的匹配结果,就需要使用 match_phrase

match_phrase 也会进行分词,但结果需要包含所有的分词,才会被匹配出来:

比如 Madison Street 会用短语去匹配,所以不会匹配到 Madison walk on the street

multi_match 查询

match 只能进行单字段查询,无法进行多字段查询

es 提供了 multi_match 进行多字段查询:

  • fields:查询的字段
  • query:需要查询的值

只要 address 或者 state 中包含 us,就会被查询出来

GET user/_search
# 请求体
{
  "query": {
    "multi_match": {
      "query": "us",
      "fields": ["address", "state"]
    }
  }
}

有时候,某个字段的权重比较高,我们可以通过 ^ 来设置权重:

  • address 最后的得分会被乘以 2
GET user/_search
# 请求体
{
  "query": {
    "multi_match": {
      "query": "us",
      "fields": ["address^2", "state"]
    }
  }
}

query_string 查询

query_stringmatch 用法差不多,区别是:

  • match 需要指定字段名
  • query_string 默认是查询所有字段,也可以指定字段查询
GET user/_search
# 请求体
{
  "query": {
    "query_string": {
      "default_field": "address",   # 指定字段查询,如果不指定 default_field,就是查询所有字段
      "query": "Madison Street",
    }
  }
}

可以在词语之间使用操作符 ORAND

  • ORmatch 类似
  • ANDmatch_parse 类似
GET user/_search
# 请求体
{
  "query": {
    "query_string": {
      "query": "Madison OR Street",
    }
  }
}

term 查询

term 查询是精确查询,不会对输入的数据进行分词,只要字段中包含输入的数据,就会被查询出来

但是 term 查询是区分大小写的,比如 Streetstreet 是不一样的

我们在写入数据时,是先分词的,这一步会把数据中大小的字母都转成小写,所以当你在用大写字母去查询时,是查不到数据的

GET user/_search
# 请求体
{
  "query": {
    "term": {
      "address": "street"
    }
  }
}
# 响应体
{
  "took": 1,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 385,
      "relation": "eq"
    },
    "max_score": 0.95395315,
    "hits": [
      {
        "_index": "user",
        "_type": "_doc",
        "_id": "6",
        "_score": 0.95395315,
        "_source": {
          "address": "671 Bristol Street"
        }
      }
    ]
  }
}
GET user/_search
# 请求体
{
  "query": {
    "term": {
      "address": "Street"
    }
  }
}
# 响应体
{
  "took": 1,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 0,
      "relation": "eq"
    },
    "max_score": null,
    "hits": []
  }
}

term 查询 —— 范围查询

term 查询还可以进行范围查询,比如查询 age1020 之间的数据,可以使用 range

GET user/_search
# 请求体
{
  "query": {
    "range": {
      "age": {
        "gte": 10,
        "lte": 20
      }
    }
  }
}
# 响应体
{
  "took": 28,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 44,
      "relation": "eq"
    },
    "max_score": 1.0,
    "hits": [
      {
        "_index": "user",
        "_type": "_doc",
        "_id": "157",
        "_score": 1.0,
        "_source": {
          "age": 10
        }
      },
      {
        "_index": "user",
        "_type": "_doc",
        "_id": "215",
        "_score": 1.0,
        "_source": {
          "age": 20
        }
      },
      {
        "_index": "user",
        "_type": "_doc",
        "_id": "816",
        "_score": 1.0,
        "_source": {
          "age": 18
        }
      }
    ]
  }
}

exists 可以查询某个字段是否存在,比如查询 age 字段是否存在

GET user/_search
# 请求体
{
  "query": {
    "exists": {
      "field": "age"
    }
  }
}

模糊查询,这里的模糊查询是指,自动纠错,比如 streetstret 是一样的

GET user/_search
# 请求体
{
  "query": {
    "fuzzy": {
      "address": "stret"
    }
  }
}

match 中也可以使用 fuzzy 查询

GET user/_search
# 请求体
{
  "query": {
    "match": {
      "address": {
        "query": "stret",
        "fuzziness": 1
      }
    }
  }
}

复合查询

bool 查询是复合查询的一种

语法是:

{
  "query": {
    "bool": {
      "must": [], // 必须全匹配,用于加分
      "should": [], // 匹配也可以,不匹配也可以;用于加分,匹配的话得分会高,不匹配得分会低
      "must_not": [], // 必须不匹配,用于过滤,不影响得分
      "filter": [] // 必须匹配,用于过滤,不影响得分
    }
  }
}

bool 查询,采用了一种匹配越多越好的方法,也就是说每个匹配的 mustshould 子句的得分会加在一起,提供最终的得分

GET user/_search
# 请求体
{
  "query": {
    "bool": {
      "must": [
        { "term": { "state": "tn" } },
        { "range": { "age": { "gte": 20, "lte": 30 } } }
      ],
      "must_not": [{ "term": { "gender": "m" } }],
      "should": [{ "match": { "firstname": "Decker" } }],
      "filter": [{ "range": { "age": { "gte": 25, "lte": 30 } } }]
    }
  }
}
# 响应体

{
  "took": 2,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 6,
      "relation": "eq"
    },
    "max_score": 11.173367,
    "hits": [
      {
        "_index": "user",
        "_type": "_doc",
        "_id": "686",
        "_score": 11.173367,
        "_source": {
          "firstname": "Decker",
          "age": 30,
          "gender": "F",
          "state": "TN"
        }
      },
      {
        "_index": "user",
        "_type": "_doc",
        "_id": "869",
        "_score": 4.6700764,
        "_source": {
          "firstname": "Corinne",
          "age": 25,
          "gender": "F",
          "state": "TN"
        }
      },
      {
        "_index": "user",
        "_type": "_doc",
        "_id": "442",
        "_score": 4.6700764,
        "_source": {
          "firstname": "Lawanda",
          "age": 27,
          "gender": "F",
          "state": "TN"
        }
      },
      {
        "_index": "user",
        "_type": "_doc",
        "_id": "707",
        "_score": 4.6700764,
        "_source": {
          "firstname": "Sonya",
          "age": 30,
          "gender": "F",
          "state": "TN"
        }
      },
      {
        "_index": "user",
        "_type": "_doc",
        "_id": "72",
        "_score": 4.6700764,
        "_source": {
          "firstname": "Barlow",
          "age": 25,
          "gender": "F",
          "state": "TN"
        }
      },
      {
        "_index": "user",
        "_type": "_doc",
        "_id": "767",
        "_score": 4.6700764,
        "_source": {
          "firstname": "Anthony",
          "age": 27,
          "gender": "F",
          "state": "TN"
        }
      }
    ]
  }
}

mapping

文档

textkeyword 的区别:

  • text 会分词,然后存入倒排索引
  • keyword 不会分词

查看索引的 mapping,使用 GET + 索引名

GET user
# 响应体
{
  "user": {
    "aliases": {},
    "mappings": {
      "properties": {
        "account_number": {
          "type": "long"
        },
        "address": {
          "type": "text",            # 这个字段被分词了
          "fields": {
            "keyword": {
              "type": "keyword",     # 字段字段可以不分词
              "ignore_above": 256
            }
          }
        },
        "age": {
          "type": "long"
        },
        "balance": {
          "type": "long"
        }
      }
    },
    "settings": {
      "index": {
        "routing": {
          "allocation": {
            "include": {
              "_tier_preference": "data_content"
            }
          }
        },
        "number_of_shards": "1",
        "provided_name": "user",
        "creation_date": "1693653631917",
        "number_of_replicas": "1",
        "uuid": "gt2Hh0upSX6ERoWNDny0lQ",
        "version": {
          "created": "7100199"
        }
      }
    }
  }
}

不分词查询

GET user/_search
# 请求体
{
  "query": {
    "match": {
      "address.keyword": "789 Madison Street"   # 不会被分词
    }
  }
}

定义索引的 mapping,使用 PUT + 索引名

PUT usertest
# 请求体
{
  "mappings": {
    "properties": {
      "age": {
        "type": "integer"
      },
      "name": {
        "type": "text"      # 会分词
      },
      "desc": {
        "type": "keyword"   # 不会分词
      }
    }
  }
}

analyzer

从最开始的一张图中,我们可以看出,不管在写入数据还是在查询数据时,有一个分析(analyzer)的过程,它是由三部分组成:

  • Character Filters:对原始文本增加、删除或者对字符做转换
  • Tokenizer:将字符流分解成单词、词语等,输出单词流。也负责记录每个单词的顺序和该单词在原始文本中的起始和结束偏移 offsets
  • Token Filters:对分词后的单词进行处理,比如转换小写,去掉没有的符号(! 等),去掉停用词(theand 等)

ElasticSearch 基本使用

内置 analyer

文档

  • Standard Analyzer:默认分词器,按词切分,小写处理
  • Simple Analyzer:按照非字母切分(符号被过滤),小写处理
  • Stop Analyzer:小写处理,停用词过滤(theais
  • Whitespace Analyzer:按照空格切分,不转小写
  • Keyword Analyzer:不分词,直接将输入当做输出
  • Patter Analyzer:正则表达式,默认 \W+
  • Language:提供了 30 多种常见语言的分词器

analyer 测试

GET _analyze
# 请求体
{
  "analyzer": "standard",
  "text": "The 2 QUICK Brown-Foxes jumped over the lazy dog's bone."
}
# 响应体

{
  "tokens": [
    {
      "token": "the",
      "start_offset": 0,
      "end_offset": 3,
      "type": "<ALPHANUM>",
      "position": 0
    },
    {
      "token": "2",
      "start_offset": 4,
      "end_offset": 5,
      "type": "<NUM>",
      "position": 1
    },
    {
      "token": "quick",
      "start_offset": 6,
      "end_offset": 11,
      "type": "<ALPHANUM>",
      "position": 2
    },
    {
      "token": "brown",
      "start_offset": 12,
      "end_offset": 17,
      "type": "<ALPHANUM>",
      "position": 3
    },
    {
      "token": "foxes",
      "start_offset": 18,
      "end_offset": 23,
      "type": "<ALPHANUM>",
      "position": 4
    },
    {
      "token": "jumped",
      "start_offset": 24,
      "end_offset": 30,
      "type": "<ALPHANUM>",
      "position": 5
    },
    {
      "token": "over",
      "start_offset": 31,
      "end_offset": 35,
      "type": "<ALPHANUM>",
      "position": 6
    },
    {
      "token": "the",
      "start_offset": 36,
      "end_offset": 39,
      "type": "<ALPHANUM>",
      "position": 7
    },
    {
      "token": "lazy",
      "start_offset": 40,
      "end_offset": 44,
      "type": "<ALPHANUM>",
      "position": 8
    },
    {
      "token": "dog's",
      "start_offset": 45,
      "end_offset": 50,
      "type": "<ALPHANUM>",
      "position": 9
    },
    {
      "token": "bone",
      "start_offset": 51,
      "end_offset": 55,
      "type": "<ALPHANUM>",
      "position": 10
    }
  ]
}

analyzer 使用策略

在搜索时,会采用以下策略:

  1. 搜索时,指定 analyzer,采用指定的 analyzer(文档)
    GET usertest/_search
    {
      "query": {
        "match": {
          "address": {
             "query": "789 Madison Street",
             "analyzer": "simple"        # 采用这个 analyzer
          }
        }
      }
    }
    
  2. 搜索时,如果不指定 analyzer,会采用创建索引时指定的 search_analyzer(文档)
    PUT usertest
    {
      "mappings": {
        "properties": {
          "address": {
            "type": "text",
            "search_analyzer": "simple"   # 采用这个 search_analyer
          }
        }
      }
    }
    
  3. 使用默认的 analyzer,在 setting 中:analysis.analyzer.default_search(文档)
    GET usertest
    {
      "settings": {
        "analysis": {
          "analyzer": {
            "default_search": {
              "type": "whitespace"     # 采用这个 default_analyzer
            }
          }
        }
      }
    }
    
  4. 使用创建索引时指定的的 analyzer(文档)
    PUT usertest
    {
      "mappings": {
        "properties": {
          "address": {
            "type": "text",
            "analyzer": "simple" # 采用这个 analyzer
          }
        }
      }
    }
    
  5. 采用默认的 analyzer,是 standard(文档)

IK 分词器

  1. elasticsearch-analysis-ik 中找到 ES 对应的版本,下载 ik 分词器
  2. 将下载的压缩包解压,然后重名为为 ik
  3. ik 文件夹放到 elasticsearchplugins 目录下
    • docker 安装的 ESplugins 目录在 /usr/share/elasticsearch/plugins
    • 如果没有权限,执行 chmod 777 -R ik
  4. 重启 ES
    • docker restart <ES_ContainerId>
  5. 查看插件
    ./bin/elasticsearch-plugin list
    

使用

GET _analyze
# 请求体
{
  "text": "中国科学技术大学",
  "analyzer": "ik_max_word"
}
# 响应体
{
  "tokens": [
    {
      "token": "中国科学技术大学",
      "start_offset": 0,
      "end_offset": 8,
      "type": "CN_WORD",
      "position": 0
    },
    {
      "token": "中国",
      "start_offset": 0,
      "end_offset": 2,
      "type": "CN_WORD",
      "position": 1
    },
    {
      "token": "科学技术",
      "start_offset": 2,
      "end_offset": 6,
      "type": "CN_WORD",
      "position": 2
    },
    {
      "token": "科学",
      "start_offset": 2,
      "end_offset": 4,
      "type": "CN_WORD",
      "position": 3
    },
    {
      "token": "技术",
      "start_offset": 4,
      "end_offset": 6,
      "type": "CN_WORD",
      "position": 4
    },
    {
      "token": "大学",
      "start_offset": 6,
      "end_offset": 8,
      "type": "CN_WORD",
      "position": 5
    }
  ]
}
# 请求体
{
  "text": "中国科学技术大学",
  "analyzer": "ik_smart"
}
# 响应体
{
  "tokens": [
    {
      "token": "中国科学技术大学",
      "start_offset": 0,
      "end_offset": 8,
      "type": "CN_WORD",
      "position": 0
    }
  ]
}

扩展词库

  1. ik/config 目录下新建 custom 目录
  2. 新建文件,文件最好以 dic 结尾
    • mydict.dic,将扩展词写入文件中
    • stopword.dic,将停用词写入文件中
  3. 编辑 IKAnalyzer.cfn.xml 文件
    • <entry key="ext_dict">mydict.dic</entry>
    • <entry key="ext_stopwords">stopword.dic</entry>
  4. 重启容器