hanks-wang

去生活,去思考


  • 首页

  • 关于

  • 标签

  • 分类

  • 归档

  • 相册

  • 电影

  • 生活

  • 公益404

  • 搜索

如何阅读技术书籍

发表于 2019-12-22 | 更新于: 2019-12-22 | 分类于 读书 | | 阅读次数:
字数统计: 2.9k 字 | 阅读时长 ≈ 9 分钟

转载自:如何阅读技术书籍

感觉这篇文章说的很对,很中肯。

不知道大家有没有这种感受?当你拿到一本书并准备读时,总是想在几天之内就把它看完;看到后面就不想往前翻;每天以看了多少页书为衡量自己今天的价值,而不是吸收了多少知识;看完一本书总感觉脑袋空空的,看了后面忘了前面的……当然你可能还会碰到很多看书的烦恼,下面我就谈谈自己对看书的一些思考。

  首先这里先做一个假定,假设你手里读的书一定是一本经典的、优秀的书。如果不是,那么就赶紧扔了,一本好书是一个好老师,而一本渣书简直害死人…

  接下来我把计算机类的书大致归为三大类:

  (1)语言方面的书。比如C、Java、Python等等

  (2)算法书籍。《算法导论》、《编程珠玑》等等

  (3)某一特定类别的书。比如操作系统、编译原理、网络、Linux内核、服务器原理等等需要很大的精力才能彻底掌握的书。

  当然这个归类是非常粗浅的,针对不同种类的书,我们的阅读策略也会有所侧重。

首先针对第一类书。诀窍就一个字:练。

  作为一个新手,千万不要陷入语言的细枝末节中,有些人学语言的方式实在是让人哭笑不得,比如他要学Java,他拿着一本java编程思想使劲地看,看了后面忘了前面,把自己搞的痛苦不堪,最后感叹一句:Java真难!(我亲眼见过,,还不止一两个,也是醉了)。还有一些人,从网上或者什么地方找来一些教学视频,然后坐在那一边翘着二郎腿一边貌似“聚精会神”地看,中途偶尔QQ上来人了还要聊几句(哥们,你当是看电影么?)。这两种学习方式看上去不同,其实本质上是一样的,都是一种被动地接受知识的方式。

  这种方式及其低效,记住:代码是敲出来的,不是看或者听出来的!无论你是看书或者看视频,其实都不要紧,关键是一定要自己动手把它敲出来。当书看完或者视频看完后,你手上应该有大量的代码,这个时候再把这些代码敲个十几遍,此时你的脑子里留下的就是代码而不是文字或者声音了。

  说到这,让我想起了我们的英语教育,,个人感觉学英语最有效的方法就是把自己置于一个英语环境或给自己创造一个英语环境,接下来就是使劲地说。。这样,不到几个月,保证你的英语棒棒哒!而不是拼命地背单词或看语法。。(好了,不吐槽英语了O(∩_∩)O!)。

接下来说说该如何看算法书。诀窍:理解+运用。

  学算法不同于学语言,学算法最重要的不是记忆,而是理解。关于如何理解算法那也没什么好说的了,看每个人的悟性还有坚持了,当然去网上找一些容易理解的资料或者跟同学探讨会对你学算法非常有帮助。

  我们这里重点说说为什么要强调运用。因为算法这种东西本质上是比较难理解的,比较难理解的东西就比较容易遗忘,比如你好不容易花了很长时间理解一个动归算法或KMP算法,然后你自认为自己彻底掌握了,然后扔在那几个月不管它。然后就没有然后了……

  因此算法一旦理解了以后相当重要的一点就是一定要去运用,在不同的场景中去运用。那么到底怎么运用呢?比如你的算法书后面的习题,还有现在很多高校有很好的ACM OJ,上面有大量的算法设计题,这些都是非常好的资源。当你用你目前已经理解的算法去解算法题时,这个过程本身会进一步促进你对这个算法的理解,并且加强你的记忆。比如可能过一段时间你把这个算法忘了,但是当你一想起你解的那道题时,你又把那个算法回忆起来了。因为我们人脑总是不太容易记住那些抽象的概念,而对那些具体的东西记得比较牢。

  话说回来,运用算法去解题是非常花费时间的,有时候一道题你可能半天都AC不了。。所以我建议那些现在还在读书的学生,如果你未来想走IT技术的道路,还是去学学算法吧。对你是绝对有好处的!你工作以后可能想学都没时间了!

最后谈谈对第三类书的学习。

  第三类书通常都是一些理论性很强的书,比较难以消化,而且短时期内看不出它有什么价值。对于很多程序员来说感觉用不上。但是可以这么说:对这一类书理解有多深决定了你是一个攻城狮还是一个程序猿。

  当然每个人都有他自己的选择,并不是所有人都想在技术这条路上走得很远,如果你是那个想在技术上成就自己的人,那么欢迎一起来探讨。如果不是,那么就当随便看看好了。

  针对这一类书籍的特点,我总结了自己的读书方法——三遍读书法。是的,没错!起码三遍。对于这类书籍,要么不读,要读起码读三遍。其实,在现实中,有很多人对于自己读过的书是不太愿意再去读的,不管是技术书还是其他方面的书。这可能跟人把读书看得过于功利有关,认为读过的书再去读就是浪费时间,还不如去读新的书。

  如果你问一个人你为什么要读一本书时,他可能会告诉你为了学新的知识或获取新的思想。的确,目的确实没错,可是往往他忽略了一个简单的道理:一本书如果只读一遍,那么你最多只能吸收10%的知识。如果你再读第二遍、第三遍。。。你所吸收的知识就能成倍地增长,当你再读下去的时候,你已经不止是简单地再吸收书本身的知识了,你会形成自己的思考,也许这就是古人说的读书百遍,其义自现吧!

  所以说,如果一本经典的书拿来,你只准备看一遍,那么还是别看了,纯粹浪费时间!既然说一本好书要读好几遍,那么问题来了:我该如何读这本书呢?是从头到脚重复地看还是有其他的方法呢?当然有。我这里为什么要提三遍读书法,这其实是对投入与产出的一种权衡,谁都知道多看几遍越好,但是现实中往往很难让你有足够的时间去反复地看,这时候就需要找到能尽可能用最少的时间来获得最大的收获的方法。

  那么三遍读书法具体是怎么来操作的呢?假如你拿到一本技术书籍

第一遍

  尽可能在比较短的时间内过完这本书。在第一遍中,不要陷入某个具体的知识点,这一遍的主要目的是能把握每个章节的重要知识点,记住,是把握,而不是理解透彻。这一遍讲究的是快、高效。千万不要拖太长的时间。而且对于第一遍读完后的“成果”要牢记,最好把它记录下来,因为它们是整本书的骨架。

第二遍

  第二遍讲究的是慢、理解。第二遍一定要慢,要尽可能地把知识点都理解,千万不要贪快,不要讲究进度。记住,在这一遍中:欲速则不达!遇到实在不懂的点,可以先放一放,等看到后面了再回过头来理解一下,或者请教一下其他人。

  不建议边看边做笔记,最好在读完每一章或者每一节后,把书本合上,然后在脑子里去回忆你看过的部分,尤其是重点知识点,然后把它记录下来,这样比单纯记笔记效率要高很多。(个人比较推荐写博客。千万别觉得自己水平不够,不好意思写博客。记住,写博客不仅仅是为了给别人看,更重要的是对自己学过知识点的一种思路上的整理,通常这种整理都是非常高效的。另外,新手往往觉得写博客很费时间,但是相信我,如果你真的用心在写博客,它绝对不会辜负你的这些时间的!)这样一遍完成后,你对整本书已经有了一个比较完整的理解了。这个时候,你可以去看看你自己写的博客,不用着急读第三遍。

第三遍

  通常如果你认认真真地读完第二遍后,你可以先放一放。人的大脑还是需要一些时间来慢慢消化的,哪怕是潜意识的,况且,短时间内连续读好几遍确实会让人感觉厌烦。这样过了一段时间后(比如去看看一些其它书啊等等),你再开始读第三遍,相信这个时候你很感觉很轻松(人往往对熟悉的知识有一种愉悦感),这个时候你不仅仅巩固了你之前的知识,甚至有可能还会让你产生新的想法,比如当你把操作系统的理论知识都掌握得差不多时,你可能会好奇它究竟是怎么工作的,你会产生想要去读内核源码的冲动。当然这就进入另一种境界啦!(当你研究过源码再来看操作系统的理论书籍时,你甚至都能知道哪些书写得好哪些书写得渣了呢!(^o^)/~)。

  最后,我还是想说,哪怕对于这些理论性很强的书,要想真正得理解它们,还需要动手实践,比如学操作系统的可以试着去看源码,学编译原理的可以试着自己做一个小型编译器,学网络原理的可以自己去搭建网络环境来加深对网络的理解等等。

  借用陆游的名句:纸上得来终觉浅,绝知此事要躬行!

  当然,以上只是我个人对看书的一些想法,也不一定对。每个人都有适合自己的方法。

ES-索引管理

发表于 2019-12-16 | 更新于: 2019-12-16 | 分类于 ES 搜索引擎 | | 阅读次数:
字数统计: 7.3k 字 | 阅读时长 ≈ 29 分钟

参考:
https://es.xiaoleilu.com/070_Index_Mgmt/00_Intro.html

创建索引

1
PUT /new_index

创建更多详细设置的索引:

1
2

删除索引

1
DELTE /new_index

索引设置

两个最重要的设置:

number_of_shards
定义一个索引的主分片个数,默认值是 5。这个配置在索引创建后不能修改。

number_of_replicas
每个主分片的复制分片个数,默认是 1。这个配置可以随时在活跃的索引上修改。

例如,我们可以创建只有一个主分片,没有复制分片的小索引。

1
PUT /my_temp_index
2
{
3
    "settings": {
4
        "number_of_shards" :   1,
5
        "number_of_replicas" : 0
6
    }
7
}

然后,我们可以用 update-index-settings API 动态修改复制分片个数:

1
PUT /my_temp_index/_settings
2
{
3
    "number_of_replicas": 1
4
}

配置分析器

设置 analysis 部分,用来配置已存在的分析器或创建自定义分析器来定制化你的索引。

standard 分析器是用于全文字段的默认分析器,对于大部分西方语系来说是一个不错的选择。它考虑了以下几点:

  • standard 分词器,在词层级上分割输入的文本。
  • standard 标记过滤器,被设计用来整理分词器触发的所有标记(但是目前什么都没做)。
  • lowercase 标记过滤器,将所有标记转换为小写。
  • stop 标记过滤器,删除所有可能会造成搜索歧义的停用词,如 a,the,and,is。

默认情况下,停用词过滤器是被禁用的。如需启用它,你可以通过创建一个基于 standard 分析器的自定义分析器,并且设置 stopwords 参数。可以提供一个停用词列表,或者使用一个特定语言的预定停用词列表。

在下面的例子中,我们创建了一个新的分析器,叫做 es_std,并使用预定义的西班牙语停用词:

1
PUT /spanish_docs
2
{
3
    "settings": {
4
        "analysis": {
5
            "analyzer": {
6
                "es_std": {
7
                    "type":      "standard",
8
                    "stopwords": "_spanish_"
9
                }
10
            }
11
        }
12
    }
13
}

es_std 分析器不是全局的,它仅仅存在于我们定义的 spanish_docs 索引中。为了用 analyze API 来测试它,我们需要使用特定的索引名。

1
GET /spanish_docs/_analyze?analyzer=es_std
2
El veloz zorro marrón

注:es7.5版本下上面的api is invalid。

下面简化的结果中显示停用词 El 被正确的删除了:

1
{
2
  "tokens" : [
3
    { "token" :    "veloz",   "position" : 2 },
4
    { "token" :    "zorro",   "position" : 3 },
5
    { "token" :    "marrón",  "position" : 4 }
6
  ]
7
}

