Fork me on GitHub

随笔分类 - elasticsearch

elasticsearch-mathc和term的区分

elasticsearch和mysql在思想上是有不同的,elasticsearch有分词一说,比如`北京奥运`分词成`北京`,`奥运`,`北京奥运`。分词要要考虑两点,一个是查询字符串要不要分词,还有就是原存储字段是不是精确值。 # 1. match 查询 无论你在任何字段上进行的是全文搜索还是精确查询,match 查询是你可用的标准查询。 `、如果你在一个全文字段上使用 match 查询,在执行查询前,它将用正确的分析器去分析查询字符串: ``` { "match": { "tweet": "About Search" }} ``` 2、如果在一个精确值的字段上使用它, 例如数字、日期、布尔或者一个 NOT_ANALYZED 字符串字段,那么它将会精确匹配给定的值: ``` { "match": { "age": 26 }} { "match": { "date": "2014-09-01" }} { "match": { "public": true }} { "match": { "tag": "full_text" }} ``` match查询会先对搜索词进行分词,分词完毕后再逐个对分词结果进行匹配,因此相比于term的精确搜索,match是分词匹配搜索,match搜索还有两个相似功能的变种,一个是match_phrase,一个是multi_match # 2. term 查询 term是代表完全匹配,也就是精确查询,搜索前不会再对搜索词进行分词,所以我们的搜索词必须是文档分词集合中的一个。比如说我们要找标题为北京奥运的所有文档 ``` $curl -XGET http://localhost:9200/index/doc/_search?pretty -d '{ "query":{ "term":{ "title":"北京奥运" } } }' ``` 将会得到如下结果 ``` { "took": 1, "timed_out": false, "_shards": { "total": 5, "successful": 5, "failed": 0 }, "hits": { "total": 1, "max_score": 0.92055845, "hits": [ { "_index": "index", "_type": "doc", "_id": "3", "_score": 0.92055845, "_source": { "content": "同一个世界同一个梦想", "title": "北京奥运", "tags": [ "和平" ] } } ] } } ``` 搜索title包含北京或者奥运的,结果也一样,但是如果你搜索词为京奥,或者北京奥这样的,那么搜索结果将为空 ``` { "took" : 1, "timed_out" : false, "_shards" : { "total" : 5, "successful" : 5, "failed" : 0 }, "hits" : { "total" : 0, "max_score" : null, "hits" : [ ] } } ``` # 3. match_phrase match_phrase为按短语搜索,match_phrase的搜索方式和match类似,先对搜索词建立索引,并要求**所有分词**必须在文档中出现(像不像operator为and的match查询),除此之外,还必须满足分词在文档中出现的**顺序**和搜索词中一致且各搜索词之间必须紧邻,因此match_phrase也可以叫做紧邻搜索。 所以,当我们搜`美国留给`时 ``` curl -XGET http://localhost:9200/index/doc/_search?pretty -d '{ "query": { "match_phrase": { "content": "美国留给" } } }' ``` 以下内容`美国留给伊拉克的是个烂摊子吗`是可以搜索出来的 ``` "_source" : { "content" : "美国留给伊拉克的是个烂摊子吗", "title" : "标题", "tags" : [ "美国", "伊拉克", "烂摊子" ] } ``` 但是我们搜索`留给美国`或`美国伊拉克`时,却没有搜索结果,因为第一个顺序不对,第二个不是紧邻(隔着留给)。 紧邻对于匹配度要求较高,为了减小精度增加可操作性,引入了slop参数。该参数可以指定相隔多少个词仍被算作匹配成功。如下, ``` curl -XGET http://localhost:9200/index/doc/_search?pretty -d '{ "query": { "match_phrase": { "content": { "query": "美国伊拉克", "slop": "1" } } } }' ``` 当我们将slop设置为1时,下面文档是可以搜索到的 ``` "_source" : { "content" : "美国留给伊拉克的是个烂摊子吗", "title" : "标题", "tags" : [ "美国", "伊拉克", "烂摊子" ] } ``` 需要注意的是,当slop的值过大时(超出文档总分词数),那么分词数据将可以是随意的,即跟operator为and的match查询效果一样。比如我们查询 ``` curl -XGET http://localhost:9200/index/doc/_search?pretty -d '{ "query": { "match_phrase": { "content": { "query": "伊拉克美国", "slop": "12" } } } }' ``` 将会得到与上面一样的结果 # 4. multi_match 如果我们希望两个字段进行匹配,其中一个字段有这个文档就满足的话,使用multi_match ``` { "query": { "multi_match": { "query" : "我的宝马多少马力", "fields" : ["title", "content"] } } } ``` 但是multi_match就涉及到匹配评分的问题了。

elasticsearch安装中文分词器

# 1. 分词器的安装 ``` ./bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v6.2.3/elasticsearch-analysis-ik-6.2.3.zip ``` NOTE: replace 6.2.3 to your own elasticsearch version github上面的地址 ``` https://github.com/medcl/elasticsearch-analysis-ik ``` 需要注意安装的版本和对应的elasticsearch相匹配 使用方法: 1> 在ElasticSearch的配置文件config/elasticsearch.yml中的最后一行添加参数 index.analysis.analyzer.default.type: ik,则设置所有索引的默认分词器为ik分词。 2> 也可以通过设置mapping来使用ik分词 # 2. IK分词器的两种分词模式。 1> ik_max_word: 会将文本做最细粒度的拆分,比如会将"北京邮电大学"拆分,会穷尽各种可能的组合; ``` { "tokens":[ { "token":"北京邮电", "start_offset":0, "end_offset":4, "type":"CN_WORD", "position":0 }, { "token":"北京", "start_offset":0, "end_offset":2, "type":"CN_WORD", "position":1 }, { "token":"邮电大学", "start_offset":2, "end_offset":6, "type":"CN_WORD", "position":2 }, { "token":"邮电", "start_offset":2, "end_offset":4, "type":"CN_WORD", "position":3 }, { "token":"电大", "start_offset":3, "end_offset":5, "type":"CN_WORD", "position":4 }, { "token":"大学", "start_offset":4, "end_offset":6, "type":"CN_WORD", "position":5 } ] } ``` 2> ik_smart: 会做最粗粒度的拆分 ``` { "tokens":[ { "token":"北京", "start_offset":0, "end_offset":2, "type":"CN_WORD", "position":0 }, { "token":"邮电大学", "start_offset":2, "end_offset":6, "type":"CN_WORD", "position":1 } ] } ```

elasticsearch安装教程

