Source
在 Elasticsearch 中,通常每个文档的每一个字段都会被存储在 shard 里存放 source 的地方,比如:
代码语言:javascript复制PUT twitter/_doc/2
{
"user": "双榆树-张三",
"message": "今儿天气不错啊,出去转转去",
"uid": 2,
"age": 20,
"city": "北京",
"province": "北京",
"country": "中国",
"name": {
"firstname": "三",
"surname": "张"
},
"address": [
"中国北京市海淀区",
"中关村29号"
],
"location": {
"lat": "39.970718",
"lon": "116.325747"
}
}
在这里,我们创建了一个 id 为2的文档。我们可以通过如下的命令来获得它的所有的存储的信息。
代码语言:javascript复制GET twitter/_doc/2
{
"_index" : "twitter",
"_type" : "_doc",
"_id" : "2",
"_version" : 1,
"_seq_no" : 0,
"_primary_term" : 1,
"found" : true,
"_source" : {
"user" : "双榆树-张三",
"message" : "今儿天气不错啊,出去转转去",
"uid" : 2,
"age" : 20,
"city" : "北京",
"province" : "北京",
"country" : "中国",
"name" : {
"firstname" : "三",
"surname" : "张"
},
"address" : [
"中国北京市海淀区",
"中关村29号"
],
"location" : {
"lat" : "39.970718",
"lon" : "116.325747"
}
}
}
在上面的 _source 里我们可以看到 Elasticsearch 为我们所存下的所有的字段。如果我们不想存储任何的字段,那么我们可以做如下的设置:
代码语言:javascript复制DELETE twitter
PUT twitter
{
"mappings": {
"_doc": {
"_source": {
"enabled": false
}
}
}
}
那么我们使用如下的命令来创建一个 id 为1的文档:
代码语言:javascript复制PUT twitter/_doc/1
{
"user": "双榆树-张三",
"message": "今儿天气不错啊,出去转转去",
"uid": 2,
"age": 20,
"city": "北京",
"province": "北京",
"country": "中国",
"name": {
"firstname": "三",
"surname": "张"
},
"address": [
"中国北京市海淀区",
"中关村29号"
],
"location": {
"lat": "39.970718",
"lon": "116.325747"
}
}
那么同样地,我们来查询一下这个文档:
代码语言:javascript复制GET twitter/_doc/1
{
"_index" : "twitter",
"_type" : "_doc",
"_id" : "1",
"_version" : 1,
"_seq_no" : 0,
"_primary_term" : 1,
"found" : true
}
显然我们的文档是被找到了,但是我们看不到任何的 source。那么我们能对这个文档进行搜索吗?尝试如下的命令:
代码语言:javascript复制GET twitter/_search
{
"query": {
"match": {
"city": "北京"
}
}
}
{
"took" : 2,
"timed_out" : false,
"_shards" : {
"total" : 5,
"successful" : 5,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : 1,
"max_score" : 0.5753642,
"hits" : [
{
"_index" : "twitter",
"_type" : "_doc",
"_id" : "1",
"_score" : 0.5753642
}
]
}
}
显然这个文档 id 为1的文档可以被正确地搜索,也就是说它有完好的 inverted index 供我们查询,虽然它没有它的 source。
那么我们如何有选择地进行存储我们想要的字段呢?这种情况适用于我们想节省自己的存储空间,只存储那些我们需要的字段到source里去。我们可以做如下的设置:
代码语言:javascript复制DELETE twitter
PUT twitter
{
"mappings": {
"_doc": {
"_source": {
"includes": [
"*.lat",
"address",
"name.*"
],
"excludes": [
"name.surname"
]
}
}
}
}
在上面,我们使用 include 来包含我们想要的字段,同时我们通过 exclude 来去除那些不需要的字段。我们尝试如下的文档输入:
代码语言:javascript复制PUT twitter/_doc/1
{
"user": "双榆树-张三",
"message": "今儿天气不错啊,出去转转去",
"uid": 2,
"age": 20,
"city": "北京",
"province": "北京",
"country": "中国",
"name": {
"firstname": "三",
"surname": "张"
},
"address": [
"中国北京市海淀区",
"中关村29号"
],
"location": {
"lat": "39.970718",
"lon": "116.325747"
}
}
通过如下的命令来进行查询,我们可以看到:
代码语言:javascript复制GET twitter/_doc/1
{
"_index" : "twitter",
"_type" : "_doc",
"_id" : "1",
"_version" : 1,
"_seq_no" : 0,
"_primary_term" : 1,
"found" : true,
"_source" : {
"address" : [
"中国北京市海淀区",
"中关村29号"
],
"name" : {
"firstname" : "三"
},
"location" : {
"lat" : "39.970718"
}
}
}
显然,我们只有很少的几个字段被存储下来了。通过这样的方法,我们可以有选择地存储我们想要的字段。
在实际的使用中,我们在查询文档时,也可以有选择地进行显示我们想要的字段,尽管有很多的字段被存于source中:
代码语言:javascript复制GET twitter/_doc/1?_source=name,location
在这里,我们只想显示和name及location相关的字段,那么显示的结果为:
代码语言:javascript复制{
"_index" : "twitter",
"_type" : "_doc",
"_id" : "1",
"_version" : 1,
"_seq_no" : 0,
"_primary_term" : 1,
"found" : true,
"_source" : {
"name" : {
"firstname" : "三"
},
"location" : {
"lat" : "39.970718"
}
}
}
倒排索引
默认情况下,Elasticsearch 在文档中的所有字段上构建一个反向索引,指向该字段所在的 Elasticsearch 文档。也就是说在每个 Elasticsearch 的Lucene里,有一个位置存放这个 inverted index。
在 Kibana 中,我们建立一个如下的文档:
代码语言:javascript复制PUT twitter/_doc/1
{
"user": "双榆树-张三",
"message": "今儿天气不错啊,出去转转去",
"uid": 2,
"age": 20,
"city": "北京",
"province": "北京",
"country": "中国",
"name": {
"firstname": "三",
"surname": "张"
},
"address": [
"中国北京市海淀区",
"中关村29号"
],
"location": {
"lat": "39.970718",
"lon": "116.325747"
}
}
当这个文档被建立好以后,Elastic 就已经帮我们建立好了相应的 inverted index 供我们进行搜索,比如:
代码语言:javascript复制GET twitter/_search
{
"query": {
"match": {
"user": "张三"
}
}
}
{
"took" : 4,
"timed_out" : false,
"_shards" : {
"total" : 5,
"successful" : 5,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : 1,
"max_score" : 0.5753642,
"hits" : [
{
"_index" : "twitter",
"_type" : "_doc",
"_id" : "1",
"_score" : 0.5753642,
"_source" : {
"user" : "双榆树-张三",
"message" : "今儿天气不错啊,出去转转去",
"uid" : 2,
"age" : 20,
"city" : "北京",
"province" : "北京",
"country" : "中国",
"name" : {
"firstname" : "三",
"surname" : "张"
},
"address" : [
"中国北京市海淀区",
"中关村29号"
],
"location" : {
"lat" : "39.970718",
"lon" : "116.325747"
}
}
}
]
}
}
如果我们想不让我们的某个字段不被搜索,也就是说不想为这个字段建立 inverted index,那么我们可以这么做:
代码语言:javascript复制DELETE twitter
PUT twitter
{
"mappings": {
"_doc": {
"properties": {
......
"uid": {
"type": "long"
},
"user": {
"type": "object",
"enabled": false
}
}
}
}
}
PUT twitter/_doc/1
{
"user": "双榆树-张三",
"message": "今儿天气不错啊,出去转转去",
"uid": 2,
"age": 20,
"city": "北京",
"province": "北京",
"country": "中国",
"name": {
"firstname": "三",
"surname": "张"
},
"address": [
"中国北京市海淀区",
"中关村29号"
],
"location": {
"lat": "39.970718",
"lon": "116.325747"
}
}
在上面,我们通过 mapping 对 user 字段进行了修改
"user":{"type": "object","enabled": false}
也就是说这个字段将不被建立索引,我们如果使用这个字段进行搜索的话,不会产生任何的结果:
代码语言:javascript复制GET twitter/_search
{
"query": {
"match": {
"user": "张三"
}
}
}
{
"took" : 2,
"timed_out" : false,
"_shards" : {
"total" : 5,
"successful" : 5,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : 0,
"max_score" : null,
"hits" : [ ]
}
}
显然是没有任何的结果。但是如果我们对这个文档进行查询的话:
代码语言:javascript复制GET twitter/_doc/1
{
"_index" : "twitter",
"_type" : "_doc",
"_id" : "1",
"_version" : 1,
"_seq_no" : 0,
"_primary_term" : 1,
"found" : true,
"_source" : {
"user" : "双榆树-张三",
"message" : "今儿天气不错啊,出去转转去",
"uid" : 2,
"age" : 20,
"city" : "北京",
"province" : "北京",
"country" : "中国",
"name" : {
"firstname" : "三",
"surname" : "张"
},
"address" : [
"中国北京市海淀区",
"中关村29号"
],
"location" : {
"lat" : "39.970718",
"lon" : "116.325747"
}
}
}
显然 user 的信息是存放于 source 里的。只是它不被我们所搜索而已。
如果我们不想我们的整个文档被搜索,我们甚至可以直接采用如下的方法:
代码语言:javascript复制DELETE twitter
PUT twitter
{
"mappings": {
"_doc": {
"enabled": false
}
}
}
那么整个 twitter 索引将不建立任何的 inverted index,那么我们通过如下的命令:
代码语言:javascript复制PUT twitter/_doc/1
{
"user": "双榆树-张三",
"message": "今儿天气不错啊,出去转转去",
"uid": 2,
"age": 20,
"city": "北京",
"province": "北京",
"country": "中国",
"name": {
"firstname": "三",
"surname": "张"
},
"address": [
"中国北京市海淀区",
"中关村29号"
],
"location": {
"lat": "39.970718",
"lon": "116.325747"
}
}
GET twitter/_search
{
"query": {
"match": {
"city": "北京"
}
}
}
但是GET twitter/_doc/1 依然是可以查询到的
上面的命令执行的结果是,没有任何搜索的结果。更多阅读,可以参阅 “Mapping parameters: enabled”。
我们也可以使用如下的方式来使得我们禁止对一个字段进行查询:
代码语言:javascript复制{ "mappings": { "properties": { "http_version": { "type": "keyword", "index": false } ... } }}
上面的设置使得 http_version 不被索引。上面的 mapping 使得我们不能对 http_version 字段进行搜索,从而节省磁盘空间,但是它并不妨碍我们对该字段进行 aggregation 及对 source 的访问。
Doc_values
之前介绍过,倒排索引的数据组织方式大概是这样的:
如果我要查询包含brown的文档有哪些?这个就是全文检索了,也相当好办,先从词典里遍历到brown这个单词,然后根据倒排索引查得 Doc_1 和 Doc_2 包含这个单词。那如果我要查 Doc_1 和 Doc_2 包含的单词分别有什么?这个用倒排索引的话开销会非常大,至少是要将整张表关于 Doc_1 和 Doc_2 的列数据遍历一遍才行。这时候我们将数据换一种组织形式,将会起到非常好的效果。
Doc_1 和 Doc_2 存了什么单词,一目了然。当然对于数字类型的字段也是一样的。我们把这种数据的组织方式叫做doc_value。
倒排索引的特点很明显,就是为了全文检索而生的,但是对于一些聚合查询(排序、求平均值等等)的场景来说,显然不适用。那么这样一来我们为了应对一些聚合场景就需要结构化数据来应付,这里说的结构化数据就是『列存储』,也就是上面说的doc_value
。
When searching, we need to be able to map a term to a list of documents.When sorting, we need to map a document to its terms. In other words, we need to “uninvert” the inverted index.This “uninverted” structure is often called a “column-store” in other systems. Essentially, it stores all the values for a single field together in a single column of data, which makes it very efficient for operations like sorting
doc_value在 ES 中有几个应用场景:
- 对某个字段排序;
- 某个字段聚合查询( max/min/count );
- 部分过滤器 ( 地理位置过滤器 );
- 某个字段的脚本执行。等等。
doc_value
是顺序存储到磁盘的,因此访问是很快的。当我们所处理的集合小于所给的 JVM 堆内存,那么整个数据集合是会被加载到内存里的;如果数据集合大于所给的堆内存,那么就会分页加载到内存之中,而不会报出『OutOfMemory Error』。
Doc values
是在文档索引时构建的磁盘数据结构,这使这种数据访问模式成为可能。它们存储与 _source 相同的值,但以面向列(column)的方式存储,这对于排序和聚合而言更为有效。几乎所有字段类型都支持Doc值,但对字符串字段除外 (text 及annotated_text)。Doc values
告诉你对于给定的文档 ID,字段的值是什么。比如,当我们向Elasticsearch中加入如下的文档:
PUT cities
{
"mappings": {
"_doc": {
"properties": {
"city": {
"type": "keyword"
}
}
}
}
}
PUT cities/_doc/1
{
"city": "Wuhan"
}
PUT cities/_doc/2
{
"city": "Beijing"
}
PUT cities/_doc/3
{
"city": "Shanghai"
}
那么将在 Elasticsearch 中将创建像如下的 doc_values 的一个列存储(Columnar store)表格:
doc id | city |
---|---|
1 | Wuhan |
2 | Beijing |
3 | Shanghai |
默认情况下,所有支持 doc 值的字段均已启用它们。如果您确定不需要对字段进行排序或汇总,也不需要通过脚本访问字段值,则可以禁用 doc 值以节省磁盘空间:
比如我们可以通过如下的方式来使得 city 字段不可以做 sort 或 aggregation:
代码语言:javascript复制DELETE twitter
PUT twitter
{
"mappings": {
"_doc": {
"properties": {
"city": {
"type": "keyword",
"doc_values": false,
"ignore_above": 256
},
......
"uid": {
"type": "long"
},
"user": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
}
}
}
}
}
在上面,我们把 city 字段的 doc_values 设置为 false。
代码语言:javascript复制"city": {"type": "keyword","doc_values": false,"ignore_above": 256}
我们通过如下的方法来创建一个文档:
代码语言:javascript复制PUT twitter/_doc/1
{
"user": "双榆树-张三",
"message": "今儿天气不错啊,出去转转去",
"uid": 2,
"age": 20,
"city": "北京",
"province": "北京",
"country": "中国",
"name": {
"firstname": "三",
"surname": "张"
},
"address": [
"中国北京市海淀区",
"中关村29号"
],
"location": {
"lat": "39.970718",
"lon": "116.325747"
}
}
那么,当我们使用如下的方法来进行 aggregation 时:
代码语言:javascript复制GET twitter/_search
{
"size": 0,
"aggs": {
"city_bucket": {
"terms": {
"field": "city",
"size": 10
}
}
}
}
在我们的 Kibana 上我们可以看到:
代码语言:javascript复制{
"error": {
"root_cause": [
{
"type": "illegal_argument_exception",
"reason": "Can't load fielddata on [city] because fielddata is unsupported on fields of type [keyword]. Use doc values instead."
}
],
"type": "search_phase_execution_exception",
"reason": "all shards failed",
"phase": "query",
"grouped": true,
"failed_shards": [
{
"shard": 0,
"index": "twitter",
"node": "nxwtuDczRaOjqdM5QhNwTw",
"reason": {
"type": "illegal_argument_exception",
"reason": "Can't load fielddata on [city] because fielddata is unsupported on fields of type [keyword]. Use doc values instead."
}
}
],
"caused_by": {
"type": "illegal_argument_exception",
"reason": "Can't load fielddata on [city] because fielddata is unsupported on fields of type [keyword]. Use doc values instead.",
"caused_by": {
"type": "illegal_argument_exception",
"reason": "Can't load fielddata on [city] because fielddata is unsupported on fields of type [keyword]. Use doc values instead."
}
}
},
"status": 400
}
显然,我们的操作是失败的。尽管我们不能做 aggregation 及 sort,但是我们还是可以通过如下的命令来得到它的 source:
代码语言:javascript复制GET twitter/_doc/1
{
"_index" : "twitter",
"_type" : "_doc",
"_id" : "1",
"_version" : 1,
"_seq_no" : 0,
"_primary_term" : 1,
"found" : true,
"_source" : {
"user" : "双榆树-张三",
"message" : "今儿天气不错啊,出去转转去",
"uid" : 2,
"age" : 20,
"city" : "北京",
"province" : "北京",
"country" : "中国",
"name" : {
"firstname" : "三",
"surname" : "张"
},
"address" : [
"中国北京市海淀区",
"中关村29号"
],
"location" : {
"lat" : "39.970718",
"lon" : "116.325747"
}
}
}