• Elasticsearch映射操作(八)


            在使用数据之前,需要构建数据的组织结构。这种组织结构在关系型数据库中叫作表结构,在ES中叫作映射。

            作为无模式搜索引擎,ES可以在数据写入时猜测数据类型,从而自动创建映射。但有时ES创建的映射中的数据类型和目标类型可能不一致。当需要严格控制数据类型时,还是需要用户手动创建映射。

    查看映射

    在ES中写入文档请求的类型是GET,其请求形式如下:

    GET /${index_name}/_mapping

    比如,查看hotel_1的mappings,请求的DSL如下:        

    GET /hotel_1/_mapping

    返回结果如下:

    1. {
    2. "hotel_1" : {
    3. "mappings" : {
    4. "properties" : {
    5. "city" : {
    6. "type" : "keyword"
    7. },
    8. "price" : {
    9. "type" : "double"
    10. },
    11. "title" : {
    12. "type" : "text"
    13. }
    14. }
    15. }
    16. }
    17. }

    扩展映射

            映射中的字段类型是不可以修改的,但是字段可以扩展。最常见的扩展方式是增加字段和为object(对象)类型的数据新增属性。下面的DSL示例为扩展hotel_1索引,并增加tag字段。

    1. POST /hotel_1/_mapping
    2. {
    3. "properties": {
    4. "tag": {
    5. "type":"keyword"
    6. }
    7. }
    8. }

    查看索引hotel_1的mappings,返回结果如下:

    1. {
    2. "hotel_1" : {
    3. "mappings" : {
    4. "properties" : {
    5. "city" : {
    6. "type" : "keyword"
    7. },
    8. "price" : {
    9. "type" : "double"
    10. },
    11. "tag" : {
    12. "type" : "keyword"
    13. },
    14. "title" : {
    15. "type" : "text"
    16. }
    17. }
    18. }
    19. }
    20. }

    由返回结果可知,tag字段已经被添加到索引hotel_1中。

    基本的数据类型

    keyword类型

            keyword类型是不进行切分的字符串类型。这里的“不进行切分”指的是:在索引时,对keyword类型的数据不进行切分,直接构建倒排索引;在搜索时,对该类型的查询字符串不进行切分后的部分匹配。keyword类型数据一般用于对文档的过滤、排序和聚合。

            在现实场景中,keyword经常用于描述姓名、产品类型、用户ID、URL和状态码等。keyword类型数据一般用于比较字符串是否相等,不对数据进行部分匹配,因此一般查询这种类型的数据时使用term查询。

    例如,建立一个人名索引,可以设定姓名字段为keyword字段:

    1. PUT /user
    2. {
    3. "mappings": {
    4. "properties": {
    5. "user_name":{"type": "keyword"}
    6. }
    7. }
    8. }

    写入一条数据,请求的DSL如下:

    1. POST /user/_doc/001
    2. {
    3. "user_name":"张三"
    4. }

    查询刚刚写入的数据,请求的DSL如下:

    1. GET /user/_search
    2. {
    3. "query": {
    4. "term": {
    5. "user_name": {
    6. "value": "张三"
    7. }
    8. }
    9. }
    10. }

    返回的结果信息如下:

    1. {
    2. "took" : 368,
    3. "timed_out" : false,
    4. "_shards" : {
    5. "total" : 1,
    6. "successful" : 1,
    7. "skipped" : 0,
    8. "failed" : 0
    9. },
    10. "hits" : {
    11. "total" : {
    12. "value" : 1,
    13. "relation" : "eq"
    14. },
    15. "max_score" : 0.2876821,
    16. "hits" : [
    17. {
    18. "_index" : "user",
    19. "_type" : "_doc",
    20. "_id" : "001",
    21. "_score" : 0.2876821,
    22. "_source" : {
    23. "user_name" : "张三"
    24. }
    25. }
    26. ]
    27. }
    28. }

    由搜索结果可以看出,使用term进行全字符串匹配“张三”可以搜索到命中文档。下面的DSL使用match搜索姓名中带有“张”的记录:

    1. GET /user/_search
    2. {
    3. "query": {
    4. "match": {
    5. "user_name": "张"
    6. }
    7. }
    8. }

    返回结果如下:

    1. {
    2. "took" : 1,
    3. "timed_out" : false,
    4. "_shards" : {
    5. "total" : 1,
    6. "successful" : 1,
    7. "skipped" : 0,
    8. "failed" : 0
    9. },
    10. "hits" : {
    11. "total" : {
    12. "value" : 0,
    13. "relation" : "eq"
    14. },
    15. "max_score" : null,
    16. "hits" : [ ]
    17. }
    18. }

    由搜索结果可见,对keyword类型使用match搜索进行匹配是不会命中文档的。

    text类型

            text类型是可进行切分的字符串类型。这里的“可切分”指的是:在索引时,可按照相应的切词算法对文本内容进行切分,然后构建倒排索引;在搜索时,对该类型的查询字符串按照用户的切词算法进行切分,然后对切分后的部分匹配打分。

            例如,一个旅馆搜索项目,我们希望可以根据旅馆名称即title字段进行模糊匹配,因此可以设定title字段为text字段,建立旅馆索引的DSL如下:

    1. PUT /hotel
    2. {
    3. "mappings": {
    4. "properties": {
    5. "title":{"type": "text"},
    6. "city":{"type": "keyword"},
    7. "price":{"type": "double"}
    8. }
    9. }
    10. }

    写入一条数据:

    1. POST /hotel/_doc/001
    2. {
    3. "title":"java旅馆",
    4. "city":"深圳",
    5. "price":50.00
    6. }

    按照普通的term进行搜索,观察能否搜索到刚刚写入的文档,请求的DSL如下:

    1. GET /hotel/_search
    2. {
    3. "query": {
    4. "term": {
    5. "title": {
    6. "value": "java旅馆"
    7. }
    8. }
    9. }
    10. }

    返回结果如下:

    1. {
    2. "took" : 0,
    3. "timed_out" : false,
    4. "_shards" : {
    5. "total" : 1,
    6. "successful" : 1,
    7. "skipped" : 0,
    8. "failed" : 0
    9. },
    10. "hits" : {
    11. "total" : {
    12. "value" : 0,
    13. "relation" : "eq"
    14. },
    15. "max_score" : null,
    16. "hits" : [ ]
    17. }
    18. }

            根据返回结果可知,上面的请求并没有搜索到文档。term搜索用于搜索值和文档对应的字段是否完全相等,而对于text类型的数据,在建立索引时ES已经进行了切分并建立了倒排索引,因此使用term没有搜索到数据。一般情况下,搜索text类型的数据时应使用match搜索。比如以下:

    1. GET /hotel/_search
    2. {
    3. "query": {
    4. "match": {
    5. "title": "java"
    6. }
    7. }
    8. }

    返回结果如下:

    1. {
    2. "took" : 1,
    3. "timed_out" : false,
    4. "_shards" : {
    5. "total" : 1,
    6. "successful" : 1,
    7. "skipped" : 0,
    8. "failed" : 0
    9. },
    10. "hits" : {
    11. "total" : {
    12. "value" : 1,
    13. "relation" : "eq"
    14. },
    15. "max_score" : 0.2876821,
    16. "hits" : [
    17. {
    18. "_index" : "hotel",
    19. "_type" : "_doc",
    20. "_id" : "001",
    21. "_score" : 0.2876821,
    22. "_source" : {
    23. "title" : "java旅馆",
    24. "city" : "深圳",
    25. "price" : 50.0
    26. }
    27. }
    28. ]
    29. }
    30. }

    数值类型

            ES支持的数值类型有long、integer、short、byte、double、float、half_float、scaled_float和unsigned_long等。各类型所表达的数值范围可以参考官方文档,网址为Numeric field types | Elasticsearch Guide [8.3] | Elastic 为节约存储空间并提升搜索和索引的效率,在实际应用中,在满足需求的情况下应尽可能选择范围小的数据类型。比如,年龄字段的取值最大值不会超过200,因此选择byte类型即可。数值类型的数据也可用于对文档进行过滤、排序和聚合。

            以旅馆搜索为例,旅馆的索引除了包含旅馆名称和城市之外,还需要定义价格、星级和评论数等,创建索引的DSL如下:

    1. PUT /hotel
    2. {
    3. "mappings": {
    4. "properties": {
    5. "title":{"type": "text"},
    6. "city":{"type": "keyword"},
    7. "price":{"type": "double"},
    8. "star":{"type":"byte"},
    9. "comment_count":{"type":"integer"}
    10. }
    11. }
    12. }

    对于数值型数据,一般使用term搜索或者范围搜索。例如,搜索价格为350~400(包含350和400)元的旅馆,搜索的DSL如下:

    1. GET /hotel/_search
    2. {
    3. "query": {
    4. "range": {
    5. "price": {
    6. "gte": 200,
    7. "lte": 300
    8. }
    9. }
    10. }
    11. }

    布尔类型

    布尔类型使用boolean定义,用于业务中的二值表示,如商品是否售罄,房屋是否已租,旅馆房间是否满房等。写入或者查询该类型的数据时,其值可以使用true和false,或者使用字符串形式的"true"和"false"。下面的DSL定义索引中增加“是否满房”的字段为布尔类型:

    1. PUT /hotel/_mapping
    2. {
    3. "properties":{
    4. "full_room":{"type":"boolean"}
    5. }
    6. }

    下面的DSL将查询满房的旅馆:

    1. GET /hotel/_search
    2. {
    3. "query": {
    4. "term": {
    5. "full_room": {
    6. "value": "true"
    7. }
    8. }
    9. }
    10. }

    日期类型

    在ES中,日期类型的名称为date。ES中存储的日期是标准的UTC格式。下面定义索引hotel,该索引增加一个create_time字段,现在把它定义成date类型。增加date类型请求的DSL如下:

    1. PUT /hotel/_mapping
    2. {
    3. "properties":{
    4. "create_time":{"type":"date"}
    5. }
    6. }

    一般使用如下形式表示日期类型数据:

    • 格式化的日期字符串。
    • 毫秒级的长整型,表示从1970年1月1日0点到现在的毫秒数。
    • 秒级别的整型,表示从1970年1月1日0点到现在的秒数。

            日期类型的默认格式为strict_date_optional_time||epoch_millis。其中,strict_date_optional_time的含义是严格的时间类型,支持yyyy-MM-dd、yyyyMMdd、yyyyMMddHHmmss、yyyy-MM-ddTHH:mm:ss、yyyy-MM-ddTHH:mm:ss.SSS和yyyy-MM-ddTHH:mm:ss.SSSZ等格式,epoch_millis的含义是从1970年1月1日0点到现在的毫秒数。

    下面写入索引的文档中有一个create_time字段是日期格式的字符串,请求的DSL如下:

    1. POST /hotel/_doc/001
    2. {
    3. "title":"java旅馆",
    4. "city":"深圳",
    5. "price":200.0,
    6. "create_time":"20220803"
    7. }

    搜索日期型数据时,一般使用ranges查询。例如,按创建日期搜索旅馆,请求的DSL如下:

    1. GET /hotel/_search
    2. {
    3. "query": {
    4. "range": {
    5. "create_time": {
    6. "gte": "20220801",
    7. "lte": "20220803"
    8. }
    9. }
    10. }
    11. }

            日期类型默认不支持yyyy-MM-dd HH:mm:ss格式,如果经常使用这种格式,可以在索引的mapping中设置日期字段的format属性为自定义格式。下面的示例将新增modify_time字段的格式为yyyy-MM-dd HH:mm:ss:

    1. PUT /hotel/_mapping
    2. {
    3. "properties":{
    4. "modify_time":{
    5. "type":"date",
    6. "format":"yyyy-MM-dd HH:mm:ss"
    7. }
    8. }
    9. }

    此时如果写入以下数据:

    1. POST /hotel/_doc/002
    2. {
    3. "title":"python旅馆",
    4. "city":"深圳",
    5. "price":200.0,
    6. "create_time":"20220803",
    7. "modify_time":"20220803"
    8. }

    此时系统返回:

    1. {
    2. "error" : {
    3. "root_cause" : [
    4. {
    5. "type" : "mapper_parsing_exception",
    6. "reason" : "failed to parse field [modify_time] of type [date] in document with id '002'. Preview of field's value: '20220803'"
    7. }
    8. ],
    9. "type" : "mapper_parsing_exception",
    10. "reason" : "failed to parse field [modify_time] of type [date] in document with id '002'. Preview of field's value: '20220803'",
    11. "caused_by" : {
    12. "type" : "illegal_argument_exception",
    13. "reason" : "failed to parse date field [20220803] with format [yyyy-MM-dd HH:mm:ss]",
    14. "caused_by" : {
    15. "type" : "date_time_parse_exception",
    16. "reason" : "Text '20220803' could not be parsed at index 0"
    17. }
    18. }
    19. },
    20. "status" : 400
    21. }

            根据错误信息可知,错误的原因是写入的数据格式和定义的数据格式不同。此时需要写入的格式为yyyy-MM-dd HH:mm:ss的文档,请求的DSL如下:

    1. POST /hotel/_doc/002
    2. {
    3. "title":"python旅馆",
    4. "city":"深圳",
    5. "price":200.0,
    6. "create_time":"20220803",
    7. "modify_time":"2022-08-03 15:00:00"
    8. }

    复杂的数据类型

    数组类型

            ES数组没有定义方式,其使用方式是开箱即用的,即无须事先声明,在写入时把数据用中括号[]括起来,由ES对该字段完成定义。

            当然,如果事先已经定义了字段类型,在写数据时以数组形式写入,ES也会将该类型转为数组。例如,为hotel索引增加一个标签字段,名称为tag,请求的DSL如下:

    1. PUT /hotel/_mapping
    2. {
    3. "properties":{
    4. "tag":{
    5. "type":"keyword"
    6. }
    7. }
    8. }

    查看一下索引hotel的mapping:

    1. {
    2. "hotel" : {
    3. "mappings" : {
    4. "properties" : {
    5. "city" : {
    6. "type" : "keyword"
    7. },
    8. "comment_count" : {
    9. "type" : "integer"
    10. },
    11. "create_time" : {
    12. "type" : "date"
    13. },
    14. "full_room" : {
    15. "type" : "boolean"
    16. },
    17. "modify_time" : {
    18. "type" : "date",
    19. "format" : "yyyy-MM-dd HH:mm:ss"
    20. },
    21. "price" : {
    22. "type" : "double"
    23. },
    24. "star" : {
    25. "type" : "byte"
    26. },
    27. "tag" : {
    28. "type" : "keyword"
    29. },
    30. "title" : {
    31. "type" : "text"
    32. }
    33. }
    34. }
    35. }
    36. }

    通过返回的mapping信息来看,新增的tag字段与普通的keyword类型字段没什么区别,现在写入一条数据:

    1. POST /hotel/_doc/003
    2. {
    3. "title":"go旅馆",
    4. "city":"深圳",
    5. "price":200.0,
    6. "create_time":"20220803",
    7. "modify_time":"2022-08-03 15:00:00",
    8. "tag":["有车位","免费Wi-Fi"]
    9. }

    查看一下写入的数据,ES返回的信息如下:

    1. GET /hotel/_doc/003
    2. {
    3. "_index" : "hotel",
    4. "_type" : "_doc",
    5. "_id" : "003",
    6. "_version" : 1,
    7. "_seq_no" : 2,
    8. "_primary_term" : 1,
    9. "found" : true,
    10. "_source" : {
    11. "title" : "go旅馆",
    12. "city" : "深圳",
    13. "price" : 200.0,
    14. "create_time" : "20220803",
    15. "modify_time" : "2022-08-03 15:00:00",
    16. "tag" : [
    17. "有车位",
    18. "免费Wi-Fi"
    19. ]
    20. }
    21. }

    通过以上信息可以看到,写入的数据的tag字段已经是数组类型了.

            数组类型的字段适用于元素类型的搜索方式,也就是说,数组元素适用于什么搜索,数组字段就适用于什么搜索。例如,在上面的示例中,数组元素类型是keyword,该类型可以适用于term搜索,则tag字段也可以适用于term搜索,搜索的DSL如下:

    1. GET /hotel/_search
    2. {
    3. "query": {
    4. "term": {
    5. "tag": {
    6. "value": "免费Wi-Fi"
    7. }
    8. }
    9. }
    10. }

    ES中的空数组可以作为missing field,即没有值的字段,下面的DSL将插入一条tag为空的数组:

    1. POST /hotel/_doc/004
    2. {
    3. "title":"go旅馆",
    4. "city":"深圳",
    5. "price":200.0,
    6. "create_time":"20220803",
    7. "modify_time":"2022-08-03 15:00:00",
    8. "tag":[]
    9. }

    对象类型

    在实际业务中,一个文档需要包含其他内部对象。例如,在旅馆搜索需求中,用户希望旅馆信息中包含评论数据。评论数据分为好评数量和差评数量。为了支持这种业务,在ES中可以使用对象类型。和数组类型一样,对象类型也不用事先定义,在写入文档的时候ES会自动识别并转换为对象类型。

    下面将在hotel索引中添加一条记录,请求的DSL如下:

    1. POST /hotel/_doc/005
    2. {
    3. "title":"go旅馆",
    4. "city":"深圳",
    5. "price":200.0,
    6. "create_time":"20220803",
    7. "modify_time":"2022-08-03 15:00:00",
    8. "tag":["有车位","免费Wi-Fi"],
    9. "comment_info":{
    10. "properties":{
    11. "favourable_comment":20,
    12. "negative_comment":30
    13. }
    14. }
    15. }

    执行以上DSL后,索引hotel增加了一个字段comment_info,它有两个属性,分别是favourable_comment和negative_comment,二者的类型都是long。下面查看mapping进行验证:

    1. GET /hotel/_mapping
    2. {
    3. "hotel" : {
    4. "mappings" : {
    5. "properties" : {
    6. "city" : {
    7. "type" : "keyword"
    8. },
    9. "comment_count" : {
    10. "type" : "integer"
    11. },
    12. "comment_info" : {
    13. "properties" : {
    14. "properties" : {
    15. "properties" : {
    16. "favourable_comment" : {
    17. "type" : "long"
    18. },
    19. "negative_comment" : {
    20. "type" : "long"
    21. }
    22. }
    23. }
    24. }
    25. },
    26. "create_time" : {
    27. "type" : "date"
    28. },
    29. "full_room" : {
    30. "type" : "boolean"
    31. },
    32. "modify_time" : {
    33. "type" : "date",
    34. "format" : "yyyy-MM-dd HH:mm:ss"
    35. },
    36. "price" : {
    37. "type" : "double"
    38. },
    39. "star" : {
    40. "type" : "byte"
    41. },
    42. "tag" : {
    43. "type" : "keyword"
    44. },
    45. "title" : {
    46. "type" : "text"
    47. }
    48. }
    49. }
    50. }
    51. }

    根据对象类型中的属性进行搜索,可以直接用“。”操作符进行指向。例如,搜索hotel索引中好评数大于10的文档,请求的DSL如下:

    1. GET /hotel/_search
    2. {
    3. "query": {
    4. "range": {
    5. "comment_info.properties.favourable_comment": {
    6. "gt": 10
    7. }
    8. }
    9. }
    10. }

    当然,对象内部还可以包含对象。例如,评论信息字段comment_info可以增加前3条好评数据,请求的DSL如下:

    1. POST /hotel/_doc/006
    2. {
    3. "title":"C++旅馆",
    4. "city":"深圳",
    5. "price":200.0,
    6. "create_time":"20220803",
    7. "modify_time":"2022-08-03 15:00:00",
    8. "tag":["有车位","免费Wi-Fi"],
    9. "comment_info":{
    10. "properties":{
    11. "favourable_comment":20,
    12. "negative_comment":30,
    13. "top3_favourable_comment":{
    14. "top1":{
    15. "content":"干净的旅馆",
    16. "score":90
    17. },
    18. "top2":{
    19. "content":"整洁的旅馆",
    20. "score":89
    21. },
    22. "top3":{
    23. "content":"服务好的旅馆",
    24. "score":88
    25. }
    26. }
    27. }
    28. }
    29. }

    以上请求,对文档的comment_info字段增加了前3条评论的内容和评分数据。

    地理类型

    在移动互联网时代,用户借助移动设备产生的消费也越来越多。例如,用户需要根据某个地理位置来搜索旅馆,此时可以把旅馆的经纬度数据设置为地理数据类型。该类型的定义需要在mapping中指定目标字段的数据类型为geo_point类型,示例如下:

    1. PUT /hotel/_mapping
    2. {
    3. "properties":{
    4. "location":{
    5. "type":"geo_point"
    6. }
    7. }
    8. }

    其中,location字段定义为地理类型,现在向索引中写入一条旅馆文档,DSL如下:

    1. POST /hotel/_doc/007
    2. {
    3. "title":"C旅馆",
    4. "city":"北京",
    5. "price":200.0,
    6. "create_time":"20220803",
    7. "modify_time":"2022-08-03 15:00:00",
    8. "tag":["有车位","免费Wi-Fi"],
    9. "location":{
    10. "lat":40.012312,
    11. "lon":116.497122
    12. }
    13. }

    动态映射

            当字段没有定义时,ES可以根据写入的数据自动定义该字段的类型,这种机制叫作动态映射。在介绍数组类型和对象类型时提到,这两种类型都不需要用户提前定义,ES将根据写入的数据自动创建mapping中对应的字段并指定类型。对于基本类型,如果字段没有定义,ES在将数据存储到索引时会进行自动映射,下表为自动映射时的JSON类型和索引数据类型的对应关系:

    JSON类型索引类型
    null不新增字段
    true或falseboolean
    integerlong
    objectobject(对象)
    array根据数组中的第一个非空值进行判断
    stringdate、double、long、text,根据数据形式进行判断

            在一般情况下,如果使用基本类型数据,最好先把数据类型定义好,因为ES的动态映射生成的字段类型可能会与用户的预期有差别。

            例如,写入数据时,由于ES对于未定义的字段没有类型约束,如果同一字段的数据形式不同(有的是字符型,有的是数值型),则ES动态映射生成的字段类型和用户的预期可能会有偏差。

            提前定义好数据类型并将索引创建语句纳入SVN或Git管理范围是良好的编程习惯,同时还能增强项目代码的连贯性和可读性。

    多字段

            针对同一个字段,有时需要不同的数据类型,这通常表现在为了不同的目的以不同的方式索引相同的字段。例如,在订单搜索系统中,既希望能够按照用户姓名进行搜索,又希望按照姓氏进行排列,可以在mapping定义中将姓名字段先后定义为text类型和keyword类型,其中,keyword类型的字段叫作子字段,这样ES在建立索引时会将姓名字段建立两份索引,即text类型的索引和keyword类型的索引。订单搜索索引的定义如下:

    1. PUT /order
    2. {
    3. "mappings": {
    4. "properties": {
    5. "order_id":{"type":"keyword"},
    6. "user_id":{"type":"keyword"},
    7. "user_name":{
    8. "type":"text",
    9. "fields": {
    10. "user_name_keyword":{
    11. "type":"keyword"
    12. }
    13. }
    14. },
    15. "hotel_id":{
    16. "type":"keyword"
    17. }
    18. }
    19. }
    20. }

    可以看出,正常定义user_name字段之后,使用fields定义其子字段的定义方式和普通字段的定义方式相同。

    下面写入数据:

    1. POST /_bulk
    2. {"index":{"_index":"order","_id":"001"}}
    3. {"order_id":"001","user_id":"user_001","user_name":"zhang san","hotel_id":"001"}
    4. {"index":{"_index":"order","_id":"002"}}
    5. {"order_id":"002","user_id":"user_002","user_name":"li si","hotel_id":"002"}
    6. {"index":{"_index":"order","_id":"003"}}
    7. {"order_id":"003","user_id":"user_003","user_name":"wang wu","hotel_id":"003"}

    可以在普通搜索中使用user_name字段,DSL如下:

    1. GET /order/_search
    2. {
    3. "query": {
    4. "match": {
    5. "user_name": "zhang"
    6. }
    7. },
    8. "sort": [
    9. {
    10. "user_name.user_name_keyword": {
    11. "order": "asc"
    12. }
    13. }
    14. ]
    15. }

    以上搜索zhang之后,命中的文档排序时是按照用户姓名的全称进行排序的。

  • 相关阅读:
    Django auth 应用模块
    20.ROS编程学习:通信的各种进阶使用python
    maven.类包冲突解决案例
    spring配置双数据源
    分类预测 | MATLAB实现SSA-FS-SVM麻雀算法同步优化特征选择结合支持向量机分类预测
    MySQL——日志
    微服务和注册中心
    【毕业设计】机器学习驾驶疲劳检测系统 - python
    可以直接调用 Thread 类的 run 方法吗?
    ElementUI浅尝辄止27:Steps 步骤条
  • 原文地址:https://blog.csdn.net/ntzzzsj/article/details/126130206