[TOC] # 1 java8 环境 elasticsearch需要安装java 8 环境,配置JAVA_HOME 查看是否有旧版本的java ``` java -verison ``` 如果没有安装,可以进入官方选择适合自己的版本,下载地址:http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html 下载并解压 ``` $ cd /usr/local/src $ wget --no-cookies --no-check-certificate --header "Cookie: gpw_e24=http%3A%2F%2Fwww.oracle.com%2F; oraclelicense=accept-securebackup-cookie" http://download.oracle.com/otn-pub/java/jdk/8u171-b11/512cd62ec5174c3487ac17c61aaa89e8/jdk-8u171-linux-x64.tar.gz $ tar zxvf jdk-8u171-linux-x64.tar.gz ``` 配置环境变量 ``` # export PATH USER LOGNAME MAIL HOSTNAME HISTSIZE HISTCONTROL 之后加入下面内容 #jdk export JAVA_HOME=/usr/local/src/jdk1.8.0_171 export PATH=$JAVA_HOME/bin:$PATH export CLASSPATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar ``` 使配置生效 ``` source /etc/profile ``` 再次验证是否安装成功 ``` java -version ``` ``` java version "1.8.0_171" Java(TM) SE Runtime Environment (build 1.8.0_171-b11) Java HotSpot(TM) 64-Bit Server VM (build 25.171-b11, mixed mode) ``` 查看变量 ``` [[email protected]_35_1_centos ~]# echo $JAVA_HOME /usr/local/src/jdk1.8.0_171 ``` # 2 安装elasticsearch 安装教程位置: https://www.elastic.co/guide/en/elasticsearch/reference/5.6/install-elasticsearch.html 这里我们使用tar包 ``` cd /usr/local/src wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-5.6.9.tar.gz tar zxvf elasticsearch-5.6.9.tar.gz cd elasticsearch-5.6.9/ ``` 内存配置 默认的内存配置是2g,我学习用的机子内存较小(实际2g)会挂掉,所以我将内存改成1g(机子实际内存的一半) ``` vim /etc/elasticsearch/jvm.options ``` 内容 ``` -Xms1g -Xmx1g ``` 启动 ``` ./bin/elasticsearch ``` 如果你使用root用户启动,则会出现下面的报错 ``` [2018-05-23T15:00:43,762][WARN ][o.e.b.ElasticsearchUncaughtExceptionHandler] [] uncaught exception in thread [main] org.elasticsearch.bootstrap.StartupException: java.lang.RuntimeException: can not run elasticsearch as root at org.elasticsearch.bootstrap.Elasticsearch.init(Elasticsearch.java:136) ~[elasticsearch-5.6.9.jar:5.6.9] at org.elasticsearch.bootstrap.Elasticsearch.execute(Elasticsearch.java:123) ~[elasticsearch-5.6.9.jar:5.6.9] at org.elasticsearch.cli.EnvironmentAwareCommand.execute(EnvironmentAwareCommand.java:70) ~[elasticsearch-5.6.9.jar:5.6.9] at org.elasticsearch.cli.Command.mainWithoutErrorHandling(Command.java:134) ~[elasticsearch-5.6.9.jar:5.6.9] at org.elasticsearch.cli.Command.main(Command.java:90) ~[elasticsearch-5.6.9.jar:5.6.9] at org.elasticsearch.bootstrap.Elasticsearch.main(Elasticsearch.java:91) ~[elasticsearch-5.6.9.jar:5.6.9] at org.elasticsearch.bootstrap.Elasticsearch.main(Elasticsearch.java:84) ~[elasticsearch-5.6.9.jar:5.6.9] Caused by: java.lang.RuntimeException: can not run elasticsearch as root at org.elasticsearch.bootstrap.Bootstrap.initializeNatives(Bootstrap.java:106) ~[elasticsearch-5.6.9.jar:5.6.9] at org.elasticsearch.bootstrap.Bootstrap.setup(Bootstrap.java:195) ~[elasticsearch-5.6.9.jar:5.6.9] at org.elasticsearch.bootstrap.Bootstrap.init(Bootstrap.java:342) ~[elasticsearch-5.6.9.jar:5.6.9] at org.elasticsearch.bootstrap.Elasticsearch.init(Elasticsearch.java:132) ~[elasticsearch-5.6.9.jar:5.6.9] ... 6 more ``` 这是出于系统安全考虑设置的条件。由于ElasticSearch可以接收用户输入的脚本并且执行,为了系统安全考虑, 建议创建一个单独的用户用来运行ElasticSearch 创建elsearch用户组及elsearch用户 ``` groupadd elsearch useradd elsearch -g elsearch -p elasticsearch cd /usr/local/src chown -R elsearch:elsearch elasticsearch-5.6.9 ``` 切换到elsearch用户再启动 ``` su elsearch ./elasticsearch ``` 这时你可能还会遇到下面的报错,max virtual memory areas vm.max_map_count [65530] is too low, increase to at least [262144] 解决方法是: ``` sudo sysctl -w vm.max_map_count=262144 ``` 再次启动 ``` [[email protected]_35_1_centos elasticsearch-5.6.9]$ ./bin/elasticsearch [2018-05-23T15:09:55,323][INFO ][o.e.n.Node ] [] initializing ... [2018-05-23T15:09:55,546][INFO ][o.e.e.NodeEnvironment ] [ndB4c4F] using [1] data paths, mounts [[/ (rootfs)]], net usable_space [44.7gb], net total_space [49gb], spins? [unknown], types [rootfs] [2018-05-23T15:09:55,546][INFO ][o.e.e.NodeEnvironment ] [ndB4c4F] heap size [1015.6mb], compressed ordinary object pointers [true] [2018-05-23T15:09:55,547][INFO ][o.e.n.Node ] node name [ndB4c4F] derived from node ID [ndB4c4FLRxWlhH_94CN70Q]; set [node.name] to override [2018-05-23T15:09:55,548][INFO ][o.e.n.Node ] version[5.6.9], pid[3857], build[877a590/2018-04-12T16:25:14.838Z], OS[Linux/3.10.0-514.21.1.el7.x86_64/amd64], JVM[Oracle Corporation/Java HotSpot(TM) 64-Bit Server VM/1.8.0_171/25.171-b11] [2018-05-23T15:09:55,548][INFO ][o.e.n.Node ] JVM arguments [-Xms1g, -Xmx1g, -XX:+UseConcMarkSweepGC, -XX:CMSInitiatingOccupancyFraction=75, -XX:+UseCMSInitiatingOccupancyOnly, -XX:+AlwaysPreTouch, -Xss1m, -Djava.awt.headless=true, -Dfile.encoding=UTF-8, -Djna.nosys=true, -Djdk.io.permissionsUseCanonicalPath=true, -Dio.netty.noUnsafe=true, -Dio.netty.noKeySetOptimization=true, -Dio.netty.recycler.maxCapacityPerThread=0, -Dlog4j.shutdownHookEnabled=false, -Dlog4j2.disable.jmx=true, -Dlog4j.skipJansi=true, -XX:+HeapDumpOnOutOfMemoryError, -Des.path.home=/usr/local/src/elasticsearch-5.6.9] [2018-05-23T15:09:57,152][INFO ][o.e.p.PluginsService ] [ndB4c4F] loaded module [aggs-matrix-stats] [2018-05-23T15:09:57,152][INFO ][o.e.p.PluginsService ] [ndB4c4F] loaded module [ingest-common] [2018-05-23T15:09:57,152][INFO ][o.e.p.PluginsService ] [ndB4c4F] loaded module [lang-expression] [2018-05-23T15:09:57,152][INFO ][o.e.p.PluginsService ] [ndB4c4F] loaded module [lang-groovy] [2018-05-23T15:09:57,153][INFO ][o.e.p.PluginsService ] [ndB4c4F] loaded module [lang-mustache] [2018-05-23T15:09:57,153][INFO ][o.e.p.PluginsService ] [ndB4c4F] loaded module [lang-painless] [2018-05-23T15:09:57,153][INFO ][o.e.p.PluginsService ] [ndB4c4F] loaded module [parent-join] [2018-05-23T15:09:57,153][INFO ][o.e.p.PluginsService ] [ndB4c4F] loaded module [percolator] [2018-05-23T15:09:57,153][INFO ][o.e.p.PluginsService ] [ndB4c4F] loaded module [reindex] [2018-05-23T15:09:57,153][INFO ][o.e.p.PluginsService ] [ndB4c4F] loaded module [transport-netty3] [2018-05-23T15:09:57,153][INFO ][o.e.p.PluginsService ] [ndB4c4F] loaded module [transport-netty4] [2018-05-23T15:09:57,153][INFO ][o.e.p.PluginsService ] [ndB4c4F] no plugins loaded [2018-05-23T15:10:00,188][INFO ][o.e.d.DiscoveryModule ] [ndB4c4F] using discovery type [zen] [2018-05-23T15:10:01,225][INFO ][o.e.n.Node ] initialized [2018-05-23T15:10:01,225][INFO ][o.e.n.Node ] [ndB4c4F] starting ... [2018-05-23T15:10:01,505][INFO ][o.e.t.TransportService ] [ndB4c4F] publish_address {127.0.0.1:9300}, bound_addresses {127.0.0.1:9300} [2018-05-23T15:10:04,656][INFO ][o.e.c.s.ClusterService ] [ndB4c4F] new_master {ndB4c4F}{ndB4c4FLRxWlhH_94CN70Q}{HfnS160SQP2XrgYtuEKX0w}{127.0.0.1}{127.0.0.1:9300}, reason: zen-disco-elected-as-master ([0] nodes joined)[, ] [2018-05-23T15:10:04,728][INFO ][o.e.h.n.Netty4HttpServerTransport] [ndB4c4F] publish_address {127.0.0.1:9200}, bound_addresses {127.0.0.1:9200} [2018-05-23T15:10:04,728][INFO ][o.e.n.Node ] [ndB4c4F] started [2018-05-23T15:10:04,809][INFO ][o.e.g.GatewayService ] [ndB4c4F] recovered [0] indices into cluster_state ``` 发现started关键字,并且发现监听127.0.0.1:9200端口,elasticsearch默认监听9200端口 使用curl测试是否安装成功 ``` [[email protected]_35_1_centos log]# curl localhost:9200 { "name" : "ndB4c4F", "cluster_name" : "elasticsearch", "cluster_uuid" : "X0k8EhwGThCjnf-cTkSpTg", "version" : { "number" : "5.6.9", "build_hash" : "877a590", "build_date" : "2018-04-12T16:25:14.838Z", "build_snapshot" : false, "lucene_version" : "6.6.1" }, "tagline" : "You Know, for Search" } ``` # 3 安装kibana Kibana是一个为 ElasticSearch 提供的数据分析的 Web 接口。可使用它对日志进行高效的搜索、可视化、分析等各种操作。 https://www.elastic.co/guide/en/kibana/5.6/install.html ``` cd /usr/local/src wget https://artifacts.elastic.co/downloads/kibana/kibana-5.6.9-linux-x86_64.tar.gz tar zxvf kibana-5.6.9-linux-x86_64.tar.gz cd kibana-5.6.9-linux-x86_64/ ``` 修改kibana配置,可以外网访问 ``` vim ./config/kibana.yml ``` 内容 ``` server.port: 5601 server.host: "0.0.0.0" ``` 可以浏览器访问xxx.xxx.xxx.xxx:5601 注意其中的Monitoring 默认是没有的,是由X-Pack集成提供的。该X-pack监控组件使您可以通过Kibana轻松地监控ElasticSearch。您可以实时查看集群的健康和性能,以及分析过去的集群、索引和节点度量。此外,可以监视Kibana本身性能。 Elasticsearch下载X-Pack 在Es的根目录(每个节点),运行 bin/elasticsearch-plugin进行安装。 ``` bin/elasticsearch-plugin install x-pack ``` 如果你在Elasticsearch已禁用自动索引的创建,在elasticsearch.yml配置action.auto_create_index允许X-pack创造以下指标: ``` action.auto_create_index: ".security*,.monitoring*,.watches,.triggered_watches,.watcher-history*" ``` Kibana下载X-Pack 在Kibana根目录运行 bin/kibana-plugin 进行安装。 ``` bin/kibana-plugin install x-pack ``` # 4. 单服务器部署多个节点 实际上一个服务器只会部署一个节点,但是为了学习elasticsearch的分布特性,这里探索启动多个节点。 使用elasticsearch-5.6.9再复制一份,然后更改用户组 ``` cd /usr/local/src cp -r elasticsearch-5.6.9 elastic-slave1 chown -R elsearch:elsearch elastic-slave1 ``` 由于内存太小,所以再次调整了master和slave的配置 /usr/local/src/elasticsearch-5.6.9/config/jvm.options /usr/local/src/elastic-slave1/config/jvm.options ``` -Xms256m -Xmx256m ``` /usr/local/src/elasticsearch-5.6.9/config/elasticsearch.yml ``` cluster.name: test node.name: master node.master: true network.host: 127.0.0.1 action.auto_create_index: ".security*,.monitoring*,.watches,.triggered_watches,.watcher-history*" ``` /usr/local/src/elastic-slave1/config/elasticsearch.yml ``` cluster.name: test node.name: slave1 network.host: 127.0.0.1 http.port: 8200 discovery.zen.ping.unicast.hosts: ["127.0.0.1"] action.auto_create_index: ".security*,.monitoring*,.watches,.triggered_watches,.watcher-history*" ``` 分别运行master和slave的程序 ``` ./bin/elasticsearch ``` 运行kibana ``` ./bin/kibana ``` 在浏览器上输入: `http://xxx.xxx.xxx.xxx:5601/` ,打开Kibana,要输入用户名和密码登录,默认分别是 `elastic` 和 `changeme`,可以很方便的看到节点的情况 ![](http://markdown.archerwong.cn/2019-01-02-04-11-58_clipboard.png) 注意:复制的elastic-salve1文件夹下包含了data文件中源文件节点数据,需要把elastic-salve1文件夹下data文件下的文件清空,否则会出现下面类似错误。 [2018-05-23T17:52:48,839][INFO ][o.e.d.z.ZenDiscovery ] [slave-2] failed to send join request to master [{master}{iErWGFrwSuCTjXaOD9jE5g}{JHJNb1U_TVaqdNVoJmEBZw}{127.0.0.1}{127.0.0.1:9300}{ml.max_open_jobs=10, ml.enabled=true}], reason [RemoteTransportException[[master][127.0.0.1:9300][internal:discovery/zen/join]]; nested: IllegalArgumentException[can't add node {slave-2}{iErWGFrwSuCTjXaOD9jE5g}{uo1SvmJyTESE_Z6bcQrOeg}{127.0.0.1}{127.0.0.1:9301}{ml.max_open_jobs=10, ml.enabled=true}, found existing node {master}{iErWGFrwSuCTjXaOD9jE5g}{JHJNb1U_TVaqdNVoJmEBZw}{127.0.0.1}{127.0.0.1:9300}{ml.max_open_jobs=10, ml.enabled=true} with the same id but is a different node instance]; ] # 参考: http://www.ruanyifeng.com/blog/2017/08/elasticsearch.html https://www.cnblogs.com/wxw16/p/6156335.html

