Elasticsearch顶尖高手系列:高手进阶篇(二)

ELK 专栏收录该内容
6 篇文章 2 订阅

Elasticsearch顶尖高手系列:高手进阶篇(二)

第59-72节

59_数据建模_关系型与document类型数据模型对比

关系型数据库的数据模型

es的document数据模型

public class Department {
	
	private Integer deptId;
	private String name;
	private String desc;
	private List<Employee> employees;

}

public class Employee {
	
	private Integer empId;
	private String name;
	private Integer age;
	private String gender;
	private Department dept;

}

关系型数据库中

department表

dept_id
name
desc

employee表

emp_id
name
age
gender
dept_id

三范式 --> 将每个数据实体拆分为一个独立的数据表,同时使用主外键关联关系将多个数据表关联起来 --> 确保没有任何冗余的数据

一份数据,只会放在一个数据表中 --> dept name,部门名称,就只会放在department表中,不会在employee表中也放一个dept name,如果说你要查看某个员工的部门名称,那么必须通过员工表中的外键,dept_id,找到在部门表中对应的记录,然后找到部门名称

es文档数据模型

{
	"deptId": "1",
	"name": "研发部门",
	"desc": "负责公司的所有研发项目",
	"employees": [
		{
			"empId": "1",
			"name": "张三",
			"age": 28,
			"gender": "男"
		},
		{
			"empId": "2",
			"name": "王兰",
			"age": 25,
			"gender": "女"
		},
		{
			"empId": "3",
			"name": "李四",
			"age": 34,
			"gender": "男"
		}
	]
}

es,更加类似于面向对象的数据模型,将所有由关联关系的数据,放在一个doc json类型数据中,整个数据的关系,还有完整的数据,都放在了一起

60_数据建模_通过应用层join实现用户与博客的关联

1、构造用户与博客数据

在构造数据模型的时候,还是将有关联关系的数据,然后分割为不同的实体,类似于关系型数据库中的模型

案例背景:博客网站, 我们会模拟各种用户发表各种博客,然后针对用户和博客之间的关系进行数据建模,同时针对建模好的数据执行各种搜索/聚合的操作

PUT /website/users/1 
{
  "name":     "小鱼儿",
  "email":    "xiaoyuer@sina.com",
  "birthday":      "1980-01-01"
}

PUT /website/blogs/1
{
  "title":    "我的第一篇博客",
  "content":     "这是我的第一篇博客,开通啦!!!"
  "userId":     1 
}

一个用户对应多个博客,一对多的关系,做了建模

建模方式,分割实体,类似三范式的方式,用主外键关联关系,将多个实体关联起来

2、搜索小鱼儿发表的所有博客

GET /website/users/_search 
{
  "query": {
    "term": {
      "name.keyword": {
        "value": "小鱼儿"
      }
    }
  }
}
{
  "took": 91,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "failed": 0
  },
  "hits": {
    "total": 1,
    "max_score": 0.2876821,
    "hits": [
      {
        "_index": "website",
        "_type": "users",
        "_id": "1",
        "_score": 0.2876821,
        "_source": {
          "name": "小鱼儿",
          "email": "xiaoyuer@sina.com",
          "birthday": "1980-01-01"
        }
      }
    ]
  }
}

比如这里搜索的是,1万个用户的博客,可能第一次搜索,会得到1万个userId

GET /website/blogs/_search 
{
  "query": {
    "constant_score": {
      "filter": {
        "terms": {
          "userId": [
            1
          ]
        }
      }
    }
  }
}

第二次搜索的时候,要放入terms中1万个userId,才能进行搜索,这个时候性能比较差了

{
  "took": 4,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "failed": 0
  },
  "hits": {
    "total": 1,
    "max_score": 1,
    "hits": [
      {
        "_index": "website",
        "_type": "blogs",
        "_id": "1",
        "_score": 1,
        "_source": {
          "title": "小鱼儿的第一篇博客",
          "content": "大家好,我是小鱼儿,这是我写的第一篇博客!",
          "userId": 1
        }
      }
    ]
  }
}

上面的操作,就属于应用层的join,在应用层先查出一份数据,然后再查出一份数据,进行关联

3、优点和缺点

优点:数据不冗余,维护方便
缺点:应用层join,如果关联数据过多,导致查询过大,性能很差

61_数据建模_通过数据冗余实现用户与博客的关联

1、构造冗余的用户和博客数据

第二种建模方式:用冗余数据,采用文档数据模型,进行数据建模,实现用户和博客的关联

PUT /website/users/1
{
  "name":     "小鱼儿",
  "email":    "xiaoyuer@sina.com",
  "birthday":      "1980-01-01"
}

PUT /website/blogs/1
{
  "title": "小鱼儿的第一篇博客",
  "content": "大家好,我是小鱼儿。。。",
  "userInfo": {
    "userId": 1,
    "username": "小鱼儿"
  }
}

冗余数据,就是说,将可能会进行搜索的条件和要搜索的数据,放在一个doc中

2、基于冗余用户数据搜索博客

GET /website/blogs/_search 
{
  "query": {
    "term": {
      "userInfo.username.keyword": {
        "value": "小鱼儿"
      }
    }
  }
}

就不需要走应用层的join,先搜一个数据,找到id,再去搜另一份数据

直接走一个有冗余数据的type即可,指定要的搜索条件,即可搜索出自己想要的数据来

3、优点和缺点

优点:性能高,不需要执行两次搜索
缺点:数据冗余,维护成本高 --> 每次如果你的username变化了,同时要更新user type和blog type

一般来说,对于es这种NoSQL类型的数据存储来讲,都是冗余模式…
当然,你要去维护数据的关联关系,也是很有必要的,所以一旦出现冗余数据的修改,必须记得将所有关联的数据全部更新

62_数据建模_对每个用户发表的博客进行分组top_hits

1、构造更多测试数据

PUT /website/users/3
{
  "name": "黄药师",
  "email": "huangyaoshi@sina.com",
  "birthday": "1970-10-24"
}

PUT /website/blogs/3
{
  "title": "我是黄药师",
  "content": "我是黄药师啊,各位同学们!!!",
  "userInfo": {
    "userId": 1,
    "userName": "黄药师"
  }
}
PUT /website/users/2
{
  "name": "花无缺",
  "email": "huawuque@sina.com",
  "birthday": "1980-02-02"
}

PUT /website/blogs/4
{
  "title": "花无缺的身世揭秘",
  "content": "大家好,我是花无缺,所以我的身世是。。。",
  "userInfo": {
    "userId": 2,
    "userName": "花无缺"
  }
}

2、对每个用户发表的博客进行分组

比如说,小鱼儿发表的那些博客,花无缺发表了哪些博客,黄药师发表了哪些博客

GET /website/blogs/_search 
{
  "size": 0, 
  "aggs": {
    "group_by_username": {
      "terms": {
        "field": "userInfo.username.keyword"
      },
      "aggs": {
        "top_blogs": {
          "top_hits": {
            "_source": {
              "include": "title"
            }, 
            "size": 5
          }
        }
      }
    }
  }
}

63_数据建模_对文件系统进行数据建模以及文件搜索

数据建模,对类似文件系统这种的有多层级关系的数据进行建模

1、文件系统数据构造

PUT /fs
{
  "settings": {
    "analysis": {
      "analyzer": {
        "paths": { 
          "tokenizer": "path_hierarchy"
        }
      }
    }
  }
}

path_hierarchy tokenizer讲解

/a/b/c/d --> path_hierarchy -> /a/b/c/d, /a/b/c, /a/b, /a

fs: filesystem

PUT /fs/_mapping/file
{
  "properties": {
    "name": { 
      "type":  "keyword"
    },
    "path": { 
      "type":  "keyword",
      "fields": {
        "tree": { 
          "type":     "text",
          "analyzer": "paths"
        }
      }
    }
  }
}
PUT /fs/file/1
{
  "name":     "README.txt", 
  "path":     "/workspace/projects/helloworld", 
  "contents": "这是我的第一个elasticsearch程序"
}

2、对文件系统执行搜索

文件搜索需求:查找一份,内容包括elasticsearch,在/workspace/projects/hellworld这个目录下的文件

GET /fs/file/_search 
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "contents": "elasticsearch"
          }
        },
        {
          "constant_score": {
            "filter": {
              "term": {
                "path": "/workspace/projects/helloworld"
              }
            }
          }
        }
      ]
    }
  }
}
{
  "took": 2,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "failed": 0
  },
  "hits": {
    "total": 1,
    "max_score": 1.284885,
    "hits": [
      {
        "_index": "fs",
        "_type": "file",
        "_id": "1",
        "_score": 1.284885,
        "_source": {
          "name": "README.txt",
          "path": "/workspace/projects/helloworld",
          "contents": "这是我的第一个elasticsearch程序"
        }
      }
    ]
  }
}

搜索需求2:搜索/workspace目录下,内容包含elasticsearch的所有的文件

/workspace/projects/helloworld doc1
/workspace/projects doc1
/workspace doc1

GET /fs/file/_search 
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "contents": "elasticsearch"
          }
        },
        {
          "constant_score": {
            "filter": {
              "term": {
                "path.tree": "/workspace"
              }
            }
          }
        }
      ]
    }
  }
}

64_数据建模_全局锁实现悲观锁并发控制

课程大纲

1、悲观锁的简要说明

基于version的乐观锁并发控制

在数据建模,结合文件系统建模的这个案例,把悲观锁的并发控制,3种锁粒度,都给大家仔细讲解一下

最粗的一个粒度,全局锁

/workspace/projects/helloworld

如果多个线程,都过来,要并发地给/workspace/projects/helloworld下的README.txt修改文件名

实际上要进行并发的控制,避免出现多线程的并发安全问题,比如多个线程修改,纯并发,先执行的修改操作被后执行的修改操作给覆盖了

get current version

带着这个current version去执行修改,如果一旦发现数据已经被别人给修改了,version号跟之前自己获取的已经不一样了; 那么必须重新获取新的version号再次尝试修改

上来就尝试给这条数据加个锁,然后呢,此时就只有你能执行各种各样的操作了,其他人不能执行操作

第一种锁:全局锁,直接锁掉整个fs index

2、全局锁的上锁实验

PUT /fs/lock/global/_create
{}

fs: 你要上锁的那个index
lock: 就是你指定的一个对这个index上全局锁的一个type
global: 就是你上的全局锁对应的这个doc的id
_create:强制必须是创建,如果/fs/lock/global这个doc已经存在,那么创建失败,报错

利用了doc来进行上锁

/fs/lock/global /index/type/id --> doc

{
  "_index": "fs",
  "_type": "lock",
  "_id": "global",
  "_version": 1,
  "result": "created",
  "_shards": {
    "total": 2,
    "successful": 1,
    "failed": 0
  },
  "created": true
}

另外一个线程同时尝试上锁

PUT /fs/lock/global/_create
{}
{
  "error": {
    "root_cause": [
      {
        "type": "version_conflict_engine_exception",
        "reason": "[lock][global]: version conflict, document already exists (current version [1])",
        "index_uuid": "IYbj0OLGQHmMUpLfbhD4Hw",
        "shard": "2",
        "index": "fs"
      }
    ],
    "type": "version_conflict_engine_exception",
    "reason": "[lock][global]: version conflict, document already exists (current version [1])",
    "index_uuid": "IYbj0OLGQHmMUpLfbhD4Hw",
    "shard": "2",
    "index": "fs"
  },
  "status": 409
}

如果失败,就再次重复尝试上锁

执行各种操作。。。

POST /fs/file/1/_update
{
  "doc": {
    "name": "README1.txt"
  }
}

{
  "_index": "fs",
  "_type": "file",
  "_id": "1",
  "_version": 2,
  "result": "updated",
  "_shards": {
    "total": 2,
    "successful": 1,
    "failed": 0
  }
}
DELETE /fs/lock/global

{
  "found": true,
  "_index": "fs",
  "_type": "lock",
  "_id": "global",
  "_version": 2,
  "result": "deleted",
  "_shards": {
    "total": 2,
    "successful": 1,
    "failed": 0
  }
}

另外一个线程,因为之前发现上锁失败,反复尝试重新上锁,终于上锁成功了,因为之前获取到全局锁的那个线程已经delete /fs/lock/global全局锁了

PUT /fs/lock/global/_create
{}
{
  "_index": "fs",
  "_type": "lock",
  "_id": "global",
  "_version": 3,
  "result": "created",
  "_shards": {
    "total": 2,
    "successful": 1,
    "failed": 0
  },
  "created": true
}
POST /fs/file/1/_update 
{
  "doc": {
    "name": "README.txt"
  }
}
{
  "_index": "fs",
  "_type": "file",
  "_id": "1",
  "_version": 3,
  "result": "updated",
  "_shards": {
    "total": 2,
    "successful": 1,
    "failed": 0
  }
}
DELETE /fs/lock/global

3、全局锁的优点和缺点

优点:操作非常简单,非常容易使用,成本低
缺点:你直接就把整个index给上锁了,这个时候对index中所有的doc的操作,都会被block住,导致整个系统的并发能力很低

上锁解锁的操作不是频繁,然后每次上锁之后,执行的操作的耗时不会太长,用这种方式,方便

65_数据建模_document锁实现悲观锁并发控制

课程大纲

1、对document level锁,详细的讲解

全局锁,一次性就锁整个index,对这个index的所有增删改操作都会被block住,如果上锁不频繁,还可以,比较简单

细粒度的一个锁,document锁,顾名思义,每次就锁你要操作的,你要执行增删改的那些doc,doc锁了,其他线程就不能对这些doc执行增删改操作了
但是你只是锁了部分doc,其他线程对其他的doc还是可以上锁和执行增删改操作的