自定义分词器

分析器 是三个顺序执行的组件的结合(字符过滤器,分词器,标记过滤器)。

字符过滤器

字符过滤器是让字符串在被分词前变得更加“整洁”。例如,如果我们的文本是 HTML 格式,它可能会包含一些我们不想被索引的 HTML 标签,诸如

或

。

我们可以使用 html_strip 字符过滤器 来删除所有的 HTML 标签,并且将 HTML 实体转换成对应的 Unicode 字符,比如将 Á 转成 Á。
一个分析器可能包含零到多个字符过滤器。

分词器

一个分析器 必须 包含一个分词器。分词器将字符串分割成单独的词(terms)或标记(tokens)。standard 分析器使用 standard 分词器将字符串分割成单独的字词,删除大部分标点符号,但是现存的其他分词器会有不同的行为特征。

例如,keyword 分词器输出和它接收到的相同的字符串,不做任何分词处理。[whitespace 分词器]只通过空格来分割文本。[pattern 分词器]可以通过正则表达式来分割文本。

标记过滤器

分词结果的 标记流 会根据各自的情况,传递给特定的标记过滤器。

标记过滤器可能修改,添加或删除标记。我们已经提过 lowercase 和 stop 标记过滤器,但是 Elasticsearch 中有更多的选择。stemmer 标记过滤器将单词转化为他们的根形态(root form)。ascii_folding 标记过滤器会删除变音符号,比如从 très 转为 tres。 ngram 和 edge_ngram 可以让标记更适合特殊匹配情况或自动完成。

创建自定义分词器

与索引设置一样,我们预先配置好 es_std 分析器,我们可以再 analysis 字段下配置字符过滤器,分词器和标记过滤器:

1
PUT /my_index
2
{
3
    "settings": {
4
        "analysis": {
5
            "char_filter": { ... custom character filters ... },
6
            "tokenizer":   { ...    custom tokenizers     ... },
7
            "filter":      { ...   custom token filters   ... },
8
            "analyzer":    { ...    custom analyzers      ... }
9
        }
10
    }
11
}

作为例子,我们来配置一个这样的分析器:

  • 用 html_strip 字符过滤器去除所有的 HTML 标签
  • 将 & 替换成 and,使用一个自定义的 mapping 字符过滤器
1
"char_filter": {
2
    "&_to_and": {
3
        "type":       "mapping",
4
        "mappings": [ "&=> and "]
5
    }
6
}
  1. 使用 standard 分词器分割单词
  2. 使用 lowercase 标记过滤器将词转为小写
  3. 用 stop 标记过滤器去除一些自定义停用词。
1
"filter": {
2
    "my_stopwords": {
3
        "type":        "stop",
4
        "stopwords": [ "the", "a" ]
5
    }
6
}

根据以上描述来将预定义好的分词器和过滤器组合成我们的分析器:

1
"analyzer": {
2
    "my_analyzer": {
3
        "type":           "custom",
4
        "char_filter":  [ "html_strip", "&_to_and" ],
5
        "tokenizer":      "standard",
6
        "filter":       [ "lowercase", "my_stopwords" ]
7
    }
8
}

用下面的方式可以将以上请求合并成一条:

1
PUT /my_index
2
{
3
    "settings": {
4
        "analysis": {
5
            "char_filter": {
6
                "&_to_and": {
7
                    "type":       "mapping",
8
                    "mappings": [ "&=> and "]
9
            }},
10
            "filter": {
11
                "my_stopwords": {
12
                    "type":       "stop",
13
                    "stopwords": [ "the", "a" ]
14
            }},
15
            "analyzer": {
16
                "my_analyzer": {
17
                    "type":         "custom",
18
                    "char_filter":  [ "html_strip", "&_to_and" ],
19
                    "tokenizer":    "standard",
20
                    "filter":       [ "lowercase", "my_stopwords" ]
21
            }}
22
}}}

创建索引后,用 analyze API 来测试新的分析器:

1
GET /my_index/_analyze?analyzer=my_analyzer
2
The quick & brown fox

注:es版本7.5下上面的语句is invalid。

下面的结果证明我们的分析器能正常工作了:

1
{
2
  "tokens" : [
3
      { "token" :   "quick",    "position" : 2 },
4
      { "token" :   "and",      "position" : 3 },
5
      { "token" :   "brown",    "position" : 4 },
6
      { "token" :   "fox",      "position" : 5 }
7
    ]
8
}

除非我们告诉 Elasticsearch 在哪里使用,否则分析器不会起作用。我们可以通过下面的映射将它应用在一个 string 类型的字段上:

1
PUT /my_index/_mapping
2
{
3
    "properties": {
4
        "title": {
5
            "type":      "string",
6
            "analyzer":  "my_analyzer"
7
        }
8
    }
9
}

类型

Lucene 如何处理文档

Lucene 中,一个文档由一组简单的键值对组成,一个字段至少需要有一个值,但是任何字段都可以有多个值。类似的,一个单独的字符串可能在分析过程中被转换成多个值。Lucene 不关心这些值是字符串,数字或日期,所有的值都被当成 不透明字节

当我们在 Lucene 中索引一个文档时,每个字段的值都被加到相关字段的倒排索引中。你也可以选择将原始数据 储存 起来以备今后取回。

类型是怎么实现的

Elasticsearch 类型是在这个简单基础上实现的。一个索引可能包含多个类型,每个类型有各自的映射和文档,保存在同一个索引中。

因为 Lucene 没有文档类型的概念,每个文档的类型名被储存在一个叫 _type 的元数据字段上。当我们搜索一种特殊类型的文档时,Elasticsearch 简单的通过 _type 字段来过滤出这些文档。

Lucene 同样没有映射的概念。映射是 Elasticsearch 将复杂 JSON 文档映射成 Lucene 需要的扁平化数据的方式。

例如,user 类型中 name 字段的映射声明这个字段是一个 string 类型,在被加入倒排索引之前,它的数据需要通过 whitespace 分析器来分析。

1
"name": {
2
    "type":     "string",
3
    "analyzer": "whitespace"
4
}

预防类型陷阱

事实上不同类型的文档可以被加到同一个索引里带来了一些预想不到的困难。

想象一下我们的索引中有两种类型:blog_en 表示英语版的博客,blog_es 表示西班牙语版的博客。两种类型都有 title 字段,但是其中一种类型使用 english 分析器,另一种使用 spanish 分析器。

使用下面的查询就会遇到问题:

1
GET /_search
2
{
3
    "query": {
4
        "match": {
5
            "title": "The quick brown fox"
6
        }
7
    }
8
}

我们在两种类型中搜索 title 字段,首先需要分析查询语句,但是应该使用哪种分析器呢,spanish 还是 english?Elasticsearch 会采用第一个被找到的 title 字段使用的分析器,这对于这个字段的文档来说是正确的,但对另一个来说却是错误的。

我们可以通过给字段取不同的名字来避免这种错误 —— 比如,用 title_en 和 title_es。或者在查询中明确包含各自的类型名。

1
GET /_search
2
{
3
    "query": {
4
        "multi_match": { <1>
5
            "query":    "The quick brown fox",
6
            "fields": [ "blog_en.title", "blog_es.title" ]
7
        }
8
    }
9
}

<1> multi_match 查询在多个字段上执行 match 查询并一起返回结果。
新的查询中 english 分析器用于 blog_en.title 字段,spanish 分析器用于 blog_es.title 字段,然后通过综合得分组合两种字段的结果。

这种办法对具有相同数据类型的字段有帮助,但是想象一下如果你将下面两个文档加入同一个索引,会发生什么:

1
类型: user
2
 { "login": "john_smith" }
3
类型: event
4
 { "login": "2014-06-01" }

Lucene 不在乎一个字段是字符串而另一个字段是日期,它会一视同仁的索引这两个字段。

然而,假如我们试图 排序 event.login 字段,Elasticsearch 需要将 login 字段的值加载到内存中,它将 任意文档 的值加入索引而不管它们的类型。

它会尝试加载这些值为字符串或日期,取决于它遇到的第一个 login 字段。这可能会导致预想不到的结果或者以失败告终。

提示:为了保证你不会遇到这些冲突,建议在同一个索引的每一个类型中,确保用同样的方式映射同名的字段

元数据:_source 字段

默认情况下,Elasticsearch 用 JSON 字符串来表示文档主体保存在 _source 字段中。像其他保存的字段一样,_source 字段也会在写入硬盘前压缩。

这几乎始终是需要的功能,因为:

  • 搜索结果中能得到完整的文档 —— 不需要额外去别的数据源中查询文档
  • 如果缺少 _source 字段,部分 更新 请求不会起作用
  • 当你的映射有变化,而且你需要重新索引数据时,你可以直接在 Elasticsearch 中操作而不需要重新从别的数据源中取回数据。
  • 你可以从 _source 中通过 get 或 search 请求取回部分字段,而不是整个文档。
  • 这样更容易排查错误,因为你可以准确的看到每个文档中包含的内容,而不是只能从一堆 ID 中猜测他们的内容。

即便如此,存储 _source 字段还是要占用硬盘空间的。假如上面的理由对你来说不重要,你可以用下面的映射禁用 _source 字段:

1
PUT /my_index
2
{
3
    "mappings": {
4
        "my_type": {
5
            "_source": {
6
                "enabled":  false
7
            }
8
        }
9
    }
10
}

在搜索请求中你可以通过限定 _source 字段来请求指定字段:

1
GET /_search
2
{
3
    "query":   { "match_all": {}},
4
    "_source": [ "title", "created" ]
5
}

这些字段会从 _source 中提取出来,而不是返回整个 _source 字段。

储存字段

除了索引字段的值,你也可以选择 储存 字段的原始值以备日后取回。使用 Lucene 做后端的用户用储存字段来选择搜索结果的返回值,事实上,_source 字段就是一个储存字段。

在 Elasticsearch 中,单独设置储存字段不是一个好做法。完整的文档已经被保存在 _source 字段中。通常最好的办法会是使用 _source 参数来过滤你需要的字段。

元数据:_all 字段

_all 字段:一个所有其他字段值的特殊字符串字段。query_string 在没有指定字段时默认用 _all 字段查询。

_all 字段在新应用的探索阶段比较管用,当你还不清楚最终文档的结构时,可以将任何查询用于这个字段,就有机会得到你想要的文档:

1
GET /_search
2
{
3
    "match": {
4
        "_all": "john smith marketing"
5
    }
6
}

随着你应用的发展,搜索需求会变得更加精准。你会越来越少的使用 _all 字段。_all 是一种简单粗暴的搜索方式。通过查询独立的字段,你能更灵活,强大和精准的控制搜索结果,提高相关性。
提示

【相关性算法】考虑的一个最重要的原则是字段的长度:字段越短,就越重要。在较短的 title 字段中的短语会比较长的 content 字段中的短语显得更重要。而字段间的这种差异在 _all 字段中就不会出现

如果你决定不再使用 _all 字段,你可以通过下面的映射禁用它:

1
PUT /my_index/_mapping/my_type
2
{
3
    "my_type": {
4
        "_all": { "enabled": false }
5
    }
6
}

通过 include_in_all 选项可以控制字段是否要被包含在 _all 字段中,默认值是 true。在一个对象上设置 include_in_all 可以修改这个对象所有字段的默认行为。

你可能想要保留 _all 字段来查询所有特定的全文字段,例如 title, overview, summary 和 tags。相对于完全禁用 _all 字段,你可以先默认禁用 include_in_all 选项,而选定字段上启用 include_in_all。

1
PUT /my_index/my_type/_mapping
2
{
3
    "my_type": {
4
        "include_in_all": false,
5
        "properties": {
6
            "title": {
7
                "type":           "string",
8
                "include_in_all": true
9
            },
10
            ...
11
        }
12
    }
13
}