elasticsearch基础概念

[TOC] # 1. 基本语法 如果你正在使用 Java,在代码中你可以使用 Elasticsearch 内置的两个客户端。所有其他语言可以使用 RESTful API 通过端口 9200 和 Elasticsearch 进行通信,你可以用你最喜爱的 web 客户端访问 Elasticsearch 。事实上,正如你所看到的,你甚至可以使用 curl 命令来和 Elasticsearch 交互。 **注意** Elasticsearch 为以下语言提供了官方客户端 --Groovy、JavaScript、.NET、 PHP、 Perl、 Python 和 Ruby--还有很多社区提供的客户端和插件,所有这些都可以在 [Elasticsearch Clients](https://www.elastic.co/guide/en/elasticsearch/client/index.html) 中找到。 一个 Elasticsearch 请求和任何 HTTP 请求一样由若干相同的部件组成: curl -X<VERB> '<PROTOCOL>://<HOST>:<PORT>/<PATH>?<QUERY_STRING>' -d '<BODY>' 被 < > 标记的部件含义: 字段 | 含义 ---|--- VERB | 适当的 HTTP 方法 或 谓词 : GET`、 `POST`、 `PUT`、 `HEAD 或者 `DELETE`。 PROTOCOL | http 或者 https`(如果你在 Elasticsearch 前面有一个 `https 代理) HOST | Elasticsearch 集群中任意节点的主机名,或者用 localhost 代表本地机器上的节点。 PORT | 运行 Elasticsearch HTTP 服务的端口号,默认是 9200 。 PATH | API 的终端路径(例如 _count 将返回集群中文档数量)。Path 可能包含多个组件,例如:_cluster/stats 和 _nodes/stats/jvm 。 QUERY_STRING | 任意可选的查询字符串参数 (例如 **?pretty将格式化地输出 JSON 返回值**,使其更容易阅读) BODY | 一个 JSON 格式的请求体 (如果请求需要的话) 例如,计算集群中文档的数量,我们可以用这个(**完整模式**): ``` curl -XGET 'http://localhost:9200/_count?pretty' -d ' { "query": { "match_all": {} } }' ``` 想要在返回结果中看到 HTTP 头信息,需要结合 `-i 参数来使用 curl 命令: curl -i -XGET 'localhost:9200/' 很多资料会使用**缩写格式**显示: 所谓的缩写格式就是省略请求中所有相同的部分,例如主机名、端口号以及 curl 命令本身。而不是像上面例子中显示的那样用一个完整的请求 ``` GET /_count { "query": { "match_all": {} } } ``` # 2. 数据架构:(官方实例:索引雇员文档) ## 2.1 文档的样子 一个 Elasticsearch 集群可以包含多个索引(类似于数据库),相应的每个索引可以包含多个类型。这些不同的类型存储着多个文档 ,每个文档又有多个属性 。 借助以下一条查询出的结果理解: ``` { "_index": "megacorp", "_type": "employee", "_id": "1", "_version": 1, "found": true, "_source": { "first_name": "John", "last_name": "Smith", "age": 25, "about": "I love to go rock climbing", "interests": [ "sports", "music" ] } } ``` 每个雇员索引一个**文档**,包含该雇员的所有信息。 每个文档都将是 employee **类型** 。 该类型位于 **索引** megacorp 内。 该索引保存在我们的 Elasticsearch **集群**中。 在 Elasticsearch 中,术语 **文档** 有着特定的含义。它是指最顶层或者根对象, 这个根对象被序列化成 JSON 并存储到 Elasticsearch 中,指定了唯一 ID。 ## 2.2 元数据 一个文档除了自身数据外,还包括元数据(有关文档的信息) 三个必须的元数据元素如下: - _index 文档在哪存放 - _type 文档表示的对象类别 - _id 文档唯一标识 1> 索引 索引名必须小写,不能以下划线开头,不能包含逗号 **实际上,在 Elasticsearch 中,我们的数据是被存储和索引在『分片』中,而一个索引仅仅是逻辑上的命名空间, 这个命名空间由一个或者多个分片组合在一起。** 然而,这是一个内部细节,我们的应用程序根本不应该关心分片,对于应用程序而言,只需知道文档位于一个 索引 内。 Elasticsearch 会处理所有的细节,当然这很有意思,有兴趣可以读下相关章节- 『集群原理』(https://www.elastic.co/guide/cn/elasticsearch/guide/current/distributed-cluster.html)。 2> 类别 数据可能在索引中只是松散的组合在一起,但是通常明确定义一些数据中的子分区是很有用的,在索引中对数据进行逻辑分区,这将更加清晰。 一个 _type 命名可以是大写或者小写,但是不能以下划线或者句号开头,不应该包含逗号, 并且长度限制为256个字符. 3> id ID 是一个字符串, 当它和 _index 以及 _type 组合就可以唯一确定 Elasticsearch 中的一个文档。 当你创建一个新的文档,要么提供自己的 _id ,要么让 Elasticsearch 帮你生成。 4> 其他一些元数据 ## 2.3 与mysql的对比 (1)关系型数据库中的数据库(DataBase),等价于ES中的索引(Index) (2)一个数据库下面有N张表(Table),等价于1个索引Index下面有N多类型(Type) (3)一个数据库表(Table)下的数据由多行(ROW)多列(column,属性)组成,等价于1个Type由多个文档(Document)和多Field组成。 (4)在一个关系型数据库里面,schema定义了表、每个表的字段,还有表和字段之间的关系。 与之对应的,在ES中:Mapping定义索引下的Type的字段处理规则,即索引如何建立、索引类型、是否保存原始索引JSON文档、是否压缩原始JSON文档、是否需要分词处理、如何进行分词处理等。 (5)在数据库中的增insert、删delete、改update、查search操作等价于ES中的增PUT/POST、删Delete、改_update、查GET. ## 2.4 核心概念 可能作为开发工程师以下的一些概念可能并不需要关心,但是了解下也是可以的。 ES核心概念 1)Cluster:集群。 ES可以作为一个独立的单个搜索服务器。不过,为了处理大型数据集,实现容错和高可用性,ES可以运行在许多互相合作的服务器上。这些服务器的集合称为集群。 2)Node:节点。 形成集群的每个服务器称为节点。 3)Shard:分片。 当有大量的文档时,由于内存的限制、磁盘处理能力不足、无法足够快的响应客户端的请求等,一个节点可能不够。这种情况下,数据可以分为较小的分片。每个分片放到不同的服务器上。 当你查询的索引分布在多个分片上时,ES会把查询发送给每个相关的分片,并将结果组合在一起,而应用程序并不知道分片的存在。即:这个过程对用户来说是透明的。 4)Replia:副本。 为提高查询吞吐量或实现高可用性,可以使用分片副本。 副本是一个分片的精确复制,每个分片可以有零个或多个副本。ES中可以有许多相同的分片,其中之一被选择更改索引操作,这种特殊的分片称为主分片。 当主分片丢失时,如:该分片所在的数据不可用时,集群将副本提升为新的主分片。 5)全文检索。 全文检索就是对一篇文章进行索引,可以根据关键字搜索,类似于mysql里的like语句。 全文索引就是把内容根据词的意义进行分词,然后分别创建索引,例如”你们的激情是因为什么事情来的” 可能会被分词成:“你们“,”激情“,“什么事情“,”来“ 等token,这样当你搜索“你们” 或者 “激情” 都会把这句搜出来。 ## 2.5 什么是ELK ELK不是一款软件,而是elasticsearch+Logstash+kibana三款开源软件组合而成的日志收集处理套件,堪称神器。其中Logstash负责日志收集,elasticsearch负责日志的搜索、统计,而kibana则是ES的展示神器,前端炫丽,点几下鼠标简单配置,就可以完成搜索、聚合功能,生成华丽的报表。 # 参考 https://cloud.tencent.com/developer/article/1066239

