[TOC]前端
搜索,就是在任何场景下,找寻想要的信息。经过关键字检索出与此关键字有关的信息。这和查询还不太同样,查询一般是在表格类型的数据中查找,字段的内容的长度每每不大。java
传统数据库的状况下,若是要查询某个字段是否包含某些关键字的话,须要使用到like关键字来进行字段匹配,很大几率致使全表扫描,自己来讲性能就不算好。若是再加上查询的字段很是长,那么使用like匹配的工做量是很大的,另外若是表的行数也不少,那么性能就更差了。node
全文检索是指计算机索引程序经过扫描文章中的每个词,对每个词创建一个索引,指明该词在文章中出现的次数和位置,当用户查询时,检索程序就根据事先创建的索引进行查找,并将查找的结果反馈给用户的检索方式。这个过程相似于经过字典中的检索字表查字的过程。全文搜索引擎数据库中的数据。而全文检索用到的关键技术就是倒排索引。什么是倒排索引?看看例子就知道了git
数据库中有以下数据 id 员工描述 1 优秀论文 2 优秀员工称号 3 优秀项目 4 优秀团队 创建倒排索引的步骤: 一、每行切词, 怎么切均可以,看实际须要 1 优秀 论文 2 优秀 员工 称号 3 优秀 项目 4 优秀 团队 二、创建倒排索引 优秀 1,2,3,4 论文 1 员工 2 称号 2 项目 3 团队 4 三、检索 倒排索引意思简单就是指定的词出如今哪些行中,这些行都用惟一id进行标识。 因此这就是为何倒排索引用到全文检索中,由于能够直接查询到包含相关关键字的内容有哪些。 好比搜索优秀,能够看到优秀这个词在1234中都有出现,而后根据id查询原始数据。
有了倒排索引,当咱们须要从不少端很长的内容中检索包含指定关键字的内容时,直接根据倒排索引就知道有没有指定关键字了。而若是使用传统数据库,那么必须扫描所有内容,若是数据有1000行,那工做量就很恐怖了。而倒排索引只是查询个关键字而已,无需扫描所有内容。es6
Lucene就是一个jar包,里面包含了封装好的各类创建倒排索引,以及进行搜索的代码,包括各类算法。咱们就用java开发的时候,引入lucene jar,而后基于lucene的api进行去进行开发就能够了。可是它只是根据文本作出索引,而后保存下来,可是自己并不提供搜索功能。
因为Lucene使用比较复杂,繁琐,因此基于Lucene开发了一个新的项目,也就是Elasticsearch(简称ES)。github
特色:算法
1)能够做为一个大型分布式集群(数百台服务器)技术,处理PB级数据,服务大公司;也能够运行在单机上,服务小公司; 2)Elasticsearch不是什么新技术,主要是将全文检索、数据分析以及分布式技术,合并在了一块儿,才造成了独一无二的ES;lucene(全文检索),商用的数据分析软件(也是有的),分布式数据库(mycat); 3)对用户而言,是开箱即用的,很是简单,做为中小型的应用,直接3分钟部署一下ES,就能够做为生产环境的系统来使用了,数据量不大,操做不是太复杂; 4)数据库的功能面对不少领域是不够用的(事务,还有各类联机事务型的操做);特殊的功能,好比全文检索,同义词处理,相关度排名,复杂数据分析,海量数据的近实时处理;Elasticsearch做为传统数据库的一个补充,提供了数据库所不能提供的不少功能。
适用场景:chrome
1)维基百科,相似百度百科,牙膏,牙膏的维基百科,全文检索,高亮,搜索推荐。 2)The Guardian(国外新闻网站),相似搜狐新闻,用户行为日志(点击,浏览,收藏,评论)+ 社交网络数据(对某某新闻的相关见解),数据分析,给到每篇新闻文章的做者,让他知道他的文章的公众反馈(好,坏,热门,垃圾,鄙视,崇拜)。 3)Stack Overflow(国外的程序异常讨论论坛),IT问题,程序的报错,提交上去,有人会跟你讨论和回答,全文检索,搜索相关问题和答案,程序报错了,就会将报错信息粘贴到里面去,搜索有没有对应的答案。 4)GitHub(开源代码管理),搜索上千亿行代码。 5)国内:站内搜索(电商,招聘,门户,等等),IT系统搜索(OA,CRM,ERP,等等),数据分析(ES热门的一个使用场景)。
近实时数据库
两个意思,从写入数据到数据能够被搜索到有一个小延迟(大概1秒);基于es执行搜索和分析能够达到秒级。
集群clusterapache
ES集群能够有多个节点,可是每一个节点属于哪一个ES集群中是经过配置集群名称来指定的。固然一个集群只有一个节点也是OK的
节点node
集群中的一个节点,节点也有一个名称(默认是随机分配的),节点名称很重要(在执行运维管理操做的时候),默认节点会去加入一个名称为“elasticsearch”的集群,若是直接启动一堆节点,那么它们会自动组成一个elasticsearch集群,固然一个节点也能够组成一个elasticsearch集群。
index--database
索引包含一堆有类似结构的文档数据,好比能够有一个客户索引,商品分类索引,订单索引,索引有一个名称。一个index包含不少document,一个index就表明了一类相似的或者相同的document。好比说创建一个product index,商品索引,里面可能就存放了全部的商品数据,全部的商品document。相似于传统数据库中的库的概念
type--table
每一个索引里均可以有一个或多个type,type是index中的一个逻辑数据分类,一个type下的document,都有相同的field,好比博客系统,有一个索引,能够定义用户数据type,博客数据type,评论数据type。相似于传统数据库中的表的概念。 要注意:es逐渐抛弃掉这个概念了,到6.x版本中,已经只容许一个index只有一个type了。
document--行
文档是es中的最小数据单元,一个document能够是一条客户数据,一条商品分类数据,一条订单数据,一般用JSON数据结构表示,每一个index下的type中,均可以去存储多个document。至关于行
field--字段
Field是Elasticsearch的最小单位。一个document里面有多个field,每一个field就是一个数据字段。 如: product document { "product_id": "1", "product_name": "高露洁牙膏", "product_desc": "高效美白", "category_id": "2", "category_name": "日化用品" 这些就是字段 }
mapping--映射约束
数据如何存放到索引对象上,须要有一个映射配置,包括:数据类型、是否存储、是否分词等。所谓映射是对type的存储的一些限制。 例子: 这样就建立了一个名为blog的Index。Type不用单首创建,在建立Mapping 时指定就能够。Mapping用来定义Document中每一个字段的类型,即所使用的 analyzer、是否索引等属性。建立Mapping 的代码示例以下: client.indices.putMapping({ index : 'blog', type : 'article', 这里还能够设置type的一些工做属性,好比_source等,后面会讲 body : { article: { properties: { id: { type: 'string', analyzer: 'ik', store: 'yes', }, title: { type: 'string', analyzer: 'ik', store: 'no', }, content: { type: 'string', analyzer: 'ik', store: 'yes', } } } } });
写流程:
一、客户端根据提供的es节点,选择一个node做为协调节点,并发送写请求 二、协调节点对写入的document进行路由,将document进行分片。每一个分片单独进行写,每一个分片默认都是双备份,写在不一样的节点上。 三、分片写入时,主备份由协调节点写入,副备份则是从主备份所在节点同步数据过去。 四、当分片都写完后,由协调节点返回写入完成给客户端
读流程:
读流程就很简单了,若是经过docid来读取,直接根据docid进行hash。判断出该doc存储在哪一个节点上,而后到相应节点上读取数据便可。
图1.1 ES存储结构
首先分为两个区域,一个是索引区域,一个是数据区域。前者用来存储生成的倒排索引,后者用来存储原始的document(能够选择不存,后面有说)。 1)索引对象(index):存储数据的表结构 ,任何搜索数据,存放在索引对象上 。 2)映射(mapping):数据如何存放到索引对象上,须要有一个映射配置, 包括:数据类型、是否存储、是否分词等。 3)文档(document):一条数据记录,存在索引对象上 。es会给每一个document生成一个惟一的documentID,用于标识该document。固然也能够手动指定docid 4)文档类型(type):一个索引对象,存放多种类型数据,数据用文档类型进行标识。
使用的es版本为:6.6.2
下载地址:https://www.elastic.co/products/elasticsearch
解压程序到指定目录:
tar zxf elasticsearch-6.6.2.tar.gz -C /opt/modules/
修改配置文件:
cd /opt/modules/elasticsearch-6.6.2/ vim config/elasticsearch.yml 修改以下内容: # ---------------------------------- Cluster ------------------------------------- # 集群名称 cluster.name: my-application # ------------------------------------ Node -------------------------------------- # 节点名称,须要保证全局惟一 node.name: bigdata121 # ----------------------------------- Paths --------------------------------------- # 配置es数据目录,以及日志目录 path.data: /opt/modules/elasticsearch-6.6.2/data path.logs: /opt/modules/elasticsearch-6.6.2/logs # ----------------------------------- Memory ----------------------------------- # 配置es不检查内存限制,内存不够时启动会检查报错 bootstrap.memory_lock: false bootstrap.system_call_filter: false # ---------------------------------- Network ------------------------------------ # 绑定ip network.host: 192.168.50.121 # --------------------------------- Discovery ------------------------------------ # 初始发现节点,用来给新添加的节点进行询问加入集群 discovery.zen.ping.unicast.hosts: ["bigdata121"]
修改Linux一些内核参数
vim /etc/security/limits.conf 添加以下内容: Es硬性要求打开最小数目最小为65536,进程数最小为4096,不然没法启动 * soft nofile 65536 * hard nofile 131072 * soft nproc 4096 * hard nproc 4096 vim /etc/security/limits.d/20-nproc.conf * soft nproc 1024 #修改成 * soft nproc 4096 这些内核参数须要重启才生效 vim /etc/sysctl.conf 添加下面配置: vm.max_map_count=655360 并执行命令: sysctl -p
建立es的数据目录以及日志目录
mkdir /opt/modules/elasticsearch-6.6.2/{logs,data}
启动es服务
bin/elasticsearch -d -d 表示之后台进程服务的方式启动,不加此选项就之前台进程方式启动
测试es
es会启动两个对外端口: 9200:restful api的端口 9300:java api端口 能够直接使用curl访问9200端口 curl http://bigdata121:9200 { "name" : "bigdata121", "cluster_name" : "my-application", "cluster_uuid" : "DM6wmLzsQv2xVDkLMBJzOQ", "version" : { "number" : "6.6.2", "build_flavor" : "default", "build_type" : "tar", "build_hash" : "3bd3e59", "build_date" : "2019-03-06T15:16:26.864148Z", "build_snapshot" : false, "lucene_version" : "7.6.0", "minimum_wire_compatibility_version" : "5.6.0", "minimum_index_compatibility_version" : "5.0.0" }, "tagline" : "You Know, for Search" } 这样就正常了
master node:master 节点主要用于元数据(metadata)的处理,好比索引的新增、删除、分片分配等。
data node:data 节点上保存了数据分片。它负责数据相关操做,好比分片的 CRUD,以及搜索和整合操做。这些操做都比较消耗 CPU、内存和 I/O 资源;
client node:client 节点起到路由请求的做用,实际上能够看作负载均衡器。
那么这三种节点该如何配置,例子:
# 配置文件中给出了三种配置高性能集群拓扑结构的模式,以下: # 1. 若是你想让节点从不选举为主节点,只用来存储数据,可做为负载器 # node.master: false # node.data: true # 2. 若是想让节点成为主节点,且不存储任何数据,并保有空闲资源,可做为协调器 # node.master: true # node.data: false # 3. 若是想让节点既不成为主节点,又不成为数据节点,那么可将他做为搜索器,从节点中获取数据,生成搜索结果等 # node.master: false # node.data: false # 4. 节点是数据节点,也是master节点,这是默认配置 # node.master: true # node.data: true
一、默认状况下,一个节点是数据节点,也是master节点。对于3-5个节点的小集群来说,一般让全部节点存储数据和具备得到主节点的资格。你能够将任何请求发送给任何节点,而且因为全部节点都具备集群状态的副本,它们知道如何路由请求。多个master的元数据也会同步,不用担忧不一致。要注意,master节点的数量最好最少为3,且为单数 二、当集群节点数量比较大时,那么一般就会将主节点、数据节点分开,专门部署在对应的节点上,而后主节点是多个均可用的,造成HA的结构。要注意,master节点的数量最好最少为3,且为单数
实际部署其实和单节点差很少,主要看部署的方案选哪一个,master有几个,数据节点有几个,设置下角色便可,这里很少说
用qq浏览器或者chrome,直接到应用商店搜索elasticsearch-head,直接安装插件便可
<dependencies> <dependency> <groupId>org.elasticsearch</groupId> <artifactId>elasticsearch</artifactId> <version>6.6.2</version> </dependency> <dependency> <groupId>org.elasticsearch.client</groupId> <artifactId>transport</artifactId> <version>6.6.2</version> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-core</artifactId> <version>2.9.0</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> </dependency> </dependencies> 另外须要本身添加一个log4j2的日志格式配置文件,添加到resource目录下 log4j2.xml <?xml version="1.0" encoding="UTF-8"?> <Configuration status="warn"> <Appenders> <Console name="Console" target="SYSTEM_OUT"> <PatternLayout pattern="%m%n"/> </Console> </Appenders> <Loggers> <Root level="INFO"> <AppenderRef ref="Console"/> </Root> </Loggers> </Configuration> 下面代码中使用 junit进行运行测试,不会用的本身百度
public class ESDemo1 { private TransportClient client; @Before public void getClient() throws UnknownHostException { //一、建立es配置对象 Settings settings = Settings.builder().put("cluster.name", "my-application").build(); //二、链接es集群 client = new PreBuiltTransportClient(settings); //配置es集群地址 client.addTransportAddress(new TransportAddress( InetAddress.getByName("192.168.50.121"), 9300 )); System.out.println(client.toString()); } }
// .get() 表示触发操做 @Test public void createBlog() { //建立索引blog //建立index须要admin用户 client.admin().indices().prepareCreate("blog").get(); client.close(); } //删除索引 @Test public void deleteIndex() { client.admin().indices().prepareDelete("blog").get(); client.close(); }
@Test public void addDocument() { //一、json方式添加document String d = "{\"id\":1, \"name\":\"山海经\"}"; //导入document,并指定源的格式为 json. IndexResponse indexResponse = client.prepareIndex("blog", "article").setSource(d, XContentType.JSON).execute().actionGet(); System.out.println(indexResponse.getId()); client.close(); } @Test public void addDocument2() throws IOException { //二、另一种方式添加document IndexResponse indexResponse = client.prepareIndex("blog3", "article") .setSource(XContentFactory.jsonBuilder() .startObject() .field("name","静夜思") .field("id",4) .endObject() ).execute().actionGet(); System.out.println(indexResponse.getResult()); client.close(); } @Test public void addDocument3() throws IOException { //三、经过hashmap组织数据 HashMap<String, Object> json = new HashMap<>(); json.put("name","spark从入门到放弃"); json.put("id","6"); IndexResponse indexResponse = client.prepareIndex("blog", "article") .setSource(json).execute().actionGet(); System.out.println(indexResponse.getResult()); client.close(); } @Test public void addMoreDocument() throws IOException { //四、一次请求内部添加多个document BulkRequestBuilder bulkRequestBuilder = client.prepareBulk(); bulkRequestBuilder.add( client.prepareIndex("blog2", "comment").setSource( XContentFactory.jsonBuilder() .startObject() .field("name", "山海经") .field("id",1) .field("commentValue","这是一部很好的做品") .endObject()) ); bulkRequestBuilder.add( client.prepareIndex("blog2", "comment").setSource( XContentFactory.jsonBuilder() .startObject() .field("name", "骆驼祥子") .field("id",2) .field("commentValue","这是讲一我的的故事") .endObject()) ); BulkResponse bulkItemResponses = bulkRequestBuilder.get(); System.out.println(bulkItemResponses); client.close(); }
要注意的是,从6.x版本开始,一个index中只能有一个type了,若是建立多个type会有如下报错
Rejecting mapping update to [blog] as the final mapping would have more than 1
根据docid搜索document //搜索单个document @Test public void getType() { GetResponse documentFields = client.prepareGet().setIndex("blog3").setType("article").setId("2OlH9WwBaToKuF8JhwB5").get(); System.out.println(documentFields.getSourceAsString()); client.close(); } //查询多个doc @Test public void getDocFromMoreIndex() { MultiGetResponse multiGetResponse = client.prepareMultiGet() .add("blog", "article", "1") .add("blog", "article", "2") .get(); //结果打印 for (MultiGetItemResponse itemResponse : multiGetResponse) { System.out.println( itemResponse.getResponse().getSourceAsString()); } client.close(); }
@Test public void updateData() throws IOException { //更新数据方式1:经过 prepareupdate方法 UpdateResponse updateResponse = client.prepareUpdate("blog", "article", "4") .setDoc(XContentFactory.jsonBuilder() .startObject() .field("name", "天黑") .field("id", "5") .endObject() ).get(); System.out.println(updateResponse.getResult()); } @Test public void updateData2() throws IOException, ExecutionException, InterruptedException { //更新数据方式2:经过update方法 UpdateRequest updateRequest = new UpdateRequest().index("blog").type("article").id("4"); updateRequest.doc(XContentFactory.jsonBuilder() .startObject() .field("name", "亚瑟") .field("id", "7") .endObject()); UpdateResponse updateResponse = client.update(updateRequest).get(); System.out.println(updateResponse.getResult()); } @Test public void upsertData() throws IOException, ExecutionException, InterruptedException { //指定doc不存在时就插入,存在就修改 //不存在就插入这个 IndexRequest indexRequest = new IndexRequest("blog","article","6").source( XContentFactory.jsonBuilder().startObject() .field("name","wang") .field("id","10") .endObject() ); //存在就更新这个,注意最后的那个 upsert操做,意思就是不存在就插入上面的 indexrequest UpdateRequest updateRequest = new UpdateRequest().index("blog").type("article").id("6"); updateRequest.doc(XContentFactory.jsonBuilder() .startObject() .field("name", "king") .field("id", "7") .endObject()).upsert(indexRequest); UpdateResponse updateResponse = client.update(updateRequest).get(); System.out.println(updateResponse.getResult()); client.close(); }
@Test public void deleteDocument() { //删除document DeleteResponse deleteResponse = client.prepareDelete("blog", "article", "6").get(); System.out.println(deleteResponse.getResult()); client.close(); }
关键性一个类是 org.elasticsearch.index.query.QueryBuilders;
@Test public void matchAll() { //构建所有查询 SearchResponse searchResponse = client.prepareSearch("blog").setQuery(QueryBuilders.matchAllQuery()).get(); //从返回结构中解析doc SearchHits hits = searchResponse.getHits(); for (SearchHit hit:hits){ System.out.println(hit.getSourceAsString()); } client.close(); }
搜索所有字段中包含指定字符的document @Test public void matchSome() { //直接全文检索指定字符 SearchResponse searchResponse = client.prepareSearch("blog3").setQuery(QueryBuilders.queryStringQuery("思")).get(); SearchHits hits = searchResponse.getHits(); for(SearchHit hit:hits) { System.out.println(hit.getId()); System.out.println(); } }
@Test public void wildMatch() { //通配符查询,*表示0或者多个字符,?表示单个字符 SearchResponse searchResponse = client.prepareSearch("blog").setTypes("article").setQuery(QueryBuilders.wildcardQuery("name", "wa*")).get(); SearchHits hits = searchResponse.getHits(); for(SearchHit h:hits) { System.out.println(h.getSourceAsString()); } } 这个方法用于匹配某个字段的整个内容,相似like操做
@Test public void matchField() { //这是对分词结果进行等值操做的方法,不是对整个字段,而是对字段的分词结果 SearchResponse searchResponse = client.prepareSearch("blog").setQuery(QueryBuilders.termQuery("name", "山")).get(); SearchHits hits = searchResponse.getHits(); for(SearchHit hit:hits) { System.out.println(hit.getSourceAsString()); } client.close(); } 这个方法必定要注意: 好比有一个字段内容以下: 我爱中国 假设分词以下: 我 爱 中国 若是使用 QueryBuilders.termQuery("name", "中") 也就是搜索“中”这个字时,实际上没有结果返回的。由于分词中并无含有单独的“中”。 因此这个方法是用于完整匹配分词结果中的某个分词的。 由此,能够得出,即使是用整个字段的内容来搜索,这个方法也不会返回任何结果的,由于分词结果不包含。
@Test public void fuzzy() { // 1 模糊查询 SearchResponse searchResponse = client.prepareSearch("blog").setTypes("article") .setQuery(QueryBuilders.fuzzyQuery("title", "lucene")).get(); // 2 打印查询结果 SearchHits hits = searchResponse.getHits(); // 获取命中次数,查询结果有多少对象 for(SearchHit hit:hits) { System.out.println(hit.getSourceAsString()); } // 3 关闭链接 client.close(); } 这个方法和 termQuery很相似,可是有区别。感兴趣的话能够本身查找资料。这个方法比较少用
映射是规定index中的一些属性,以及各自type下的字段的属性(再强调一遍,如今6.x版本一个index下只能有一个type,其实就是变相地去除掉了type)。Elasticsearch映射虽然有idnex和type两层关系,可是实际索引时是以index为基础的。若是同一个index下不一样type的字段出现mapping不一致的状况,虽然数据依然能够成功写入并生成各自的mapping,但实际上fielddata中的索引结果却依然是以index内第一个mapping类型来生成的
定义mapping时,依旧是使用json格式定义定义。通常格式以下:
{ 元数据属性字段,如: _type:是哪一个type的mapping,仍是那句话,type基本不怎么提了 _index:属于哪一个index 。。。。。。。 properties:{ "field1":{ 字段属性字段,如: type:字段数据类型 } "field2":{ 字段属性字段,如: type:字段数据类型 } 。。。。。。。。。 } } 基本格式就是这样,分为两大部分,一个是整个index 的元数据信息,一个是针对具体type中的字段信息。
核心数据类型 字符串:text,keyword 数字:long, integer, short, byte, double, float, half_float, scaled_float 布尔值:boolean 时间:date 二进制:binary 范围:integer_range, float_range, long_range, double_range, date_range 复杂数据类型 数组:array 对象:object 堆叠/嵌套对象: nested 地理:geo_point,geo_point IP: ip 字符个数:token_count(输入一个字符串,保存的是它的长度)
元数据字段:
_all : 它是文档中全部字段的值整合成的一个大字符串,用空格分割。它进行了索引但没有存储,因此咱们只能对他进行搜索不能获取。若是咱们没有指定搜索的字段,就默认是在_all字段上进行搜索。 _source :文档信息 包含在文档在建立时的实际主体,它会被存储但不会被索引,用于get或search是返回主体。若是你并不关系数据的主体,只注重数量,那能够将此字段禁用 _routing :路由字段 es会使用下面的计算公式计算数据应保存在哪一个分片,索引指定一个路由字段能够本身来控制哪些值放在一块儿。 shard_num = hash(_routing) % num_primary_shards _meta 自定义的元数据 ,由于元数据是每一个文档都会带的,索引若是你想要在每一个文档上标注一些信息,就可使用此属性,自定义一些元数据。 _field_names :保存着非空值得属性名集合,能够经过它查询包含某个字段非空值的文档 _id :主键 _index :索引 _type :类型 _uid :类型和id的组合 uid字段的值能够在查询、聚合、脚本和排序中访问: _parent :父类,可用于关联两个索引
字段属性:
type 数据类型 改属性用来指定字段的数据类型,一但指点后就不能再修改,若是数据不是以设置的数据类型传入,es会去转换数据,装换不成功则报错。具体可配置的参数,可看前面的数据类型说明。 analyzer 分析器 用于指定索引建立时使用的分析器是什么,即对同一段内容,不一样的分析器会用不一样的方式分词,最后在倒排索引上的值是不一样的。 index 是否索引 索引选项控制字段值是否被索引。它接受true或false,默认为true。没有索引的字段不是可查询的。 store 属性值是否被存储,默认状况下字段是能够被搜索可是内容不存储的,值通常都是保存在_source中。但好比一篇文章你有它的内容和原网址,如今须要对内容进行检索,但查看是跳转到它原网址的,那这时就不须要存储内容了。 fielddata 现场数据 若是你要对一个text类型进行聚合操做,你必须设置这个参数为true。 doc_values 文档数据 创建一个文档对应字段的“正排索引”,其实就是把文档的字段按列存储了,它不会保存分析的字段。方便聚合排序时访问。 format 默认格式 通常用于时间格式的数据,指定默认的数据格式, “yyyy-MM-dd HH:mm:ss” search_analyzer 搜索分析器 指定搜索时使用的分析器,通常不设置在搜索时就会使用建立索引时使用的分析器,若是要本身指定不一样的也只要配置便可。 boost 分值 指定字段的相关性评分默认是1.0,数值越大,搜索时排序时使用。也能够直接经过查询时指定分值的方式 coerce 是否转换 在插入数据时,在插入数据类型和映射类型不一致的状况下是否强制转换数据类型。默认是开启的 normalizer 转换器 由于keyword类型的字段是不进行分析的,可是咱们又想要将其统一成一个规则,好比都是小写,好比用ASCILL进行编码,其实就是个给keyword用的分析器。能够在setting下的analysis下定义本身的normalizer使用 copy_to 同步复制 在插入值是,会把值一同放到另外一个字段中。主要用于本身定义一个相似于_all字段的字端。 dynamic 动态映射控制 该字段是用来控制动态映射的,它有三个值 -true-自动添加映射 -false-新值不索引,不能被搜索,但返回的命中源字段中会存在这个值 -strict-遇到新值抛出异常 enabled 是否启动 这个值是否要用于搜索 ignore_above 忽视上限 一个字符串超过指定长度后就不会索引了 ignore_malformed 忽视错误数据 好比一个文档数据传过来,只用一个字段的数据时不能被存储,ES会抛出异常而且不会存储此数据。咱们就能够配置此属性保证数据被存储 include_in_all 是否保存在_all字段中 fields 多字段配置 好比出现标题既要索引,又有不用索引的情景。咱们不能对一个字段设置两个类型,又不想再建一个不一样类型的相同字段。咱们可使用多字段的方式,在保存数据时,咱们只需保存一个字段,ES会默认将数据保存到这个字段下的多字段上。 null_value 空值 假如你插入的数据为空,或者数据中没有这个字段的值。那这个文档的这个字段就不参与搜索了。咱们能够经过指定一个显示的空值来让他可以参与搜索 norms 规范 若是一个字段只用于聚合,能够设置为false
背景: 首先,咱们要知道一点,当doc传入es时,es会根据配置给doc的每一个字段生成索引,而且会将生成的索引保存到es中。可是至于doc的原始数据是否保存到es中,是能够选择的。这点要先搞清楚,并必定非得把doc的原始数据保存在es中的,es非保存不可的是生成的索引,而不是原始数据 ======================== _all: 这是一个特殊字段,是把全部其它字段中的值,以空格为分隔符组成一个大字符串,而后被分析和索引,可是不存储原始数据,也就是说它能被查询,但不能被取回显示。注意这个字段是能够被索引的。默认状况下,若是要进行全文检索,须要指定在哪一个字段上检索,若是不知道在哪一个字段上,那么_all就起到做用了。_all能让你在不知道要查找的内容是属于哪一个具体字段的状况下进行搜索 ====================== _source: true/false,默认为true 保存的是doc的原本的原数数据,也就是是json格式的doc。他和_all不一样,他是json格式的字符串。并且这个字段不会被索引。 当咱们执行检索操做时,是到倒排索引中查询,而后得到含有指定关键字的doc的id, 当 _source 设置为 true时 能够根据上面查询到的docid,返回对应id的document的原始数据。 当 _source 设置为 false时 就只能返回对应的document的id,没法回显对应document的原始数据 这种状况下,通常是使用额外的方式来保存document的原始数据的,好比hbase。而es就单纯保存索引而已 ======================= store:true/false,默认为false 这个属性用于指定是否保存document中对应字段的value,这个的概念和上面的source有点相似了,只不过这里store是针对某个field的原始数据,source是针对整个document的原始数据。 当执行想获取一个document的数据时, 一、采用source方式时: 只需产生一次磁盘IO,由于_source存储的时候,直接把整个doc当作一个字段来存储。当咱们须要doc中的某个字段时,是先从source读取数据,而后再解析成json,获取到指定字段内容 二、采用store方式时, 由于每一个字段都单独存储了,当须要得到整个doc的数据时,就须要单独每一个字段进行取值,有多少个字段就产生多少次磁盘IO。 三、store和source混合使用时 若是操做是获取整个doc的数据,那么es会优先从source读取数据。 若是操做是获取某些字段的数据,那么es会优先从store存储中读取数据。由于这样读取的数据量相对较少,无需读取整个doc的数据再解析。 可是注意的是,这两个属性都是单独本身保存数据的,因此若是两个启用的话,至关于数据存储了两次,挺浪费存储空间的,增大了索引的体积
建立mapping,要注意,mapping建立以后不能更改
@Test public void createMapping() throws Exception { // 1设置mapping,使用jsonbuilder构建mapping XContentBuilder builder = XContentFactory.jsonBuilder() .startObject() .startObject("article") .startObject("properties") .startObject("id1") .field("type", "string") .field("store", "yes") .endObject() .startObject("title2") .field("type", "string") .field("store", "no") .endObject() .startObject("content") .field("type", "string") .field("store", "yes") .endObject() .endObject() .endObject() .endObject(); // 2 添加mapping PutMappingRequest mapping = Requests.putMappingRequest("blog4").type("article").source(builder); client.admin().indices().putMapping(mapping).get(); // 3 关闭资源 client.close(); }
查看map
@Test public void getIndexMapping() throws ExecutionException, InterruptedException { //构建查看mapping的请求,查看blog3这个index的mapping GetMappingsResponse mappingsResponse = client.admin().indices().getMappings(new GetMappingsRequest().indices("blog3")).get(); //获取mapping ImmutableOpenMap<String, ImmutableOpenMap<String, MappingMetaData>> mappings = mappingsResponse.getMappings(); //迭代打印mapping数据 for (ObjectObjectCursor<String, ImmutableOpenMap<String, MappingMetaData>> mapping : mappings) { if (mapping.value.isEmpty()) { continue; } //最外层的key是index的名称 System.out.println("index key:" + mapping.key); //value包裹的是每一个type的mapping,里面以type为key,mapping为value for (ObjectObjectCursor<String, MappingMetaData> mapValue : mapping.value) { System.out.println("type key:" + mapValue.key); System.out.println("type value:" + mapValue.value.sourceAsMap()); } } client.close(); } /* 结果以下: index key:blog3 type key:article type value:{_source={enabled=false}, properties={id={type=long}, name={type=text, fields={keyword={type=keyword, ignore_above=256}}}}} */
在spark.2.1和es6.6项目中混合使用,报错:
java.lang.NoSuchMethodError: io.netty.buffer.ByteBuf.retainedSlice(II)Lio/netty/buffer/ByteBuf;
这种问题,通常都是使用的某个依赖包的版本问题。使用mvn dependency:tree 看了下,原来spark和es各自依赖的版本不一致,spark使用的是3.x版本,es使用的是4.1.32.Final版本。可是由于spark的依赖在pom.xml中写在前面,迫使es使用的是3.x版本的依赖,致使有些方法不存在,就报错。解决方式很简答,直接指定使用新版本的就好,以下:
<dependency> <groupId>io.netty</groupId> <artifactId>netty-all</artifactId> <version>4.1.32.Final</version> </dependency>
咱们知道,创建索引过程当中,最重要的一个步骤就是分词,分词的策略有不少,咱们看看es默认的中文分词器的效果
[root@bigdata121 elasticsearch-6.6.2]# curl -H "Content-Type:application/json" -XGET 'http://bigdata121:9200/_analyze?pretty' -d '{"analyzer":"standard","text":"中华人民共和国"}' { "tokens" : [ { "token" : "中", "start_offset" : 0, "end_offset" : 1, "type" : "<IDEOGRAPHIC>", "position" : 0 }, { "token" : "华", "start_offset" : 1, "end_offset" : 2, "type" : "<IDEOGRAPHIC>", "position" : 1 }, { "token" : "人", "start_offset" : 2, "end_offset" : 3, "type" : "<IDEOGRAPHIC>", "position" : 2 }, { "token" : "民", "start_offset" : 3, "end_offset" : 4, "type" : "<IDEOGRAPHIC>", "position" : 3 }, { "token" : "共", "start_offset" : 4, "end_offset" : 5, "type" : "<IDEOGRAPHIC>", "position" : 4 }, { "token" : "和", "start_offset" : 5, "end_offset" : 6, "type" : "<IDEOGRAPHIC>", "position" : 5 }, { "token" : "国", "start_offset" : 6, "end_offset" : 7, "type" : "<IDEOGRAPHIC>", "position" : 6 } ] }
能够看到,标准的中文分词器只是单纯将字分开,其实并不智能,没有词语考虑进去。因此须要更增强大的分词器。经常使用的有ik分词器
cd /opt/modules/elasticsearch-6.6.2 执行下面的命令安装,须要联网 bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v6.6.2/elasticsearch-analysis-ik-6.6.2.zip 注意要根据ES的版本安装对应版本的ik
分两种模式:ik_smart 和 ik_max_word
一、 ik_smart 模式,智能解析词语结构 curl -H "Content-Type:application/json" -XGET 'http://bigdata121:9200/_analyze?pretty' -d '{"analyzer":"ik_smart","text":"中华人民共和国"}' { "tokens" : [ { "token" : "中华人民共和国", "start_offset" : 0, "end_offset" : 7, "type" : "CN_WORD", "position" : 0 } ] } 二、ik_max_word 模式,智能解析字和词语 curl -H "Content-Type:application/json" -XGET 'http://192.168.109.133:9200/_analyze?pretty' -d '{"analyzer":"ik_max_word","text":"中华人民共和国"}' { "tokens" : [ { "token" : "中华人民共和国", "start_offset" : 0, "end_offset" : 7, "type" : "CN_WORD", "position" : 0 }, { "token" : "中华人民", "start_offset" : 0, "end_offset" : 4, "type" : "CN_WORD", "position" : 1 }, { "token" : "中华", "start_offset" : 0, "end_offset" : 2, "type" : "CN_WORD", "position" : 2 }, { "token" : "华人", "start_offset" : 1, "end_offset" : 3, "type" : "CN_WORD", "position" : 3 }, { "token" : "人民共和国", "start_offset" : 2, "end_offset" : 7, "type" : "CN_WORD", "position" : 4 }, { "token" : "人民", "start_offset" : 2, "end_offset" : 4, "type" : "CN_WORD", "position" : 5 }, { "token" : "共和国", "start_offset" : 4, "end_offset" : 7, "type" : "CN_WORD", "position" : 6 }, { "token" : "共和", "start_offset" : 4, "end_offset" : 6, "type" : "CN_WORD", "position" : 7 }, { "token" : "国", "start_offset" : 6, "end_offset" : 7, "type" : "CN_CHAR", "position" : 8 } ] }
这里其实和mapping的使用差很少,只是在mapping的字段属性中添加一个 “analyzer” 属性,指定使用的分词器而已。其余都没有区别,这里不重复
ES在数十亿级别的数据如何提升检索效率?
这个问题说白了,就是看你有没有实际用过 ES,由于啥?其实 ES 性能并无你想象中那么好的。不少时候数据量大了,特别是有几亿条数据的时候,可能你会懵逼的发现,跑个搜索怎么一下 5~10s,坑爹了。第一次搜索的时候,是 5~10s,后面反而就快了,可能就几百毫秒。
而后你就很懵,每一个用户第一次访问都会比较慢,比较卡么?因此你要是没玩儿过 ES,或者就是本身玩玩儿 Demo,被问到这个问题容易懵逼,显示出你对 ES 确实玩的不怎么样?说实话,ES 性能优化是没有银弹的。啥意思呢?就是不要期待着随手调一个参数,就能够万能的应对全部的性能慢的场景。也许有的场景是你换个参数,或者调整一下语法,就能够搞定,可是绝对不是全部场景均可以这样。
下面看看几个优化的手段
图5.1 ES filesytem cache
你往 ES 里写的数据,实际上都写到磁盘文件里去了,查询的时候,操做系统会将磁盘文件里的数据自动缓存到 Filesystem Cache 里面去。ES 的搜索引擎严重依赖于底层的 Filesystem Cache,你若是给 Filesystem Cache 更多的内存,尽可能让内存能够容纳全部的 IDX Segment File 索引数据文件,那么你搜索的时候就基本都是走内存的,性能会很是高。
问题:直接读取硬盘数据和从缓存读取数据,性能差距究竟能够有多大?
回答:
咱们以前不少的测试和压测,若是走磁盘通常确定上秒,搜索性能绝对是秒级别的,1 秒、5 秒、10 秒。但若是是走 Filesystem Cache,是走纯内存的,那么通常来讲性能比走磁盘要高一个数量级,基本上就是毫秒级的,从几毫秒到几百毫秒不等。
案例:
来看一个真实的案例:某个公司 ES 节点有 3 台机器,每台机器看起来内存不少 64G,总内存就是 64 3 = 192G。每台机器给 ES JVM Heap 是 32G,那么剩下来留给 Filesystem Cache 的就是每台机器才 32G,总共集群里给 Filesystem Cache 的就是 32 3 = 96G 内存。
而此时,整个磁盘上索引数据文件,在 3 台机器上一共占用了 1T 的磁盘容量,ES 数据量是 1T,那么每台机器的数据量是 300G。这样性能会好吗?
Filesystem Cache 的内存才 100G,十分之一的数据能够放内存,其余的都在磁盘,而后你执行搜索操做,大部分操做都是走磁盘,性能确定差。
首先要知道一点:归根结底,你要让 ES 性能好,最佳的状况下,就是你的机器的内存,至少能够容纳你的总数据量的一半。固然若是内存能容纳所有数据,天然是最好,然而基本生产中没有那么多钱的啦。走内存能够知足秒级之内的查询要求
一、去掉写入ES的doc中没必要要的字段
若是一个doc中有不少字段,可是有些字段压根是没用的(也就是说该字段不会用于搜索),可是读取的时候仍旧会将这些字段都读取,而后缓存到filesytem cache中,占据了大量空间,致使后面的数据只能从新从硬盘中读取。这个时候就要想着取消一些没怎么用的字段了。减少索引的体积。从而节省filesytem cache空间
二、采用 ES+HBase架构
以前也说到,es能够只存储索引,不存储原始doc数据;或者只存储某些字段的原始数据。一般完整的原始数据都保存在hbase中,而后经过rowkey做为docid导入到es中,最终经过这个rowkey进行惟一性关联。为何要采用这种架构呢?
好比说你如今有一行数据:id,name,age .... 30 个字段。可是你如今搜索,只须要根据 id,name,age 三个字段来搜索。若是你傻乎乎往 ES 里写入一行数据全部的字段,就会致使 90% 的数据是不用来搜索的。可是呢,这些数据硬是占据了 ES 机器上的 Filesystem Cache 的空间,单条数据的数据量越大,就会致使 Filesystem Cahce 能缓存的数据就越少。其实,仅仅写入 ES 中要用来检索的少数几个字段就能够了,好比说就写入 es id,name,age 三个字段。而后你能够把其余的字段数据存在 MySQL/HBase 里,咱们通常是建议用 ES + HBase 这么一个架构(官方建议的方案)。
HBase是列式数据库,其特色是适用于海量数据的在线存储,就是对 HBase 能够写入海量数据,可是不要作复杂的搜索,作很简单的一些根据 id 或者范围进行查询的这么一个操做就能够了。hbase很是适合这种简单经过key直接获取数据的应用场景。
例如:从 ES 中根据 name 和 age 去搜索,拿到的结果可能就 20 个 doc id,而后根据 doc id 到 HBase 里去查询每一个 doc id 对应的完整的数据,给查出来,再返回给前端。而写入 ES 的数据最好小于等于,或者是略微大于 ES 的 Filesystem Cache 的内存容量。而后你从 ES 检索可能就花费 20ms,而后再根据 ES 返回的 id 去 HBase 里查询,查 20 条数据,可能也就耗费个 30ms。若是你像原来那么玩儿,1T 数据都放 ES,可能会每次查询都是 5~10s,而如今性能就会很高,每次查询就是 50ms。
从几率上来讲,大部分的访问量每每集中小部分的数据上,也就是咱们所说的数据热点的状况。数据预热一般就是事先将一些可能有大量访问的数据先经过手动访问让它们提早缓存到cache中,然然后面的用户访问这些数据时,就直接走cache查询了,很是快。并且这些数据由于访问量多,因此还须要保证这些热点数据不要被其余非热点数据加载到cache时,被覆盖掉了。这就须要时常手动访问,加载数据到cache中。
例子:
好比电商,你能够将平时查看最多的一些商品,好比说 iPhone 8,热数据提早后台搞个程序,每隔 1 分钟本身主动访问一次,刷到 Filesystem Cache 里去。
总之,就是对于那些你以为比较热的、常常会有人访问的数据,最好作一个专门的缓存预热子系统。而后对热数据每隔一段时间,就提早访问一下,让数据进入 Filesystem Cache 里面去。这样下次别人访问的时候,性能必定会好不少。
这个也是数据热点的问题。ES 能够作相似于 MySQL 的水平拆分,就是说将大量的访问不多、频率很低的数据,单独写一个索引,而后将访问很频繁的热数据单独写一个索引。最好是将冷数据写入一个索引中,而后热数据写入另一个索引中,这样能够确保热数据在被预热以后,尽可能都让他们留在 Filesystem OS Cache 里,别让冷数据给冲刷掉。
仍是来一个例子,假设你有 6 台机器,2 个索引,一个放冷数据,一个放热数据,每一个索引 3 个 Shard。3 台机器放热数据 Index,另外 3 台机器放冷数据 Index。这样的话,你大量的时间是在访问热数据 Index,热数据可能就占总数据量的 10%,此时数据量不多,几乎全都保留在 Filesystem Cache 里面了,就能够确保热数据的访问性能是很高的。
可是对于冷数据而言,是在别的 Index 里的,跟热数据 Index 不在相同的机器上,你们互相之间都没什么联系了。若是有人访问冷数据,可能大量数据是在磁盘上的,此时性能差点,就 10% 的人去访问冷数据,90% 的人在访问热数据,也无所谓了。
对于 MySQL,咱们常常有一些复杂的关联查询,在 ES 里该怎么玩儿?ES 里面的复杂的关联查询尽可能别用,一旦用了性能通常都不太好。最好是先在 Java 系统里就完成关联,将关联好的数据直接写入 ES 中。搜索的时候,就不须要利用 ES 的搜索语法来完成 Join 之类的关联搜索了。
Document 模型设计是很是重要的,不少操做,不要在搜索的时候才想去执行各类复杂的乱七八糟的操做。
ES 能支持的操做就那么多,不要考虑用 ES 作一些它很差操做的事情。若是真的有那种操做,尽可能在 Document 模型设计的时候,写入的时候就完成。另外对于一些太复杂的操做,好比 join/nested/parent-child 搜索都要尽可能避免,性能都不好的。
总结一句就是说,ES不适合执行复杂查询操做
背景:
ES 的分页是较坑的,为啥呢?举个例子吧,假如你每页是 10 条数据,你如今要查询第 100 页,其实是会把每一个 Shard 上存储的前 1000 条数据都查到一个协调节点上。若是你有 5 个 Shard,那么就有 5000 条数据,接着协调节点对这 5000 条数据进行一些合并、处理,再获取到最终第 100 页的 10 条数据。 因为是分布式的,你要查第 100 页的 10 条数据,不可能说从 5 个 Shard,每一个 Shard 就查 2 条数据,最后到协调节点合并成 10 条数据吧?你必须得从每一个 Shard 都查 1000 条数据过来,而后根据你的需求进行排序、筛选等等操做,最后再次分页,拿到里面第 100 页的数据。 也就是说,你翻页的时候,翻的越深,每一个 Shard 返回的数据就越多,并且协调节点处理的时间越长,很是坑爹。因此用 ES 作分页的时候,你会发现越翻到后面,就越是慢。 咱们以前也是遇到过这个问题,用 ES 做分页,前几页就几十毫秒,翻到 10 页或者几十页的时候,基本上就要 5~10 秒才能查出来一页数据了。
解决方案:
一、不容许深度分页(默认深度分页性能不好)。跟产品经理说,你系统不容许翻那么深的页,默认翻的越深,性能就越差。 二、相似于 App 里的推荐商品不断下拉出来一页一页的;相似于微博中,下拉刷微博,刷出来一页一页的,你能够用 Scroll API,关于如何使用,你们能够自行上网搜索学习一下。 Scroll是如何作的呢?它会一次性给你生成全部数据的一个快照,而后每次滑动向后翻页就是经过游标 scroll_id 移动,获取下一页、下一页这样子,性能会比上面说的那种分页性能要高不少不少,基本上都是毫秒级的。 可是,惟一的一点就是,这个适合于那种相似微博下拉翻页的,不能随意跳到任何一页的场景。也就是说,你不能先进入第 10 页,而后去第 120 页,而后又回到第 58 页,不能随意乱跳页。因此如今不少产品,都是不容许你随意翻页的,你只能往下拉,一页一页的翻。 使用时须要注意,初始化必须指定 Scroll 参数,告诉 ES 要保存这次搜索的上下文多长时间。你须要确保用户不会持续不断翻页翻几个小时,不然可能由于超时而失败。 除了用 Scroll API,你也能够用 search_after 来作。search_after 的思想是使用前一页的结果来帮助检索下一页的数据。 显然,这种方式也不容许你随意翻页,你只能一页页日后翻。初始化时,须要使用一个惟一值的字段做为 Sort 字段。