document锁,是用脚本进行上锁

POST /fs/lock/1/_update
{
  "upsert": { "process_id": 123 },
  "script": "if ( ctx._source.process_id != process_id ) { assert false }; ctx.op = 'noop';"
  "params": {
    "process_id": 123
  }
}

/fs/lock,是固定的,就是说fs下的lock type,专门用于进行上锁
/fs/lock/id,比如1,id其实就是你要上锁的那个doc的id,代表了某个doc数据对应的lock(也是一个doc)
_update + upsert:执行upsert操作

params,里面有个process_id,process_id,是你的要执行增删改操作的进程的唯一id,比如说可以在java系统,启动的时候,给你的每个线程都用UUID自动生成一个thread id,你的系统进程启动的时候给整个进程也分配一个UUID。process_id + thread_id就代表了某一个进程下的某个线程的唯一标识。可以自己用UUID生成一个唯一ID

process_id很重要,会在lock中,设置对对应的doc加锁的进程的id,这样其他进程过来的时候,才知道,这条数据已经被别人给锁了

assert false,不是当前进程加锁的话,则抛出异常
ctx.op=‘noop’,不做任何修改

如果该document之前没有被锁,/fs/lock/1之前不存在,也就是doc id=1没有被别人上过锁; upsert的语法,那么执行index操作,创建一个/fs/lock/id这条数据,而且用params中的数据作为这个lock的数据。process_id被设置为123,script不执行。这个时候象征着process_id=123的进程已经锁了一个doc了。

如果document被锁了,就是说/fs/lock/1已经存在了,代表doc id=1已经被某个进程给锁了。那么执行update操作,script,此时会比对process_id,如果相同,就是说,某个进程,之前锁了这个doc,然后这次又过来,就可以直接对这个doc执行操作,说明是该进程之前锁的doc,则不报错,不执行任何操作,返回success; 如果process_id比对不上,说明doc被其他doc给锁了,此时报错

/fs/lock/1
{
  "process_id": 123
}

POST /fs/lock/1/_update
{
  "upsert": { "process_id": 123 },
  "script": "if ( ctx._source.process_id != process_id ) { assert false }; ctx.op = 'noop';"
  "params": {
    "process_id": 123
  }
}

script:ctx._source.process_id,123
process_id:加锁的upsert请求中带过来额proess_id

如果两个process_id相同,说明是一个进程先加锁,然后又过来尝试加锁,可能是要执行另外一个操作,此时就不会block,对同一个process_id是不会block,ctx.op= ‘noop’,什么都不做,返回一个success

如果说已经有一个进程加了锁了

/fs/lock/1
{
  "process_id": 123
}

POST /fs/lock/1/_update
{
  "upsert": { "process_id": 123 },
  "script": "if ( ctx._source.process_id != process_id ) { assert false }; ctx.op = 'noop';"
  "params": {
    "process_id": 234
  }
}
"script": "if ( ctx._source.process_id != process_id ) { assert false }; ctx.op = 'noop';"

ctx._source.process_id:123
process_id: 234

process_id不相等,说明这个doc之前已经被别人上锁了,process_id=123上锁了; process_id=234过来再次尝试上锁,失败,assert false,就会报错

此时遇到报错的process,就应该尝试重新上锁,直到上锁成功

有报错的话,如果有些doc被锁了,那么需要重试

直到所有锁定都成功,执行自己的操作。。。

释放所有的锁

2、上document锁的完整实验过程

scripts/judge-lock.groovy: if ( ctx._source.process_id != process_id ) { assert false }; ctx.op = 'noop';
POST /fs/lock/1/_update
{
  "upsert": { "process_id": 123 },
  "script": {
    "lang": "groovy",
    "file": "judge-lock", 
    "params": {
      "process_id": 123
    }
  }
}

{
  "_index": "fs",
  "_type": "lock",
  "_id": "1",
  "_version": 1,
  "result": "created",
  "_shards": {
    "total": 2,
    "successful": 1,
    "failed": 0
  }
}
GET /fs/lock/1

{
  "_index": "fs",
  "_type": "lock",
  "_id": "1",
  "_version": 1,
  "found": true,
  "_source": {
    "process_id": 123
  }
}
POST /fs/lock/1/_update
{
  "upsert": { "process_id": 234 },
  "script": {
    "lang": "groovy",
    "file": "judge-lock", 
    "params": {
      "process_id": 234
    }
  }
}
{
  "error": {
    "root_cause": [
      {
        "type": "remote_transport_exception",
        "reason": "[4onsTYV][127.0.0.1:9300][indices:data/write/update[s]]"
      }
    ],
    "type": "illegal_argument_exception",
    "reason": "failed to execute script",
    "caused_by": {
      "type": "script_exception",
      "reason": "error evaluating judge-lock",
      "caused_by": {
        "type": "power_assertion_error",
        "reason": "assert false\n"
      },
      "script_stack": [],
      "script": "",
      "lang": "groovy"
    }
  },
  "status": 400
}
POST /fs/lock/1/_update
{
  "upsert": { "process_id": 123 },
  "script": {
    "lang": "groovy",
    "file": "judge-lock", 
    "params": {
      "process_id": 123
    }
  }
}
{
  "_index": "fs",
  "_type": "lock",
  "_id": "1",
  "_version": 1,
  "result": "noop",
  "_shards": {
    "total": 0,
    "successful": 0,
    "failed": 0
  }
}
POST /fs/file/1/_update
{
  "doc": {
    "name": "README1.txt"
  }
}
{
  "_index": "fs",
  "_type": "file",
  "_id": "1",
  "_version": 4,
  "result": "updated",
  "_shards": {
    "total": 2,
    "successful": 1,
    "failed": 0
  }
}
POST /fs/_refresh 

GET /fs/lock/_search?scroll=1m
{
  "query": {
    "term": {
      "process_id": 123
    }
  }
}

{
  "_scroll_id": "DnF1ZXJ5VGhlbkZldGNoBQAAAAAAACPkFjRvbnNUWVZaVGpHdklqOV9zcFd6MncAAAAAAAAj5RY0b25zVFlWWlRqR3ZJajlfc3BXejJ3AAAAAAAAI-YWNG9uc1RZVlpUakd2SWo5X3NwV3oydwAAAAAAACPnFjRvbnNUWVZaVGpHdklqOV9zcFd6MncAAAAAAAAj6BY0b25zVFlWWlRqR3ZJajlfc3BXejJ3",
  "took": 51,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "failed": 0
  },
  "hits": {
    "total": 1,
    "max_score": 1,
    "hits": [
      {
        "_index": "fs",
        "_type": "lock",
        "_id": "1",
        "_score": 1,
        "_source": {
          "process_id": 123
        }
      }
    ]
  }
}

PUT /fs/lock/_bulk
{ "delete": { "_id": 1}}

{
  "took": 20,
  "errors": false,
  "items": [
    {
      "delete": {
        "found": true,
        "_index": "fs",
        "_type": "lock",
        "_id": "1",
        "_version": 2,
        "result": "deleted",
        "_shards": {
          "total": 2,
          "successful": 1,
          "failed": 0
        },
        "status": 200
      }
    }
  ]
}

POST /fs/lock/1/_update
{
  "upsert": { "process_id": 234 },
  "script": {
    "lang": "groovy",
    "file": "judge-lock", 
    "params": {
      "process_id": 234
    }
  }
}

process_id=234上锁就成功了

66_数据建模_共享锁和排他锁实现悲观锁并发控制

课程大纲

1、共享锁和排他锁的说明

共享锁:这份数据是共享的,然后多个线程过来,都可以获取同一个数据的共享锁,然后对这个数据执行读操作
排他锁:是排他的操作,只能一个线程获取排他锁,然后执行增删改操作

读写锁的分离

如果只是要读取数据的话,那么任意个线程都可以同时进来然后读取数据,每个线程都可以上一个共享锁
但是这个时候,如果有线程要过来修改数据,那么会尝试上排他锁,排他锁会跟共享锁互斥,也就是说,如果有人已经上了共享锁了,那么排他锁就不能上,就得等

如果有人在读数据,就不允许别人来修改数据

反之,也是一样的

如果有人在修改数据,就是加了排他锁
那么其他线程过来要修改数据,也会尝试加排他锁,此时会失败,锁冲突,必须等待,同时只能有一个线程修改数据
如果有人过来同时要读取数据,那么会尝试加共享锁,此时会失败,因为共享锁和排他锁是冲突的

如果有在修改数据,就不允许别人来修改数据,也不允许别人来读取数据

2、共享锁和排他锁的实验

第一步:有人在读数据,其他人也能过来读数据

judge-lock-2.groovy: if (ctx._source.lock_type == ‘exclusive’) { assert false }; ctx._source.lock_count++

POST /fs/lock/1/_update 
{
  "upsert": { 
    "lock_type":  "shared",
    "lock_count": 1
  },
  "script": {
  	"lang": "groovy",
  	"file": "judge-lock-2"
  }
}

POST /fs/lock/1/_update 
{
  "upsert": { 
    "lock_type":  "shared",
    "lock_count": 1
  },
  "script": {
  	"lang": "groovy",
  	"file": "judge-lock-2"
  }
}
GET /fs/lock/1

{
  "_index": "fs",
  "_type": "lock",
  "_id": "1",
  "_version": 3,
  "found": true,
  "_source": {
    "lock_type": "shared",
    "lock_count": 3
  }
}

就给大家模拟了,有人上了共享锁,你还是要上共享锁,直接上就行了,没问题,只是lock_count加1

2、已经有人上了共享锁,然后有人要上排他锁

PUT /fs/lock/1/_create
{ "lock_type": "exclusive" }

排他锁用的不是upsert语法,create语法,要求lock必须不能存在,直接自己是第一个上锁的人,上的是排他锁

{
  "error": {
    "root_cause": [
      {
        "type": "version_conflict_engine_exception",
        "reason": "[lock][1]: version conflict, document already exists (current version [3])",
        "index_uuid": "IYbj0OLGQHmMUpLfbhD4Hw",
        "shard": "3",
        "index": "fs"
      }
    ],
    "type": "version_conflict_engine_exception",
    "reason": "[lock][1]: version conflict, document already exists (current version [3])",
    "index_uuid": "IYbj0OLGQHmMUpLfbhD4Hw",
    "shard": "3",
    "index": "fs"
  },
  "status": 409
}

如果已经有人上了共享锁,明显/fs/lock/1是存在的,create语法去上排他锁,肯定会报错

3、对共享锁进行解锁

POST /fs/lock/1/_update
{
  "script": {
  	"lang": "groovy",
  	"file": "unlock-shared"
  }
}

连续解锁3次,此时共享锁就彻底没了

每次解锁一个共享锁,就对lock_count先减1,如果减了1之后,是0,那么说明所有的共享锁都解锁完了,此时就就将/fs/lock/1删除,就彻底解锁所有的共享锁

3、上排他锁,再上排他锁

PUT /fs/lock/1/_create
{ "lock_type": "exclusive" }

其他线程

PUT /fs/lock/1/_create
{ "lock_type": "exclusive" }
{
  "error": {
    "root_cause": [
      {
        "type": "version_conflict_engine_exception",
        "reason": "[lock][1]: version conflict, document already exists (current version [7])",
        "index_uuid": "IYbj0OLGQHmMUpLfbhD4Hw",
        "shard": "3",
        "index": "fs"
      }
    ],
    "type": "version_conflict_engine_exception",
    "reason": "[lock][1]: version conflict, document already exists (current version [7])",
    "index_uuid": "IYbj0OLGQHmMUpLfbhD4Hw",
    "shard": "3",
    "index": "fs"
  },
  "status": 409
}

4、上排他锁,上共享锁

POST /fs/lock/1/_update 
{
  "upsert": { 
    "lock_type":  "shared",
    "lock_count": 1
  },
  "script": {
  	"lang": "groovy",
  	"file": "judge-lock-2"
  }
}
{
  "error": {
    "root_cause": [
      {
        "type": "remote_transport_exception",
        "reason": "[4onsTYV][127.0.0.1:9300][indices:data/write/update[s]]"
      }
    ],
    "type": "illegal_argument_exception",
    "reason": "failed to execute script",
    "caused_by": {
      "type": "script_exception",
      "reason": "error evaluating judge-lock-2",
      "caused_by": {
        "type": "power_assertion_error",
        "reason": "assert false\n"
      },
      "script_stack": [],
      "script": "",
      "lang": "groovy"
    }
  },
  "status": 400
}

5、解锁排他锁

DELETE /fs/lock/1

67_数据建模_nested嵌套查询

1、做一个实验,引出来为什么需要nested object

冗余数据方式的来建模,其实用的就是object类型,我们这里又要引入一种新的object类型,nested object类型

博客,评论,做的这种数据模型

PUT /website/blogs/6
{
  "title": "花无缺发表的一篇帖子",
  "content":  "我是花无缺,大家要不要考虑一下投资房产和买股票的事情啊。。。",
  "tags":  [ "投资", "理财" ],
  "comments": [ 
    {
      "name":    "小鱼儿",
      "comment": "什么股票啊?推荐一下呗",
      "age":     28,
      "stars":   4,
      "date":    "2016-09-01"
    },
    {
      "name":    "黄药师",
      "comment": "我喜欢投资房产,风,险大收益也大",
      "age":     31,
      "stars":   5,
      "date":    "2016-10-22"
    }
  ]
}

被年龄是28岁的黄药师评论过的博客,搜索