elasticsearch增删改查操作

[TOC] # 1. 插入数据 关于下面的代码如何使用,可以借助于kibana的console,浏览器打开地址: ``` http://xxx.xxx.xxx.xxx:5601/app/kibana#/dev_tools/console?_g=() ``` 在console中输入代码,然后运行即可,也可以自己改成curl形式在命令行输入 插入数据可以指定id或者不指定id 1> 使用自定义的id 使用put方式,并自己提供id 类似于下面的格式 ``` PUT /{index}/{type}/{id} { "field": "value", ... } ``` 请求 ``` PUT /website/blog/123 { "title": "My first blog entry", "text": "Just trying this out...", "date": "2014/01/01" } ``` 响应 ``` { "_index": "website", "_type": "blog", "_id": "123", "_version": 1, "created": true } ``` 在 Elasticsearch 中每个文档都有一个版本号。当每次对文档进行修改时(包括删除), _version 的值会递增。 2> 自动生成id 使用post方式 ``` POST /website/blog/ { "title": "My second blog entry", "text": "Still trying this out...", "date": "2014/01/01" } ``` ``` { "_index": "website", "_type": "blog", "_id": "AVFgSgVHUP18jI2wRx0w", "_version": 1, "created": true } ``` 自动生成的 ID 是 URL-safe、 基于 Base64 编码且长度为20个字符的 GUID 字符串。 这些 GUID 字符串由可修改的 FlakeID 模式生成,这种模式允许多个节点并行生成唯一 ID ,且互相之间的冲突概率几乎为零。 # 2. 更改数据 控制台输入 ``` PUT /website/blog/123 { "title": "My first blog entry", "text": "Just trying this out...", "date": "2014/01/01" } ``` 在响应体中,我们能看到 Elasticsearch 已经增加了 _version 字段值,created 标志设置成 false ,是因为相同的索引、类型和 ID 的文档已经存在。 ``` { "_index": "website", "_type": "blog", "_id": "123", "_version": 2, "created": false } ``` # 3. 删除数据 ``` DELETE /website/blog/123 ``` 如果找到该文档,Elasticsearch 将要返回一个 200 ok 的 HTTP 响应码,和一个类似以下结构的响应体。注意,字段 _version 值已经增加: ``` { "found" : true, "_index" : "website", "_type" : "blog", "_id" : "123", "_version" : 3 } ``` 如果文档没有 找到,我们将得到 404 Not Found 的响应码和类似这样的响应体: ``` { "found" : false, "_index" : "website", "_type" : "blog", "_id" : "123", "_version" : 4 } ``` # 4. 检索文档 这里只是先简单的介绍下如何检索文档,后面会详细介绍这部分内容 1> 检索id为1的员工 在bibana的console中输入运行 ``` GET /megacorp/employee/1 ``` 返回结果包含了文档的一些元数据,以及 _source 属性,内容是 John Smith 雇员的原始 JSON 文档: ``` { "_index": "megacorp", "_type": "employee", "_id": "1", "_version": 1, "found": true, "_source": { "first_name": "John", "last_name": "Smith", "age": 25, "about": "I love to go rock climbing", "interests": [ "sports", "music" ] } } ``` 2> 搜索所有雇员 ``` GET /megacorp/employee/_search ``` 返回结果包括了所有三个文档,放在数组 hits 中。**一个搜索默认返回十条结果**。 **elasticsearch 提供了两种查询模式** 1> Query-string:通过url参数来搜索,被称为查询字符串搜索。 ``` GET /megacorp/employee/_search?q=last_name:Smith ``` 2> Query-DSL:使用查询表达式搜索,被称为DSL查询,它支持构建更加复杂和健壮的查询,一般来说我们重点学习这种方法。 ``` GET /megacorp/employee/_search { "query" : { "match" : { "last_name" : "Smith" } } } ``` 3> 更复杂的搜索 ``` GET /megacorp/employee/_search { "query" : { "bool": { "must": { "match" : { "last_name" : "smith" } }, "filter": { "range" : { "age" : { "gt" : 30 } } } } } } ``` 注意这里搜索last_name是smith的人,同时年龄大于30。注意下语法bool里面有must,filter类型,当然以后还会学到更多类型,这里先有个意识。 4> 全文搜索 比如想要搜索下所有喜欢攀岩(rock climbing)的雇员 ``` GET /megacorp/employee/_search { "query" : { "match" : { "about" : "rock climbing" } } } ``` ``` { "took": 2, "timed_out": false, "_shards": { "total": 5, "successful": 5, "skipped": 0, "failed": 0 }, "hits": { "total": 2, "max_score": 0.53484553, "hits": [ { "_index": "megacorp", "_type": "employee", "_id": "1", "_score": 0.53484553, "_source": { "first_name": "John", "last_name": "Smith", "age": 25, "about": "I love to go rock climbing", "interests": [ "sports", "music" ] } }, { "_index": "megacorp", "_type": "employee", "_id": "2", "_score": 0.26742277, "_source": { "first_name": "Jane", "last_name": "Smith", "age": 32, "about": "I like to collect rock albums", "interests": [ "music" ] } } ] } } ``` 注意这里稍有不同,about字段中包含两个单词,搜索的结果并不是完全匹配,是根据单词去做了相关性匹配。 Elasticsearch 默认按照相关性得分排序,即每个文档跟查询的匹配程度。 **Elasticsearch中的 相关性 概念非常重要**,也是完全区别于传统关系型数据库的一个概念,数据库中的一条记录要么匹配要么不匹配。 5> 短语搜索 上面如果一个短语包含多个单词,那岂不是不能精确查询了,当然不是,可以使用短语搜索。 ``` GET /megacorp/employee/_search { "query" : { "match_phrase" : { "about" : "rock climbing" } } } ``` 这样结果就是精确匹配了,仅匹配同时包含 “rock” 和 “climbing” ,并且 二者以短语 “rock climbing” 的形式紧挨着的结果 6> 高亮搜索 ``` GET /megacorp/employee/_search { "query" : { "match_phrase" : { "about" : "rock climbing" } }, "highlight": { "fields" : { "about" : {} } } } ``` 结果中多了一个highlight部分 ``` { ... "hits": { "total": 1, "max_score": 0.23013961, "hits": [ { ... "_score": 0.23013961, "_source": { "first_name": "John", "last_name": "Smith", "age": 25, "about": "I love to go rock climbing", "interests": [ "sports", "music" ] }, "highlight": { "about": [ "I love to go <em>rock</em> <em>climbing</em>" ] } } ] } } ``` 7> 聚合 聚合类似于SQL中的GROUP_BY,但功能更强大 ``` GET /megacorp/employee/_search { "aggs": { "all_interests": { "terms": { "field": "interests" } } } } ``` 结果 ``` { ... "hits": { ... }, "aggregations": { "all_interests": { "buckets": [ { "key": "music", "doc_count": 2 }, { "key": "forestry", "doc_count": 1 }, { "key": "sports", "doc_count": 1 } ] } } } ``` 以上是对所有的雇员进行统计,我们也可以其中的一部分雇员进行组合查询统计,比如我们想知道叫smith的雇员最受欢迎的兴趣爱好。 ``` GET /megacorp/employee/_search { "query": { "match": { "last_name": "smith" } }, "aggs": { "all_interests": { "terms": { "field": "interests" } } } } ``` 聚合还支持分级汇总,查询特定兴趣爱好的 员工的平均年龄 ``` GET /megacorp/employee/_search { "aggs" : { "all_interests" : { "terms" : { "field" : "interests" }, "aggs" : { "avg_age" : { "avg" : { "field" : "age" } } } } } } ``` 结果 ``` ... "all_interests": { "buckets": [ { "key": "music", "doc_count": 2, "avg_age": { "value": 28.5 } }, { "key": "forestry", "doc_count": 1, "avg_age": { "value": 35 } }, { "key": "sports", "doc_count": 1, "avg_age": { "value": 25 } } ] } ```

elasticsearch索引和映射