谨记 _all 字段仅仅是一个经过分析的 string 字段。它使用默认的分析器来分析它的值,而不管这值本来所在的字段指定的分析器。而且像所有 string 类型字段一样,你可以配置 _all 字段使用的分析器:

1
PUT /my_index/my_type/_mapping
2
{
3
    "my_type": {
4
        "_all": { "analyzer": "whitespace" }
5
    }
6
}

文档 ID

文档唯一标识由四个元数据字段组成:

  • _id:文档的字符串 ID
  • _type:文档的类型名
  • _index:文档所在的索引
  • _uid:_type 和 _id 连接成的 type#id

默认情况下,_uid 是被保存(可取回)和索引(可搜索)的。_type 字段被索引但是没有保存,_id 和 _index 字段则既没有索引也没有储存,它们并不是真实存在的。

尽管如此,你仍然可以像真实字段一样查询 _id 字段。Elasticsearch 使用 _uid 字段来追溯 _id。虽然你可以修改这些字段的 index 和 store 设置,但是基本上不需要这么做。

_id 字段有一个你可能用得到的设置:path 设置告诉 Elasticsearch 它需要从文档本身的哪个字段中生成 _id

1
PUT /my_index
2
{
3
    "mappings": {
4
        "my_type": {
5
            "_id": {
6
                "path": "doc_id" <1>
7
            },
8
            "properties": {
9
                "doc_id": {
10
                    "type":   "string",
11
                    "index":  "not_analyzed"
12
                }
13
            }
14
        }
15
    }
16
}

<1> 从 doc_id 字段生成 _id

然后,当你索引一个文档时:

1
POST /my_index/my_type
2
{
3
    "doc_id": "123"
4
}

_id 值由文档主体的 doc_id 字段生成。

1
{
2
    "_index":   "my_index",
3
    "_type":    "my_type",
4
    "_id":      "123", <1>
5
    "_version": 1,
6
    "created":  true
7
}

<1> _id 正确的生成了。

警告:虽然这样很方便,但是注意它对 bulk 请求(见【bulk 格式】)有个轻微的性能影响。处理请求的节点将不能仅靠解析元数据行来决定将请求分配给哪一个分片,而需要解析整个文档主体。

动态映射

当 Elasticsearch 处理一个位置的字段时,它通过【动态映射】来确定字段的数据类型且自动将该字段加到类型映射中。

有时这是理想的行为,有时却不是。或许你不知道今后会有哪些字段加到文档中,但是你希望它们能自动被索引。或许你仅仅想忽略它们。特别是当你使用 Elasticsearch 作为主数据源时,你希望未知字段能抛出一个异常来警示你。

幸运的是,你可以通过 dynamic 设置来控制这些行为,它接受下面几个选项:

  • true:自动添加字段(默认)
  • false:忽略字段
  • strict:当遇到未知字段时抛出异常

dynamic 设置可以用在根对象或任何 object 对象上。你可以将 dynamic 默认设置为 strict,而在特定内部对象上启用它:

1
PUT /my_index
2
{
3
    "mappings": {
4
        "my_type": {
5
            "dynamic":      "strict", <1>
6
            "properties": {
7
                "title":  { "type": "string"},
8
                "stash":  {
9
                    "type":     "object",
10
                    "dynamic":  true <2>
11
                }
12
            }
13
        }
14
    }
15
}

<1> 当遇到未知字段时,my_type 对象将会抛出异常
<2> stash 对象会自动创建字段

通过这个映射,你可以添加一个新的可搜索字段到 stash 对象中:

1
PUT /my_index/my_type/1
2
{
3
    "title":   "This doc adds a new field",
4
    "stash": { "new_field": "Success!" }
5
}

但是在顶层做同样的操作则会失败:

1
PUT /my_index/my_type/1
2
{
3
    "title":     "This throws a StrictDynamicMappingException",
4
    "new_field": "Fail!"
5
}

备注:将 dynamic 设置成 false 完全不会修改 _source 字段的内容。_source 将仍旧保持你索引时的完整 JSON 文档。然而,没有被添加到映射的未知字段将不可被搜索。

自定义动态索引

如果你想在运行时的增加新的字段,你可能会开启动态索引。虽然有时动态映射的 规则 显得不那么智能,幸运的是我们可以通过设置来自定义这些规则。

日期检测

当 Elasticsearch 遇到一个新的字符串字段时,它会检测这个字段是否包含一个可识别的日期,比如 2014-01-01。如果它看起来像一个日期,这个字段会被作为 date 类型添加,否则,它会被作为 string 类型添加。

有些时候这个规则可能导致一些问题。想象你有一个文档长这样:

{ “note”: “2014-01-01” }

假设这是第一次见到 note 字段,它会被添加为 date 字段,但是如果下一个文档像这样:

{ “note”: “Logged out” }

这显然不是一个日期,但为时已晚。这个字段已经被添加为日期类型,这个 不合法的日期 将引发异常。

日期检测可以通过在根对象上设置 date_detection 为 false 来关闭:

1
PUT /my_index
2
{
3
    "mappings": {
4
        "my_type": {
5
            "date_detection": false
6
        }
7
    }
8
}

使用这个映射,字符串将始终是 string 类型。假如你需要一个 date 字段,你得手动添加它。

提示:

Elasticsearch 判断字符串为日期的规则可以通过 dynamic_date_formats 配置 来修改。

动态模板

使用 dynamic_templates,你可以完全控制新字段的映射,你设置可以通过字段名或数据类型应用一个完全不同的映射。

每个模板都有一个名字用于描述这个模板的用途,一个 mapping 字段用于指明这个映射怎么使用,和至少一个参数(例如 match)来定义这个模板适用于哪个字段。

模板按照顺序来检测,第一个匹配的模板会被启用。例如,我们给 string 类型字段定义两个模板:

  • es: 字段名以 _es 结尾需要使用 spanish 分析器。
  • en: 所有其他字段使用 english 分析器。

我们将 es 模板放在第一位,因为它比匹配所有字符串的 en 模板更特殊一点

1
PUT /my_index
2
{
3
    "mappings": {
4
        "my_type": {
5
            "dynamic_templates": [
6
                { "es": {
7
                      "match":              "*_es", <1>
8
                      "match_mapping_type": "string",
9
                      "mapping": {
10
                          "type":           "string",
11
                          "analyzer":       "spanish"
12
                      }
13
                }},
14
                { "en": {
15
                      "match":              "*", <2>
16
                      "match_mapping_type": "string",
17
                      "mapping": {
18
                          "type":           "string",
19
                          "analyzer":       "english"
20
                      }
21
                }}
22
            ]
23
}}}

<1> 匹配字段名以 _es 结尾的字段.
<2> 匹配所有字符串类型字段。

match_mapping_type 允许你限制模板只能使用在特定的类型上,就像由标准动态映射规则检测的一样,(例如 strong 和 long)

match 参数只匹配字段名,path_match 参数则匹配字段在一个对象中的完整路径,所以 address.*.name 规则将匹配一个这样的字段:

1
    "address": {
2
        "city": {
3
            "name": "New York"
4
        }
5
    }
6
}

默认映射
通常,一个索引中的所有类型具有共享的字段和设置。用 default 映射来指定公用设置会更加方便,而不是每次创建新的类型时重复操作。default 映射像新类型的模板。所有在 _default 映射 之后 的类型将包含所有的默认设置,除非在自己的类型映射中明确覆盖这些配置。
例如,我们可以使用 default 映射对所有类型禁用 all 字段,而只在 blog 字段上开启它:
PUT /my_index
{
“mappings”: {
“_default
“: {
“all”: { “enabled”: false }
},
“blog”: {
“_all”: { “enabled”: true }
}
}
}
_default
映射也是定义索引级别的动态模板的好地方。
unmatch 和 path_unmatch 规则将用于排除未被匹配的字段。

默认映射

通常,一个索引中的所有类型具有共享的字段和设置。用 default 映射来指定公用设置会更加方便,而不是每次创建新的类型时重复操作。

default 映射像新类型的模板。所有在 _default 映射 之后 的类型将包含所有的默认设置,除非在自己的类型映射中明确覆盖这些配置。

例如,我们可以使用 default 映射对所有类型禁用 _all 字段,而只在 blog 字段上开启它:

1
PUT /my_index
2
{
3
    "mappings": {
4
        "_default_": {
5
            "_all": { "enabled":  false }
6
        },
7
        "blog": {
8
            "_all": { "enabled":  true  }
9
        }
10
    }
11
}

default 映射也是定义索引级别的动态模板的好地方。

重新索引数据

虽然你可以给索引添加新的类型(es版本7.5已经不支持),或给类型添加新的字段,但是你不能添加新的分析器或修改已有字段。假如你这样做,已被索引的数据会变得不正确而你的搜索也不会正常工作。

修改在已存在的数据最简单的方法是重新索引:创建一个新配置好的索引,然后将所有的文档从旧的索引复制到新的上。

_source 字段的一个最大的好处是你已经在 Elasticsearch 中有了完整的文档,你不再需要从数据库中重建你的索引,这样通常会比较慢。

为了更高效的索引旧索引中的文档,使用【scan-scoll】来批量读取旧索引的文档,然后将通过【bulk API】来将它们推送给新的索引。

批量重新索引:

你可以在同一时间执行多个重新索引的任务,但是你显然不愿意它们的结果有重叠。所以,可以将重建大索引的任务通过日期或时间戳字段拆分成较小的任务:

1
GET /old_index/_search?search_type=scan&scroll=1m
2
{
3
    "query": {
4
        "range": {
5
            "date": {
6
                "gte":  "2014-01-01",
7
                "lt":   "2014-02-01"
8
            }
9
        }
10
    },
11
    "size":  1000
12
}

假如你继续在旧索引上做修改,你可能想确保新增的文档被加到了新的索引中。这可以通过重新运行重建索引程序来完成,但是记得只要过滤出上次执行后新增的文档就行了。

索引别名和零停机时间

前面提到的重新索引过程中的问题是必须更新你的应用,来使用另一个索引名。索引别名正是用来解决这个问题的!

索引 别名 就像一个快捷方式或软连接,可以指向一个或多个索引,也可以给任何需要索引名的 API 使用。别名带给我们极大的灵活性,允许我们做到:

  • 在一个运行的集群上无缝的从一个索引切换到另一个
  • 给多个索引分类(例如,last_three_months)
  • 给索引的一个子集创建 视图

我们以后会讨论更多别名的使用场景。现在我们将介绍用它们怎么在零停机时间内从旧的索引切换到新的索引。

这里有两种管理别名的途径:_alias 用于单个操作,_aliases 用于原子化多个操作。

在这一章中,我们假设你的应用采用一个叫 my_index 的索引。而事实上,my_index 是一个指向当前真实索引的别名。真实的索引名将包含一个版本号:my_index_v1, my_index_v2 等等。

开始,我们创建一个索引 my_index_v1,然后将别名 my_index 指向它:

1
PUT /my_index_v1 <1>
2
PUT /my_index_v1/_alias/my_index <2>

<1> 创建索引 my_index_v1。
<2> 将别名 my_index 指向 my_index_v1。

你可以检测这个别名指向哪个索引:

1
GET /*/_alias/my_index

或哪些别名指向这个索引:

1
GET /my_index_v1/_alias/*

两者都将返回下列值:

1
{
2
    "my_index_v1" : {
3
        "aliases" : {
4
            "my_index" : { }
5
        }
6
    }
7
}

然后,我们决定修改索引中一个字段的映射。当然我们不能修改现存的映射,索引我们需要重新索引数据。首先,我们创建有新的映射的索引

1
PUT /my_index_v2
2
{
3
    "mappings": {
4
        "my_type": {
5
            "properties": {
6
                "tags": {
7
                    "type":   "string",
8
                    "index":  "not_analyzed"
9
                }
10
            }
11
        }
12
    }
13
}

然后我们从将数据从 my_index_v1 迁移到 my_index_v2,下面的过程在【重新索引】中描述过了。一旦我们认为数据已经被正确的索引了,我们就将别名指向新的索引。

别名可以指向多个索引,所以我们需要在新索引中添加别名的同时从旧索引中删除它。这个操作需要原子化,所以我们需要用 _aliases 操作:

1
POST /_aliases
2
{
3
    "actions": [
4
        { "remove": { "index": "my_index_v1", "alias": "my_index" }},
5
        { "add":    { "index": "my_index_v2", "alias": "my_index" }}
6
    ]
7
}

这样,你的应用就从旧索引迁移到了新的,而没有停机时间。

提示:

即使你认为现在的索引设计已经是完美的了,当你的应用在生产环境使用时,还是有可能在今后有一些改变的。
所以请做好准备:在应用中使用别名而不是索引。然后你就可以在任何时候重建索引。别名的开销很小,应当广泛使用。

ES-结构化查询

发表于 2019-12-16 | 更新于: 2019-12-16 | 分类于 ES 搜索引擎 | | 阅读次数:
字数统计: 3.6k 字 | 阅读时长 ≈ 15 分钟

参考:
https://es.xiaoleilu.com/054_Query_DSL/55_Request_body_search.html

请求体查询

1
GET /_search
2
{}
3
4
分页
5
GET /_search
6
{
7
    "from": 30,
8
    "size": 10
9
}
10
11
查询匹配多个index
12
GET /index_2014*/_search
13
{}
14
15
因为Get请求携带请求体数据,不被广泛接受,所以上面的用Post请求可ok。

结构化查询

使用结构化查询,你需要传递query参数:

1
GET /_search
2
{
3
    "query": YOUR_QUERY_HERE
4
}

空查询 - {} - 在功能上等同于使用match_all查询子句,正如其名字一样,匹配所有的文档:

1
GET /_search
2
{
3
    "query": {
4
        "match_all": {}
5
    }
6
}

查询子句

一个查询子句一般使用这种结构:

1
{
2
    QUERY_NAME: {
3
        ARGUMENT: VALUE,
4
        ARGUMENT: VALUE,...
5
    }
6
}

或指向一个指定的字段:

1
{
2
    QUERY_NAME: {
3
        FIELD_NAME: {
4
            ARGUMENT: VALUE,
5
            ARGUMENT: VALUE,...
6
        }
7
    }
8
}

例如,你可以使用match查询子句用来找寻在tweet字段中找寻包含elasticsearch的成员:

1
{
2
    "match": {
3
        "tweet": "elasticsearch"
4
    }
5
}

完整的查询请求会是这样:

1
GET /_search
2
{
3
    "query": {
4
        "match": {
5
            "tweet": "elasticsearch"
6
        }
7
    }
8
}

查询与过滤

可以使用两种结构化语句: 结构化查询(Query DSL)和结构化过滤(Filter DSL)。 查询与过滤语句非常相似,但是它们由于使用目的不同而稍有差异。

一条过滤语句会询问每个文档的字段值是否包含着特定值:

  • created 的日期范围是否在 2013 到 2014 ?
  • status 字段中是否包含单词 “published” ?
  • lat_lon 字段中的地理位置与目标点相距是否不超过10km ?

一条查询语句与过滤语句相似,但问法不同:

查询语句会询问每个文档的字段值与特定值的匹配程度如何?

查询语句的典型用法是为了找到文档:

  • 查找与 full text search 这个词语最佳匹配的文档
  • 查找包含单词 run ,但是也包含runs, running, jog 或 sprint的文档
  • 同时包含着 quick, brown 和 fox — 单词间离得越近,该文档的相关性越高
  • 标识着 lucene, search 或 java — 标识词越多,该文档的相关性越高

一条查询语句会计算每个文档与查询语句的相关性,会给出一个相关性评分 _score,并且 按照相关性对匹配到的文档进行排序。 这种评分方式非常适用于一个没有完全配置结果的全文本搜索。

性能差异

使用过滤语句得到的结果集 – 一个简单的文档列表,快速匹配运算并存入内存是十分方便的, 每个文档仅需要1个字节。这些缓存的过滤结果集与后续请求的结合使用是非常高效的。

查询语句不仅要查找相匹配的文档,还需要计算每个文档的相关性,所以一般来说查询语句要比过滤语句更耗时,并且查询结果也不可缓存。

幸亏有了倒排索引,一个只匹配少量文档的简单查询语句在百万级文档中的查询效率会与一条经过缓存的过滤语句旗鼓相当,甚至略占上风。 但是一般情况下,一条经过缓存的过滤查询要远胜一条查询语句的执行效率。

过滤语句的目的就是缩小匹配的文档结果集,所以需要仔细检查过滤条件。

什么情况下使用

原则上来说,使用查询语句做全文本搜索或其他需要进行相关性评分的时候,剩下的全部用过滤语句。

过滤查询

term过滤

term主要用于精确匹配哪些值,比如数字,日期,布尔值或 not_analyzed的字符串(未经分析的文本数据类型):

1
{ "term": { "age":    26           }}
2
{ "term": { "date":   "2014-09-01" }}
3
{ "term": { "public": true         }}
4
{ "term": { "tag":    "full_text"  }}

terms 过滤

terms 跟 term 有点类似,但 terms 允许指定多个匹配条件。 如果某个字段指定了多个值,那么文档需要一起去做匹配:

1
{
2
    "terms": {
3
        "tag": [ "search", "full_text", "nosql" ]
4
        }
5
}

range 过滤

range过滤允许我们按照指定范围查找一批数据:

1
{
2
    "range": {
3
        "age": {
4
            "gte":  20,
5
            "lt":   30
6
        }
7
    }
8
}

范围操作符包含:

  • gt :: 大于
  • gte:: 大于等于
  • lt :: 小于
  • lte:: 小于等于

exists 和 missing 过滤

exists 和 missing 过滤可以用于查找文档中是否包含指定字段或没有某个字段,类似于SQL语句中的IS_NULL条件

1
{
2
    "exists":   {
3
        "field":    "title"
4
    }
5
}

这两个过滤只是针对已经查出一批数据来,但是想区分出某个字段是否存在的时候使用。

bool 过滤

bool 过滤可以用来合并多个过滤条件查询结果的布尔逻辑,它包含一下操作符:

  • must :: 多个查询条件的完全匹配,相当于 and。
  • must_not :: 多个查询条件的相反匹配,相当于 not。
  • should :: 至少有一个查询条件匹配, 相当于 or。

这些参数可以分别继承一个过滤条件或者一个过滤条件的数组:

1
{
2
    "bool": {
3
        "must":     { "term": { "folder": "inbox" }},
4
        "must_not": { "term": { "tag":    "spam"  }},
5
        "should": [
6
                    { "term": { "starred": true   }},
7
                    { "term": { "unread":  true   }}
8
        ]
9
    }
10
}

match_all 查询

使用match_all 可以查询到所有文档,是没有查询条件下的默认语句。

1
{
2
    "match_all": {}
3
}

此查询常用于合并过滤条件。 比如说你需要检索所有的邮箱,所有的文档相关性都是相同的,所以得到的_score为1

match 查询

match查询是一个标准查询,不管你需要全文本查询还是精确查询基本上都要用到它。

如果你使用 match 查询一个全文本字段,它会在真正查询之前用分析器先分析match一下查询字符:

1
{
2
    "match": {
3
        "tweet": "About Search"
4
    }
5
}

如果用match下指定了一个确切值,在遇到数字,日期,布尔值或者not_analyzed 的字符串时,它将为你搜索你给定的值:

1
{ "match": { "age":    26           }}
2
{ "match": { "date":   "2014-09-01" }}
3
{ "match": { "public": true         }}
4
{ "match": { "tag":    "full_text"  }}

提示: 做精确匹配搜索时,你最好用过滤语句,因为过滤语句可以缓存数据。

match查询不可以用类似”+usid:2 +tweet:search”这样的语句。 它只能就指定某个确切字段某个确切的值进行搜索,而你要做的就是为它指定正确的字段名以避免语法错误。

multi_match 查询

multi_match查询允许你做match查询的基础上同时搜索多个字段:

1
{
2
    "multi_match": {
3
        "query":    "full text search",
4
        "fields":   [ "title", "body" ]
5
    }
6
}

bool 查询

bool 查询与 bool 过滤相似,用于合并多个查询子句。不同的是,bool 过滤可以直接给出是否匹配成功, 而bool 查询要计算每一个查询子句的 _score (相关性分值)。

  • must:: 查询指定文档一定要被包含。
  • must_not:: 查询指定文档一定不要被包含。
  • should:: 查询指定文档,有则可以为文档相关性加分。

以下查询将会找到 title 字段中包含 “how to make millions”,并且 “tag” 字段没有被标为 spam。 如果有标识为 “starred” 或者发布日期为2014年之前,那么这些匹配的文档将比同类网站等级高:

1
{
2
    "bool": {
3
        "must":     { "match": { "title": "how to make millions" }},
4
        "must_not": { "match": { "tag":   "spam" }},
5
        "should": [
6
            { "match": { "tag": "starred" }},
7
            { "range": { "date": { "gte": "2014-01-01" }}}
8
        ]
9
    }
10
}

提示: 如果bool查询下没有must子句,那至少应该有一个should子句。但是如果有must子句,那么没有should子句也可以进行查询。

查询与过滤条件的合并

查询语句和过滤语句可以放在各自的上下文中。 在 ElasticSearch API 中我们会看到许多带有 query 或 filter 的语句。 这些语句既可以包含单条 query 语句,也可以包含一条 filter 子句。 换句话说,这些语句需要首先创建一个query或filter的上下文关系。

复合查询语句可以加入其他查询子句,复合过滤语句也可以加入其他过滤子句。 通常情况下,一条查询语句需要过滤语句的辅助,全文本搜索除外。

所以说,查询语句可以包含过滤子句,反之亦然。 以便于我们切换 query 或 filter 的上下文。这就要求我们在读懂需求的同时构造正确有效的语句。

带过滤的查询语句

过滤一条查询语句

比如说我们有这样一条查询语句:

1
{
2
    "match": {
3
        "email": "business opportunity"
4
    }
5
}

然后我们想要让这条语句加入 term 过滤,在收信箱中匹配邮件:

1
{
2
    "term": {
3
        "folder": "inbox"
4
    }
5
}

search API中只能包含 query 语句,所以我们需要用 filtered 来同时包含 “query” 和 “filter” 子句:

1
{
2
    "filtered": {
3
        "query":  { "match": { "email": "business opportunity" }},
4
        "filter": { "term":  { "folder": "inbox" }}
5
    }
6
}

我们在外层再加入 query 的上下文关系:

1
GET /_search
2
{
3
    "query": {
4
        "filtered": {
5
            "query":  { "match": { "email": "business opportunity" }},
6
            "filter": { "term": { "folder": "inbox" }}
7
        }
8
    }
9
}

单条过滤语句

在 query 上下文中,如果你只需要一条过滤语句,比如在匹配全部邮件的时候,你可以 省略 query 子句:

1
GET /_search
2
{
3
    "query": {
4
        "filtered": {
5
            "filter":   { "term": { "folder": "inbox" }}
6
        }
7
    }
8
}

如果一条查询语句没有指定查询范围,那么它默认使用 match_all 查询,所以上面语句 的完整形式如下:

1
GET /_search
2
{
3
    "query": {
4
        "filtered": {
5
            "query":    { "match_all": {}},
6
            "filter":   { "term": { "folder": "inbox" }}
7
        }
8
    }
9
}

查询语句中的过滤

有时候,你需要在 filter 的上下文中使用一个 query 子句。下面的语句就是一条带有查询功能 的过滤语句, 这条语句可以过滤掉看起来像垃圾邮件的文档:

1
GET /_search
2
{
3
    "query": {
4
        "filtered": {
5
            "filter":   {
6
                "bool": {
7
                    "must":     { "term":  { "folder": "inbox" }},
8
                    "must_not": {
9
                        "query": { <1>
10
                            "match": { "email": "urgent business proposal" }
11
                        }
12
                    }
13
                }
14
            }
15
        }
16
    }
17
}

<1> 过滤语句中可以使用query查询的方式代替 bool 过滤子句。

提示: 我们很少用到的过滤语句中包含查询,保留这种用法只是为了语法的完整性。 只有在过滤中用到全文本匹配的时候才会使用这种结构。

验证查询

查询语句可以变得非常复杂,特别是与不同的分析器和字段映射相结合后,就会有些难度。
validate API 可以验证一条查询语句是否合法。

1
GET /gb/_validate/query
2
{
3
   "query": {
4
      "tweet" : {
5
         "match" : "really powerful"
6
      }
7
   }
8
}

以上请求的返回值告诉我们这条语句是非法的:

1
{
2
  "valid" :         false
3
}

理解错误信息

想知道语句非法的具体错误信息,需要加上 explain 参数:

1
GET /gb/_validate/query?explain <1>
2
{
3
   "query": {
4
      "tweet" : {
5
         "match" : "really powerful"
6
      }
7
   }
8
}

<1> explain 参数可以提供语句错误的更多详情。
很显然,我们把 query 语句的 match 与字段名位置弄反了:

1
{
2
  "valid" : false,
3
  "error" : "org.elasticsearch.common.ParsingException: no [query] registered for [tweet]"
4
}

理解查询语句

如果是合法语句的话,使用 explain 参数可以返回一个带有查询语句的可阅读描述, 可以帮助了解查询语句在ES中是如何执行的:

1
GET /gb/_validate/query?explain
2
{
3
   "query": {
4
      "match" : {
5
         "tweet": "really powerful"
6
      }
7
   }
8
}

explanation 会为每一个索引返回一段描述,因为每个索引会有不同的映射关系和分析器:

1
{
2
  "_shards" : {
3
    "total" : 1,
4
    "successful" : 1,
5
    "failed" : 0
6
  },
7
  "valid" : true,
8
  "explanations" : [
9
    {
10
      "index" : "gb",
11
      "valid" : true,
12
      "explanation" : "tweet:really tweet:powerful"
13
    }
14
  ]
15
}

从返回的 explanation 你会看到 match 是如何为查询字符串 “really powerful” 进行查询的, 首先,它被拆分成两个独立的词分别在 tweet 字段中进行查询。

而且,在索引us中这两个词为”really”和”powerful”,在索引gb中被拆分成”really” 和 “power”。 这是因为我们在索引gb中使用了english分析器。

ES映射和分析

发表于 2019-12-14 | 更新于: 2019-12-15 | 分类于 ES 搜索引擎 | | 阅读次数:
字数统计: 2.5k 字 | 阅读时长 ≈ 10 分钟

https://es.xiaoleilu.com/052_Mapping_Analysis/00_Intro.html

概念

映射(mapping)机制用于进行字段类型确认,将每个字段匹配为一种确定的数据类型(string, number, booleans, date等)。

分析(analysis)机制用于进行全文文本(Full Text)的分词,以建立供搜索用的反向索引。

数据类型差异

1
GET /gb/_mapping

返回:

1
{
2
  "gb" : {
3
    "mappings" : {
4
      "properties" : {
5
        "date" : {
6
          "type" : "date"
7
        },
8
        "email" : {
9
          "type" : "text",
10
          "fields" : {
11
            "keyword" : {
12
              "type" : "keyword",
13
              "ignore_above" : 256
14
            }
15
          }
16
        },
17
        "name" : {
18
          "type" : "text",
19
          "fields" : {
20
            "keyword" : {
21
              "type" : "keyword",
22
              "ignore_above" : 256
23
            }
24
          }
25
        },
26
        "tweet" : {
27
          "type" : "text",
28
          "fields" : {
29
            "keyword" : {
30
              "type" : "keyword",
31
              "ignore_above" : 256
32
            }
33
          }
34
        },
35
        "user_id" : {
36
          "type" : "long"
37
        },
38
        "username" : {
39
          "type" : "text",
40
          "fields" : {
41
            "keyword" : {
42
              "type" : "keyword",
43
              "ignore_above" : 256
44
            }
45
          }
46
        }
47
      }
48
    }
49
  }
50
}

查询:

1
GET /gb/_search?q=2019            # 返回空
2
GET /gb/_search?q=2019-12-12      # 返回3个结果
3
GET /gb/_search?q=date:2019-12-12 # 返回3个结果
4
GET /gb/_search?q=date:2019       # 返回空

可以,是因为date被es推测为date类型,而_all里是string类型,只会完整匹配2019-12-12。

确切值VS全文索引

确切值 及 全文文本:

确切值是确定的,正如它的名字一样。比如一个date或用户ID,也可以包含更多的字符串比如username或email地址。

确切值”Foo”和”foo”就并不相同。确切值2014和2014-09-15也不相同。

全文文本,从另一个角度来说是文本化的数据(常常以人类的语言书写),比如一篇推文(Twitter的文章)或邮件正文。

全文文本常常被称为非结构化数据,其实是一种用词不当的称谓,实际上自然语言是高度结构化的。

问题是自然语言的语法规则是如此的复杂,计算机难以正确解析。例如这个句子:
May is fun but June bores me.

到底是说的月份还是人呢?

确切值是很容易查询的,因为结果是二进制的 – 要么匹配,要么不匹配。下面的查询很容易以SQL表达:

1
WHERE name    = "John Smith"
2
  AND user_id = 2
3
  AND date    > "2014-09-15"

倒排索引

单词 -》 文档Id

分析

  • 当你查询全文(full text)字段,查询将使用相同的分析器来分析查询字符串,以产生正确的词列表。
  • 当你查询一个确切值(exact value)字段,查询将不分析查询字符串,但是你可以自己指定。

测试分析器

尤其当你是Elasticsearch新手时,对于如何分词以及存储到索引中理解起来比较困难。为了更好的理解如何进行,你可以使用analyze API来查看文本是如何被分析的。在查询字符串参数中指定要使用的分析器,被分析的文本做为请求体:

1
GET /_analyze?analyzer=standard&text=Text to analyze

结果中每个节点在代表一个词:

1
{
2
   "tokens": [
3
      {
4
         "token":        "text",
5
         "start_offset": 0,
6
         "end_offset":   4,
7
         "type":         "<ALPHANUM>",
8
         "position":     1
9
      },
10
      {
11
         "token":        "to",
12
         "start_offset": 5,
13
         "end_offset":   7,
14
         "type":         "<ALPHANUM>",
15
         "position":     2
16
      },
17
      {
18
         "token":        "analyze",
19
         "start_offset": 8,
20
         "end_offset":   15,
21
         "type":         "<ALPHANUM>",
22
         "position":     3
23
      }
24
   ]
25
}

这个例子在es7.5版本报错。

为了手动指定特定字段的分析器,我们必须通过映射(mapping)人工设置这些字段。

映射

在上面查询映射中,GET /gb/_mapping返回字段的映射关系。
index参数控制字符串以何种方式被索引。它包含以下三个值当中的一个:

  • analyzed 首先分析这个字符串,然后索引。换言之,以全文形式索引此字段。
  • not_analyzed 索引这个字段,使之可以被搜索,但是索引内容和指定值一样。不分析此字段。
  • no 不索引这个字段。这个字段不能为搜索到。

对于analyzed类型的字符串字段,使用analyzer参数来指定哪一种分析器将在搜索和索引的时候使用。默认的,Elasticsearch使用standard分析器,但是你可以通过指定一个内建的分析器来更改它,例如whitespace、simple或english。

1
{
2
    "tweet": {
3
        "type":     "string",
4
        "analyzer": "english"
5
    }
6
}

更新映射

你可以在第一次创建索引的时候指定映射的类型。此外,你也可以晚些时候为新类型添加映射(或者为已有的类型更新映射)。

重要:

你可以向已有映射中增加字段,但你不能修改它。如果一个字段在映射中已经存在,这可能意味着那个字段的数据已经被索引。如果你改变了字段映射,那已经被索引的数据将错误并且不能被正确的搜索到。

复合类型

多值字段

我们想让tag字段包含多个字段,这非常有可能发生。我们可以索引一个标签数组来代替单一字符串:

{ “tag”: [ “search”, “nosql” ]}

对于数组不需要特殊的映射。任何一个字段可以包含零个、一个或多个值,同样对于全文字段将被分析并产生多个词。

言外之意,这意味着数组中所有值必须为同一类型。你不能把日期和字符窜混合。如果你创建一个新字段,这个字段索引了一个数组,Elasticsearch将使用第一个值的类型来确定这个新字段的类型。

当你从Elasticsearch中取回一个文档,任何一个数组的顺序和你索引它们的顺序一致。你取回的_source字段的顺序同样与索引它们的顺序相同。

然而,数组是做为多值字段被索引的,它们没有顺序。在搜索阶段你不能指定“第一个值”或者“最后一个值”。倒不如把数组当作一个值集合(bag of values)

空字段

当然数组可以是空的。这等价于有零个值。事实上,Lucene没法存放null值,所以一个null值的字段被认为是空字段。
这四个字段将被识别为空字段而不被索引:

“empty_string”: “”,
“null_value”: null,
“empty_array”: [],
“array_with_null_value”: [ null ]

多层对象

我们需要讨论的最后一个自然JSON数据类型是对象(object)——在其它语言中叫做hash、hashmap、dictionary 或者 associative array.

内部对象(inner objects)经常用于在另一个对象中嵌入一个实体或对象。例如,做为在tweet文档中user_name和user_id的替代,我们可以这样写:

1
{
2
    "tweet":            "Elasticsearch is very flexible",
3
    "user": {
4
        "id":           "@johnsmith",
5
        "gender":       "male",
6
        "age":          26,
7
        "name": {
8
            "full":     "John Smith",
9
            "first":    "John",
10
            "last":     "Smith"
11
        }
12
    }
13
}

内部对象的映射

Elasticsearch 会动态的检测新对象的字段,并且映射它们为 object 类型,将每个字段加到 properties 字段下

1
{
2
  "gb": {
3
    "tweet": { <1>
4
      "properties": {
5
        "tweet":            { "type": "string" },
6
        "user": { <2>
7
          "type":             "object",
8
          "properties": {
9
            "id":           { "type": "string" },
10
            "gender":       { "type": "string" },
11
            "age":          { "type": "long"   },
12
            "name":   { <3>
13
              "type":         "object",
14
              "properties": {
15
                "full":     { "type": "string" },
16
                "first":    { "type": "string" },
17
                "last":     { "type": "string" }
18
              }
19
            }
20
          }
21
        }
22
      }
23
    }
24
  }
25
}

<1> 根对象.
<2><3> 内部对象.
对user和name字段的映射与tweet类型自己很相似。事实上,type映射只是object映射的一种特殊类型,我们将 object 称为根对象。它与其他对象一模一样,除非它有一些特殊的顶层字段,比如 _source, _all 等等。

内部对象是怎样被索引的

Lucene 并不了解内部对象。 一个 Lucene 文件包含一个键-值对应的扁平表单。 为了让 Elasticsearch 可以有效的索引内部对象,将文件转换为以下格式:

1
{
2
    "tweet":            [elasticsearch, flexible, very],
3
    "user.id":          [@johnsmith],
4
    "user.gender":      [male],
5
    "user.age":         [26],
6
    "user.name.full":   [john, smith],
7
    "user.name.first":  [john],
8
    "user.name.last":   [smith]
9
}

内部对象数组

最后,一个包含内部对象的数组如何索引。 我们有个数组如下所示:

1
{
2
    "followers": [
3
        { "age": 35, "name": "Mary White"},
4
        { "age": 26, "name": "Alex Jones"},
5
        { "age": 19, "name": "Lisa Smith"}
6
    ]
7
}

此文件会如我们以上所说的被扁平化,但其结果会像如此:

1
{
2
    "followers.age":    [19, 26, 35],
3
    "followers.name":   [alex, jones, lisa, smith, mary, white]
4
}

{age: 35}与{name: Mary White}之间的关联会消失,因每个多值的栏位会变成一个值集合,而非有序的阵列。 这让我们可以知道:

  • 是否有26岁的追随者?

但我们无法取得准确的资料如:

  • 是否有26岁的追随者且名字叫Alex Jones?

关联内部对象可解决此类问题。

ES索引操作

发表于 2019-12-14 | 更新于: 2019-12-15 | 分类于 ES 搜索引擎 | | 阅读次数:
字数统计: 2.1k 字 | 阅读时长 ≈ 9 分钟

https://es.xiaoleilu.com/030_Data/05_Document.html
《ELasticsearch in Action》

以下的操作在ES7.5版本下。

文档

一个文档不只有数据,还包含了元数据,三个必须的元数据是:

_index: 索引,可以理解为mysql中数据库。
_type: 7.5版本后已经强制单索引单类型。
_id: 创建文档的时候可以指定,也可以不指定,es会自定生成。

检索文档

指定id检索

1
GET /website/123?pretty

这里指定了id来检索文档,将会只返回一个结果。同时返回的结果中将包括_source自定,其内容是我们新建文档123的时候发送的全部内容。

检索文档的一部分

1
GET /website/123?_source=title,text

返回的source里面将只会有title和text字段。

检查文档知否存在

1
HEAD /megacorp/_doc/1

将返回200 - OK
如果不存在将返回404 - Not Found

更新文档

1
PUT /website/_doc/123
2
{
3
  "title": "My first blog entry",
4
  "text":  "I am starting to get the hang of this...",
5
  "date":  "2014/01/02"
6
}

存在将更新,不存在将创建。可以从返回结果的”result” : “updated”,看出

1
{
2
  "_index" : "website",
3
  "_type" : "_doc",
4
  "_id" : "12",
5
  "_version" : 2,
6
  "result" : "updated",
7
  "_shards" : {
8
    "total" : 2,
9
    "successful" : 1,
10
    "failed" : 0
11
  },
12
  "_seq_no" : 3,
13
  "_primary_term" : 1
14
}

Es的更新文档操作过程:

  1. 从旧文档中检索JSON
  2. 修改它
  3. 删除旧文档
  4. 索引新文档

创建一个文档

如果想创建一个文档,而不是更新,使用:

1
POST /website/_doc/12?op_type=create

或者:

1
POST /website/_doc/12/_create

删除一个文档

1
DELETE /website/_doc/12

版本控制

内部版本号

每个文档都有一个_version号码,这个号码在文档被改变时加一。Elasticsearch使用这个_version保证所有修改都被正确排序。当一个旧版本出现在新版本之后,它会被简单的忽略。
我们利用_version的这一优点确保数据不会因为修改冲突而丢失。我们可以指定文档的version来做想要的更改。如果那个版本号不是现在的,我们的请求就失败了。

Let’s create a new blog post: 让我们创建一个新的博文:

1
PUT /website/_doc/1/_create
2
{
3
  "title": "My first blog entry",
4
  "text":  "Just trying this out..."
5
}

响应体告诉我们这是一个新建的文档,它的_version是1。现在假设我们要编辑这个文档:把数据加载到web表单中,修改,然后保存成新版本。
首先我们检索文档:

1
GET /website/_doc/1
2
响应体包含相同的_version是1
3
{
4
  "_index" :   "website",
5
  "_type" :    "blog",
6
  "_id" :      "1",
7
  "_version" : 1,
8
  "found" :    true,
9
  "_source" :  {
10
      "title": "My first blog entry",
11
      "text":  "Just trying this out..."
12
  }
13
}

现在,当我们通过重新索引文档保存修改时,我们这样指定了version参数:

1
PUT /website/_doc/1?version=1
2
{
3
  "title": "My first blog entry",
4
  "text":  "Starting to get the hang of this..."
5
}

外部版本号

如果主数据库有版本字段——或一些类似于timestamp等可以用于版本控制的字段——是你就可以在Elasticsearch的查询字符串后面添加version_type=external来使用这些版本号。版本号必须是整数,大于零小于9.2e+18——Java中的正的long。

外部版本号与之前说的内部版本号在处理的时候有些不同。它不再检查_version是否与请求中指定的一致,而是检查是否小于指定的版本。如果请求成功,外部版本号就会被存储到_version中

局部更新

可以使用以下请求为博客添加一个tags字段和一个views字段:

1
POST /website/blog/1/_update
2
{
3
   "doc" : {
4
      "tags" : [ "testing" ],
5
      "views": 0
6
   }
7
}

如果文档不存在,将返回404。

更新不奴存在的文档

在这种情况下,我们可以使用upsert参数定义文档来使其不存在时被创建。

1
POST /website/_update/1
2
{
3
   "script" : "ctx._source.views+=1",
4
   "upsert": {
5
       "views": 1
6
   }
7
}

检索多个文档

检索多个文档依旧非常快。合并多个请求可以避免每个请求单独的网络开销。如果你需要从Elasticsearch中检索多个文档,相对于一个一个的检索,更快的方式是在一个请求中使用multi-get或者mget API。

mget API参数是一个docs数组,数组的每个节点定义一个文档的_index、_type、_id元数据。如果你只想检索一个或几个确定的字段,也可以定义一个_source参数:

1
POST /_mget
2
{
3
   "docs" : [
4
      {
5
         "_index" : "website",
6
         "_id" :    2
7
      },
8
      {
9
         "_index" : "megacorp",
10
         "_id" :    1,
11
         "_source": "views"
12
      }
13
   ]
14
}

响应体也包含一个docs数组,每个文档还包含一个响应,它们按照请求定义的顺序排列。

1
{
2
  "docs" : [
3
    {
4
      "_index" : "website",
5
      "_type" : "blog",
6
      "_id" : "2",
7
      "found" : false
8
    },
9
    {
10
      "_index" : "megacorp",
11
      "_type" : "employee",
12
      "_id" : "1",
13
      "_version" : 2,
14
      "_seq_no" : 1,
15
      "_primary_term" : 1,
16
      "found" : true,
17
      "_source" : { }
18
    }
19
  ]
20
}

批量操作

为了将这些放在一起,bulk请求表单是这样的:

1
POST /_bulk
2
{ "delete": { "_index": "website", "_type": "blog", "_id": "123" }} <1>
3
{ "create": { "_index": "website", "_type": "blog", "_id": "123" }}
4
{ "title":    "My first blog post" }
5
{ "index":  { "_index": "website", "_type": "blog" }}
6
{ "title":    "My second blog post" }
7
{ "update": { "_index": "website", "_type": "blog", "_id": "123", "_retry_on_conflict" : 3} }
8
{ "doc" : {"title" : "My updated blog post"} } <2>

<1> 注意delete行为(action)没有请求体,它紧接着另一个行为(action)
<2> 记得最后一个换行符

搜索

为了充分挖掘Elasticsearch的潜力,你需要理解以下三个概念:

映射:Mapping,数据在每个字段中的解释说明
分析:Analysis,全部是如何处理的可以被搜索的
DSL:领域特定语言查询,es中使用的灵活的、强大的查询语言。

空搜索

1
GET /_search

将会返回集群中所有文档。

重要:

搜索一个索引有5个主分片和5个索引各有一个分片事实上是一样的。

简易搜索

search API有两种表单:一种是“简易版”的查询字符串(query string)将所有参数通过查询字符串定义,另一种版本使用JSON完整的表示请求体(request body),这种富搜索语言叫做结构化查询语句(DSL)。

_all字段

返回包含”mary”字符的所有文档的简单搜索:

1
GET /_search?q=mary

在前一个例子中,我们搜索tweet或name字段中包含某个字符的结果。然而,这个语句返回的结果在三个不同的字段中包含”mary”:

用户的名字是“Mary”
“Mary”发的六个推文
针对“@mary”的一个推文

Elasticsearch是如何设法找到三个不同字段的结果的?
当你索引一个文档,Elasticsearch把所有字符串字段值连接起来放在一个大字符串中,它被索引为一个特殊的字段_all。例如,当索引这个文档:

1
{
2
    "tweet":    "However did I manage before Elasticsearch?",
3
    "date":     "2014-09-14",
4
    "name":     "Mary Jones",
5
    "user_id":  1
6
}

这好比我们增加了一个叫做_all的额外字段值:

“However did I manage before Elasticsearch? 2014-09-14 Mary Jones 1”

若没有指定字段,查询字符串搜索(即q=xxx)使用_all字段搜索。

ES入门

发表于 2019-12-14 | 更新于: 2019-12-15 | 分类于 ES 搜索引擎 | | 阅读次数:
字数统计: 1.7k 字 | 阅读时长 ≈ 8 分钟

https://es.xiaoleilu.com/010_Intro/10_Installing_ES.html

安装

https://www.elastic.co/cn/downloads/
在上面ES官网下载ES7.5和kibana6.5版本。后者是可视化操作软件。同时下载页面也有配置启动的方法,很简单,es基本直接启动,kibana只需要改下elasticsearch.hosts即可。

一般首先在本地安装,之后打开http://localhost:5601/app/kibana
可以发现新版的(7.5)可以直接在界面上操作安装一些插件。

以下下的操作都在es7.5版本。改版本已经强制单索引单类型。类型推荐使用_doc,当然也可以指定。但只能有一个。

概念

索引:index。可以对应于m数据库中数据库。
类型:type。可以对应与数据库中的表。但又有不同,参考Es
中type理解
。type 字段会和文档的 _id 一起生成一个 _uid 字段,因此在同一个索引下的不同类型的文档的 _id 可以具有相同的值。参考:es中索引与类型前世今生
索引:倒排索引。

创建索引

新版的es已经不要求使用type,而是直接操作index,用_doc统一表示。因为type也只是一个逻辑概念。

1
PUT /megacorp/_doc/1
2
{
3
    "first_name" : "John",
4
    "last_name" :  "Smith",
5
    "age" :        25,
6
    "about" :      "I love to go rock climbing",
7
    "interests": [ "sports", "music" ]
8
}

上面的操作可以自动创建索引megacorp,并且添加了一个文档。可以多次执行,后面的执行将会update该文档。返回如下:

1
{
2
  "_index" : "megacorp",
3
  "_type" : "_doc",
4
  "_id" : "1",
5
  "_version" : 2,
6
  "result" : "updated",
7
  "_shards" : {
8
    "total" : 2,
9
    "successful" : 1,
10
    "failed" : 0
11
  },
12
  "_seq_no" : 1,
13
  "_primary_term" : 1
14
}

检索

1
GET /megacorp/_doc/1

返回:

1
{
2
  "_index" : "megacorp",
3
  "_type" : "_doc",
4
  "_id" : "1",
5
  "_version" : 2,
6
  "_seq_no" : 1,
7
  "_primary_term" : 1,
8
  "found" : true,
9
  "_source" : {
10
    "first_name" : "John",
11
    "last_name" : "Smith",
12
    "age" : 25,
13
    "about" : "I love to go rock climbing",
14
    "interests" : [
15
      "sports",
16
      "music"
17
    ]
18
  }
19
}

简单搜索

1
GET /megacorp/_search

返回结果:

1
{
2
  "took" : 1,
3
  "timed_out" : false,
4
  "_shards" : {
5
    "total" : 1,
6
    "successful" : 1,
7
    "skipped" : 0,
8
    "failed" : 0
9
  },
10
  "hits" : {
11
    "total" : {
12
      "value" : 2,
13
      "relation" : "eq"
14
    },
15
    "max_score" : 1.0,
16
    "hits" : [
17
      {
18
        "_index" : "megacorp",
19
        "_type" : "employee",
20
        "_id" : "1",
21
        "_score" : 1.0,
22
        "_source" : {
23
          "first_name" : "John",
24
          "last_name" : "Smith",
25
          "age" : 25,
26
          "about" : "I love to go rock climbing",
27
          "interests" : [
28
            "sports",
29
            "music"
30
          ]
31
        }
32
      },
33
      {
34
        "_index" : "megacorp",
35
        "_type" : "employee",
36
        "_id" : "2",
37
        "_score" : 1.0,
38
        "_source" : {
39
          "first_name" : "John",
40
          "last_name" : "jack",
41
          "age" : 27,
42
          "about" : "I love to go swimming",
43
          "interests" : [
44
            "sports",
45
            "movie"
46
          ]
47
        }
48
      }
49
    ]
50
  }
51
}

搜索last name包含smith的员工:

1
GET /megacorp/_search?q=last_name:Smith

将会返回id为1的文档。

使用DSL查询

DSL(Domain Specific Language特定领域语言)以JSON请求体的形式出现。

之前的查询last name为smith的员工可以这样查询:

1
GET /megacorp/_search
2
{
3
    "query" : {
4
        "match" : {
5
            "last_name" : "Smith"
6
        }
7
    }
8
}

将得到和之前一样的结果。

更复杂的查询

让搜索稍微再变的复杂一些。我们依旧想要找到姓氏为“Smith”的员工,但是我们只想得到年龄大于30岁的员工。我们的语句将添加过滤器(filter),它使得我们高效率的执行一个结构化搜索:

1
GET /megacorp/_search
2
{
3
    "query" : {
4
        "filtered" : {
5
            "filter" : {
6
                "range" : {
7
                    "age" : { "gt" : 30 }
8
                }
9
            },
10
            "query" : {
11
                "match" : {
12
                    "last_name" : "smith"
13
                }
14
            }
15
        }
16
    }
17
}

到目前为止,上面的查询将会返回错误:

1
{
2
  "error": {
3
    "root_cause": [
4
      {
5
        "type": "parsing_exception",
6
        "reason": "no [query] registered for [filtered]",
7
        "line": 3,
8
        "col": 22
9
      }
10
    ],
11
    "type": "parsing_exception",
12
    "reason": "no [query] registered for [filtered]",
13
    "line": 3,
14
    "col": 22
15
  },
16
  "status": 400
17
}

原因之后将会讲述。

全文索引

搜索所有喜欢“rock climbing”的员工:

1
GET /megacorp/_search
2
{
3
    "query" : {
4
        "match" : {
5
            "about" : "rock climbing"
6
        }
7
    }
8
}

将得到id为1的文档。

短语搜索

目前我们可以在字段中搜索单独的一个词,这挺好的,但是有时候你想要确切的匹配若干个单词或者短语(phrases)。例如我们想要查询同时包含”rock”和”climbing”(并且是相邻的)的员工记录。

要做到这个,我们只要将match查询变更为match_phrase查询即可:

1
GET /megacorp/_search
2
{
3
    "query" : {
4
        "match_phrase" : {
5
            "about" : "rock climbing"
6
        }
7
    }
8
}

高亮搜索

在Elasticsearch中高亮片段是非常容易的。让我们在之前的语句上增加highlight参数:

1
GET /megacorp/_search
2
{
3
    "query" : {
4
        "match_phrase" : {
5
            "about" : "rock climbing"
6
        }
7
    },
8
    "highlight": {
9
        "fields" : {
10
            "about" : {}
11
        }
12
    }
13
}

得到的结果中将会增加一个字段highlight:

1
{
2
  "took" : 195,
3
  "timed_out" : false,
4
  "_shards" : {
5
    "total" : 1,
6
    "successful" : 1,
7
    "skipped" : 0,
8
    "failed" : 0
9
  },
10
  "hits" : {
11
    "total" : {
12
      "value" : 1,
13
      "relation" : "eq"
14
    },
15
    "max_score" : 1.3365866,
16
    "hits" : [
17
      {
18
        "_index" : "megacorp",
19
        "_type" : "employee",
20
        "_id" : "1",
21
        "_score" : 1.3365866,
22
        "_source" : {
23
          "first_name" : "John",
24
          "last_name" : "Smith",
25
          "age" : 25,
26
          "about" : "I love to go rock climbing",
27
          "interests" : [
28
            "sports",
29
            "music"
30
          ]
31
        },
32
        "highlight" : {
33
          "about" : [
34
            "I love to go <em>rock</em> <em>climbing</em>"
35
          ]
36
        }
37
      }
38
    ]
39
  }
40
}

分析

最后,我们还有一个需求需要完成:允许管理者在职员目录中进行一些分析。 Elasticsearch有一个功能叫做聚合(aggregations),它允许你在数据上生成复杂的分析统计。它很像SQL中的GROUP BY但是功能更强大。

这段内容将放到后面来说。

ES中index和type区分

发表于 2019-12-14 | 更新于: 2019-12-15 | 分类于 ES 搜索引擎 | | 阅读次数:
字数统计: 1.2k 字 | 阅读时长 ≈ 4 分钟

https://bayescafe.com/database/elasticsearch-using-index-or-type.html
https://www.cnblogs.com/huangfox/p/9460361.html

elasticsearch-中的索引与类型的前生今世

type理解

Type 是什么

使用 type 允许我们在一个 index 里存储多种类型的数据,这样就可以减少 index 的数量了。在使用时,向每个文档加入 _type 字段,在指定 type 搜索时就会被用于过滤。使用 type 的一个好处是,搜索一个 index 下的多个 type,和只搜索一个 type 相比没有额外的开销 —— 需要合并结果的分片数量是一样的。

但是,这也是有限制的:

  • 不同 type 里的字段需要保持一致。例如,一个 index 下的不同 type 里有两个名字相同的字段,他们的类型(string, date 等等)和配置也必须相同。
  • 只在某个 type 里存在的字段,在其他没有该字段的 type 中也会消耗资源。这是 Lucene Index 带来的常见问题:它不喜欢稀疏。由于连续文档之间的差异太大,稀疏的 posting list 的压缩效率不高。这个问题在 doc value 上更为严重:为了提高速度,doc value 通常会为每个文档预留一个固定大小的空间,以便文档可以被高速检索。这意味着,如果 Lucene 确定它需要一个字节来存储某个数字类型的字段,它同样会给没有这个字段的文档预留一个字节。未来版本的 ES 会在这方面做一些改进,但是我仍然建议你在建模的时候尽量避免稀疏。[1]
  • 得分是由 index 内的统计数据来决定的。也就是说,一个 type 中的文档会影响另一个 type 中的文档的得分。

这意味着,只有同一个 index 的中的 type 都有类似的映射 (mapping) 时,才应该使用 type。否则,使用多个 type 可能比使用多个 index 消耗的资源更多。

我应该用哪个

这是个困难的问题,它的答案取决于你用的硬件、数据和用例。首先你要明白 type 是有用的,因为它能减少 ES 需要管理的 Lucene Index 的数量。但是也有另外一种方式可以减少这个数量:创建 index 的时候让它的分片少一些。例如,与其在一个 index 里塞上 5 个 type,不如创建 5 个只有一个分片的 index。

在你做决定的时候可以问自己下面几个问题:

  1. 你需要使用父子文档吗?如果需要,只能在一个 index 里建立多个 type。
  2. 你的文档的映射是否相似?如果不相似,使用多个 index。
  3. 如果你的每个 type 都有足够多的文档,Lucene Index 的开销可以被分摊掉,你就可以安全的使用多个 index 了。如果有必要的话,可以把分片数量设小一点。
  4. 如果文档不够多,你可以考虑把文档放进一个 index 里的多个 type 里,甚至放进一个 type 里。

移除映射类型的计划

修改这种模型将会是一个比较大的变化,因此 Elasticsearch 也在尽量少影响用户的前提下,做了一些功能的规划:

ElasticSearch 5.6.0

在索引上设置 index.mapping.single_type: true来启用单个类型单个索引,6.0 版本后会进一步增强
从 5.6.0 版本开始,使用 join 字段来替换 父子关系

Elasticsearch 6.x

5.x 创建的索引在 6.x 版中将继续可以使用
6.x 中将只允许单个类型单个索引, 比较推荐的类型名字为 _doc, 这样可以让索引的API具备相同的路径 PUT {index}/_doc/{id} 和 POST {index}/_doc
_type 字段名称将不再与 _id 字段合并生成 _uid字段, _uid 字段将作为 _id 的别名。
新的索引将不再支持父子关系,应该采用 join 字段类进行替代
_default 映射类型将不推荐使用

Elasticsearch 7.x

URL 中的 type 参数做变为可选。例如,所以文档将不再需要 type。指定 id 的 URL 将变为 PUT {index}/_doc/{id}, 自动生成 id 的 URL 为:POST {index}/_doc
GET | PUT _mapping API 支持查询字符串参数(include_type_name),该参数指示主体是否应包含类型名称。 它默认为 true, 7.x 没有显式类型的索引将使用虚拟类型名称 _doc。

_default 映射类型将被移除

Elasticsearch 8.x

参数 type 在 URL 中将不再被支持
参数include_type_name 默认seize为 false

分布式事务,解决方案

发表于 2019-12-06 | 更新于: 2019-12-06 | 分类于 分布式 事务 | | 阅读次数:
字数统计: 2.7k 字 | 阅读时长 ≈ 9 分钟

聊聊分布式事务,再说说解决方案
分布式事务CAP理解论证-解决方案
分布式系统的2PC、3PC详细分析
github tcc示例
分布式事务、重复消费、顺序消费

一、理论

CAP相关:

CAP与BASE相关:我的博客

而对于分布式中的问题的解决方案,CAP原则出现,描述如下:

一致性(Consistency):

像A节点写入一条信息之后,同一时刻,在其他节点都可以读到这条信息

可用性(Availability):

多布一些节点A,B,C…,任何时刻,用户访问,都应该以可预期的结果返回,而不是浏览器报错,404,500,页面丢失…等用户体验不好的情况发生

分区容忍性(PartitionTolerance):

当各系统模块间通信出现问题时,设计一个策略,使系统仍可对外提供满足一致性或可用性

刚接触cap时,有些不理解分区容忍性,我们自己倒推一下:

  1. 为了保证一致性,我们需要各个节点同步消息
  2. 为了保证可用性我们可以多部署节点,部分节点挂了仍可对外提供服务
  3. 为了保证分区容忍性:此刻卡壳了,怎么做?没了一种具体的方式,然而他还是客观存在的。后来发现:进入了思维盲点:只要在分布式场景中,分区必然存在,那么如果不处理分区发生时的情况,节点无法通讯时会发生什么?–此刻如果仍对外提供服务,那么导致无法同步消息,即保证不了强一致性;如果要保证强一致性,那么就需要节点阻塞,一直等待通讯恢复,即保证不了可用性.

所以分区容忍性就是:当发生分区问题时,我们使用策略,在一致性和可用性二者间选择
注意: 无法通信包括网络问题,或者节点机器宕机

误区: CAP理论中说三者不可兼得,但实际情况是,在分布式场景中分区一定存在,即必须有分区容忍性对应的策略,之后才能在一致性和可用性间二者之间选择.所以对主流架构来说不是三选二,而是二选一。

对P的理解