GET /website/blogs/_search
{
  "query": {
    "bool": {
      "must": [
        { "match": { "comments.name": "黄药师" }},
        { "match": { "comments.age":  28      }} 
      ]
    }
  }
}
{
  "took": 102,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "failed": 0
  },
  "hits": {
    "total": 1,
    "max_score": 1.8022683,
    "hits": [
      {
        "_index": "website",
        "_type": "blogs",
        "_id": "6",
        "_score": 1.8022683,
        "_source": {
          "title": "花无缺发表的一篇帖子",
          "content": "我是花无缺,大家要不要考虑一下投资房产和买股票的事情啊。。。",
          "tags": [
            "投资",
            "理财"
          ],
          "comments": [
            {
              "name": "小鱼儿",
              "comment": "什么股票啊?推荐一下呗",
              "age": 28,
              "stars": 4,
              "date": "2016-09-01"
            },
            {
              "name": "黄药师",
              "comment": "我喜欢投资房产,风,险大收益也大",
              "age": 31,
              "stars": 5,
              "date": "2016-10-22"
            }
          ]
        }
      }
    ]
  }
}

结果是。。。好像不太对啊???

object类型数据结构的底层存储。。。

{
  "title":            [ "花无缺", "发表", "一篇", "帖子" ],
  "content":             [ "我", "是", "花无缺", "大家", "要不要", "考虑", "一下", "投资", "房产", "买", "股票", "事情" ],
  "tags":             [ "投资", "理财" ],
  "comments.name":    [ "小鱼儿", "黄药师" ],
  "comments.comment": [ "什么", "股票", "推荐", "我", "喜欢", "投资", "房产", "风险", "收益", "大" ],
  "comments.age":     [ 28, 31 ],
  "comments.stars":   [ 4, 5 ],
  "comments.date":    [ 2016-09-01, 2016-10-22 ]
}

object类型底层数据结构,会将一个json数组中的数据,进行扁平化

所以,直接命中了这个document,name=黄药师,age=28,正好符合

2、引入nested object类型,来解决object类型底层数据结构导致的问题

修改mapping,将comments的类型从object设置为nested

PUT /website
{
  "mappings": {
    "blogs": {
      "properties": {
        "comments": {
          "type": "nested", 
          "properties": {
            "name":    { "type": "string"  },
            "comment": { "type": "string"  },
            "age":     { "type": "short"   },
            "stars":   { "type": "short"   },
            "date":    { "type": "date"    }
          }
        }
      }
    }
  }
}
{ 
  "comments.name":    [ "小鱼儿" ],
  "comments.comment": [ "什么", "股票", "推荐" ],
  "comments.age":     [ 28 ],
  "comments.stars":   [ 4 ],
  "comments.date":    [ 2014-09-01 ]
}
{ 
  "comments.name":    [ "黄药师" ],
  "comments.comment": [ "我", "喜欢", "投资", "房产", "风险", "收益", "大" ],
  "comments.age":     [ 31 ],
  "comments.stars":   [ 5 ],
  "comments.date":    [ 2014-10-22 ]
}
{ 
  "title":            [ "花无缺", "发表", "一篇", "帖子" ],
  "body":             [ "我", "是", "花无缺", "大家", "要不要", "考虑", "一下", "投资", "房产", "买", "股票", "事情" ],
  "tags":             [ "投资", "理财" ]
}

再次搜索,成功了。。。

GET /website/blogs/_search 
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "title": "花无缺"
          }
        },
        {
          "nested": {
            "path": "comments",
            "query": {
              "bool": {
                "must": [
                  {
                    "match": {
                      "comments.name": "黄药师"
                    }
                  },
                  {
                    "match": {
                      "comments.age": 28
                    }
                  }
                ]
              }
            }
          }
        }
      ]
    }
  }
}

score_mode:max,min,avg,none,默认是avg

如果搜索命中了多个nested document,如何讲个多个nested document的分数合并为一个分数

68_数据建模_nested嵌套聚合

我们讲解一下基于nested object中的数据进行聚合分析

聚合数据分析的需求1:按照评论日期进行bucket划分,然后拿到每个月的评论的评分的平均值

GET /website/blogs/_search 
{
  "size": 0, 
  "aggs": {
    "comments_path": {
      "nested": {
        "path": "comments"
      }, 
      "aggs": {
        "group_by_comments_date": {
          "date_histogram": {
            "field": "comments.date",
            "interval": "month",
            "format": "yyyy-MM"
          },
          "aggs": {
            "avg_stars": {
              "avg": {
                "field": "comments.stars"
              }
            }
          }
        }
      }
    }
  }
}
{
  "took": 52,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "failed": 0
  },
  "hits": {
    "total": 2,
    "max_score": 0,
    "hits": []
  },
  "aggregations": {
    "comments_path": {
      "doc_count": 4,
      "group_by_comments_date": {
        "buckets": [
          {
            "key_as_string": "2016-08",
            "key": 1470009600000,
            "doc_count": 1,
            "avg_stars": {
              "value": 3
            }
          },
          {
            "key_as_string": "2016-09",
            "key": 1472688000000,
            "doc_count": 2,
            "avg_stars": {
              "value": 4.5
            }
          },
          {
            "key_as_string": "2016-10",
            "key": 1475280000000,
            "doc_count": 1,
            "avg_stars": {
              "value": 5
            }
          }
        ]
      }
    }
  }
}

ES操作之嵌套查询(nested)与退出嵌套(reverse_nested)操作

