《ElasticStack从入门到实践》学习笔记8

八、ElasticSearch的数据建模

    1.数据建模简介

        英文为Data Modeling,即创建数据模型的过程。

        数据模型:是对现实世界进行抽象描述的一种工具和方法,通过抽象的实体及实体间联系的形式,去描述业务规则。从而实现对现实世界的映射。其建模过程如下:

        1)概念模型(10%)。确定系统的核心需求和范围边界,实际实体与实体之间的关系。        基础。

        2)逻辑模型(60-70%)。进一步梳理业务需求,确定每个实体间的属性,关系和约束等。        核心。

        3)物理模型(20-30%)。结合具体的数据库产品,在满足业务读写性能等需求的前提下确定最终的定义。        落地实现。

    2.ES中的数据建模

        1)ES是基于Luence以倒排索引为基础实现的存储体系,不遵循关系型数据库中的范式约定。

《ElasticStack从入门到实践》学习笔记8_第1张图片

        2)Mapping字段相关设置

            A、enabled。            true/false。仅存储,不做搜索或聚合分析是设为false。

            B、index。                 true/false。是否构建倒排索引。不需进行字段的检索的时候设为false。

            C、index_options。  docs/freqs/positions/offsets。确定存储倒排索引的哪些信息。

            D、norms。               true/false。是否存储归一化相关系数,若字段仅用于过滤和聚合分析,则可关闭。

            E、doc_values。        true/false。是否启用doc_values,用于排序和聚类分析。默认开启。

            F、field_data。          true/false。是否设text类型为fielddata,实现排序和聚合分析。默认关闭。

            G、store。                 false/true。是否存储该字段。

            H、coerce。              true/false。 是否开启数值类型转换功能,如:字符串转数字等。

            I、multifields。          多字段。灵活使用多字段特性来解决多样业务需求。

            J、dynamic。            true/false/strict。控制mapping自动更新。

            K、date_detection。 true/false。是否启用自定识别日期类型,一般设为false,避免不必要的识别字符串中的日期。

        3)Mapping字段属性设定流程

《ElasticStack从入门到实践》学习笔记8_第2张图片

        A、是何种类型?

            a、字符串类型。

                需要分词,则设为text,否则设为keyword

            b、枚举类型。

                基于性能考虑,设为keyword,即便该数据为整型。

            c、数值类型。

                尽量选择贴近的类型,如byte即可表示所有数值时,即用byte,而不是所有都用long。

            d、其他类型。

                如:布尔型,日期类型,地理位置类型等。

        B、是否需要检索?

            a、完全不需要检索、排序、聚合分析的字段。

                enabled设为false。

            b、不需检索的字段。

                index设为false。

            c、需检索的字段,可通过如下配置设定需要的存储粒度。

                index_options 结合需要设定。

                norms      不需归一化数据时可关闭。

        C、是否需要排序和聚合分析?

            当不需要排序和聚合分析功能时:

            doc_values设为false。

            field_data设为false。

        D、是否需要另行存储?

            store设为true。即可存储该字段的原始内容(且与_source无关),一般结合_source的enabled设为false时使用。

    3.一个ES建模的实例

        针对博客文章设定索引blog_index,包含字段:

        标题:title;

        发布日期:publish_data;

        作者:author;

        摘要:abstract;

        网址:url。

        1)一个简易的数据模型:

#简易模型blog_index
PUT blog_index
{
 "mappings":{
  "doc":{
   "properties":{
    "title":{
     "type":"text",    #title设为text,包含自字段keyword。支持检索、排序、聚合分析
     "fields":{
      "keyword":{
       "type":"keyword"
      }
     }
    },
    "publish_data":{    #publish_data设为date,支持检索、排序、聚合分析
     "type":"date"
    },
    "author":{
     "type":"keyword"    #author设为keyword,支持检索、排序、聚合分析
    },
    "abstract":{
     "type":"text"    #abstract设为text,支持检索、排序、聚合分析
    },
    "url":{
     "enabled":false    #url设为date,不需进行检索
    }
   }
  }
 }
}

        2)如果在blog_index中加入一个内容字段content:

#为blog_index增加content字段
PUT blog_index
{
 "mappings":{
  "doc":{
   "_source":{
    "enabled":false
   },
   "properties":{
    "title":{
     "type":"text",    #title设为text,包含自字段keyword。支持检索、排序、聚合分析
     "fields":{
      "keyword":{
       "type":"keyword"
      }
     },
     "store":true    #对数据进行存储
    },
    "publish_data":{    #publish_data设为date,支持检索、排序、聚合分析
     "type":"date",
     "store":true    #对数据进行存储
    },
    "author":{
     "type":"keyword",    #author设为keyword,支持检索、排序、聚合分析
     "store":true    #对数据进行存储
    },
    "abstract":{
     "type":"text",    #abstract设为text,支持检索、排序、聚合分析
     "store":true    #对数据进行存储
    },
    "content":{
     "type":"text",    #content设为text,支持检索、排序、聚合分析
     "store":true    #对数据进行存储
    },
    "url":{
     "type":"keyword",    #url设为keyword
     "doc_values":false,    #url不支持排序和聚合分析
     "norms":false,    #url也不需要归一化数据
     "ignore_above":100,    #预设内容长度为100
     "store":true    #对数据进行存储
    }
   }
  }
 }
}

        3)在搜索时增加高亮:

            在此时,content里面的数据会存储大量的内容数据,数据量可能达到上千、上万,甚至几十万。那么在搜索的时候,根据search机制,如果还是像之前一样进行_search搜索,并只显示其他字段的话,其实依然还是每次获取了content字段的内容,影响性能,所以,使用stored_fields参数,控制返回的字段。节省了大量资源:

#使用stored_fields返回指定的存储后的字段
GET blog_index/_search
{
 "stored_fields":["title","publish_data","author","Abstract","url"],
 "query":{
  "match":{
   "content":"world"    #依然进行content搜索,但是不返回所有的content字段
  }
 },
 "highlight":{    #针对content字段进行高亮显示
  "fields":{
   "content":{}
  }
 }
}

            注意:GET blog_index/_search?_source=title 虽然只显示了title,但是search机制决定了,会把所有_source内容获取到,但只是显示title。

    4.ES中关联关系处理

        ES不擅长处理关系型数据库中的关联关系,因为底层使用的倒排索引,如:文章表blog和评论表comment之间通过blog_id关联。在ES中可以通过两种方式变相解决:

        ①Nested Object;

        ②Parent/Child。

        现在新增一个评论索引:comment,包含:文章ID:blog_id、评论人:username、评论日期:date、评论内容:content。

        1)使用Nested Object:

#使用Nested Object处理关联关系
Blog1
{
 "title":"Blog Number One",
 "author":"alfred",
 "comments":[
  {
   "username":"lee",
   "date":"2017-01-02",
   "content":"awesome acticle!"
  },
  {
   "username":"fax",
   "date":"2018-04-02",
   "content":"thanks!"
  }
 ]
}

        查询:用户lee评论了thanks的blog文档:

#查询:用户lee评论了thanks的blog文档
GET blog_index/_search
{
 "query":{
  "bool":{
   "must":[
    {
     "match":{
      "comments.username":"lee"
     }
    },
    {
     "match":{
      "comments.content":"thanks"
     }
    }
   ]
  }
 }
}

        返回结果会直接返回包含lee和包含thanks的文档,而不是同时包含lee和thanks的文档。此时,comments默认是Object Array,存储结构类似于以下形式:

#存储结构1:
{
 "title":"Blog Number One",
 "author":"alfred",
 "comments.username":[
  "lee",
  "fax"
 ],
 "comments.date":[                     #此时结构为一个Object
  "2017-01-02",
  "2017-04-02"
 ],
 "comments.content":[
 "awesome article!",
 "thanks!"
 ]
}

        使用Nested Object可以解决这个问题:

#使用Nested Object
PUT blog_index_nested
{
 "mappings":{
  "doc":{
   "properties":{
    "title":{
     "type":"text",
     "fields":{
      "keyword":{
       "type":"keyword",
       "ignore_above":100
      }
     }
    },
    "publish_date":{
     "type":"date"
    },
    "author":{
     "type":"keyword",
     "ignore_above":100
    },
    "abstract":{
     "type":"text"
    },
    "url":{
     "enabled":false
    },
    "comments":{    #将comments放在blog_index的内部,作为一个字段进行存储
     "type":"nested",    #此时设置内部的comments的类型为nested
     "properties":{
      "username":{
       "type":"keyword",
       "ignore_above":100
      },
      "date":{
       "type":"date"
      },
      "content":{
       "type":"text"
      }
     }
    }
   }
  }
 }
}

        那么在进行查询的时候,使用nested关键词:

#使用nested关键词的查询
GET blog_index/_search
{
 "query":{
  "nested":{    #使用nested关键词
   "path":"comments",    #与之前的查询的不同点
   "query":{
    "bool":{
     "must":[
      {
       "match":{
        "comments.username":"lee"
       }
      },
      {
       "match":{
        "comments.content":"thanks"
       }
      }
     ]
    }
   } 
  }
 }
}

        返回结果会直接返回同时包含lee和thanks的文档,而不是包含lee和包含thanks的文档。此时,comments是Nested Object Array,存储结构类似于以下形式:

#Nested Object Array存储结构:
{
 title","Blog Number One",
 "authoe":"alfred"
}
{
 "comments.username":"lee",
 "comments.date":"2017-01-02",
 "comments.content":"awesome article!"    #此时Nested Object独立存在
}
{
 "comments.username":"fax",
 "comments.date":"2017-04-02",
 "comments.content":"thanks!"             #两个Object
}

        2)使用Parent/Child:

        ES还提供了类似于关系型数据库中join的实现的方式:使用数据类型join:

#使用Parent/Child实现类似join的方式:
PUT blog_index_parent_child
{
 "mappings":{
  "doc":{
   "properties":{
    "join":{    #字段名
     "type":"join",    #指明类型
     "relations":{    #指明父子类型
      "blog":"comment"   #前为父类型,后为子类型
     }
    }
   }
  }
 }
}
#创建父文档:
PUT blog_index_parent_child/doc/1
{
 "title":"blog",
 "join":"blog"    #指明父类型
}
#创建子文档:
PUT blog_index_parent_child/doc/comment-1?routing=1    #指明routing值,确保父子文档在一
{                                                      #个shard上,一般使用父文档的id
 "comment":"comment world",
 "join":{    #字段名
  "name":"comment", #指明子类型
  "parent":1    #指明父文档id
 }
}

        常见query语法包括以下几种:

            ①parent_id:返回某父文档的子文档;

            ②has_child:返回包含某子文档的父文档;

            ③has_parent:返回包含某父文档的子文档

        1)parent_id查询:

#返回某父文档的子文档
GET blog_index_parent_child/_search
{
 "query":{
  "parent_id":{
   "type":"comment",    #指定子文档类型
   "id":1        #指明父文档id
  }
 }
}

        2)has_child查询:

#返回包含某子文档的父文档
GET blog_index_parent_child/_search
{
 "query":{
  "has_child":{
   "type":"comment",    #指明子文档类型
   "query":{
    "match":{
     "comment":"world"    #指明查询条件
    }
   }
  }
 }
}

        3)has_parent查询:

#返回包含某父文档的子文档
GET blog_index_parent_child/_search
{
 "query":{
  "has_parent":{
   "parent_type":"blog",    #指明父文档类型
   "query":{
    "match":{
     "title":"blog"    #指明查询条件
    }
   }
  }
 }
}

            Nested Object 对比 Parent/Child:

对比 Nested Object

Parent/Child

优点 文档存储在一起,因此读写性能较高 父子文档可独立更新,二者互不影响
缺点 更新父子文档需要同时更新整个文档 为了维护join的关系,需占用部分内存,读写性能较差
使用场景 子文档偶尔更新,且查询频繁 子文档更新频繁

            建议:尽量使用Nested Object来解决问题,避免使用Parent/Child。

    5.ES中的reindex

        reindex:指重建所有数据的过程,一般发生在一下情况:

            ①mapping设置变更,如:字段类型变化,分词器字典更新等;

            ②index设置变更,如:分片数变化;

            ③迁移数据

        ES提供了线程的api用于完成数据重建:

        _update_by_query:在现有索引上重建;

        _reindex:在其他索引上重建。

#将blog_index中所有文档重建一遍:
POST blog_index/_update_by_query?conflicts=proceed    #如果遇到版本冲突,依然执行。
#此时如果blog_index中没有store的数据,则会报错

        1)使用_update_by_query,更新文档的字段值和部分文档:

#更新文档的字段值及部分文档
POST blog_index/_update_by_query
{
 "script":{    #更新文档的字段值
  "source":"ctx._source.likes++",    #代码
  "lang":"painless"    #ES自带script语法
 },
 "query":{    #更新部分文档
  "term":{
   "user":"tom"
  }
 }
}

        在reindex发起后进入的文档,不会参与重建,类似于快照的机制。因此:一般在文档不再发生变更时,进行文档的reindex。

        2)使用_reindex,重建数据:

#使用_reindex:
POST _reindex
{
 "source":{    #被重建索引
  "index":"blog_index"
 },
 "dest":{    #目标索引
  "index":"blog_new_index"
 }
} 

            数据重建时间,受到索引文档规模的影响,此时设定url参数wait_for_completionfalse,来异步执行。

            ES通过task来描述此类执行任务,并提供了task api来查看任务的执行进度和相关数据:

#使用task api
POST blog_index/_update_by_query?comflicts=proceed&wait_for_completion=false

#使用返回的taskid,查看任务的执行进度和相关数据
GET _tasks/<返回的task id>

    6、其他建议:

        1)对mapping进行版本管理:

            要么写文件/注释,加入到Git仓库,一眼可见;

            要么增加metadata字段,维护版本,并在每次更新mapping设置的时候加1。

"metadata":{
 "version":1
}

        2)防止字段过多:

            index.mapping.total_fields_limit,默认1000个。一般是因为没有高质量的数据建模导致,如:dynamic设为true。此时考虑查分多个索引来解决问题。

你可能感兴趣的:(大数据相关,ElasticStack,数据分析,搜索引擎)