[TOC] # 1. elasticsearch如何实现搜索 我们使用sql数据库的时候,要先创建数据库,然后创建表,之后才可以将数据存入到表中,这个表规定了数据的构成结构。虽然elasticsearch是一个无模式的搜索引擎并且可以自动匹配数据的结构(ES 会尽量根据 JSON 源数据的基础类型猜测你想要的字段类型映射),但是我们认为人为定义和控制数据结构是更好的方式。所以我们有必要学习如何创建索引和如何创建映射。 ## 1.1 搜索实例 在mysql中我们查询`created_at = '2014-09-15'`那么该`created_at`字段只有是`2014-09-15`和不是两种情况,但是在elsaticsearch中则很有趣,看下下面的例子 索引中有12条推文,其中只有一条包含日期 `2014-09-15` ,但是看一看下面查询命中的 总数 (total): ``` GET /_search?q=2014 # 12 results GET /_search?q=2014-09-15 # 12 results ! GET /_search?q=date:2014-09-15 # 1 result GET /_search?q=date:2014 # 0 results ! ``` 分析: - 第一个和第二个搜索是全文搜索,起作用的是 `_all字段` ,这里都返回了12条推文,但是我们提到了只有一条包含了日期 `2014-09-15`,为什么12条数据都能匹配? - 第三和第四条数据时在 `data字段` 搜索,不难理解,第三条数据一条命中,第四条只是个年份,不可能有匹配项。 为什么会出现上面的结果呢,全文搜索好像有点不一般啊,推测起来,这可能是数据在 _all 字段与 date 字段的索引方式不同。带着这个疑问我们一起分析下。 可以查看下索引的类型映射 ``` GET /gb/_mapping/tweet ``` 结果如下 ``` { "gb": { "mappings": { "tweet": { "properties": { "date": { "type": "date", "format": "strict_date_optional_time||epoch_millis" }, "name": { "type": "string" }, "tweet": { "type": "string" }, "user_id": { "type": "long" } } } } } } ``` 尽管我们没有定义该索引的映射,但是es动态为我们产生了一个映射,这个响应告诉我们 date 字段被认为是 date 类型的。由于 _all 是默认字段,所以没有提及它。但是我们知道 _all 字段是 string 类型的。所以 date 字段和 string 字段 索引方式不同,数据类型不同,因此搜索结果也不一样,但这只是表象,深层次原因呢? ## 1.2 es中数据的类型 Elasticsearch 中的数据可以概括的分为两类:『精确值』和 『全文』。 ▶『精确值』 如它们听起来那样精确。例如日期或者用户 ID,但字符串也可以表示精确值,例如用户名或邮箱地址。对于精确值来讲,Foo 和 foo 是不同的,2014 和 2014-09-15 也是不同的。 精确值很容易查询。结果是二进制的:要么匹配查询,要么不匹配。这种查询很容易用 SQL 表示: ``` WHERE name = "John Smith" AND user_id = 2 AND date > "2014-09-15" ``` ▶『全文』 是指文本数据(通常以人类容易识别的语言书写),例如一个推文的内容或一封邮件的内容。 查询全文数据要微妙的多。我们问的不只是“这个文档匹配查询吗”,而是“该文档匹配查询的程度有多大?”。 我们很少对全文类型的域做精确匹配。相反,我们希望在文本类型的域中搜索。不仅如此,我们还希望搜索**能够理解我们的 意图** : - 搜索 UK ,会返回包含 United Kindom 的文档。 - 搜索 jump ,会匹配 jumped , jumps , jumping ,甚至是 leap 。 - 搜索 johnny walker 会匹配 Johnnie Walker , johnnie depp 应该匹配 Johnny Depp 。 - fox news hunting 应该返回福克斯新闻( Foxs News )中关于狩猎的故事,同时, fox hunting news 应该返回关于猎狐的故事。 es是如何做到以上的想法的呢? Elasticsearch 首先会 **分析文档**,之后根据结果创建 **倒排索引** ,接下来一起讨论分析过程和倒排索引 ## 1.3 倒排索引 一个倒排索引由文档中所有不重复词的列表构成,对于其中每个词,有一个包含它的文档列表。 例如,假设我们有两个文档,每个文档的 content 域包含如下内容: ``` The quick brown fox jumped over the lazy dog Quick brown foxes leap over lazy dogs in summer ``` 为了创建倒排索引,我们首先将每个文档的 content 域拆分成单独的 词(我们称它为 词条 或 tokens ),创建一个包含所有不重复词条的排序列表,然后列出每个词条出现在哪个文档。结果如下所示: ``` Term Doc_1 Doc_2 ------------------------- Quick | | X The | X | brown | X | X dog | X | dogs | | X fox | X | foxes | | X in | | X jumped | X | lazy | X | X leap | | X over | X | X quick | X | summer | | X the | X | ------------------------ ``` 现在,如果我们想搜索 quick brown ,我们只需要查找包含每个词条的文档: ``` Term Doc_1 Doc_2 ------------------------- brown | X | X quick | X | ------------------------ Total | 2 | 1 ``` 两个文档都匹配,但是第一个文档比第二个匹配度更高。如果我们使用仅计算匹配词条数量的简单 相似性算法 ,那么,我们可以说,对于我们查询的相关性来讲,第一个文档比第二个文档更佳。 但是,我们目前的倒排索引有一些问题: - Quick 和 quick 以独立的词条出现,然而用户可能认为它们是相同的词。 - fox 和 foxes 非常相似, 就像 dog 和 dogs ;他们有相同的词根。 - jumped 和 leap, 尽管没有相同的词根,但他们的意思很相近。他们是同义词。 使用前面的索引搜索 +Quick +fox 不会得到任何匹配文档。(记住,+ 前缀表明这个词必须存在。)只有同时出现 Quick 和 fox 的文档才满足这个查询条件,但是第一个文档包含 quick fox ,第二个文档包含 Quick foxes 。 我们的用户可以合理的期望两个文档与查询匹配。我们可以做的更好。 如果我们将词条规范为标准模式,那么我们可以找到与用户搜索的词条不完全一致,但具有足够相关性的文档。例如: - Quick 可以小写化为 quick 。 - foxes 可以 词干提取 --变为词根的格式-- 为 fox 。类似的, dogs 可以为提取为 dog 。 - jumped 和 leap 是同义词,可以索引为相同的单词 jump 。 现在索引看上去像这样: ``` Term Doc_1 Doc_2 ------------------------- brown | X | X dog | X | X fox | X | X in | | X jump | X | X lazy | X | X over | X | X quick | X | X summer | | X the | X | X ------------------------ ``` 这还远远不够。我们搜索 +Quick +fox 仍然 会失败,因为在我们的索引中,已经没有 Quick 了。但是,如果我们对搜索的字符串使用与 content 域相同的标准化规则,查询字符串就会变成查询 +quick +fox ,这样两个文档都会匹配! 注意:**这非常重要**。我们要用相同标准处理两部分的数据 **查询字符串** 和 **索引文本** ,你只能搜索在索引中出现的词条,所以索引文本和查询字符串必须标准化为相同的格式。 ## 1.4 分析与分析器 ### 1.4.1 什么是分析器 首先,将一块文本分成适合于倒排索引的独立的 词条 , 之后,将这些词条统一化为标准格式以提高它们的“可搜索性”,或者 recall 分析器执行上面的工作。 分析器 实际上是**将三个功能封装到了一个包里**: - 字符过滤器 首先,字符串按顺序通过每个 字符过滤器 。字符过滤器 用来 整理 一个尚未被分词的字符串。例如,如果我们的文本是HTML格式的,它会包含像 `<p>` 或者 `<div>` 这样的HTML标签,这些标签是我们不想索引的。我们可以使用 html清除 字符过滤器 来移除掉所有的HTML标签,并且像把 `&Aacute`; 转换为相对应的Unicode字符 `Á` 这样,转换HTML实体。 一个分析器可能有0个或者多个字符过滤器。 - 分词器 其次,字符串被 分词器 分为单个的词条。 分词器把字符串分解成单个词条或者词汇单元。 `标准 分析器` 把一个字符串根据单词边界分解成单个词条,并且移除掉大部分的标点符号,然而还有其他不同行为的分词器存在。 例如, `关键词 分词器` 完整地输出 接收到的同样的字符串,并不做任何分词。 `空格 分词器` 只根据空格分割文本 。 `正则 分词器` 根据匹配正则表达式来分割文本 。 一个分析器 必须 有一个唯一的分词器。 - 词单元过滤器 最后,词条按顺序通过每个 token 过滤器 。这个过程可能会改变词条(例如,小写化 Quick ),删除词条(例如, 像 a`, `and`, `the 等无用词),或者增加词条(例如,像 jump 和 leap 这种同义词)。 Elasticsearch提供了开箱即用的字符过滤器、分词器和词单元过滤器。 这些可以组合起来形成自定义的分析器以用于不同的目的。 ### 1.4.2 内置分析器种类 但是, Elasticsearch还附带了可以直接使用的预包装的分析器。 接下来我们会列出最重要的分析器。为了证明它们的差异,我们看看每个分析器会从下面的字符串得到哪些词条: ``` "Set the shape to semi-transparent by calling set_trans(5)" ``` - 标准分析器 标准分析器是Elasticsearch默认使用的分析器。它是分析各种语言文本最常用的选择。它根据 Unicode 联盟 定义的 单词边界 划分文本。删除绝大部分标点。最后,将词条小写。它会产生 ``` set, the, shape, to, semi, transparent, by, calling, set_trans, 5 ``` - 简单分析器 简单分析器在任何不是字母的地方分隔文本,将词条小写。它会产生 ``` set, the, shape, to, semi, transparent, by, calling, set, trans ``` - 空格分析器 空格分析器在空格的地方划分文本。它会产生 ``` Set, the, shape, to, semi-transparent, by, calling, set_trans(5) ``` - 语言分析器 特定语言分析器可用于 很多语言。它们可以考虑指定语言的特点。例如, 英语 分析器附带了一组英语无用词(常用单词,例如 and 或者 the ,它们对相关性没有多少影响),它们会被删除。 由于理解英语语法的规则,这个分词器可以提取英语单词的 词干 。 英语 分词器会产生下面的词条: ``` set, shape, semi, transpar, call, set_tran, 5 ``` 注意看 `transparent`、 `calling` 和 `set_trans` 已经变为词根格式。 ### 1.4.3 分析行为(开篇实例解答) 当我们 索引 一个文档,它的全文域被分析成词条以用来创建倒排索引。 但是,当我们在全文域 搜索 的时候,我们需要将查询字符串通过 相同的分析过程 ,以保证我们搜索的词条格式与索引中的词条格式一致。 全文查询,理解每个域是如何定义的,因此它们可以做 正确的事: - 当你查询一个 全文 域时, 会对查询字符串应用相同的分析器,以产生正确的搜索词条列表。 - 当你查询一个 精确值 域时,不会分析查询字符串, 而是搜索你指定的精确值。 现在你可以理解在 开始章节 的查询为什么返回那样的结果: - date 域包含一个精确值:单独的词条 `2014-09-15`。 - _all 域是一个全文域,所以分词进程将日期(2014-09-15)转化为三个词条: `2014`, `09`, 和 `15`。 当我们在 _all 域查询 `2014`,它匹配所有的12条推文,因为它们都含有 `2014` : ``` GET /_search?q=2014 # 12 results ``` 当我们在 _all 域查询 `2014-09-15`,它首先分析查询字符串,产生匹配 `2014`, `09`, 或 `15` 中 任意 词条的查询。这也会匹配所有12条推文,因为它们都含有 `2014` : ``` GET /_search?q=2014-09-15 # 12 results ! ``` 当我们在 date 域查询 `2014-09-15`,它寻找 精确 日期,只找到一个推文: ``` GET /_search?q=date:2014-09-15 # 1 result ``` 当我们在 date 域查询 `2014`,它找不到任何文档,因为没有文档含有这个精确日志: ``` GET /_search?q=date:2014 # 0 results ! ``` ### 1.4.4 测试分析器 可以使用 analyze API 来看文本是如何被分析的。在消息体里,指定分析器和要分析的文本: ``` GET /_analyze { "analyzer": "standard", "text": "Text to analyze" } ``` 结果 ``` { "tokens": [ { "token": "text", "start_offset": 0, "end_offset": 4, "type": "<ALPHANUM>", "position": 1 }, { "token": "to", "start_offset": 5, "end_offset": 7, "type": "<ALPHANUM>", "position": 2 }, { "token": "analyze", "start_offset": 8, "end_offset": 15, "type": "<ALPHANUM>", "position": 3 } ] } ``` token 是实际存储到索引中的词条。 position 指明词条在原始文本中出现的位置。 start_offset 和 end_offset 指明字符在原始字符串中的位置。 ### 1.4.5 指定分析器 当Elasticsearch在你的文档中检测到一个新的字符串域 ,它会自动设置其为一个全文 字符串 域,使用 标准 分析器对它进行分析。 你不希望总是这样。可能你想使用一个不同的分析器,适用于你的数据使用的语言。有时候你想要一个字符串域就是一个字符串域--不使用分析,直接索引你传入的精确值,例如用户ID或者一个内部的状态域或标签。 要做到指定数据类型和指定锁使用的分析器,我们**必须手动指定这些域的映射**。关于如何配置和指定分析器,将在索引的构建中讲解 ## 1.5 映射 上面已经知道了倒排索引,分析和分析器内容,那么如何才能够是es实现以上特性呢,就是编写映射规则,我们配置映射的时候会规定数据的类型、是否使用分析器、使用何种分析器等内容。 为了能够将时间域视为时间,数字域视为数字,字符串域视为全文或精确值字符串, Elasticsearch 需要知道每个域中数据的类型。映射定义了类型中的域,每个域的数据类型,以及Elasticsearch如何处理这些域。映射也用于配置与类型有关的元数据。我们这里先了解下映射的概念,下一章节会讲解如何编写映射。 ### 1.5.1 简单核心域类型 简单域类型: - 字符串: string - 整数 : byte, short, integer, long - 浮点数: float, double - 布尔型: boolean - 日期: date 如果没有定义数据的类型,也没有使用定义动态映射规则,那么会通过JSON中基本数据类型,尝试猜测域类型,使用如下规则: JSON type | 域 type ---|--- 布尔型: true 或者 false | boolean 整数: 123 | long 浮点数: 123.45 | double 字符串,有效日期: 2014-09-15 | date 字符串: foo bar | string ### 1.5.2 复杂核心域类型 除了简单标量数据类型, JSON 还有 null 值,数组,和对象,这些 Elasticsearch 都是支持的。 - 多值域 对于数组,没有特殊的映射需求,在es中没有专门的数组类型。**任何域**都可以包含0、1或者多个值,就像全文域分析得到多个词条。 数组中所有的值必须是相同数据类型的 。你不能将日期和字符串混在一起。如果你通过索引数组来创建新的域,Elasticsearch 会用数组中第一个值的数据类型作为这个域的 类型 。 数据: ``` { "tag": [ "search", "nosql" ]} ``` - 空域 当然,数组可以为空。 这相当于存在零值。 事实上,在 Lucene 中是不能存储 null 值的,所以我们认为存在 null 值的域为空域。 下面三种域被认为是空的,它们将不会被索引: ``` "null_value": null, "empty_array": [], "array_with_null_value": [ null ] ``` - 多层级对象 数据: ``` { "tweet": "Elasticsearch is very flexible", "user": { "id": "@johnsmith", "gender": "male", "age": 26, "name": { "full": "John Smith", "first": "John", "last": "Smith" } } } ``` 映射: ``` { "gb": { "tweet": { "properties": { "tweet": { "type": "string" }, "user": { "type": "object", "properties": { "id": { "type": "string" }, "gender": { "type": "string" }, "age": { "type": "long" }, "name": { "type": "object", "properties": { "full": { "type": "string" }, "first": { "type": "string" }, "last": { "type": "string" } } } } } } } } } ``` type 映射(tweet)只是一种特殊的 对象 映射,我们称之为 根对象。 Lucene 不理解内部对象。 Lucene 文档是由一组键值对列表组成的。为了能让 Elasticsearch 有效地索引内部类,它把我们的文档转化成这样: ``` { "tweet": [elasticsearch, flexible, very], "user.id": [@johnsmith], "user.gender": [male], "user.age": [26], "user.name.full": [john, smith], "user.name.first": [john], "user.name.last": [smith] } ``` 内部域 可以通过名称引用(例如, first )。为了区分同名的两个域,我们可以使用全 路径 (例如, user.name.first ) 或 type 名加路径( tweet.user.name.first )。 - 内部对象数组 假设我们有个 followers 数组: ``` { "followers": [ { "age": 35, "name": "Mary White"}, { "age": 26, "name": "Alex Jones"}, { "age": 19, "name": "Lisa Smith"} ] } ``` 这个文档会像我们之前描述的那样被扁平化处理,结果如下所示: ``` { "followers.age": [19, 26, 35], "followers.name": [alex, jones, lisa, smith, mary, white] } ``` {age: 35} 和 {name: Mary White} 之间的相关性已经丢失了,因为每个多值域只是一包无序的值,而不是有序数组。这足以让我们问,“有一个26岁的追随者?” 但是我们不能得到一个准确的答案:“是否有一个26岁 名字叫 Alex Jones 的追随者?” 相关内部对象被称为 nested 嵌套对象,可以回答上面的查询,这里暂不做研究。 # 2. 索引 ## 2.1 什么是索引 索引是es中存储数据的一种逻辑,大部分人对mysql会更熟悉一些,可以对比下 (1)关系型数据库中的数据库(DataBase),等价于ES中的索引(Index) (2)一个数据库下面有N张表(Table),等价于1个索引Index下面有N多类型(Type) (3)一个数据库表(Table)下的数据由多行(ROW)多列(column,属性)组成,等价于1个Type由多个文档(Document)和多Field组成。 (4)在一个关系型数据库里面,schema定义了表、每个表的字段,还有表和字段之间的关系。 与之对应的,在ES中:Mapping定义索引下的Type的字段处理规则,即索引如何建立、索引类型、是否保存原始索引JSON文档、是否压缩原始JSON文档、是否需要分词处理、如何进行分词处理等。 ## 2.2 创建一个默认配置的索引 ``` curl -XPOST 'http://localhost:9200/posts' ``` 以上索引采用的是默认配置,新的字段通过『动态映射』的方式被添加到类型映射。当然我们可以做更多的控制,手动创建索引,在请求体里传入设置或类型映射: ``` PUT /my_index { "settings": { ... any settings ... }, "mappings": { "type_one": { ... any mappings ... }, "type_two": { ... any mappings ... }, ... } } ``` 上面分为两部分,第一部分是settings,第二部分是mappings ## 2.3 索引的设置(settings部分) 可以通过修改配置来自定义索引行为,但是除非你理解这些配置的作用并且知道为什么要去修改,否则不要随意修改,Elasticsearch 已经提供了优化好的默认配置。 配置: ▶ 1、number_of_shards 每个索引的主分片数,默认值是 5 。这个配置在索引创建后不能修改。 ▶ 2、number_of_replicas 每个主分片的副本数,默认值是 1 。对于活动的索引库,这个配置可以随时修改。 例如,我们可以创建只有 一个主分片,没有副本的小索引: ``` PUT /my_temp_index { "settings": { "number_of_shards" : 1, "number_of_replicas" : 0 } } ``` ▶ 3、配置分析器 第三个重要的索引设置是 analysis 部分, 用来配置已存在的分析器或针对你的索引创建新的自定义分析器,后面编写映射的 前面我们已经介绍了一些内置的分析器,用于将全文字符串转换为适合搜索的倒排索引。 standard 分析器是用于全文字段的默认分析器, 对于大部分西方语系来说是一个不错的选择。 它包括了以下几点: - standard 分词器,通过单词边界分割输入的文本。 - standard 语汇单元过滤器,目的是整理分词器触发的语汇单元(但是目前什么都没做)。 - lowercase 语汇单元过滤器,转换所有的语汇单元为小写。 - stop 语汇单元过滤器,删除停用词--对搜索相关性影响不大的常用词,如 a , the , and , is 默认情况下,停用词过滤器是被禁用的。如需启用它,你可以通过创建一个基于 standard 分析器的自定义分析器并设置 stopwords 参数。 可以给分析器提供一个停用词列表,或者告知使用一个基于特定语言的预定义停用词列表。 在下面的例子中,我们创建了一个新的分析器,叫做 es_std , 并使用预定义的 西班牙语停用词列表: ``` PUT /spanish_docs { "settings": { "analysis": { "analyzer": { "es_std": { "type": "standard", "stopwords": "_spanish_" } } } } } ``` es_std 分析器不是全局的--它仅仅存在于我们定义的 spanish_docs 索引中。 为了使用 analyze API来对它进行测试,我们必须使用特定的索引名: ``` GET /spanish_docs/_analyze?analyzer=es_std El veloz zorro marrón ``` 简化的结果显示西班牙语停用词 El 已被正确的移除: ``` { "tokens" : [ { "token" : "veloz", "position" : 2 }, { "token" : "zorro", "position" : 3 }, { "token" : "marrón", "position" : 4 } ] } ``` ▶ ▶自定义分析器 和我们之前配置 es_std 分析器一样,我们可以在 analysis 下的相应位置设置 `字符过滤器` 、 `分词器` 和 `词单元过滤器`: ``` PUT /my_index { "settings": { "analysis": { "char_filter": { ... custom character filters ... }, "tokenizer": { ... custom tokenizers ... }, "filter": { ... custom token filters ... }, "analyzer": { ... custom analyzers ... } } } } ``` 作为示范,让我们一起来创建一个自定义分析器吧,`字符过滤器` 实现1、2、 `分词器` 实现3, `词单元过滤器`实现4、5: 1、使用 html清除 字符过滤器移除HTML部分。(html_strip) 2、使用一个自定义的 映射 字符过滤器把 & 替换为 " and " :(_to_and) ``` "char_filter": { "&_to_and": { "type": "mapping", "mappings": [ "&=> and "] } } ``` 3、使用 标准 分词器分词。(standard) 4、小写词条,使用 小写 词过滤器处理。(lowercase) 5、使用自定义 停止 词过滤器移除自定义的停止词列表中包含的词:(my_stopwords) ``` "filter": { "my_stopwords": { "type": "stop", "stopwords": [ "the", "a" ] } } ``` 一个完整的示例 ``` PUT /my_index { "settings": { "analysis": { "char_filter": { "&_to_and": { "type": "mapping", "mappings": [ "&=> and "] }}, "filter": { "my_stopwords": { "type": "stop", "stopwords": [ "the", "a" ] }}, "analyzer": { "my_analyzer": { "type": "custom", "char_filter": [ "html_strip", "&_to_and" ], "tokenizer": "standard", "filter": [ "lowercase", "my_stopwords" ] }} }}} ``` 索引被创建以后,使用 analyze API 来 测试这个新的分析器: ``` GET /my_index/_analyze?analyzer=my_analyzer The quick & brown fox ``` 下面的缩略结果展示出我们的分析器正在正确地运行: ``` { "tokens" : [ { "token" : "quick", "position" : 2 }, { "token" : "and", "position" : 3 }, { "token" : "brown", "position" : 4 }, { "token" : "fox", "position" : 5 } ] } ``` 这个分析器现在是没有多大用处的,除非我们告诉 Elasticsearch在哪里用上它。我们可以像下面这样把这个分析器应用在一个 string 字段上: ``` PUT /my_index/_mapping/my_type { "properties": { "title": { "type": "string", "analyzer": "my_analyzer" } } } ``` ## 2.4 索引的映射配置(mappings部分) ### 2.4.1 映射写法 比如索引gb下有tweet和user类型,那么映射应该如何定义呢,类似于下面 ``` { "gb": { "mappings": { "tweet": { "properties": { "date": { "type": "date", "format": "strict_date_optional_time||epoch_millis" }, "name": { "type": "string" }, "tweet": { "type": "string" }, "user_id": { "type": "long" } } }, "user":{ ... } } } } ``` 上面json数据层次如下 ``` 索引gb-> mappings-> 类型tweet-> properites-> 字段date 字段name ...... 类型user-> properites-> 字段aa 字段bb ...... ``` 接下来梳理下映射的配置项 ### 2.4.2 类型 关于配置中类型怎么处理很简单,如果有多个类型,那么就并列的嵌套在mappings对象下。 但是,这里我们有必要进一步了解下类型 类型实际上是个逻辑区分,每个文档的类型名被存储在一个叫 _type 的元数据字段上。 当我们要检索某个类型的文档时, Elasticsearch 通过在 _type 字段上使用过滤器限制只返回这个类型的文档。 如果有两个不同的类型,每个类型都有同名的字段,但映射不同(例如:一个是字符串一个是数字),那么当配置这样的映射时候会出现异常。因为在 Elasticsearch 中的所有类型最终都共享相同的映射, 以 data 索引中两种类型的映射为例(只是演示,不真实有效): ``` { "data": { "mappings": { "people": { "properties": { "name": { "type": "string", }, "address": { "type": "string" } } }, "transactions": { "properties": { "timestamp": { "type": "date", "format": "strict_date_optional_time" }, "message": { "type": "string" } } } } } } ``` 每个类型定义两个字段 (分别是 "name"/"address" 和 "timestamp"/"message" )。它们看起来是相互独立的,但在后台 Lucene 将创建一个映射,如: ``` { "data": { "mappings": { "_type": { "type": "string", "index": "not_analyzed" }, "name": { "type": "string" } "address": { "type": "string" } "timestamp": { "type": "long" } "message": { "type": "string" } } } } ``` 结论: - 多个类型可以在相同的索引中存在,只要它们的字段不冲突,类型可以很好的区分同一个集合中的不同细分, - 类型不适合 完全不同类型的数据 。如果两个类型的字段集是互不相同的,这就意味着索引中将有一半的数据是空的(字段将是 稀疏的 ),最终将导致性能问题。在这种情况下,最好是使用两个单独的索引。 ### 2.4.3 根对象 映射的最高一层被称为 根对象 ,它可能包含下面几项: - 一个 properties 节点,列出了文档中可能包含的每个字段的映射 - 各种元数据字段,它们都以一个下划线开头,例如 _type 、 _id 和 _source - 设置项,控制如何动态处理新的字段,例如 analyzer 、 dynamic_date_formats 和 dynamic_templates - 其他设置,可以同时应用在根对象和其他 object 类型的字段上,例如 enabled 、 dynamic 和 include_in_all 在一个映射配置中的部分选项的位置示例: ``` { "gb": { "mappings": { "people": { "_source": { "enabled": false }, "_all": { "enabled": false }, "properties": { "name": { "type": "string", "index": "analyzed", "analyzer": "english" }, "address": { "type": "string" } } }, } } } ``` ▶ 1、属性 - type属性 字段的数据类型,例如 string 或 date 对于不是 string 的域,你一般只需要设置 type : ``` { "number_of_clicks": { "type": "integer" } } ``` 默认, string 类型域会被认为包含全文。就是说,它们的值在索引前,会通过 一个分析器,针对于这个域的查询在搜索前也会经过一个分析器。 - index属性 字段是否应当被当成全文来搜索( analyzed ),或被当成一个准确的值( not_analyzed ),还是完全不可被搜索( no ) index 属性控制怎样索引字符串。它可以是下面三个值: > analyzed 首先分析字符串,然后索引它。换句话说,以全文索引这个域。 > > not_analyzed 索引这个域,所以它能够被搜索,但索引的是精确值。不会对它进行分析。 > > no 不索引这个域。这个域不会被搜索到。 string 域 index 属性默认是 analyzed 。如果我们想映射这个字段为一个精确值,我们需要设置它为 not_analyzed : ``` { "tag": { "type": "string", "index": "not_analyzed" } } ``` 其他简单类型(例如 long , double , date 等)也接受 index 参数,但有意义的值只有 no 和 not_analyzed , 因为它们永远不会被分析。 - analyzer属性 对于 analyzed 字符串域,用 analyzer 属性指定在搜索和索引时使用的分析器。上面我们用了很多篇幅介绍了分析器内容,在这里就可以使用这些分析器了。默认, Elasticsearch 使用 standard 分析器, 但你可以指定一个内置的分析器替代它,例如 whitespace 、 simple 和 `english`: ``` { "tweet": { "type": "string", "analyzer": "english" } } ``` - multi_field类型 有时候需要将同一个值,存入到不同的字段,例如一个字段用于搜索,另一个字段用于统计。这时我们就可以使用multi_field来实现 ``` { "gb": { "mappings": { "user": { "properties": { "name": { "type": "multi_field", "fields": { "name": { "type": "string", "index": "analyzed" }, "facet": { "type": "string", "index": "not_analyzed" } } }, } }, } } } ``` 上述定义会创建两个字段,一个通过name来引用,另一个通过name.facet引用。当然创建索引的时候没有必要专门指定两个不同字段,一个name字段就可以了,es会自动完成剩余的工作。 ▶ 2、元数据:_source字段 默认地,Elasticsearch 在 _source 字段存储代表文档体的JSON字符串, _source 字段在被写入磁盘之前先会被压缩。 拥有该字段意味着下面这些: - 搜索结果包括了整个可用的文档——不需要额外的从另一个的数据仓库来取文档。 - 如果没有 _source 字段,部分 update 请求不会生效。 - 当你的映射改变时,你需要重新索引你的数据,有了_source字段你可以直接从Elasticsearch这样做,而不必从另一个(通常是速度更慢的)数据仓库取回你的所有文档。 - 当你不需要看到整个文档时,单个字段可以从 _source 字段提取和通过 get 或者 search 请求返回。 - 调试查询语句更加简单,因为你可以直接看到每个文档包括什么,而不是从一列id猜测它们的内容。 然而,存储 _source 字段的确要使用磁盘空间。如果上面的原因对你来说没有一个是重要的,你可以用下面的映射禁用 _source 字段: ``` PUT /my_index { "mappings": { "my_type": { "_source": { "enabled": false }, "properties": {......} } } } ``` 在一个搜索请求里,你可以通过在请求体中指定 _source 参数,来达到只获取特定的字段的效果: ``` GET /_search { "query": { "match_all": {}}, "_source": [ "title", "created" ] } ``` 这些字段的值会从 _source 字段被提取和返回,而不是返回整个 _source 。 ▶ 3、 元数据:_all字段 _all 字段:一个把其它字段值 当作一个大字符串来索引的特殊字段。 query_string 查询子句(搜索 ?q=john )在没有指定字段时默认使用 _all 字段。 _all 字段在新应用的探索阶段,当你还不清楚文档的最终结构时是比较有用的。你可以使用这个字段来做任何查询,并且有很大可能找到需要的文档: ``` GET /_search { "match": { "_all": "john smith marketing" } } ``` 随着应用的发展,搜索需求变得更加明确,你会发现自己越来越少使用 _all 字段。 _all 字段是搜索的应急之策。通过查询指定字段,你的查询更加灵活、强大,你也可以对相关性最高的搜索结果进行更细粒度的控制。 如果你不再需要 _all 字段,你可以通过下面的映射来禁用: ``` PUT /my_index/_mapping/my_type { "my_type": { "_all": { "enabled": false }, "properties": {......} } } ``` _all字段会增加索引的大小,因为在不需要使用的时候最好禁用。 通过 `include_in_all` 设置来逐个控制字段是否要包含在 _all 字段中,默认值是 `true`。在一个对象(或根对象)上设置 `include_in_all` 可以修改这个对象中的所有字段的默认行为。 你可能想要保留 _all 字段作为一个只包含某些特定字段的全文字段,例如只包含 `title`,`overview`,`summary` 和 `tags`。 相对于完全禁用 `_all` 字段,你可以为所有字段默认禁用 include_in_all 选项,仅在你选择的字段上启用: ``` PUT /my_index/my_type/_mapping { "my_type": { "include_in_all": false, "properties": { "title": { "type": "string", "include_in_all": true }, ... } } } ``` 记住,_all 字段仅仅是一个 经过分词的 string 字段。它使用默认分词器来分析它的值,不管这个值原本所在字段指定的分词器。就像所有 string 字段,你可以配置 _all 字段使用的分词器: ``` PUT /my_index/my_type/_mapping { "my_type": { "_all": { "analyzer": "whitespace" } } } ``` ### 2.4.4 动态映射 我们开头就说过es会自动匹配数据结构,这种机制其实就是动态映射,当 Elasticsearch 遇到文档中以前 未遇到的字段,它用 dynamic mapping 来确定字段的数据类型并自动把新的字段添加到类型映射。 上面我们又考虑到,想要自己定义数据结构,所有要自己实现配置分析器,定义映射关系,但是我们想要新加一个字段,这时候还是会用到动态映射。 有时这是想要的行为有时又不希望这样。通常没有人知道以后会有什么新字段加到文档,但是又希望这些字段被自动的索引。也许你只想忽略它们。如果Elasticsearch是作为重要的数据存储,可能就会期望遇到新字段就会抛出异常,这样能及时发现问题。 幸运的是可以用 dynamic 配置来控制这种行为 ,可接受的选项如下: - true 动态添加新的字段--缺省 - false 忽略新的字段 - strict 如果遇到新字段抛出异常 配置参数 dynamic 可以用在根 object 或任何 object 类型的字段上。你可以将 dynamic 的默认值设置为 strict , 而只在指定的内部对象中开启它, 例如: ``` PUT /my_index { "mappings": { "my_type": { "dynamic": "strict", "properties": { "title": { "type": "string"}, "stash": { "type": "object", "dynamic": true } } } } } ``` 把 dynamic 设置为 false 一点儿也不会改变 _source 的字段内容。 _source 仍然包含被索引的整个JSON文档。只是新的字段不会被加到映射中也不可搜索。 ### 2.4.5 自定义动态映射 有时候你想增加新的字段,也想用动态映射,但是动态映射又不是很只能,幸运的是,es允许我们自己定义动态映射的规则。 通过动态模板,你可以完全控制 新检测生成字段的映射。你甚至可以通过字段名称或数据类型来应用不同的映射。 每个模板都有一个名称, 你可以用来描述这个模板的用途, 一个 mapping 来指定映射应该怎样使用,以及至少一个参数 (如 match) 来定义这个模板适用于哪个字段。 模板按照顺序来检测;第一个匹配的模板会被启用。例如,我们给 string 类型字段定义两个模板: - es :以 _es 结尾的字段名需要使用 spanish 分词器。 - en :所有其他字段使用 english 分词器。 我们将 es 模板放在第一位,因为它比匹配所有字符串字段的 en 模板更特殊: ``` PUT /my_index { "mappings": { "my_type": { "dynamic_templates": [ { "es": { "match": "*_es", "match_mapping_type": "string", "mapping": { "type": "string", "analyzer": "spanish" } }}, { "en": { "match": "*", "match_mapping_type": "string", "mapping": { "type": "string", "analyzer": "english" } }} ] }}} ``` - match_mapping_type 允许你应用模板到特定类型的字段上,就像有标准动态映射规则检测的一样, (例如 string 或 long)。 - match 参数只匹配字段名称 - path_match 参数匹配字段在对象上的完整路径,所以 address.*.name 将匹配这样的字段: ``` { "address": { "city": { "name": "New York" } } } ``` - unmatch 和 path_unmatch将被用于未被匹配的字段。 ### 2.4.6 缺省映射 通常,一个索引中的所有类型共享相同的字段和设置。 _default_ 映射更加方便地指定通用设置,而不是每次创建新类型时都要重复设置。 _default_ 映射是新类型的模板。在设置 _default_ 映射之后创建的所有类型都将应用这些缺省的设置,除非类型在自己的映射中明确覆盖这些设置。 例如,我们可以使用 _default_ 映射为所有的类型禁用 _all 字段, 而只在 blog 类型启用: ``` PUT /my_index { "mappings": { "_default_": { "_all": { "enabled": false } }, "blog": { "_all": { "enabled": true } } } } ``` 不难发现,_default_ 映射也可以指定映射规则,所以_default_ 映射也是一个指定索引 dynamic templates 的好方法。 ## 2.5 索引的管理 ### 2.5.1 创建索引 创建一个使用默认配置的索引 ``` curl -XPOST 'http://localhost:9200/posts' ``` 创建一个手动定义规则的索引 ``` PUT /my_index { "settings": { ... any settings ... }, "mappings": { "type_one": { ... any mappings ... }, "type_two": { ... any mappings ... }, ... } } ``` ### 2.5.2 查看索引 ``` GET /gb/_mapping/tweet ``` 通过 /_mapping ,我们可以查看 Elasticsearch 在一个或多个索引中的一个或多个类型的映射 ``` { "gb": { "mappings": { "tweet": { "properties": { "date": { "type": "date", "format": "strict_date_optional_time||epoch_millis" }, "name": { "type": "string" }, "tweet": { "type": "string" }, "user_id": { "type": "long" } } } } } } ``` ### 2.5.3 删除索引 ``` # 删除一个索引 DELETE /my_index # 删除 多个 索引: DELETE /index_one,index_two DELETE /index_* # 删除 全部 索引: DELETE /_all DELETE /* ``` ### 2.5.4 重构索引 - 如果索引中还没有任何数据 最简单的方法就是删除索引,然后重新按照自己的需求建立。 - 如果已经有数据 对现有数据的这类改变最简单的办法就是重新索引:用新的设置创建新的索引并把文档从旧的索引复制到新的索引。 字段 _source 的一个优点是在Elasticsearch中已经有整个文档。你不必从源数据中重建索引,而且那样通常比较慢。 为了有效的重新索引所有在旧的索引中的文档,用 scroll 从旧的索引检索批量文档 , 然后用 bulk API (批量写入)把文档推送到新的索引中。