GET /website/blogs/_search 
{
  "size": 0,
  "aggs": {
    "comments_path": {
      "nested": {
        "path": "comments"
      },
      "aggs": {
        "group_by_comments_age": {
          "histogram": {
            "field": "comments.age",
            "interval": 10
          },
          "aggs": {
            "reverse_path": {
              "reverse_nested": {}, 
              "aggs": {
                "group_by_tags": {
                  "terms": {
                    "field": "tags.keyword"
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}
{
  "took": 5,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "failed": 0
  },
  "hits": {
    "total": 2,
    "max_score": 0,
    "hits": []
  },
  "aggregations": {
    "comments_path": {
      "doc_count": 4,
      "group_by_comments_age": {
        "buckets": [
          {
            "key": 20,
            "doc_count": 1,
            "reverse_path": {
              "doc_count": 1,
              "group_by_tags": {
                "doc_count_error_upper_bound": 0,
                "sum_other_doc_count": 0,
                "buckets": [
                  {
                    "key": "投资",
                    "doc_count": 1
                  },
                  {
                    "key": "理财",
                    "doc_count": 1
                  }
                ]
              }
            }
          },
          {
            "key": 30,
            "doc_count": 3,
            "reverse_path": {
              "doc_count": 2,
              "group_by_tags": {
                "doc_count_error_upper_bound": 0,
                "sum_other_doc_count": 0,
                "buckets": [
                  {
                    "key": "大侠",
                    "doc_count": 1
                  },
                  {
                    "key": "投资",
                    "doc_count": 1
                  },
                  {
                    "key": "理财",
                    "doc_count": 1
                  },
                  {
                    "key": "练功",
                    "doc_count": 1
                  }
                ]
              }
            }
          }
        ]
      }
    }
  }
}

69_数据建模_父子关系建模

nested object的建模,有个不好的地方,就是采取的是类似冗余数据的方式,将多个数据都放在一起了,维护成本就比较高

parent child建模方式,采取的是类似于关系型数据库的三范式类的建模,多个实体都分割开来,每个实体之间都通过一些关联方式,进行了父子关系的关联,各种数据不需要都放在一起,父doc和子doc分别在进行更新的时候,都不会影响对方

一对多关系的建模,维护起来比较方便,而且我们之前说过,类似关系型数据库的建模方式,应用层join的方式,会导致性能比较差,因为做多次搜索。父子关系的数据模型,不会,性能很好。因为虽然数据实体之间分割开来,但是我们在搜索的时候,由es自动为我们处理底层的关联关系,并且通过一些手段保证搜索性能。

父子关系数据模型,相对于nested数据模型来说,优点是父doc和子doc互相之间不会影响

要点:父子关系元数据映射,用于确保查询时候的高性能,但是有一个限制,就是父子数据必须存在于一个shard中

父子关系数据存在一个shard中,而且还有映射其关联关系的元数据,那么搜索父子关系数据的时候,不用跨分片,一个分片本地自己就搞定了,性能当然高咯

案例背景:研发中心员工管理案例,一个IT公司有多个研发中心,每个研发中心有多个员工

PUT /company
{
  "mappings": {
    "rd_center": {},
    "employee": {
      "_parent": {
        "type": "rd_center" 
      }
    }
  }
}

父子关系建模的核心,多个type之间有父子关系,用_parent指定父type

POST /company/rd_center/_bulk
{ "index": { "_id": "1" }}
{ "name": "北京研发总部", "city": "北京", "country": "中国" }
{ "index": { "_id": "2" }}
{ "name": "上海研发中心", "city": "上海", "country": "中国" }
{ "index": { "_id": "3" }}
{ "name": "硅谷人工智能实验室", "city": "硅谷", "country": "美国" }

shard路由的时候,id=1的rd_center doc,默认会根据id进行路由,到某一个shard

PUT /company/employee/1?parent=1 
{
  "name":  "张三",
  "birthday":   "1970-10-24",
  "hobby": "爬山"
}

维护父子关系的核心,parent=1,指定了这个数据的父doc的id

此时,parent-child关系,就确保了说,父doc和子doc都是保存在一个shard上的。内部原理还是doc routing,employee和rd_center的数据,都会用parent id作为routing,这样就会到一个shard

就不会根据id=1的employee doc的id进行路由了,而是根据parent=1进行路由,会根据父doc的id进行路由,那么就可以通过底层的路由机制,保证父子数据存在于一个shard中

POST /company/employee/_bulk
{ "index": { "_id": 2, "parent": "1" }}
{ "name": "李四", "birthday": "1982-05-16", "hobby": "游泳" }
{ "index": { "_id": 3, "parent": "2" }}
{ "name": "王二", "birthday": "1979-04-01", "hobby": "爬山" }
{ "index": { "_id": 4, "parent": "3" }}
{ "name": "赵五", "birthday": "1987-05-11", "hobby": "骑马" }

70_数据建模_父子关系查询

我们已经建立了父子关系的数据模型之后,就要基于这个模型进行各种搜索和聚合了

1、搜索有1980年以后出生的员工的研发中心

GET /company/rd_center/_search
{
  "query": {
    "has_child": {
      "type": "employee",
      "query": {
        "range": {
          "birthday": {
            "gte": "1980-01-01"
          }
        }
      }
    }
  }
}
{
  "took": 33,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "failed": 0
  },
  "hits": {
    "total": 2,
    "max_score": 1,
    "hits": [
      {
        "_index": "company",
        "_type": "rd_center",
        "_id": "1",
        "_score": 1,
        "_source": {
          "name": "北京研发总部",
          "city": "北京",
          "country": "中国"
        }
      },
      {
        "_index": "company",
        "_type": "rd_center",
        "_id": "3",
        "_score": 1,
        "_source": {
          "name": "硅谷人工智能实验室",
          "city": "硅谷",
          "country": "美国"
        }
      }
    ]
  }
}

2、搜索有名叫张三的员工的研发中心

GET /company/rd_center/_search
{
  "query": {
    "has_child": {
      "type":       "employee",
      "query": {
        "match": {
          "name": "张三"
        }
      }
    }
  }
}
{
  "took": 2,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "failed": 0
  },
  "hits": {
    "total": 1,
    "max_score": 1,
    "hits": [
      {
        "_index": "company",
        "_type": "rd_center",
        "_id": "1",
        "_score": 1,
        "_source": {
          "name": "北京研发总部",
          "city": "北京",
          "country": "中国"
        }
      }
    ]
  }
}

3、min_children搜索有至少2个以上员工的研发中心

GET /company/rd_center/_search
{
  "query": {
    "has_child": {
      "type":         "employee",
      "min_children": 2, 
      "query": {
        "match_all": {}
      }
    }
  }
}
{
  "took": 5,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "failed": 0
  },
  "hits": {
    "total": 1,
    "max_score": 1,
    "hits": [
      {
        "_index": "company",
        "_type": "rd_center",
        "_id": "1",
        "_score": 1,
        "_source": {
          "name": "北京研发总部",
          "city": "北京",
          "country": "中国"
        }
      }
    ]
  }
}

4、搜索在中国的研发中心的员工

GET /company/employee/_search 
{
  "query": {
    "has_parent": {
      "parent_type": "rd_center",
      "query": {
        "term": {
          "country.keyword": "中国"
        }
      }
    }
  }
}

{
  "took": 5,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "failed": 0
  },
  "hits": {
    "total": 3,
    "max_score": 1,
    "hits": [
      {
        "_index": "company",
        "_type": "employee",
        "_id": "3",
        "_score": 1,
        "_routing": "2",
        "_parent": "2",
        "_source": {
          "name": "王二",
          "birthday": "1979-04-01",
          "hobby": "爬山"
        }
      },
      {
        "_index": "company",
        "_type": "employee",
        "_id": "1",
        "_score": 1,
        "_routing": "1",
        "_parent": "1",
        "_source": {
          "name": "张三",
          "birthday": "1970-10-24",
          "hobby": "爬山"
        }
      },
      {
        "_index": "company",
        "_type": "employee",
        "_id": "2",
        "_score": 1,
        "_routing": "1",
        "_parent": "1",
        "_source": {
          "name": "李四",
          "birthday": "1982-05-16",
          "hobby": "游泳"
        }
      }
    ]
  }
}

71_数据建模_父子关系聚合

统计每个国家的喜欢每种爱好的员工有多少个

GET /company/rd_center/_search 
{
  "size": 0,
  "aggs": {
    "group_by_country": {
      "terms": {
        "field": "country.keyword"
      },
      "aggs": {
        "group_by_child_employee": {
          "children": {
            "type": "employee"
          },
          "aggs": {
            "group_by_hobby": {
              "terms": {
                "field": "hobby.keyword"
              }
            }
          }
        }
      }
    }
  }
}
{
  "took": 15,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "failed": 0
  },
  "hits": {
    "total": 3,
    "max_score": 0,
    "hits": []
  },
  "aggregations": {
    "group_by_country": {
      "doc_count_error_upper_bound": 0,
      "sum_other_doc_count": 0,
      "buckets": [
        {
          "key": "中国",
          "doc_count": 2,
          "group_by_child_employee": {
            "doc_count": 3,
            "group_by_hobby": {
              "doc_count_error_upper_bound": 0,
              "sum_other_doc_count": 0,
              "buckets": [
                {
                  "key": "爬山",
                  "doc_count": 2
                },
                {
                  "key": "游泳",
                  "doc_count": 1
                }
              ]
            }
          }
        },
        {
          "key": "美国",
          "doc_count": 1,
          "group_by_child_employee": {
            "doc_count": 1,
            "group_by_hobby": {
              "doc_count_error_upper_bound": 0,
              "sum_other_doc_count": 0,
              "buckets": [
                {
                  "key": "骑马",
                  "doc_count": 1
                }
              ]
            }
          }
        }
      ]
    }
  }
}

72_数据建模_父子关系祖孙三层练习

父子关系,祖孙三层关系的数据建模,搜索

PUT /company
{
  "mappings": {
    "country": {},
    "rd_center": {
      "_parent": {
        "type": "country" 
      }
    },
    "employee": {
      "_parent": {
        "type": "rd_center" 
      }
    }
  }
}

country -> rd_center -> employee,祖孙三层数据模型

POST /company/country/_bulk
{ "index": { "_id": "1" }}
{ "name": "中国" }
{ "index": { "_id": "2" }}
{ "name": "美国" }
POST /company/rd_center/_bulk
{ "index": { "_id": "1", "parent": "1" }}
{ "name": "北京研发总部" }
{ "index": { "_id": "2", "parent": "1" }}
{ "name": "上海研发中心" }
{ "index": { "_id": "3", "parent": "2" }}
{ "name": "硅谷人工智能实验室" }
PUT /company/employee/1?parent=1&routing=1
{
  "name":  "张三",
  "dob":   "1970-10-24",
  "hobby": "爬山"
}

routing参数的讲解,必须跟grandparent相同,否则有问题

country,用的是自己的id去路由; rd_center,parent,用的是country的id去路由; employee,如果也是仅仅指定一个parent,那么用的是rd_center的id去路由,这就导致祖孙三层数据不会在一个shard上

孙子辈儿,要手动指定routing,指定为爷爷辈儿的数据的id

搜索有爬山爱好的员工所在的国家

GET /company/country/_search
{
  "query": {
    "has_child": {
      "type": "rd_center",
      "query": {
        "has_child": {
          "type": "employee",
          "query": {
            "match": {
              "hobby": "爬山"
            }
          }
        }
      }
    }
  }
}

{
  "took": 10,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "failed": 0
  },
  "hits": {
    "total": 1,
    "max_score": 1,
    "hits": [
      {
        "_index": "company",
        "_type": "country",
        "_id": "1",
        "_score": 1,
        "_source": {
          "name": "中国"
        }
      }
    ]
  }
}

第73-81节

73_进阶_term vector探查

课程大纲

1、term vector介绍

获取document中的某个field内的各个term的统计信息

term information: term frequency in the field, term positions, start and end offsets, term payloads

term statistics: 设置term_statistics=true; total term frequency, 一个term在所有document中出现的频率; document frequency,有多少document包含这个term

field statistics: document count,有多少document包含这个field; sum of document frequency,一个field中所有term的df之和; sum of total term frequency,一个field中的所有term的tf之和

GET /twitter/tweet/1/_termvectors
GET /twitter/tweet/1/_termvectors?fields=text

term statistics和field statistics并不精准,不会被考虑有的doc可能被删除了

我告诉大家,其实很少用,用的时候,一般来说,就是你需要对一些数据做探查的时候。比如说,你想要看到某个term,某个词条,大话西游,这个词条,在多少个document中出现了。或者说某个field,film_desc,电影的说明信息,有多少个doc包含了这个说明信息。

2、index-iime term vector实验

term vector,涉及了很多的term和field相关的统计信息,有两种方式可以采集到这个统计信息

(1)index-time,你在mapping里配置一下,然后建立索引的时候,就直接给你生成这些term和field的统计信息了
(2)query-time,你之前没有生成过任何的Term vector信息,然后在查看term vector的时候,直接就可以看到了,会on the fly,现场计算出各种统计信息,然后返回给你

这一讲,不会手敲任何命令,直接copy我做好的命令,因为这一讲的重点,不是掌握什么搜索或者聚合的语法,而是说,掌握,如何采集term vector信息,然后如何看懂term vector信息,你能掌握利用term vector进行数据探查

PUT /my_index
{
  "mappings": {
    "my_type": {
      "properties": {
        "text": {
            "type": "text",
            "term_vector": "with_positions_offsets_payloads",
            "store" : true,
            "analyzer" : "fulltext_analyzer"
         },
         "fullname": {
            "type": "text",
            "analyzer" : "fulltext_analyzer"
        }
      }
    }
  },
  "settings" : {
    "index" : {
      "number_of_shards" : 1,
      "number_of_replicas" : 0
    },
    "analysis": {
      "analyzer": {
        "fulltext_analyzer": {
          "type": "custom",
          "tokenizer": "whitespace",
          "filter": [
            "lowercase",
            "type_as_payload"
          ]
        }
      }
    }
  }
}
PUT /my_index/my_type/1
{
  "fullname" : "Leo Li",
  "text" : "hello test test test "
}

PUT /my_index/my_type/2
{
  "fullname" : "Leo Li",
  "text" : "other hello test ..."
}
GET /my_index/my_type/1/_termvectors
{
  "fields" : ["text"],
  "offsets" : true,
  "payloads" : true,
  "positions" : true,
  "term_statistics" : true,
  "field_statistics" : true
}
{
  "_index": "my_index",
  "_type": "my_type",
  "_id": "1",
  "_version": 1,
  "found": true,
  "took": 10,
  "term_vectors": {
    "text": {
      "field_statistics": {
        "sum_doc_freq": 6,
        "doc_count": 2,
        "sum_ttf": 8
      },
      "terms": {
        "hello": {
          "doc_freq": 2,
          "ttf": 2,
          "term_freq": 1,
          "tokens": [
            {
              "position": 0,
              "start_offset": 0,
              "end_offset": 5,
              "payload": "d29yZA=="
            }
          ]
        },
        "test": {
          "doc_freq": 2,
          "ttf": 4,
          "term_freq": 3,
          "tokens": [
            {
              "position": 1,
              "start_offset": 6,
              "end_offset": 10,
              "payload": "d29yZA=="
            },
            {
              "position": 2,
              "start_offset": 11,
              "end_offset": 15,
              "payload": "d29yZA=="
            },
            {
              "position": 3,
              "start_offset": 16,
              "end_offset": 20,
              "payload": "d29yZA=="
            }
          ]
        }
      }
    }
  }
}

3、query-time term vector实验

GET /my_index/my_type/1/_termvectors
{
  "fields" : ["fullname"],
  "offsets" : true,
  "positions" : true,
  "term_statistics" : true,
  "field_statistics" : true
}

一般来说,如果条件允许,你就用query time的term vector就可以了,你要探查什么数据,现场去探查一下就好了

4、手动指定doc的term vector

GET /my_index/my_type/_termvectors
{
  "doc" : {
    "fullname" : "Leo Li",
    "text" : "hello test test test"
  },
  "fields" : ["text"],
  "offsets" : true,
  "payloads" : true,
  "positions" : true,
  "term_statistics" : true,
  "field_statistics" : true
}

手动指定一个doc,实际上不是要指定doc,而是要指定你想要安插的词条,hello test,那么就可以放在一个field中

将这些term分词,然后对每个term,都去计算它在现有的所有doc中的一些统计信息

这个挺有用的,可以让你手动指定要探查的term的数据情况,你就可以指定探查“大话西游”这个词条的统计信息

5、手动指定analyzer来生成term vector

GET /my_index/my_type/_termvectors
{
  "doc" : {
    "fullname" : "Leo Li",
    "text" : "hello test test test"
  },
  "fields" : ["text"],
  "offsets" : true,
  "payloads" : true,
  "positions" : true,
  "term_statistics" : true,
  "field_statistics" : true,
  "per_field_analyzer" : {
    "text": "standard"
  }
}

6、terms filter

GET /my_index/my_type/_termvectors
{
  "doc" : {
    "fullname" : "Leo Li",
    "text" : "hello test test test"
  },
  "fields" : ["text"],
  "offsets" : true,
  "payloads" : true,
  "positions" : true,
  "term_statistics" : true,
  "field_statistics" : true,
  "filter" : {
      "max_num_terms" : 3,
      "min_term_freq" : 1,
      "min_doc_freq" : 1
    }
}

这个就是说,根据term统计信息,过滤出你想要看到的term vector统计结果
也挺有用的,比如你探查数据把,可以过滤掉一些出现频率过低的term,就不考虑了

7、multi term vector

GET _mtermvectors
{
   "docs": [
      {
         "_index": "my_index",
         "_type": "my_type",
         "_id": "2",
         "term_statistics": true
      },
      {
         "_index": "my_index",
         "_type": "my_type",
         "_id": "1",
         "fields": [
            "text"
         ]
      }
   ]
}
GET /my_index/_mtermvectors
{
   "docs": [
      {
         "_type": "test",
         "_id": "2",
         "fields": [
            "text"
         ],
         "term_statistics": true
      },
      {
         "_type": "test",
         "_id": "1"
      }
   ]
}

GET /my_index/my_type/_mtermvectors
{
   "docs": [
      {
         "_id": "2",
         "fields": [
            "text"
         ],
         "term_statistics": true
      },
      {
         "_id": "1"
      }
   ]
}

GET /_mtermvectors
{
   "docs": [
      {
         "_index": "my_index",
         "_type": "my_type",
         "doc" : {
            "fullname" : "Leo Li",
            "text" : "hello test test test"
         }
      },
      {
         "_index": "my_index",
         "_type": "my_type",
         "doc" : {
           "fullname" : "Leo Li",
           "text" : "other hello test ..."
         }
      }
   ]
}

74_进阶_highlight高亮

课程大纲

1、一个最基本的高亮例子

PUT /blog_website
{
  "mappings": {
    "blogs": {
      "properties": {
        "title": {
          "type": "text",
          "analyzer": "ik_max_word"
        },
        "content": {
          "type": "text",
          "analyzer": "ik_max_word"
        }
      }
    }
  }
}
PUT /blog_website/blogs/1
{
  "title": "我的第一篇博客",
  "content": "大家好,这是我写的第一篇博客,特别喜欢这个博客网站!!!"
}
GET /blog_website/blogs/_search 
{
  "query": {
    "match": {
      "title": "博客"
    }
  },
  "highlight": {
    "fields": {
      "title": {}
    }
  }
}
{
  "took": 103,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "failed": 0
  },
  "hits": {
    "total": 1,
    "max_score": 0.28582606,
    "hits": [
      {
        "_index": "blog_website",
        "_type": "blogs",
        "_id": "1",
        "_score": 0.28582606,
        "_source": {
          "title": "我的第一篇博客",
          "content": "大家好,这是我写的第一篇博客,特别喜欢这个博客网站!!!"
        },
        "highlight": {
          "title": [
            "我的第一篇<em>博客</em>"
          ]
        }
      }
    ]
  }
}

表现,会变成红色,所以说你的指定的field中,如果包含了那个搜索词的话,就会在那个field的文本中,对搜索词进行红色的高亮显示

GET /blog_website/blogs/_search 
{
  "query": {
    "bool": {
      "should": [
        {
          "match": {
            "title": "博客"
          }
        },
        {
          "match": {
            "content": "博客"
          }
        }
      ]
    }
  },
  "highlight": {
    "fields": {
      "title": {},
      "content": {}
    }
  }
}

highlight中的field,必须跟query中的field一一对齐的

2、三种highlight介绍

2.1 plain highlight,lucene highlight,默认

2.2 posting highlight,index_options=offsets

(1)性能比plain highlight要高,因为不需要重新对高亮文本进行分词
(2)对磁盘的消耗更少
(3)将文本切割为句子,并且对句子进行高亮,效果更好

PUT /blog_website
{
  "mappings": {
    "blogs": {
      "properties": {
        "title": {
          "type": "text",
          "analyzer": "ik_max_word"
        },
        "content": {
          "type": "text",
          "analyzer": "ik_max_word",
          "index_options": "offsets"
        }
      }
    }
  }
}
PUT /blog_website/blogs/1
{
  "title": "我的第一篇博客",
  "content": "大家好,这是我写的第一篇博客,特别喜欢这个博客网站!!!"
}
GET /blog_website/blogs/_search 
{
  "query": {
    "match": {
      "content": "博客"
    }
  },
  "highlight": {
    "fields": {
      "content": {}
    }
  }
}

2.3 fast vector highlight

index-time term vector设置在mapping中,就会用fast verctor highlight

(1)对大field而言(大于1mb),性能更高

PUT /blog_website
{
  "mappings": {
    "blogs": {
      "properties": {
        "title": {
          "type": "text",
          "analyzer": "ik_max_word"
        },
        "content": {
          "type": "text",
          "analyzer": "ik_max_word",
          "term_vector" : "with_positions_offsets"
        }
      }
    }
  }
}

强制使用某种highlighter,比如对于开启了term vector的field而言,可以强制使用plain highlight

GET /blog_website/blogs/_search 
{
  "query": {
    "match": {
      "content": "博客"
    }
  },
  "highlight": {
    "fields": {
      "content": {
        "type": "plain"
      }
    }
  }
}

总结一下,其实可以根据你的实际情况去考虑,一般情况下,用plain highlight也就足够了,不需要做其他额外的设置
如果对高亮的性能要求很高,可以尝试启用posting highlight
如果field的值特别大,超过了1M,那么可以用fast vector highlight

3、设置高亮html标签,默认是标签

GET /blog_website/blogs/_search 
{
  "query": {
    "match": {
      "content": "博客"
    }
  },
  "highlight": {
    "pre_tags": ["<tag1>"],
    "post_tags": ["</tag2>"], 
    "fields": {
      "content": {
        "type": "plain"
      }
    }
  }
}

4、高亮片段fragment的设置

GET /_search
{
    "query" : {
        "match": { "user": "kimchy" }
    },
    "highlight" : {
        "fields" : {
            "content" : {"fragment_size" : 150, "number_of_fragments" : 3, "no_match_size": 150 }
        }
    }
}

fragment_size: 你一个Field的值,比如有长度是1万,但是你不可能在页面上显示这么长啊。。。设置要显示出来的fragment文本判断的长度,默认是100
number_of_fragments:你可能你的高亮的fragment文本片段有多个片段,你可以指定就显示几个片段

75_进阶_search template搜索模板

课程大纲

搜索模板,search template,高级功能,就可以将我们的一些搜索进行模板化,然后的话,每次执行这个搜索,就直接调用模板,给传入一些参数就可以了

越高级的功能,越少使用,可能只有在你真的遇到特别合适的场景的时候,才会去使用某个高级功能。但是,这些高级功能你是否掌握,其实就是普通的es开发人员,和es高手之间的一个区别。高手,一般来说,会把一个技术掌握的特别好,特别全面,特别深入,也许他平时用不到这个技术,但是当真的遇到一定的场景的时候,高手可以基于自己的深厚的技术储备,立即反应过来,找到一个合适的解决方案。

如果是一个普通的技术人员,一般只会学习跟自己当前工作相关的一些知识和技术,只要求自己掌握的技术可以解决工作目前遇到的问题就可以了,就满足了,就会止步不前了,然后就不会更加深入的去学习一个技术。但是,当你的项目真正遇到问题的时候,遇到了一些难题,你之前的那一点技术储备已经没法去应付这些更加困难的问题了,此时,普通的技术人员就会扎耳挠腮,没有任何办法。

高手,对技术是很有追求,能够精通很多自己遇到过的技术,但是也许自己学的很多东西,自己都没用过,但是不要紧,这是你的一种技术储备。

1、search template入门

GET /blog_website/blogs/_search/template
{
  "inline" : {
    "query": { 
      "match" : { 
        "{{field}}" : "{{value}}" 
      } 
    }
  },
  "params" : {
      "field" : "title",
      "value" : "博客"
  }
}
GET /blog_website/blogs/_search
{
  "query": { 
    "match" : { 
      "title" : "博客" 
    } 
  }
}

search template:"{{field}}" : “{{value}}”

2、toJson

GET /blog_website/blogs/_search/template
{
  "inline": "{\"query\": {\"match\": {{#toJson}}matchCondition{{/toJson}}}}",
  "params": {
    "matchCondition": {
      "title": "博客"
    }
  }
}
GET /blog_website/blogs/_search
{
  "query": { 
    "match" : { 
      "title" : "博客" 
    } 
  }
}

3、join

GET /blog_website/blogs/_search/template
{
  "inline": {
    "query": {
      "match": {
        "title": "{{#join delimiter=' '}}titles{{/join delimiter=' '}}"
      }
    }
  },
  "params": {
    "titles": ["博客", "网站"]
  }
}

博客,网站

GET /blog_website/blogs/_search
{
  "query": { 
    "match" : { 
      "title" : "博客 网站" 
    } 
  }
}

4、default value

POST /blog_website/blogs/1/_update
{
  "doc": {
    "views": 5
  }
}
GET /blog_website/blogs/_search/template
{
  "inline": {
    "query": {
      "range": {
        "views": {
          "gte": "{{start}}",
          "lte": "{{end}}{{^end}}20{{/end}}"
        }
      }
    }
  },
  "params": {
    "start": 1,
    "end": 10
  }
}
GET /blog_website/blogs/_search
{
  "query": {
    "range": {
      "views": {
        "gte": 1,
        "lte": 10
      }
    }
  }
}
GET /blog_website/blogs/_search/template
{
  "inline": {
    "query": {
      "range": {
        "views": {
          "gte": "{{start}}",
          "lte": "{{end}}{{^end}}20{{/end}}"
        }
      }
    }
  },
  "params": {
    "start": 1
  }
}
GET /blog_website/blogs/_search
{
  "query": {
    "range": {
      "views": {
        "gte": 1,
        "lte": 20
      }
    }
  }
}

5、conditional

es的config/scripts目录下,预先保存这个复杂的模板,后缀名是.mustache,文件名是conditonal

{
  "query": {
    "bool": {
      "must": {
        "match": {
          "line": "{{text}}" 
        }
      },
      "filter": {
        {{#line_no}} 
          "range": {
            "line_no": {
              {{#start}} 
                "gte": "{{start}}" 
                {{#end}},{{/end}} 
              {{/start}} 
              {{#end}} 
                "lte": "{{end}}" 
              {{/end}} 
            }
          }
        {{/line_no}} 
      }
    }
  }
}
GET /my_index/my_type/_search 

{
  "took": 4,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "failed": 0
  },
  "hits": {
    "total": 1,
    "max_score": 1,
    "hits": [
      {
        "_index": "my_index",
        "_type": "my_type",
        "_id": "1",
        "_score": 1,
        "_source": {
          "line": "我的博客",
          "line_no": 5
        }
      }
    ]
  }
}
GET /my_index/my_type/_search/template
{
  "file": "conditional",
  "params": {
    "text": "博客",
    "line_no": true,
    "start": 1,
    "end": 10
  }
}

6、保存search template

config/scripts,.mustache

提供一个思路

比如说,一般在大型的团队中,可能不同的人,都会想要执行一些类似的搜索操作
这个时候,有一些负责底层运维的一些同学,就可以基于search template,封装一些模板出来,然后是放在各个es进程的scripts目录下的
其他的团队,其实就不用各个团队自己反复手写复杂的通用的查询语句了,直接调用某个搜索模板,传入一些参数就好了

76_进阶_completion suggest搜索提示

课程大纲

suggest,completion suggest,自动完成,搜索推荐,搜索提示 --> 自动完成,auto completion

auto completion

比如说我们在百度,搜索,你现在搜索“大话西游” -->
百度,自动给你提示,“大话西游电影”,“大话西游小说”, “大话西游手游”

不用你把所有你想要输入的文本都输入完,搜索引擎会自动提示你可能是你想要搜索的那个文本

PUT /news_website
{
  "mappings": {
    "news" : {
      "properties" : {
        "title" : {
          "type": "text",
          "analyzer": "ik_max_word",
          "fields": {
            "suggest" : {
              "type" : "completion",
              "analyzer": "ik_max_word"
            }
          }
        },
        "content": {
          "type": "text",
          "analyzer": "ik_max_word"
        }
      }
    }
  }
}

completion,es实现的时候,是非常高性能的,会构建不是倒排索引,也不是正拍索引,就是纯的用于进行前缀搜索的一种特殊的数据结构,而且会全部放在内存中,所以auto completion进行的前缀搜索提示,性能是非常高的

大话西游

PUT /news_website/news/1
{
  "title": "大话西游电影",
  "content": "大话西游的电影时隔20年即将在2017年4月重映"
}
PUT /news_website/news/2
{
  "title": "大话西游小说",
  "content": "某知名网络小说作家已经完成了大话西游同名小说的出版"
}
PUT /news_website/news/3
{
  "title": "大话西游手游",
  "content": "网易游戏近日出品了大话西游经典IP的手游,正在火爆内测中"
}
GET /news_website/news/_search
{
  "suggest": {
    "my-suggest" : {
      "prefix" : "大话西游",
      "completion" : {
        "field" : "title.suggest"
      }
    }
  }
}
{
  "took": 6,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "failed": 0
  },
  "hits": {
    "total": 0,
    "max_score": 0,
    "hits": []
  },
  "suggest": {
    "my-suggest": [
      {
        "text": "大话西游",
        "offset": 0,
        "length": 4,
        "options": [
          {
            "text": "大话西游小说",
            "_index": "news_website",
            "_type": "news",
            "_id": "2",
            "_score": 1,
            "_source": {
              "title": "大话西游小说",
              "content": "某知名网络小说作家已经完成了大话西游同名小说的出版"
            }
          },
          {
            "text": "大话西游手游",
            "_index": "news_website",
            "_type": "news",
            "_id": "3",
            "_score": 1,
            "_source": {
              "title": "大话西游手游",
              "content": "网易游戏近日出品了大话西游经典IP的手游,正在火爆内测中"
            }
          },
          {
            "text": "大话西游电影",
            "_index": "news_website",
            "_type": "news",
            "_id": "1",
            "_score": 1,
            "_source": {
              "title": "大话西游电影",
              "content": "大话西游的电影时隔20年即将在2017年4月重映"
            }
          }
        ]
      }
    ]
  }
}
GET /news_website/news/_search 
{
  "query": {
    "match": {
      "content": "大话西游电影"
    }
  }
}
{
  "took": 9,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "failed": 0
  },
  "hits": {
    "total": 3,
    "max_score": 1.3495269,
    "hits": [
      {
        "_index": "news_website",
        "_type": "news",
        "_id": "1",
        "_score": 1.3495269,
        "_source": {
          "title": "大话西游电影",
          "content": "大话西游的电影时隔20年即将在2017年4月重映"
        }
      },
      {
        "_index": "news_website",
        "_type": "news",
        "_id": "3",
        "_score": 1.217097,
        "_source": {
          "title": "大话西游手游",
          "content": "网易游戏近日出品了大话西游经典IP的手游,正在火爆内测中"
        }
      },
      {
        "_index": "news_website",
        "_type": "news",
        "_id": "2",
        "_score": 1.1299736,
        "_source": {
          "title": "大话西游小说",
          "content": "某知名网络小说作家已经完成了大话西游同名小说的出版"
        }
      }
    ]
  }
}

77_进阶_dynamic_templates动态映射模板定制自己的映射策略

课程大纲

高级的用法

比如说,我们本来没有某个type,或者没有某个field,但是希望在插入数据的时候,es自动为我们做一个识别,动态映射出这个type的mapping,包括每个field的数据类型,一般用的动态映射,dynamic mapping

这里有个问题,如果说,我们其实对dynamic mapping有一些自己独特的需求,比如说,es默认来说,如经过识别到一个数字,field: 10,默认是搞成这个field的数据类型是long,再比如说,如果我们弄了一个field : “10”,默认就是text,还会带一个keyword的内置field。我们没法改变。

但是我们现在就是希望动态映射的时候,根据我们的需求去映射,而不是让es自己按照默认的规则去玩儿

dyanmic mapping template,动态映射模板

我们自己预先定义一个模板,然后插入数据的时候,相关的field,如果能够根据我们预先定义的规则,匹配上某个我们预定义的模板,那么就会根据我们的模板来进行mapping,决定这个Field的数据类型

0、默认的动态映射的效果咋样

DELETE /my_index
PUT /my_index/my_type/1
{
  "test_string": "hello world",
  "test_number": 10
}

es的自动的默认的,动态映射是咋样的。。。

GET /my_index/_mapping/my_type

{
  "my_index": {
    "mappings": {
      "my_type": {
        "properties": {
          "test_number": {
            "type": "long"
          },
          "test_string": {
            "type": "text",
            "fields": {
              "keyword": {
                "type": "keyword",
                "ignore_above": 256
              }
            }
          }
        }
      }
    }
  }
}

这个就是es的默认的动态映射规则,可能就不是我们想要的。。。

我们比如说,现在想要的效果是啥。。。

test_number,如果是个数字,我们希望默认就是integer类型的
test_string,如果是字符串,我们希望默认是个text,这个没问题,但是内置的field名字,叫做raw,不叫座keyword,类型还是keyword,保留500个字符

1、根据类型匹配映射模板

动态映射模板,有两种方式,第一种,是根据新加入的field的默认的数据类型,来进行匹配,匹配上某个预定义的模板;第二种,是根据新加入的field的名字,去匹配预定义的名字,或者去匹配一个预定义的通配符,然后匹配上某个预定义的模板

PUT my_index
{
  "mappings": {
    "my_type": {
      "dynamic_templates": [
        {
          "integers": {
            "match_mapping_type": "long",
            "mapping": {
              "type": "integer"
            }
          }
        },
        {
          "strings": {
            "match_mapping_type": "string",
            "mapping": {
              "type": "text",
              "fields": {
                "raw": {
                  "type": "keyword",
                  "ignore_above": 500
                }
              }
            }
          }
        }
      ]
    }
  }
}
PUT /my_index/my_type/1
{
  "test_long": 1,
  "test_string": "hello world"
}
{
  "my_index": {
    "mappings": {
      "my_type": {
        "dynamic_templates": [
          {
            "integers": {
              "match_mapping_type": "long",
              "mapping": {
                "type": "integer"
              }
            }
          },
          {
            "strings": {
              "match_mapping_type": "string",
              "mapping": {
                "fields": {
                  "raw": {
                    "ignore_above": 500,
                    "type": "keyword"
                  }
                },
                "type": "text"
              }
            }
          }
        ],
        "properties": {
          "test_number": {
            "type": "integer"
          },
          "test_string": {
            "type": "text",
            "fields": {
              "raw": {
                "type": "keyword",
                "ignore_above": 500
              }
            }
          }
        }
      }
    }
  }

2、根据字段名配映射模板

PUT /my_index 
{
  "mappings": {
    "my_type": {
      "dynamic_templates": [
        {
          "string_as_integer": {
            "match_mapping_type": "string",
            "match": "long_*",
            "unmatch": "*_text",
            "mapping": {
              "type": "integer"
            }
          }
        }
      ]
    }
  }
}

举个例子,field : “10”,把类似这种field,弄成long型

{
  "my_index": {
    "mappings": {
      "my_type": {
        "dynamic_templates": [
          {
            "string_as_integer": {
              "match": "long_*",
              "unmatch": "*_text",
              "match_mapping_type": "string",
              "mapping": {
                "type": "integer"
              }
            }
          }
        ],
        "properties": {
          "long_field": {
            "type": "integer"
          },
          "long_field_text": {
            "type": "text",
            "fields": {
              "keyword": {
                "type": "keyword",
                "ignore_above": 256
              }
            }
          }
        }
      }
    }
  }
}

场景,有些时候,dynamic mapping + template,每天有一堆日志,每天有一堆数据

这些数据,每天的数据都放一个新的type中,每天的数据都会哗哗的往新的tye中写入,此时你就可以定义一个模板,搞一个脚本,每天都预先生成一个新type的模板,里面讲你的各个Field都匹配到一个你预定义的模板中去,就好了

78_进阶_geo point地理位置数据类型

课程大纲

这一讲开始,后面会跟着几讲内容,将地理位置相关的知识给大家讲解一下

主要是es支持基于地理位置的搜索,和聚合分析的

举个例子,比如说,我们后面就会给大家演示一下,你现在如果说做了一个酒店o2o app,让你的用户在任何地方,都可以根据当前所在的位置,找到自己身边的符合条件的一些酒店,那么此时就完全可以使用es来实现,非常合适

我现在在上海某个大厦附近,我要搜索到距离我2公里以内的5星级的带游泳池的一个酒店s,用es就完全可以实现类似这样的基于地理位置的搜索引擎

1、建立geo_point类型的mapping

第一个地理位置的数据类型,就是geo_point,geo_point,说白了,就是一个地理位置坐标点,包含了一个经度,一个维度,经纬度,就可以唯一定位一个地球上的坐标

PUT /my_index 
{
  "mappings": {
    "my_type": {
      "properties": {
        "location": {
          "type": "geo_point"
        }
      }
    }
  }
}

2、写入geo_point的3种方法

PUT my_index/my_type/1
{
  "text": "Geo-point as an object",
  "location": { 
    "lat": 41.12,
    "lon": -71.34
  }
}

latitude:维度
longitude:经度

我们这里就不用去关心,这些坐标到底代表什么地方,其实都是我自己随便写的,只要能够作为课程,给大家演示清楚就可以了,自己去找一些提供地理位置的一些公司,供应商,api,百度地图,也是提供各个地方的经纬度的

不建议用下面两种语法

PUT my_index/my_type/2
{
  "text": "Geo-point as a string",
  "location": "41.12,-71.34" 
}

PUT my_index/my_type/4
{
  "text": "Geo-point as an array",
  "location": [ -71.34, 41.12 ] 
}

3、根据地理位置进行查询

最最简单的,根据地理位置查询一些点,比如说,下面geo_bounding_box查询,查询某个矩形的地理位置范围内的坐标点

GET /my_index/my_type/_search 
{
  "query": {
    "geo_bounding_box": {
      "location": {
        "top_left": {
          "lat": 42,
          "lon": -72
        },
        "bottom_right": {
          "lat": 40,
          "lon": -74
        }
      }
    }
  }
}
{
  "took": 81,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "failed": 0
  },
  "hits": {
    "total": 1,
    "max_score": 1,
    "hits": [
      {
        "_index": "my_index",
        "_type": "my_type",
        "_id": "1",
        "_score": 1,
        "_source": {
          "location": {
            "lat": 41.12,
            "lon": -71.34
          }
        }
      }
    ]
  }
}

比如41.12,-71.34就是一个酒店,然后我们现在搜索的是从42,-72(代表了大厦A)和40,-74(代表了马路B)作为矩形的范围,在这个范围内的酒店,是什么

79_进阶_酒店o2o搜索案例以及搜索指定区域内的酒店

稍微真实点的案例,酒店o2o app作为一个背景,用各种各样的方式,去搜索你当前所在的地理位置附近的酒店

搜索指定区域范围内的酒店,比如说,我们可以在搜索的时候,指定两个地点,就要在东方明珠大厦和上海路组成的矩阵的范围内,搜索我想要的酒店

PUT /hotel_app
{
    "mappings": {
        "hotels": {
            "properties": {
                "pin": {
                    "properties": {
                        "location": {
                            "type": "geo_point"
                        }
                    }
                }
            }
        }
    }
}
PUT /hotel_app/hotels/1
{
    "name": "喜来登大酒店",
    "pin" : {
        "location" : {
            "lat" : 40.12,
            "lon" : -71.34
        }
    }
}
GET /hotel_app/hotels/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match_all": {}
        }
      ],
      "filter": {
        "geo_bounding_box": {
          "pin.location": {
            "top_left" : {
                "lat" : 40.73,
                "lon" : -74.1
            },
            "bottom_right" : {
                "lat" : 40.01,
                "lon" : -71.12
            }
          }
        }
      }
    }
  }
}
GET /hotel_app/hotels/_search 
{
  "query": {
    "bool": {
      "must": [
        {
          "match_all": {}
        }
      ],
      "filter": {
        "geo_polygon": {
          "pin.location": {
            "points": [
              {"lat" : 40.73, "lon" : -74.1},
              {"lat" : 40.01, "lon" : -71.12},
              {"lat" : 50.56, "lon" : -90.58}
            ]
          }
        }
      }
    }
  }
}

我们现在要指定东方明珠大厦,上海路,上海博物馆,这三个地区组成的多边形的范围内,我要搜索这里面的酒店

80_进阶_搜索距离当前位置一定范围内的酒店

酒店o2o app,作为案例背景

比如说,现在用户,所在的位置,是个地理位置的坐标,我是知道我的坐标的,app是知道的,android,地理位置api,都可以拿到当前手机app的经纬度

我说,我现在就要搜索出,举例我200m,或者1公里内的酒店

重要!!!!

我们之前出去玩儿,都会用一些酒店o2o app,典型的代表,很多旅游app,一般来说,我们怎么搜索,到了一个地方,就会搜索说,我这里举例几百米,2公里内的酒店,搜索一下

上节课讲解的,其实也很重要,一般来说,发生在我们在去旅游之前,会现在旅游app上搜索一个区域内的酒店,比如说,指定了西安火车站、西安博物馆,拿指定的几个地方的地理位置,组成一个多边形区域范围,去搜索这个区域内的酒店

承认,一些案例,当然不可能说达到讲解真实的复杂的大型的项目的效果来的那么好,光是学知识,学技术而言,有一些案例就非常不错了

后面,会讲解真正的企业级的大型的搜索引擎,真实复杂业务的数据分析系统的项目

GET /hotel_app/hotels/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match_all": {}
        }
      ],
      "filter": {
        "geo_distance": {
          "distance": "200km",
          "pin.location": {
            "lat": 40,
            "lon": -70
          }
        }
      }
    }
  }
}

