在使用数据之前,需要构建数据的组织结构。这种组织结构在关系型数据库中叫作表结构,在ES中叫作映射。
作为无模式搜索引擎,ES可以在数据写入时猜测数据类型,从而自动创建映射。但有时ES创建的映射中的数据类型和目标类型可能不一致。当需要严格控制数据类型时,还是需要用户手动创建映射。
在ES中写入文档请求的类型是GET,其请求形式如下:
GET /${index_name}/_mapping
比如,查看hotel_1的mappings,请求的DSL如下:
GET /hotel_1/_mapping
返回结果如下:
- {
- "hotel_1" : {
- "mappings" : {
- "properties" : {
- "city" : {
- "type" : "keyword"
- },
- "price" : {
- "type" : "double"
- },
- "title" : {
- "type" : "text"
- }
- }
- }
- }
- }
映射中的字段类型是不可以修改的,但是字段可以扩展。最常见的扩展方式是增加字段和为object(对象)类型的数据新增属性。下面的DSL示例为扩展hotel_1索引,并增加tag字段。
- POST /hotel_1/_mapping
- {
- "properties": {
- "tag": {
- "type":"keyword"
- }
- }
- }
查看索引hotel_1的mappings,返回结果如下:
- {
- "hotel_1" : {
- "mappings" : {
- "properties" : {
- "city" : {
- "type" : "keyword"
- },
- "price" : {
- "type" : "double"
- },
- "tag" : {
- "type" : "keyword"
- },
- "title" : {
- "type" : "text"
- }
- }
- }
- }
- }
由返回结果可知,tag字段已经被添加到索引hotel_1中。
keyword类型是不进行切分的字符串类型。这里的“不进行切分”指的是:在索引时,对keyword类型的数据不进行切分,直接构建倒排索引;在搜索时,对该类型的查询字符串不进行切分后的部分匹配。keyword类型数据一般用于对文档的过滤、排序和聚合。
在现实场景中,keyword经常用于描述姓名、产品类型、用户ID、URL和状态码等。keyword类型数据一般用于比较字符串是否相等,不对数据进行部分匹配,因此一般查询这种类型的数据时使用term查询。
例如,建立一个人名索引,可以设定姓名字段为keyword字段:
- PUT /user
- {
- "mappings": {
- "properties": {
- "user_name":{"type": "keyword"}
- }
- }
- }
写入一条数据,请求的DSL如下:
- POST /user/_doc/001
- {
- "user_name":"张三"
- }
查询刚刚写入的数据,请求的DSL如下:
- GET /user/_search
- {
- "query": {
- "term": {
- "user_name": {
- "value": "张三"
- }
- }
- }
- }
返回的结果信息如下:
- {
- "took" : 368,
- "timed_out" : false,
- "_shards" : {
- "total" : 1,
- "successful" : 1,
- "skipped" : 0,
- "failed" : 0
- },
- "hits" : {
- "total" : {
- "value" : 1,
- "relation" : "eq"
- },
- "max_score" : 0.2876821,
- "hits" : [
- {
- "_index" : "user",
- "_type" : "_doc",
- "_id" : "001",
- "_score" : 0.2876821,
- "_source" : {
- "user_name" : "张三"
- }
- }
- ]
- }
- }
由搜索结果可以看出,使用term进行全字符串匹配“张三”可以搜索到命中文档。下面的DSL使用match搜索姓名中带有“张”的记录:
- GET /user/_search
- {
- "query": {
- "match": {
- "user_name": "张"
- }
- }
- }
返回结果如下:
- {
- "took" : 1,
- "timed_out" : false,
- "_shards" : {
- "total" : 1,
- "successful" : 1,
- "skipped" : 0,
- "failed" : 0
- },
- "hits" : {
- "total" : {
- "value" : 0,
- "relation" : "eq"
- },
- "max_score" : null,
- "hits" : [ ]
- }
- }
由搜索结果可见,对keyword类型使用match搜索进行匹配是不会命中文档的。
text类型是可进行切分的字符串类型。这里的“可切分”指的是:在索引时,可按照相应的切词算法对文本内容进行切分,然后构建倒排索引;在搜索时,对该类型的查询字符串按照用户的切词算法进行切分,然后对切分后的部分匹配打分。
例如,一个旅馆搜索项目,我们希望可以根据旅馆名称即title字段进行模糊匹配,因此可以设定title字段为text字段,建立旅馆索引的DSL如下:
- PUT /hotel
- {
- "mappings": {
- "properties": {
- "title":{"type": "text"},
- "city":{"type": "keyword"},
- "price":{"type": "double"}
- }
- }
- }
写入一条数据:
- POST /hotel/_doc/001
- {
- "title":"java旅馆",
- "city":"深圳",
- "price":50.00
- }
按照普通的term进行搜索,观察能否搜索到刚刚写入的文档,请求的DSL如下:
- GET /hotel/_search
- {
- "query": {
- "term": {
- "title": {
- "value": "java旅馆"
- }
- }
- }
- }
返回结果如下:
- {
- "took" : 0,
- "timed_out" : false,
- "_shards" : {
- "total" : 1,
- "successful" : 1,
- "skipped" : 0,
- "failed" : 0
- },
- "hits" : {
- "total" : {
- "value" : 0,
- "relation" : "eq"
- },
- "max_score" : null,
- "hits" : [ ]
- }
- }
根据返回结果可知,上面的请求并没有搜索到文档。term搜索用于搜索值和文档对应的字段是否完全相等,而对于text类型的数据,在建立索引时ES已经进行了切分并建立了倒排索引,因此使用term没有搜索到数据。一般情况下,搜索text类型的数据时应使用match搜索。比如以下:
- GET /hotel/_search
- {
- "query": {
- "match": {
- "title": "java"
- }
- }
- }
返回结果如下:
- {
- "took" : 1,
- "timed_out" : false,
- "_shards" : {
- "total" : 1,
- "successful" : 1,
- "skipped" : 0,
- "failed" : 0
- },
- "hits" : {
- "total" : {
- "value" : 1,
- "relation" : "eq"
- },
- "max_score" : 0.2876821,
- "hits" : [
- {
- "_index" : "hotel",
- "_type" : "_doc",
- "_id" : "001",
- "_score" : 0.2876821,
- "_source" : {
- "title" : "java旅馆",
- "city" : "深圳",
- "price" : 50.0
- }
- }
- ]
- }
- }
ES支持的数值类型有long、integer、short、byte、double、float、half_float、scaled_float和unsigned_long等。各类型所表达的数值范围可以参考官方文档,网址为Numeric field types | Elasticsearch Guide [8.3] | Elastic 为节约存储空间并提升搜索和索引的效率,在实际应用中,在满足需求的情况下应尽可能选择范围小的数据类型。比如,年龄字段的取值最大值不会超过200,因此选择byte类型即可。数值类型的数据也可用于对文档进行过滤、排序和聚合。
以旅馆搜索为例,旅馆的索引除了包含旅馆名称和城市之外,还需要定义价格、星级和评论数等,创建索引的DSL如下:
- PUT /hotel
- {
- "mappings": {
- "properties": {
- "title":{"type": "text"},
- "city":{"type": "keyword"},
- "price":{"type": "double"},
- "star":{"type":"byte"},
- "comment_count":{"type":"integer"}
- }
- }
- }
对于数值型数据,一般使用term搜索或者范围搜索。例如,搜索价格为350~400(包含350和400)元的旅馆,搜索的DSL如下:
- GET /hotel/_search
- {
- "query": {
- "range": {
- "price": {
- "gte": 200,
- "lte": 300
- }
- }
- }
- }
布尔类型使用boolean定义,用于业务中的二值表示,如商品是否售罄,房屋是否已租,旅馆房间是否满房等。写入或者查询该类型的数据时,其值可以使用true和false,或者使用字符串形式的"true"和"false"。下面的DSL定义索引中增加“是否满房”的字段为布尔类型:
- PUT /hotel/_mapping
- {
- "properties":{
- "full_room":{"type":"boolean"}
- }
- }
下面的DSL将查询满房的旅馆:
- GET /hotel/_search
- {
- "query": {
- "term": {
- "full_room": {
- "value": "true"
- }
- }
- }
- }
在ES中,日期类型的名称为date。ES中存储的日期是标准的UTC格式。下面定义索引hotel,该索引增加一个create_time字段,现在把它定义成date类型。增加date类型请求的DSL如下:
- PUT /hotel/_mapping
- {
- "properties":{
- "create_time":{"type":"date"}
- }
- }
一般使用如下形式表示日期类型数据:
日期类型的默认格式为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如下:
- POST /hotel/_doc/001
- {
- "title":"java旅馆",
- "city":"深圳",
- "price":200.0,
- "create_time":"20220803"
- }
搜索日期型数据时,一般使用ranges查询。例如,按创建日期搜索旅馆,请求的DSL如下:
- GET /hotel/_search
- {
- "query": {
- "range": {
- "create_time": {
- "gte": "20220801",
- "lte": "20220803"
- }
- }
- }
- }
日期类型默认不支持yyyy-MM-dd HH:mm:ss格式,如果经常使用这种格式,可以在索引的mapping中设置日期字段的format属性为自定义格式。下面的示例将新增modify_time字段的格式为yyyy-MM-dd HH:mm:ss:
- PUT /hotel/_mapping
- {
- "properties":{
- "modify_time":{
- "type":"date",
- "format":"yyyy-MM-dd HH:mm:ss"
- }
- }
- }
此时如果写入以下数据:
- POST /hotel/_doc/002
- {
- "title":"python旅馆",
- "city":"深圳",
- "price":200.0,
- "create_time":"20220803",
- "modify_time":"20220803"
- }
此时系统返回:
- {
- "error" : {
- "root_cause" : [
- {
- "type" : "mapper_parsing_exception",
- "reason" : "failed to parse field [modify_time] of type [date] in document with id '002'. Preview of field's value: '20220803'"
- }
- ],
- "type" : "mapper_parsing_exception",
- "reason" : "failed to parse field [modify_time] of type [date] in document with id '002'. Preview of field's value: '20220803'",
- "caused_by" : {
- "type" : "illegal_argument_exception",
- "reason" : "failed to parse date field [20220803] with format [yyyy-MM-dd HH:mm:ss]",
- "caused_by" : {
- "type" : "date_time_parse_exception",
- "reason" : "Text '20220803' could not be parsed at index 0"
- }
- }
- },
- "status" : 400
- }
根据错误信息可知,错误的原因是写入的数据格式和定义的数据格式不同。此时需要写入的格式为yyyy-MM-dd HH:mm:ss的文档,请求的DSL如下:
- POST /hotel/_doc/002
- {
- "title":"python旅馆",
- "city":"深圳",
- "price":200.0,
- "create_time":"20220803",
- "modify_time":"2022-08-03 15:00:00"
- }
ES数组没有定义方式,其使用方式是开箱即用的,即无须事先声明,在写入时把数据用中括号[]括起来,由ES对该字段完成定义。
当然,如果事先已经定义了字段类型,在写数据时以数组形式写入,ES也会将该类型转为数组。例如,为hotel索引增加一个标签字段,名称为tag,请求的DSL如下:
- PUT /hotel/_mapping
- {
- "properties":{
- "tag":{
- "type":"keyword"
- }
- }
- }
查看一下索引hotel的mapping:
- {
- "hotel" : {
- "mappings" : {
- "properties" : {
- "city" : {
- "type" : "keyword"
- },
- "comment_count" : {
- "type" : "integer"
- },
- "create_time" : {
- "type" : "date"
- },
- "full_room" : {
- "type" : "boolean"
- },
- "modify_time" : {
- "type" : "date",
- "format" : "yyyy-MM-dd HH:mm:ss"
- },
- "price" : {
- "type" : "double"
- },
- "star" : {
- "type" : "byte"
- },
- "tag" : {
- "type" : "keyword"
- },
- "title" : {
- "type" : "text"
- }
- }
- }
- }
- }
通过返回的mapping信息来看,新增的tag字段与普通的keyword类型字段没什么区别,现在写入一条数据:
- POST /hotel/_doc/003
- {
- "title":"go旅馆",
- "city":"深圳",
- "price":200.0,
- "create_time":"20220803",
- "modify_time":"2022-08-03 15:00:00",
- "tag":["有车位","免费Wi-Fi"]
- }
查看一下写入的数据,ES返回的信息如下:
- GET /hotel/_doc/003
- {
- "_index" : "hotel",
- "_type" : "_doc",
- "_id" : "003",
- "_version" : 1,
- "_seq_no" : 2,
- "_primary_term" : 1,
- "found" : true,
- "_source" : {
- "title" : "go旅馆",
- "city" : "深圳",
- "price" : 200.0,
- "create_time" : "20220803",
- "modify_time" : "2022-08-03 15:00:00",
- "tag" : [
- "有车位",
- "免费Wi-Fi"
- ]
- }
- }
通过以上信息可以看到,写入的数据的tag字段已经是数组类型了.
数组类型的字段适用于元素类型的搜索方式,也就是说,数组元素适用于什么搜索,数组字段就适用于什么搜索。例如,在上面的示例中,数组元素类型是keyword,该类型可以适用于term搜索,则tag字段也可以适用于term搜索,搜索的DSL如下:
- GET /hotel/_search
- {
- "query": {
- "term": {
- "tag": {
- "value": "免费Wi-Fi"
- }
- }
- }
- }
ES中的空数组可以作为missing field,即没有值的字段,下面的DSL将插入一条tag为空的数组:
- POST /hotel/_doc/004
- {
- "title":"go旅馆",
- "city":"深圳",
- "price":200.0,
- "create_time":"20220803",
- "modify_time":"2022-08-03 15:00:00",
- "tag":[]
- }
在实际业务中,一个文档需要包含其他内部对象。例如,在旅馆搜索需求中,用户希望旅馆信息中包含评论数据。评论数据分为好评数量和差评数量。为了支持这种业务,在ES中可以使用对象类型。和数组类型一样,对象类型也不用事先定义,在写入文档的时候ES会自动识别并转换为对象类型。
下面将在hotel索引中添加一条记录,请求的DSL如下:
- POST /hotel/_doc/005
- {
- "title":"go旅馆",
- "city":"深圳",
- "price":200.0,
- "create_time":"20220803",
- "modify_time":"2022-08-03 15:00:00",
- "tag":["有车位","免费Wi-Fi"],
- "comment_info":{
- "properties":{
- "favourable_comment":20,
- "negative_comment":30
- }
- }
- }
执行以上DSL后,索引hotel增加了一个字段comment_info,它有两个属性,分别是favourable_comment和negative_comment,二者的类型都是long。下面查看mapping进行验证:
- GET /hotel/_mapping
- {
- "hotel" : {
- "mappings" : {
- "properties" : {
- "city" : {
- "type" : "keyword"
- },
- "comment_count" : {
- "type" : "integer"
- },
- "comment_info" : {
- "properties" : {
- "properties" : {
- "properties" : {
- "favourable_comment" : {
- "type" : "long"
- },
- "negative_comment" : {
- "type" : "long"
- }
- }
- }
- }
- },
- "create_time" : {
- "type" : "date"
- },
- "full_room" : {
- "type" : "boolean"
- },
- "modify_time" : {
- "type" : "date",
- "format" : "yyyy-MM-dd HH:mm:ss"
- },
- "price" : {
- "type" : "double"
- },
- "star" : {
- "type" : "byte"
- },
- "tag" : {
- "type" : "keyword"
- },
- "title" : {
- "type" : "text"
- }
- }
- }
- }
- }
根据对象类型中的属性进行搜索,可以直接用“。”操作符进行指向。例如,搜索hotel索引中好评数大于10的文档,请求的DSL如下:
- GET /hotel/_search
- {
- "query": {
- "range": {
- "comment_info.properties.favourable_comment": {
- "gt": 10
- }
- }
- }
- }
当然,对象内部还可以包含对象。例如,评论信息字段comment_info可以增加前3条好评数据,请求的DSL如下:
- POST /hotel/_doc/006
- {
- "title":"C++旅馆",
- "city":"深圳",
- "price":200.0,
- "create_time":"20220803",
- "modify_time":"2022-08-03 15:00:00",
- "tag":["有车位","免费Wi-Fi"],
- "comment_info":{
- "properties":{
- "favourable_comment":20,
- "negative_comment":30,
- "top3_favourable_comment":{
- "top1":{
- "content":"干净的旅馆",
- "score":90
- },
- "top2":{
- "content":"整洁的旅馆",
- "score":89
- },
- "top3":{
- "content":"服务好的旅馆",
- "score":88
- }
- }
- }
- }
- }
以上请求,对文档的comment_info字段增加了前3条评论的内容和评分数据。
在移动互联网时代,用户借助移动设备产生的消费也越来越多。例如,用户需要根据某个地理位置来搜索旅馆,此时可以把旅馆的经纬度数据设置为地理数据类型。该类型的定义需要在mapping中指定目标字段的数据类型为geo_point类型,示例如下:
- PUT /hotel/_mapping
- {
- "properties":{
- "location":{
- "type":"geo_point"
- }
- }
- }
其中,location字段定义为地理类型,现在向索引中写入一条旅馆文档,DSL如下:
- POST /hotel/_doc/007
- {
- "title":"C旅馆",
- "city":"北京",
- "price":200.0,
- "create_time":"20220803",
- "modify_time":"2022-08-03 15:00:00",
- "tag":["有车位","免费Wi-Fi"],
- "location":{
- "lat":40.012312,
- "lon":116.497122
- }
- }
当字段没有定义时,ES可以根据写入的数据自动定义该字段的类型,这种机制叫作动态映射。在介绍数组类型和对象类型时提到,这两种类型都不需要用户提前定义,ES将根据写入的数据自动创建mapping中对应的字段并指定类型。对于基本类型,如果字段没有定义,ES在将数据存储到索引时会进行自动映射,下表为自动映射时的JSON类型和索引数据类型的对应关系:
JSON类型 | 索引类型 |
null | 不新增字段 |
true或false | boolean |
integer | long |
object | object(对象) |
array | 根据数组中的第一个非空值进行判断 |
string | date、double、long、text,根据数据形式进行判断 |
在一般情况下,如果使用基本类型数据,最好先把数据类型定义好,因为ES的动态映射生成的字段类型可能会与用户的预期有差别。
例如,写入数据时,由于ES对于未定义的字段没有类型约束,如果同一字段的数据形式不同(有的是字符型,有的是数值型),则ES动态映射生成的字段类型和用户的预期可能会有偏差。
提前定义好数据类型并将索引创建语句纳入SVN或Git管理范围是良好的编程习惯,同时还能增强项目代码的连贯性和可读性。
针对同一个字段,有时需要不同的数据类型,这通常表现在为了不同的目的以不同的方式索引相同的字段。例如,在订单搜索系统中,既希望能够按照用户姓名进行搜索,又希望按照姓氏进行排列,可以在mapping定义中将姓名字段先后定义为text类型和keyword类型,其中,keyword类型的字段叫作子字段,这样ES在建立索引时会将姓名字段建立两份索引,即text类型的索引和keyword类型的索引。订单搜索索引的定义如下:
- PUT /order
- {
- "mappings": {
- "properties": {
- "order_id":{"type":"keyword"},
- "user_id":{"type":"keyword"},
- "user_name":{
- "type":"text",
- "fields": {
- "user_name_keyword":{
- "type":"keyword"
- }
- }
- },
- "hotel_id":{
- "type":"keyword"
- }
- }
- }
- }
可以看出,正常定义user_name字段之后,使用fields定义其子字段的定义方式和普通字段的定义方式相同。
下面写入数据:
- POST /_bulk
- {"index":{"_index":"order","_id":"001"}}
- {"order_id":"001","user_id":"user_001","user_name":"zhang san","hotel_id":"001"}
- {"index":{"_index":"order","_id":"002"}}
- {"order_id":"002","user_id":"user_002","user_name":"li si","hotel_id":"002"}
- {"index":{"_index":"order","_id":"003"}}
- {"order_id":"003","user_id":"user_003","user_name":"wang wu","hotel_id":"003"}
可以在普通搜索中使用user_name字段,DSL如下:
- GET /order/_search
- {
- "query": {
- "match": {
- "user_name": "zhang"
- }
- },
- "sort": [
- {
- "user_name.user_name_keyword": {
- "order": "asc"
- }
- }
- ]
- }
以上搜索zhang之后,命中的文档排序时是按照用户姓名的全称进行排序的。