很多人可能对分区容忍性不太理解,知乎有一个回答对这个解释的比较清楚CAP理论中的P到底是个什么意思?,这里引用一下:

  • 一个分布式系统里面,节点组成的网络本来应该是连通的。然而可能因为一些故障,使得有些节点之间不连通了,整个网络就分成了几块区域。数据就散布在了这些不连通的区域中。这就叫分区。
  • 当你一个数据项只在一个节点中保存,那么分区出现后,和这个节点不连通的部分就访问不到这个数据了。这时分区就是无法容忍的。
  • 提高分区容忍性的办法就是一个数据项复制到多个节点上,那么出现分区之后,这一数据项就可能分布到各个区里,容忍性就提高了。
  • 然而,要把数据复制到多个节点,就会带来一致性的问题,就是多个节点上面的数据可能是不一致的。
  • 要保证一致,每次写操作就都要等待全部节点写成功,而这等待又会带来可用性的问题。
  • 总的来说就是,数据存在的节点越多,分区容忍性越高,但要复制更新的数据就越多,一致性就越难保证。为了保证一致性,更新所有节点数据所需要的时间就越长,可用性就会降低。

XA规范:

http://www.jasongj.com/big_data/two_phase_commit/
https://www.cnblogs.com/zhoujinyi/p/5257558.html

XA规范中,事务管理器主要通过以下的接口对资源管理器进行管理

  • xa_open,xa_close:建立和关闭与资源管理器的连接。
  • xa_start,xa_end:开始和结束一个本地事务。
  • xa_prepare,xa_commit,xa_rollback:预提交、提交和回滚一个本地事务。
  • xa_recover:回滚一个已进行预提交的事务。

XA规范:https://www.cnblogs.com/wt645631686/p/10882998.html

解决方案

一些具体实现

  • 维护本地消息表
  • 使用rocketmq事务消息:https://blog.csdn.net/weixin_40533111/article/details/84451219
  • 两阶段提交协议(2PC)
  • TCC事务补偿机制

使用限制:

a. XA事务和本地事务以及锁表操作是互斥的

开启了xa事务就无法使用本地事务和锁表操作:

1
mysql> xa start 't1xa';
2
Query OK, 0 rows affected (0.04 sec)
3
mysql> begin;
4
ERROR 1399 (XAE07): XAER_RMFAIL: The command cannot be executed when global transaction is in the ACTIVE state
5
mysql> lock table t1 read;
6
ERROR 1399 (XAE07): XAER_RMFAIL: The command cannot be executed when global transaction is in the ACTIVE state

开启了本地事务就无法使用xa事务:

1
mysql> begin;
2
Query OK, 0 rows affected (0.00 sec)
3
mysql> xa start 'rrrr';
4
ERROR 1400 (XAE09): XAER_OUTSIDE: Some work is done outside global transaction

b. xa start 之后必须xa end, 否则不能执行xa commit 和xa rollback

所以如果在执行xa事务过程中有语句出错了,你也需要先xa end一下,然后才能xarollback。

注意事项:

a. mysql只是提供了xa事务的接口,分布式事务中的mysql实例之间是互相独立的不感知的。 所以用户必须
自己实现分布式事务的调度器
b. xa事务有一些使用上的bug, 参考http://www.mysqlops.com/2012/02/24/mysql-xa-optimize.html

主要是:
“MySQL数据库的主备数据库的同步,通过Binlog的复制完成。而Binlog是MySQL数据库内部XA事务的协调者,并且MySQL数据库为binlog做了优化——binlog不写prepare日志,只写commit日志。
所有的参与节点prepare完成,在进行xa commit前crash。crash recover如果选择commit此事务。由于binlog在prepare阶段未写,因此主库中看来,此分布式事务最终提交了,但是此事务的操作并未 写到binlog中,因此也就未能成功复制到备库,从而导致主备库数据不一致的情况出现。
而crash recover如果选rollback, 那么就会出现全局不一致(该分布式事务对应的节点,部分已经提交,无法回滚,而部分节点回滚。最终导致同一分布式事务,在各参与节点,最终状态不一致)”

参考的那篇blog中给出的办法是修改mysql代码,这个无法在DBScale中使用。 所以可选的替代方案是不使用
主从复制进行备份,而是直接使用xa事务实现同步写来作为备份。

二、两阶段提交2PC

1. 介绍

两个角色:

  1. 协调者
  2. 参与者

两个阶段:

  1. 阶段一:提交事务请求
  2. 阶段二:执行事务提交

牺牲了一部分可用性来换取的一致性。解决方案有:springboot+Atomikos or Bitronix

优点: 原理简单,实现方便

缺点:

  1. 同步阻塞:在提交的过程中,所有参与者都处于阻塞状态,大大降低并发度
  2. 单点问题:一旦协调者出现问题,则所有参与者处于锁定状态,无法对外服务
  3. 数据不一致:在阶段二,协调者发送了commit之后,发生了局部网络异常或者协调者尚未发送完commit请求就宕机了,导致部分参与者收到commit,导致系统出现不一致
  4. 太过保守:协调者在阶段一中,参与者出现故障而导致协调者无法获取到所有参与者的响应,协调者只能依靠超时时间来判断是否中断事务。换句话说,没有完善的容错机制。

2. 实现

JTA(Java Transaction API)定义了对XA事务的支持。像很多其他的Java规范一样,JTA仅仅定义了接口,具体的实现则是由供应商(如J2EE厂商)负责提供,目前JTA的实现主要有以下几种:

  • J2EE容器所提供的JTA实现(如JBoss)。
  • 独立的JTA实现:如JOTM(Java Open Transaction Manager),Atomikos。这些实现可以应用在那些不使用J2EE应用服务器的环境里用以提供分布事事务保证。

MySQL中的XA实现分为:外部XA和内部XA。前者是指我们通常意义上的分布式事务实现;后者是指单台MySQL服务器中,Server层作为TM(事务协调者),而服务器中的多个数据库实例作为RM,而进行的一种分布式事务,也就是MySQL跨库事务;也就是一个事务涉及到同一条MySQL服务器中的两个innodb数据库(因为其它引擎不支持XA)。

三、三阶段提交3PC

是二阶段的改进版,将二阶段的提交事务请求过程一分为二,形成了:

  1. CanCommit:协调者发送事务询问、参与者反馈
  2. PreCommit:协调者发送预提交请求、参与者事务预提交(执行事务操作,写undo、redo日志)、参与者响应
  3. doCommit:协调者发送提交请求、参与者事务提交(事务提交,释放资源)、参与者响应

在阶段二中,参与者可能会响应no,或者协调者等待超时时间后还无法收到所有参与者的反馈,则中断事务:协调者向所有参与者发送abort请求。参与者无论是收到协调者的abort请求,或者等待协调者请求过程中超时,都会中断事务。

在阶段三中,如果有任一参与者发送了no,或者等待超时后协调者还没收到所有参与者的反馈,则中断事务。需要注意的事,进入阶段三,可能会有下面两种故障:

  • 协调者出现问题
  • 协调者、参与者之间的网络出现问题

无论哪种情况,都会导致参与者无法及时收到来自协调者的doCommit或者abort请求,这种情况,参与者在等待超时后继续进行事务提交。

优点:

  1. 降低了参与者的阻塞范围(二阶段中如果参与者与协调者断开,参与者abort;三阶段,提交),并且能够在单点故障后继续达成一致。

缺点:

  1. 参与者在收到preCommit后出现网络分区,参与者依然会提交事务,会造成不一致。

四、实现

todo

分布式CAP与BASE理论

发表于 2019-12-01 | 更新于: 2019-12-01 | 分类于 分布式 事务 | | 阅读次数:
字数统计: 1.2k 字 | 阅读时长 ≈ 4 分钟

分布式Base理论

参考:
CAP和BASE理论
https://juejin.im/post/5d720e86f265da03cc08de74
https://github.com/changmingxie/tcc-transaction
《从Paxos到Zookeeper》

1. CAP理论

2000年7月,加州大学伯克利分校的Eric Brewer教授在ACM PODC会议上提出CAP猜想。2年后,麻省理工学院的Seth Gilbert和Nancy Lynch从理论上证明了CAP。之后,CAP理论正式成为分布式计算领域的公认定理。

CAP理论为:一个分布式系统最多只能同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance)这三项中的两项。

1.1 一致性(Consistency)

一致性指“all nodes see the same data at the same time”,即更新操作成功并返回客户端完成后,所有节点在同一时间的数据完全一致。

1.2 可用性(Availability)

可用性指“Reads and writes always succeed”,即服务一直可用,而且是正常响应时间。

1.3 分区容错性(Partition tolerance)

分区容错性指“the system continues to operate despite arbitrary message loss or failure of part of the system”,即分布式系统在遇到某节点或网络分区故障的时候,仍然能够对外提供满足一致性和可用性的服务。

2. CAP权衡

通过CAP理论,我们知道无法同时满足一致性、可用性和分区容错性这三个特性,那要舍弃哪个呢?

对于多数大型互联网应用的场景,主机众多、部署分散,而且现在的集群规模越来越大,所以节点故障、网络故障是常态,而且要保证服务可用性达到N个9,即保证P和A,舍弃C(退而求其次保证最终一致性)。虽然某些地方会影响客户体验,但没达到造成用户流程的严重程度。

对于涉及到钱财这样不能有一丝让步的场景,C必须保证。网络发生故障宁可停止服务,这是保证CA,舍弃P。貌似这几年国内银行业发生了不下10起事故,但影响面不大,报到也不多,广大群众知道的少。还有一种是保证CP,舍弃A。例如网络故障事只读不写。

孰优孰略,没有定论,只能根据场景定夺,适合的才是最好的。

需要明确的一点是,对于一个分布式系统而言,分区容错性可以说是一个基本的要求。很简单,既然是分布式系统,则系统上的组件必然部署到不同的节点,必然出现子网络。

3. BASE理论

Basical Available(基本可用)、Soft state(软状态)、Eventually consistent(最终一致性)

eBay的架构师Dan Pritchett源于对大规模分布式系统的实践总结,在ACM上发表文章提出BASE理论,BASE理论是对CAP理论的延伸,核心思想是即使无法做到强一致性(Strong Consistency,CAP的一致性就是强一致性),但应用可以采用适合的方式达到最终一致性(Eventual Consitency)。

BASE是指基本可用(Basically Available)、软状态( Soft State)、最终一致性( Eventual Consistency)。

3.1 基本可用(Basically Available)

基本可用是指分布式系统在出现故障的时候,允许损失部分可用性,即保证核心可用。
电商大促时,为了应对访问量激增,部分用户可能会被引导到降级页面,服务层也可能只提供降级服务。这就是损失部分可用性的体现。

两个体现:

  1. 响应时间上的损失;更长了
  2. 功能上的损失;降级页面

3.2 软状态( Soft State)

软状态是指允许系统存在中间状态,而该中间状态不会影响系统整体可用性。分布式存储中一般一份数据至少会有三个副本,允许不同节点间副本同步的延时就是软状态的体现。mysql replication的异步复制也是一种体现。认为该中间状态不会影响系统的整体可用性。

3.3 最终一致性( Eventual Consistency)

最终一致性是指系统中的所有数据副本经过一定时间后,最终能够达到一致的状态。弱一致性和强一致性相反,最终一致性是弱一致性的一种特殊情况。

4. ACID和BASE的区别与联系

ACID是传统数据库常用的设计理念,追求强一致性模型。BASE支持的是大型分布式系统,提出通过牺牲强一致性获得高可用性。

在分布式系统设计的场景中,系统组件对一致性要求是不同的,因此ACID和BASE又会结合使用。

冰雪奇缘2

发表于 2019-12-01 | 更新于: 2019-12-01 | 分类于 电影 | | 阅读次数:
字数统计: 25 字 | 阅读时长 ≈ 1 分钟

《冰雪奇缘2》

2019-11-23

在东湖渠华谊电影院。

9.0分。唯美公主梦

12<i class="fa fa-angle-right"></i>
hanks-wang

hanks-wang

14 日志
7 分类
8 标签
0%
© 2019 hanks-wang
由 Hexo 强力驱动
|
主题 — NexT.Pisces v5.1.4