81_进阶_统计当前位置每个距离范围内有多少家酒店

最后一个知识点,基于地理位置进行聚合分析

我的需求就是,统计一下,举例我当前坐标的几个范围内的酒店的数量,比如说举例我0100m有几个酒店,100m300m有几个酒店,300m以上有几个酒店

一般来说,做酒店app,一般来说,我们是不是会有一个地图,用户可以在地图上直接查看和搜索酒店,此时就可以显示出来举例你当前的位置,几个举例范围内,有多少家酒店,让用户知道,心里清楚,用户体验就比较好

GET /hotel_app/hotels/_search
{
  "size": 0,
  "aggs": {
    "agg_by_distance_range": {
      "geo_distance": {
        "field": "pin.location",
        "origin": {
          "lat": 40,
          "lon": -70
        },
        "unit": "mi", 
        "ranges": [
          {
            "to": 100
          },
          {
            "from": 100,
            "to": 300
          },
          {
            "from": 300
          }
        ]
      }
    }
  }
}
{
  "took": 5,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "failed": 0
  },
  "hits": {
    "total": 1,
    "max_score": 0,
    "hits": []
  },
  "aggregations": {
    "agg_by_distance_range": {
      "buckets": [
        {
          "key": "*-100.0",
          "from": 0,
          "to": 100,
          "doc_count": 1
        },
        {
          "key": "100.0-300.0",
          "from": 100,
          "to": 300,
          "doc_count": 0
        },
        {
          "key": "300.0-*",
          "from": 300,
          "doc_count": 0
        }
      ]
    }
  }
}

m (metres) but it can also accept: m (miles), km (kilometers)

sloppy_arc (the default), arc (most accurate) and plane (fastest)

第82-92节

82_Java API_client集群自动探查以及汽车零售店案例背景

课程大纲

快速入门篇,讲解过了一些基本的java api,包括了document增删改查,基本的搜索,基本的聚合

高手进阶篇,必须将java api这块深入讲解一下,介绍一些最常用的,最核心的一些api的使用,用一个模拟现实的案例背景,让大家在学习的时候更加贴近业务

话说在前面,我们是不可能将所有的java api用视频全部录制一遍的,因为api太多了。。。。

我们之前讲解各种功能,各种知识点,花了那么多的时间,哪儿些才是最最关键的,知识,原理,功能,es restful api,最次最次,哪怕是搞php,搞python的人也可以来学习

如果说,现在要将所有所有的api全部用java api实现一遍和讲解,太耗费时间了,几乎不可能接受

采取的粗略,将核心的java api语法,还有最最常用的那些api都给大家上课演示了

然后最后一讲,会告诉大家,在掌握了之前那些课程讲解的各种知识点之后,如果要用java api去实现和开发,应该怎么自己去探索和掌握

java api,api的学习,实际上是最最简单的,纯用,没什么难度,技术难度,你掌握了课上讲解的这些api之后,自己应该就可以举一反三,后面自己去探索和尝试出自己要用的各种功能对应的java api是什么。。。

1、client集群自动探查

默认情况下,是根据我们手动指定的所有节点,依次轮询这些节点,来发送各种请求的,如下面的代码,我们可以手动为client指定多个节点

TransportClient client = new PreBuiltTransportClient(settings)
				.addTransportAddress(new InetSocketTransportAddress(InetAddress.getByName("localhost1"), 9300))
				.addTransportAddress(new InetSocketTransportAddress(InetAddress.getByName("localhost2"), 9300))
				.addTransportAddress(new InetSocketTransportAddress(InetAddress.getByName("localhost3"), 9300));

但是问题是,如果我们有成百上千个节点呢?难道也要这样手动添加吗?

es client提供了一种集群节点自动探查的功能,打开这个自动探查机制以后,es client会根据我们手动指定的几个节点连接过去,然后通过集群状态自动获取当前集群中的所有data node,然后用这份完整的列表更新自己内部要发送请求的node list。默认每隔5秒钟,就会更新一次node list。

但是注意,es cilent是不会将Master node纳入node list的,因为要避免给master node发送搜索等请求。

这样的话,我们其实直接就指定几个master node,或者1个node就好了,client会自动去探查集群的所有节点,而且每隔5秒还会自动刷新。非常棒。

Settings settings = Settings.builder()
.put(“client.transport.sniff”, true).build();
TransportClient client = new PreBuiltTransportClient(settings);

使用上述的settings配置,将client.transport.sniff设置为true即可打开集群节点自动探查功能

在实际的生产环境中,都是这么玩儿的。。。

2、汽车零售案例背景

简单来说,会涉及到三个数据,汽车信息,汽车销售记录,汽车4S店信息

83_Java API_基于upsert实现汽车最新价格的调整

课程大纲

做一个汽车零售数据的mapping,我们要做的第一份数据,其实汽车信息

PUT /car_shop
{
    "mappings": {
        "cars": {
            "properties": {
                "brand": {
                    "type": "text",
                    "analyzer": "ik_max_word",
                    "fields": {
                        "raw": {
                            "type": "keyword"
                        }
                    }
                },
                "name": {
                    "type": "text",
                    "analyzer": "ik_max_word",
                    "fields": {
                        "raw": {
                            "type": "keyword"
                        }
                    }
                }
            }
        }
    }
}

首先的话呢,第一次调整宝马320这个汽车的售价,我们希望将售价设置为32万,用一个upsert语法,如果这个汽车的信息之前不存在,那么就insert,如果存在,那么就update

IndexRequest indexRequest = new IndexRequest("car_shop", "cars", "1")
        .source(jsonBuilder()
            .startObject()
                .field("brand", "宝马")
                .field("name", "宝马320")
                .field("price", 320000)
                .field("produce_date", "2017-01-01")
            .endObject());

UpdateRequest updateRequest = new UpdateRequest("car_shop", "cars", "1")
        .doc(jsonBuilder()
            .startObject()
                .field("price", 320000)
            .endObject())
        .upsert(indexRequest);       
               
client.update(updateRequest).get();
IndexRequest indexRequest = new IndexRequest("car_shop", "cars", "1")
        .source(jsonBuilder()
            .startObject()
                .field("brand", "宝马")
                .field("name", "宝马320")
                .field("price", 310000)
                .field("produce_date", "2017-01-01")
            .endObject());
UpdateRequest updateRequest = new UpdateRequest("car_shop", "cars", "1")
        .doc(jsonBuilder()
            .startObject()
                .field("price", 310000)
            .endObject())
        .upsert(indexRequest);              
client.update(updateRequest).get();

84_Java API_mget实现多辆汽车的配置与价格对比

课程大纲

场景,一般来说,我们都可以在一些汽车网站上,或者在混合销售多个品牌的汽车4S店的内部,都可以在系统里调出来多个汽车的信息,放在网页上,进行对比

mget,一次性将多个document的数据查询出来,放在一起显示,多个汽车的型号,一次性拿出了多辆汽车的信息

PUT /car_shop/cars/2
{
	"brand": "奔驰",
	"name": "奔驰C200",
	"price": 350000,
	"produce_date": "2017-01-05"
}

MultiGetResponse multiGetItemResponses = client.prepareMultiGet()
    .add("car_shop", "cars", "1")           
    .add("car_shop", "cars", "2")        
    .get();

for (MultiGetItemResponse itemResponse : multiGetItemResponses) { 
    GetResponse response = itemResponse.getResponse();
    if (response.isExists()) {                      
        String json = response.getSourceAsString(); 
    }
}

85_Java API_基于bulk实现多4S店销售数据批量上传

课程大纲

业务场景:有一个汽车销售公司,拥有很多家4S店,这些4S店的数据,都会在一段时间内陆续传递过来,汽车的销售数据,现在希望能够在内存中缓存比如1000条销售数据,然后一次性批量上传到es中去

PUT /car_shop/sales/1
{
    "brand": "宝马",
    "name": "宝马320",
    "price": 320000,
    "produce_date": "2017-01-01",
    "sale_price": 300000,
    "sale_date": "2017-01-21"
}

PUT /car_shop/sales/2
{
    "brand": "宝马",
    "name": "宝马320",
    "price": 320000,
    "produce_date": "2017-01-01",
    "sale_price": 300000,
    "sale_date": "2017-01-21"
}
BulkRequestBuilder bulkRequest = client.prepareBulk();

bulkRequest.add(client.prepareIndex("car_shop", "sales", "3")
        .setSource(jsonBuilder()
                    .startObject()
                        .field("brand", "奔驰")
                        .field("name", "奔驰C200")
                        .field("price", 350000)
                        .field("produce_date", "2017-01-05")
                        .field("sale_price", 340000)
                        .field("sale_date", "2017-02-03")
                    .endObject()
                  )
        );

bulkRequest.add(client.prepareUpdate("car_shop", "sales", "1")
        .setDoc(jsonBuilder()               
		            .startObject()
		                .field("sale_price", "290000")
		            .endObject()
		        )
        );

bulkRequest.add(client.prepareDelete("car_shop", "sales", "2"));

BulkResponse bulkResponse = bulkRequest.get();

if (bulkResponse.hasFailures()) {}

86_Java API_基于scroll实现月度销售数据批量下载

课程大纲

比如说,现在要下载大批量的数据,从es,放到excel中,我们说,月度,或者年度,销售记录,很多,比如几千条,几万条,几十万条

其实就要用到我们之前讲解的es scroll api,对大量数据批量的获取和处理

PUT /car_shop/sales/4
{
    "brand": "宝马",
    "name": "宝马320",
    "price": 320000,
    "produce_date": "2017-01-01",
    "sale_price": 280000,
    "sale_date": "2017-01-25"
}

就是要看宝马的销售记录

2条数据,做一个演示,每个批次下载一条宝马的销售记录,分2个批次给它下载完

SearchResponse scrollResp = client.prepareSearch("car_shop")
		.addTypes("sales")
        .setScroll(new TimeValue(60000))
        .setQuery(termQuery("brand.raw", "宝马"))
        .setSize(1)
        .get(); 

do {
    for (SearchHit hit : scrollResp.getHits().getHits()) {
    	
    }
    
    scrollResp = client.prepareSearchScroll(scrollResp.getScrollId())
            .setScroll(new TimeValue(60000))
            .execute()
            .actionGet();
} while(scrollResp.getHits().getHits().length != 0);

87_Java API_基于search template实现按品牌分页查询模板

课程大纲

搜索模板的功能,java api怎么去调用一个搜索模板

page_query_by_brand.mustache

{
  "from": {{from}},
  "size": {{size}},
  "query": {
    "match": {
      "brand.keyword": "{{brand}}" 
    }
  }
}

SearchResponse sr = new SearchTemplateRequestBuilder(client)
    .setScript("page_query_by_brand")                 
    .setScriptType(ScriptService.ScriptType.FILE) 
    .setScriptParams(template_params)             
    .setRequest(new SearchRequest())              
    .get()                                        
    .getResponse(); 

88_Java API_对汽车品牌进行全文检索、精准查询和前缀搜索

课程大纲

PUT /car_shop/cars/5
{
        "brand": "华晨宝马",
        "name": "宝马318",
        "price": 270000,
        "produce_date": "2017-01-20"
}

SearchResponse response = client.prepareSearch("car_shop")
        .setTypes("cars")
        .setQuery(QueryBuilders.matchQuery("brand", "宝马"))                
        .get();

SearchResponse response = client.prepareSearch("car_shop")
        .setTypes("cars")
        .setQuery(QueryBuilders.multiMatchQuery("宝马", "brand", "name"))                
        .get();

SearchResponse response = client.prepareSearch("car_shop")
        .setTypes("cars")
        .setQuery(QueryBuilders.commonTermsQuery("name", "宝马320"))                
        .get();

SearchResponse response = client.prepareSearch("car_shop")
        .setTypes("cars")
        .setQuery(QueryBuilders.prefixQuery("name", "宝"))                
        .get();

89_Java API_对汽车品牌进行多种条件的组合搜索

课程大纲

QueryBuilder qb = boolQuery()
    .must(matchQuery("brand", "宝马"))    
    .mustNot(termQuery("name.raw", "宝马318")) 
    .should(termQuery("produce_date", "2017-01-02"))  
    .filter(rangeQuery("price").gte("280000").lt("350000"));

SearchResponse response = client.prepareSearch("car_shop")
        .setTypes("cars")
        .setQuery(qb)                
        .get();

90_Java API_基于地理位置对周围汽车4S店进行搜索

课程大纲

<dependency>
    <groupId>org.locationtech.spatial4j</groupId>
    <artifactId>spatial4j</artifactId>
    <version>0.6</version>                        
</dependency>

<dependency>
    <groupId>com.vividsolutions</groupId>
    <artifactId>jts</artifactId>
    <version>1.13</version>                         
    <exclusions>
        <exclusion>
            <groupId>xerces</groupId>
            <artifactId>xercesImpl</artifactId>
        </exclusion>
    </exclusions>
</dependency>

比如我们有很多的4s店,然后呢给了用户一个app,在某个地方的时候,可以根据当前的地理位置搜索一下,自己附近的4s店

POST /car_shop/_mapping/shops
{
  "properties": {
      "pin": {
          "properties": {
              "location": {
                  "type": "geo_point"
              }
          }
      }
  }
}

PUT /car_shop/shops/1
{
    "name": "上海至全宝马4S店",
    "pin" : {
        "location" : {
            "lat" : 40.12,
            "lon" : -71.34
        }
    }
}

第一个需求:搜索两个坐标点组成的一个区域

QueryBuilder qb = geoBoundingBoxQuery("pin.location").setCorners(40.73, -74.1, 40.01, -71.12); 

第二个需求:指定一个区域,由三个坐标点,组成,比如上海大厦,东方明珠塔,上海火车站

List<GeoPoint> points = new ArrayList<>();             
points.add(new GeoPoint(40.73, -74.1));
points.add(new GeoPoint(40.01, -71.12));
points.add(new GeoPoint(50.56, -90.58));

QueryBuilder qb = geoPolygonQuery("pin.location", points); 

第三个需求:搜索距离当前位置在200公里内的4s店

QueryBuilder qb = geoDistanceQuery("pin.location").point(40, -70).distance(200, DistanceUnit.KILOMETERS);   

SearchResponse response = client.prepareSearch("car_shop")
        .setTypes("shops")
        .setQuery(qb)                
        .get();

91_Java API_如何自己尝试API以掌握所有搜索和聚合的语法

课程大纲

1、自己要什么query,自己去用QueryBuilders去尝试,disMaxQuery
2、自己的某种query,有一些特殊的参数,tieBreaker,自己拿着query去尝试,点一下,看ide的自动提示,自己扫一眼,不就知道有哪些query,哪些参数了
3、如果不是query,是聚合,也是一样的,你就拿AggregationBuilders,点一下,看一下ide的自动提示,我们讲过的各种语法,都有了
4、包括各种聚合的参数,也是一样的,找到对应的AggregationBuilder之后,自己点一下,找需要的参数就可以了
5、自己不断尝试,就o了,组装了一个搜索,或者聚合,自己试一下,测一下

如果你实在找不到,搞不定,就QQ来找我,当然别自己一开始就跑来找我,先自己努力研究一下

有些人说,可以上官网,官网api也没这么全

92_快速入门篇以及高手进阶篇课程总结,以及后续阶段课程介绍

课程大纲

1、快速入门:能了解最最基本的es的一些使用
2、分布式原理:了解es的基本原理
3、分布式文档系统:基本精通es的document相关的一些操作和开发
4、初识搜索引擎:掌握es最核心,最基本的一些搜索的技术
5、索引管理:掌握了基本的es的索引相关的操作
6、内核原理探秘:深入理解的es的底层的原理
7、Java API初步使用:掌握最基础的java api的使用

开始把es用起来,可以玩儿起来,掌握了一些基本的知识,自己在公司做一些最最简单的小项目,也ok

1、深度探秘搜索技术:彻底深入的了解各种高级搜索技术,精通搜索底层原理
2、彻底掌握IK中文分词器:彻底掌握,连源码的修改都讲过了,怎么基于mysql热加载你的词库
3、深入聚合数据分析:彻底深入的掌握了各种各样的数据聚合分析的功能
4、数据建模实战:对模拟真实世界的有复杂关系的数据模型,讲解了建模、搜索和聚合
5、elasticsearch高手进阶:高级功能,搜索模板,term vector,地理位置的搜索和聚合
6、java api:核心的java api的现场演示,如何自己去摸索所有的java api的使用

你做一些小型的项目,数据量不大,简单在自己公司部署几个节点的es,3个节点

玩儿各种各样的搜索,聚合,中文分词,有关联和层次关系的数据如何建模,document如何管理和操作,索引的基本管理,es的核心原理,java api的系统的使用,高级的功能,基于地理位置的应用的开发

你都能搞定了

你只能做es的小型项目,或者大型项目,但是数据量大不了

============================================================================================================================

两个篇章

1、运维优化:生产环境的大规模集群的部署、运维管理、监控、安全管理、升级、性能优化、索引管理,大型es集群的运维知识,包括海量数据场景下的性能的调优,还有一个大数据场景的应用系统的设计,范式

搞java的,了解什么es的运维。。。。

你要是搞java的,结果不了解es运维,你也别做es的大型项目,大数据场景下的,你根本就不了解集群,不了解大数据集群环境下的一些特殊的配置,安全,监控,es全景图,概览

你要是搞java,基于es集群,大数据量做开发,你不了解上面这些东西,你碰到了问题,就抓瞎

你如果真是搞java的,最自己的技术有追求,希望自己出去找工作,技术牛逼一些,不要给自己设限制,开发,不要了解运维。如果你是个java架构师,你连es集群相关的知识都不懂,你碰到了问题,你的项目遇到了一些的报错,你都搞不定,你还当什么架构师,或者项目经理

如果你对技术有追求,就好好学一下

2、项目实战:各种各样的案例,作为背景,模拟现实,来用业务驱动课程的讲解,和动手的实战,更好的理解、吸收、刺激你的对技术如何运用的灵感

大型门户网站的搜索引擎系统:安全模拟真实大型搜索引擎系统的架构,特殊的点,降级,防止雪崩,缓存,架构怎么拆分,复杂的搜索引擎怎么构建,讲解

大型电商网站的数据分析系统:完全用真实的复杂的电商的业务场景,去开发一整套完整的涵盖数十个分析模块的es数据分析系统

后面两个篇章才是真正的拔高的地方,如果你对自己技术有追求,想把技术学好一些,建议,前面两个篇章,至少看个2遍,彻底掌握;后面2个篇章可以到时候好好看看

93-103节

93_es生产集群部署之硬件配置、jvm以及集群规划建议

我们之前一直是在windows环境下去启动一个单节点的es进程,然后去学习和练习各种es的高阶的搜索技术,聚合技术

主要针对的是es的开发,你可以认为是,如果你是一个java工程师的话,然后你们公司已经有人去维护和搭建一个es集群了,那么你直接掌握之前的内容,就足够了

你就可以建立需要的索引,写入数据,搜索,聚合

很多同学,还是需要自己去规划、设计和搭建一个es集群出来,先,然后才能基于es去做一些开发这样的

肯定不是在windows操作系统上去搞了,肯定是在linux集群上面去部署咯

规划一下,比如你需要几台机器,物理机,还是虚拟机呢?每台机器要多大的资源,多少G内存,多少个cpu,多大的磁盘空间,等等,然后对生产环境的机器相关的配置有没有什么特殊的要求,对于es来说。你需要处理多大的数据量?需要多大的集群才能支撑的住?

这块规划好之后,就是搞到一堆你需要的机器,符合你的需求和要求的机器,机器弄好之后,就可以开始在上面部署和玩儿你的es了

是这样的,我们的课程,肯定是只能用虚拟机去演练和模拟,在机器的配置上,能够在课程现场支撑的数据量上,是绝对达不到生产环境的

尽量保证,给大家用虚拟机去还原和模拟一些场景,然后讲解各种相关的技术知识

一般来说,如果我们刚开始用es,都是先在自己的笔记本电脑上,或者是几个虚拟机组成的小集群上,安装一个es,然后开始学习和试用其中的功能。但是如果我们要将es部署到生产环境中,那么是由很多额外的事情要做的。需要考虑我们部署的机器的内存、CPU、磁盘、JVM等各种资源和配置。

  • 1、内存

es是很吃内存的,es吃的主要不是你的jvm的内存,一般来说es用jvm heap(堆内存)还是用的比较少的,主要吃的是你的机器的内存

es底层基于lucene,lucene是基于磁盘文件来读写和保存你的索引数据的,倒排索引,正排索引,lucene的特点就是会基于os filesystem cache,会尽量将频繁访问的磁盘文件的数据在操作系统的内存中进行缓存,然后尽量提升磁盘文件读写的性能

很多同学都问我说,es的性能感觉不太理想,es的性能80%取决于说,你的机器上,除了分配给jvm heap内存以外,还剩下多少内存,剩下的内存会留给es的磁盘索引文件做缓存,如果os cache能够缓存更多的磁盘文件的数据,索引文件的数据,索引读写的性能都会高很多,特别是检索

但是如果你的大量的索引文件在os cache中放不下,还是停留在磁盘上,那么搜索、聚合的时候大量的都是读写磁盘文件,性能当然低了,一个数量级,ms级,s级

问我,es上千万数据的搜索,要耗费10s,大量读写磁盘了

如果在es生产环境中,哪种资源最容易耗尽,那么就是内存了。排序和聚合都会耗费掉很多内存,所以给es进程分配足够的jvm heap内存是很重要的。除了给jvm heap分配内存,还需要给予足够的内存给os filesystem cache。因为lucene用的数据结构都是给予磁盘的格式,es是通过os cache来进行高性能的磁盘文件读写的。

关于机器的内存相关的知识,后面会有很深入的讲解,这里先简单提一下,一般而言,除非说你的数据量很小,比如就是一些os系统,各种信息管理系统,要做一个内部的检索引擎,几万,几十万,几百万的数据量,对机器的资源配置要求还是蛮低的。一般而言,如果你的数据量过亿,几亿,几十亿。那么其实建议你的每台机器都给64G的内存的量。

如果一个机器有64G的内存,那么是比较理想的状态,但是32GB和16GB的内存也是ok的。具体的内存数量还是根据数据量来决定。但是如果一个机器的内存数量小于8G,那么就不太适合生产环境了,因为我们可能就需要很多小内存的机器来搭建集群。而大于64G的机器也不是很有必要。

笔记本电脑,24G内存(16G+8G),双核,虚拟机4台2核4G

  • 2、CPU

大多数的es集群对于cpu的要求都会比较低一些,因此一台机器有多少个cpu core其实对生产环境的es集群部署相对来说没有那么的重要了,至少没有内存来的重要。当然,肯定是要用多核处理器的,一般来说2个cpu core~8个cpu core都是可以的。

此外,如果要选择是较少的cpu core但是cpu性能很高,还是较多的cpu core但是cpu性能较为一般,那么肯定是选择性能较为一般但是更多的cpu core。因为更多的cpu core可以提供更强的并发处理能力,远比单个cpu性能高带来的效果更加明显。

  • 3、磁盘

对于es的生产环境来说,磁盘是非常重要的,尤其是对那些大量写入的es集群,比如互联网公司将每天的实时日志数据以高并发的速度写入es集群。在服务器上,磁盘是最慢的那个资源,所以对于大量写入的es集群来说,会很容易因为磁盘的读写性能造成整个集群的性能瓶颈。

如果我们能够使用SSD固态硬盘,而不是机械硬盘,那么当然是最好的,SSD的性能比机械硬盘可以高很多倍,可以让es的读写性能都高很多倍。所以,如果公司出的起钱大量使用固态硬盘,那么当然是最好的。

连我的笔记本电脑,都是有100多G的SSD啊,还有1T的机械硬盘

如果我们在用SSD硬盘的化,那么需要检查我们的I/O scheduler,需要正确的配置IO scheduler。当我们将数据写入磁盘时,IO scheduler会决定什么时候数据才会真正的写入磁盘,而不是停留在os cache内存缓冲中。大
机器上,默认的IO scheduler是cfq,也就是completely fair queuing。

这个scheduler会给每个进程都分配一些时间分片,time slice,然后会优化每个进程的数据如何写入磁盘中,优化的思路主要 是根据磁盘的物理布局来决定如何将数据写入磁盘,进而提升写入磁盘的性能。这是针对机械硬盘做出的优化,因为机械硬盘是一种旋转存储介质,是通过机械旋转磁盘+磁头进行磁盘读写的机制。

但是scheduler的这种默认的执行机制,对于SSD来说是不太高效的,因为SSD跟机械硬盘是不一样的,SSD不涉及到机械磁盘旋转和磁头读取这种传统的读写机制。对于SSD来说,应该用deadline/noop scheduler。deadline scheduler会基于写操作被pending了多长时间来进行写磁盘优化,而noop scheduler就是一个简单的FIFO队列先进先出的机制。

调整io scheduler可以带来很大的性能提升,甚至可以达到数百倍。

如果我们没有办法使用SSD,只能使用机械硬盘,那么至少得尽量正确读写速度最快的磁盘,比如高性能的服务器磁盘。

此外,使用RAID 0也是一种提升磁盘读写速度的高效的方式,无论是对于机械硬盘,还是SSD,都一样。RAID 0也被称之为条带式存储机制,striping,在RAID各种级别中性能是最高的。RAID 0的基本原理,是把连续的数据分散存储到多个磁盘上进行读写,也就是对数据进行条带式存储。这样系统的磁盘读写请求就可以被分散到多个磁盘上并行执行。但是没有必要使用镜像或者RAID的其他模式,因为我们不需要通过RAID来实现数据高可用存储,es的replica副本机制本身已经实现了数据高可用存储。

最后,我们要避免跟网络相关的存储模式,network-attached storage,NAS,比如基于网络的分布式存储模式。虽然很多供应商都说他们的NAS解决方案性能非常高,而且比本地存储的可靠性更高。但是实际上用起来会有很多性能和可靠性上的风险,一般因为网络传输会造成较高的延时,同时还有单点故障的风险。

  • 4、网络

对于es这种分布式系统来说,快速而且可靠的网络是非常的重要的。因为高速网络通信可以让es的节点间通信达到低延时的效果,高带宽可以让shard的移动和恢复,以及分配等操作更加的快速。现代的数据中心的网络对于大多数的集群来说,性能都足够高了。比如千兆网卡,这都是可以的。

但是要避免一个集群横跨多个数据中心,比如异地多机房部署一个集群,因为那样的话跨机房,跨地域的传输会导致网络通信和数据传输性能较差。es集群是一种p2p模式的分布式系统架构,不是master-slave主从分布式系统。在es集群中,所有的node都是相等的,任意两个node间的互相通信都是很频繁和正常的。因此如果部署在异地多机房,那么可能会导致node间频繁跨地域进行通信,通信延时会非常高,甚至造成集群运行频繁不正常。

就跟NAS存储模式一样,很多供应商都说跨地域的多数据中心是非常可靠的,而且低延时的。一般来说,可能的确是这样,但是一旦发生了网络故障,那么集群就完了。通常来说,跨地域多机房部署一个es集群带来的效益,远远低于维护这样的集群所带来的额外成本。

  • 5、自建集群 vs 云部署

现在一般很容易就可以拿到高性能的机器来部署集群:很多高性能的机器可以有上百G的内存资源,还有几十个cpu core。但是同时我们也可以再云供应商上,比如阿里云,租用大量的小资源的虚拟机。那么对于自己购买昂贵高性能服务器自建集群,以及租用云机器来部署,该选择哪种方案呢?

你是自己购买5台,比如说,8核64G的物理机,搭建es集群

或者是,比如说,上阿里云,或者其他的云服务,购买了2核4G,16台,虚拟机,搭建es集群

你上阿里云,也可以买到大资源量的虚拟机,4/8/16核64G

一般来说,对于es集群而言,是建议拥有少数机器,但是每个机器的资源都非常多,尽量避免拥有大量的少资源的虚拟机。因为对于运维和管理来说,管理5个物理机组成的es集群,远远比管理100个虚拟机组成的es集群要简单的多。

同时即使是自建集群,也要尽量避免那种超大资源量的超级服务器,因为那样可能造成资源无法完全利用,然后在一个物理机上部署多个es节点,这会导致我们的集群管理更加的复杂。

  • 6、JVM

对于最新的es版本,一般多建议用最新的jvm版本,除非es明确说明要用哪个jdk版本。es和lucene都是一种满足特殊需求的软件,lucene的单元测试和集成测试中,经常会发现jvm自身的一些bug。这些bug涵盖的范围很广,因此尽量用最新的jvm版本,bug会少一些。

就目前es 5.x版本而言,建议用jdk 8,而不是jdk 7,同时jdk 6已经不再被支持了。

如果我们用java编写es应用程序,而且在使用transport client或者node client,要确保运行我们的应用程序的jvm版本跟es服务器运行的jvm版本是一样的。在es中,有些java的本地序列化机制都被使用了,比如ip地址,异常信息,等等。而jvm可能在不同的minor版本之间修改序列化格式,所以如果client和server的jvm版本不一致,可能有序列化的问题。

同时官方推荐,绝对不要随便调整jvm的设置。虽然jvm有几百个配置选项,而且我们可以手动调优jvm的几乎方方面面。同时遇到一个性能场景的时候,每个人都会第一时间想到去调优jvm,但是es官方还是推荐我们不要随便调节jvm参数。因为es是一个非常复杂的分布式软件系统,而且es的默认jvm配置都是基于真实业务场景中长期的实践得到的。随便调节jvm配置反而有可能导致集群性能变得更加差,以及出现一些未知的问题。反而是很多情况下,将自定义的jvm配置全部删除,性能是保持的最好的。

  • 7、容量规划

在规划你的es集群的时候,一般要规划你需要多少台服务器,每台服务器要有多少资源,能够支撑你预计的多大的数据量。但是这个东西其实不是一概而论的,要视具体的读写场景,包括你执行多么复杂的操作,读写QPS来决定的。不过一般而言,根据讲师的实际经验,对于很多的中小型公司,都是建议es集群承载的数据量在10亿规模以内。用最合理的技术做最合理的事情。

这里可以给出几个在国内es非常适合的几个场景,es是做搜索的,当然可以做某个系统的搜索引擎。比如网站或者app的搜索引擎,或者是某些软件系统的搜索引擎,此外es还可以用来做数据分析。那么针对这几个不同的场景,都可以给出具体建议。比如做网站或者app的搜索引擎,一般数据量会相对来说大一些,但是通常而言,一个网站或者app的内容都是有限的,不会无限膨胀,通常数据量从百万级到亿级不等,因此用于搜索的数据都放在es中是合理的。

然后一些软件系统或者特殊项目的搜索引擎,根据项目情况不同,数据量也是从百万量级到几十亿,甚至几百亿,或者每日增量几亿,都有可能,那么此时就要根据具体的业务场景来决定了。如果数据量特别大,日增量都几亿规模,那么其实建议不要将每天全量的数据都写入es中,es也不适合这种无限规模膨胀的场景。es是很耗费内存的,无限膨胀的数据量,会导致我们无法提供足够的资源来支撑这么大的数据量。可以考虑是不是就将部分热数据,比如最近几天的数据,放到es中做高频高性能搜索,然后将大量的很少访问的冷数据放大数据系统做离线批量处理,比如hadoop系统里面。

比如说,你预计一下,你的数据量有多大,需要多少台机器,每台机器要多少资源,来支撑,可以达到多大的性能

数据量 -> 性能,10亿 -> 1s

es达到ms级的化,你必须要有足够的os cache去缓存几乎大部分的索引数据

10亿,每条数据是多大,比如多少个字节,1k -> 100G

5台,64G,8核,300G -> 100G总数据量,300G,一般要分给es jvm heap,150G -> 100G,100G落地到磁盘文件加入很多es自己的信息,100G -> 200G

200G落地磁盘的数据,物理内存剩余的只有150G,可能还有一些操作系统,还有其他的损耗100G

200G落地磁盘的数据,100G物理内存可以用来做os cache,50%的概率是基于os cache做磁盘索引文件的读写,几秒,很正常啦。。。

根据我们的实践经验而言,一般来说,除非是你的机器的内存资源,完全可以容纳所有的落地的磁盘文件的os cache,ms,否则的话,如果不是的话,会大量走磁盘,几秒

同时如果数据量在10亿以内的规模,那么一般而言,如果提供5台以上的机器,每台机器的配置到8核64G的配置,一般而言都能hold住。当然,这个也看具体的使用场景,如果你读写特别频繁,或者查询特别复杂,那么可能还需要更多的机器资源。如果你要承载更大的数据量,那么就相应的提供更多的机器和资源。

要提升你的es的性能,最重要的,还是说规划合理的数据量,物理内存资源大小,os cache

  • 0
    点赞
  • 0
    评论
  • 1
    收藏
  • 打赏
    打赏
  • 扫一扫,分享海报

©️2022 CSDN 皮肤主题:编程工作室 设计师:CSDN官方博客 返回首页

打赏作者

hd20086996

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值