全文检索
一、什么是全文检索
- 数据的分类:
- 结构化数据
格式固定、长度固定、数据类型固定
例如:数据库中的数据
- 非结构化数据
格式不固定、长度不固定、数据类型不固定
例如:word文档、pdf文档、邮件、html、txt
- 结构化数据
- 数据的查询:
- 结构化数据的查询
sql语句。查询结构化数据的方法简单、速度快。 - 非结构化数据的查询
-
目测
-
使用程序把文档读取到内存中,然后匹配字符串,顺序扫描
-
把非结构数据变成结构化数据
(举例英文文档)先根据空格进行字符串拆分,得到一个单词列表,基于单词列表创建一个索引。
然后查询索引,根据单词和文档的对应关系找到文档列表,这个过程叫做全文检索。索引:一个为了提高查询速度,创建某种数据结构的集合。
-
- 结构化数据的查询
- 全文检索:
先创建索引然后查询索引的过程叫做全文索引
索引一次创建可以多次使用,表现为每次查询速度很快。
二、 全文检索的应用场景
- 搜索引擎
百度、360搜索、谷歌、搜狗 - 站内引擎
论坛搜索、微博、文章搜索 - 电商搜索
淘宝搜索、京东搜索(搜索到的商品数据)
- 只要是有搜索的地方就可以使用全文检索技术
Lucene工具
lucene是一个基于java开发全文检索的工具包
Lucene 实现全文检索的流程
- 创建索引
-
获得文档
1. 原始文档:要基于哪些数据来进行搜索,那么这些数据就是原始文档
2. 搜索引擎:使用爬虫获取原始文档
3. 站内搜索:数据库中的数据
案例:直接使用io读取到磁盘上的文件 -
构建文档对象
对应每个原始文档创建一个Document对象
每个document对象中包含多个域(field)
域中保存就是原始文档数据- 域的名称
- 域的值
每个文档都有一个唯一的编号,就是文档id
-
分析分档
就是分词的过程- 根据空格进行字符串拆分,得到一个单词列表
- 把单词统一转换为小写
- 去除标点符号
- 去除停用词
停用词:无意义的词(the a)
每个关键词都封装成一个Term对象中。
Term中包含两部分内容:- 关键词所在的域
- 关键词本身
不同的域中拆分出来的相同的关键词是不同的Term
-
创建索引
基于关键词列表创建一个索引,保存到索引库中。
索引库中:
索引
document对象
关键词和文档的对应关系
通过词语找到文档,这种索引的结构叫倒排索引结构
-
- 查询索引
- 用户查询接口
- 用户输入查询条件的地方
- 把关键词封装成一个查询对象
- 要查询的域
- 要搜索的关键词
- 执行查询
- 根据要查询的关键词到对应的域上进行搜索
- 找到关键词,根据关键词找到对应的文档
- 渲染结果
- 根据文档的id找到id文档对象
- 对关键词进行高亮显示
- 分页处理
- 最终展示给用户
- 用户查询接口
(索引和搜索流程图)
红框表示索引过程,对要索引的原始内容进行索引构建一个索引库,索引过程包括:确定原始内容即要搜索的内容–>采集文档–>创建文档–>分析文档–>索引文档
-
入门程序
需求:
实现一个文件的搜索功能,通过关键字搜索文件,凡是文件名或文件内容包括关键字的文件都需要找出来,还可以根据中文词语进行查询,并且需要支持多个条件查询。
案例中原始内容就是磁盘上的文件:
-
创建索引
-
环境:最低jdk1.8
-
工程搭建:
-
创建Maven项目
-
添加jar:
<dependency> <groupId>org.apache.lucene</groupId> <artifactId>lucene-core</artifactId> <version>7.4.0</version> </dependency> <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>2.6</version> </dependency> <dependency> <groupId>org.apache.lucene</groupId> <artifactId>lucene-analyzers-common</artifactId> <version>7.4.0</version> </dependency>
-
-
步骤:
- 创建一个Directory对象,指定索引库保存的位置
- 基于Directory 对象创建一个IndexWriter对象
- 读取磁盘上的文件,对应每个文件创建一个文档对象
- 向文档对象中添加域
- 把文档对象写入索引库
- 关闭IndexWriter对象
@Test public void createIndex() throws Exception{ Directory directory = FSDirectory.open(new File("D:\\FZ\\index").toPath()); IndexWriter indexWriter = new IndexWriter(directory,new IndexWriterConfig()); File dir = new File("D:\\AllProjectText"); File[] files =dir.listFiles(); for (File f : files){ String fileName =f.getName(); String filePath =f.getPath(); String fileContent = FileUtils.readFileToString(f,"utf-8"); long fileSize =FileUtils.sizeOf(f); // System.out.println(fileName+" " +filePath+" "+fileSize); Field fieldName= new TextField("name",fileName,Field.Store.YES); Field fieldPath= new TextField("path",filePath,Field.Store.YES); Field fieldContend= new TextField("content",fileContent,Field.Store.YES); Field fieldSize= new TextField("size",fileSize+"",Field.Store.YES); Document document = new Document(); document.add(fieldName); document.add(fieldPath); document.add(fieldContend); document.add(fieldSize); indexWriter.addDocument(document); } indexWriter.close(); System.out.println("success"); }
-
-
mark:luke
GitHub:https://github.com/DmitryKey/luke/releases
使用luke查看索引库中的内容 -
查询索引库
步骤:
1. 创建一个Directory对象,指定索引库的位置
2. 创建一个IndexReader对象
3. 创建一个IndexSearcher对象,构造方法中的参数indexReader对象
4. 创建一个Query对象,TermQuery
5. 执行查询,得到一个TopDocs对象
6. 取查询结果的总记录数
7. 取文档列表
8. 打印文档中的内容
9. 关闭IndexReader对象@Test public void searchIndex() throws Exception { Directory directory = FSDirectory.open(new File("D:\\FZ\\index").toPath()); IndexReader indexReader = DirectoryReader.open(directory); IndexSearcher indexSearcher = new IndexSearcher(indexReader); Query query = new TermQuery(new Term("content","spring")); TopDocs topDocs = indexSearcher.search(query,10); System.out.println("查询总记录数:"+ topDocs.totalHits); ScoreDoc[] scoreDocs =topDocs.scoreDocs; for (ScoreDoc doc : scoreDocs){ int docId = doc.doc; Document document = indexSearcher.doc(docId); System.out.println(document.get("name")+" "+document.get("path")+" "+document.get("size")); System.out.println(document.get("content")); System.out.println("----------------------------------------"); } indexReader.close(); }
结果如图:
注意:出现乱码的原因是文件的编码格式要为UTF-8,即可正常显示
-
-
分析器
默认使用的标准分词器StsandardAnalyzer
查看分析器的分析效果
1. 使用Analyzer对象的tokenStream方法返回一个TokenStream对象。词对象中包含了最终分词结果
2. 实现步骤:1. 创建一个Analyzer对象,StandardAnalyzer对象 2. 使用分析器对象的tokenStream方法获得一个TokenStream对象 3. 向TokenStream对象中设置一个引用,相当于一个指针 4. 调用TokenStream对象的rest方法,如果不调用抛异常 5. 使用while循环遍历Tokenstream对象 6. 关闭TOkenStream对象
elasticsearch基础
创建索引
在关系型数据库中,需要使用DDL语句来创建数据库和表,然后才可以插入数据, es 不是必须的
数据类型
• text:全文搜索字符串
• keyword:用于精确字符串匹配和聚合
• date 及 date_nanos:格式化为日期或数字日期的字符串
• byte,short,integer,long:整数类型 boolean:布尔类型
• float,double,half_float:浮点数类型 分级的类型:object 及 nested
一般情况下,es可以很好的理解文档的结构并自动创建映射(mapping)定义。自动创建映射使用无模式(schemaless)方法快速摄取数据,无需担心字段类型。
为了在索引中获得更好搜索的结果和更好性能,我们有时需要需要手动定义映射。
再者需要明确一点,elasticsearch中字段的数据类型无法做更新处理,这不像mysql中字段数据类型可以更新。
因此修改字段数据类型的思路是:
• 先新建一份新的索引A,新的索引A中,将需要修改的字段数据类型改为你需要更正的类型,其余字段与旧的索引B保持一致
• 将旧索引B中数据复制到新索引A的数据中
• 删除旧索引B数据
•
•
创建文档
-
创建索引
在关系型数据库中,创建表(对应es的文档)我们必须指定数据类型和长度,而es可以动态创建索引 mapping(mapping 指数据类型) 。即当我们创建文档时,如果没有创建对应的mapping,那么 es会根据所输入字段的数据猜测数据类型,例如上面的 user 被被认为是 text 类型,而 uid 将被猜测为整数类型。
-
插入文档
在插入文档时,如果该文档的 ID 已经存在,那么就更新现有的文档;如果该文档从来没有存在过,那么就创建新的文档。
如果更新时该文档有新的字段并且这个字段在现有的 mapping 中没有出现,那么 es会根据 schem on write 的策略来推测该字段的类型,并更新当前的 mapping 到最新的状态。
动态 mapping 可能会导致某些字段不是我们想要的数据类型,从而导致索引请求失败
更新文档
修改文档通常使用 PUT 来进行操作,我们需要指定一个特定的 id 来进行修改:
PUT twitter/_doc/1
{
"user": "GB",
"uid": 1,
"city": "北京",
"province": "北京",
"country": "中国",
"location":{
"lat":"29.084661",
"lon":"111.335210"
}
}
但是PUT每次修改一个文档我们需要把文档的每一项都要写出来,我们可以使用如下的方法来进行修改:
POST twitter/_update/1
{
"doc": {
"city": "成都",
"province": "四川"
}
}
Elasticsearch是面向文档(document oriented)的,这意味着它可以存储整个对象或文档(document)。然而它不仅仅是存储,还会索引(index) 每个文档的内容使之可以被搜索。在elasticsearch中,开发者可以对文档(而非成列的数据)j进行索引、搜索、排序、过滤。Elasticsearch比传统关系型数据库:
- 索引 index
相当于开发者使用的Mysql中的库,一个索引由一个名字来标识(必须全部是小写字母的),并且当开发者要对对应于这个索引中的文档进行索引、搜索、更新、和删除的时候,都要使用到这个名字。在一个集群中,可以定义任意多的索引。 - 类型type
相当于开发者使用Mysql中的表,在一个索引中,可以定义一种或多种类型。一个类型是elasticsearch索引中的一个逻辑上的分类/分区。通常,会为具有一组共同字段的文档定义一个类型。比如说,假设开发者运营一个博客平台并且将开发者将所有的数据存储到一个索引中。在这个索引中,你可以为用户数据定义一个类型,为博客数据定义另一个类型,当然,也可以为评论数据定义另一个数据。 - 字段Field
相当于关系型数据表的字段,对文档数据根据不同属性进行的分类标识 - 映射mapping
相当于开发者使用的Mysql中的表结构定义,mapping是处理数据的方式和规则方面做一些限制,如某个字段的数据类型,默认值,分析器,是否被索引等等,这些都是映射里面可以设置的,其它就是处理es里面数据的一些使用规则设置也叫做映射,按照最优规则处理数据对性能提高很大,因此才需要建立映射,并且需要思考如何建立映射才能对性能更好。 - 文档document
相当于开发者使用的Mysql数据表中的一条数据。一个文档是一个可被索引的基本信息单元。比如,你可以拥有某一个客户的文档,某一个产品的一个文档。文档以JSON格式来表示,而JSON是一个到处存在的互联网数据交互格式。
在一个index/type里面,你可以存储任意多的文档。注意,尽管一个文档,物理上存在于一个索引之中,文档必须被索引/赋予一个索引的type。 - 接近实时NRT
elasticsearch是一个接近实时的搜索平台。这意味着,从索引一个文档直到这个文档能够被索引到有一个轻微的延迟(通常在1s以内) - 集群cluster
一个集群由一个或多个节点组织在一起,它们共同持有整个的数据,并一起提供索引和搜索功能。一个集群有一个唯一的名字标识,这个名字默认就是elasticsearch,这个名字是重要的,因为一个节点只能通过制定某个集群的名字,来加入这个集群
- 节点Node
一个节点就是集群中的一个服务器,作为集群的一部分,它存储数据,参与集群的索引和搜索功能。 - 分片和复制
一个索引可以存储超过单个节点硬件限制的大量数据。比如,一个具有10亿文档的索引占据1TB的磁盘空间,而任一节点都没有这样大的磁盘空间;或者单个节点处理搜索请求,响应太慢。为了解决这个问题。elasticsearch提供了将索引划分成多份的功能,这些份就叫做分片。
索引片映射
为了能够将时间域视为时间,数字域视为数字,字符串域视为全文或精确值字符串, Elasticsearch 需要知道每个域中数据的类型。这个信息包含在映射中。
Elasticsearch 支持如下简单域类型:
• 字符串: string
• 整数 : byte, short, integer, long
• 浮点数: float, double
• 布尔型: boolean
• 日期: date
查询结果结构
• took:执行整个搜索请求耗费了多少毫秒
• timed_out:查询是否超时
• _shards:查询中参与分片的总数,以及这些分片成功了多少个失败了多少个
• hits: 返回结果中最重要的部分是 hits ,它包含 total 字段来表示匹配到的文档总数
• hits.total:搜索到的总条数
• hits.max_score:与查询所匹配文档的 _score 的最大值
• hits.hits:一个 hits 数组包含所查询结果的前十个文档
• hits.hits._type:文档类型
• hits.hits._id:文档id
• hits.hits._index:索引库
• hits.hits._score:每个结果还有一个 _score ,它衡量了文档与查询的匹配程度
• hits.hits._source:文档的源数据
• aggregations: 聚合查询的结果
elasticsearch数据处理
是一个开源高扩展的分布式全文检索引擎,es可近乎实时的存储、检索数据;且自身扩展性很好,开源扩展到上百台服务器、处理PB级别的数据。es也使用java开发并使用Lucene作为其核心来实现所有索引和搜索功能,但是它的目的是通过简单的restful api 来隐藏lucene的复杂性,从而让全文搜索变得简单
下载elasticsearch安装包进行解压后得到目录如下结构:
进入bin目录里,点击elasticsearch可启动服务,
windows点击浏览器输入localhost:9200 即可测试连接
linux输入命令curl ‘192.168.33.100:9200/’ 即可测试连接
在启动成功后就是进行页面访问,若使用elasticsearch-head插件访问,则需要 修改config目录elasticsearch.yml服务配置文件,进行开启跨域访问
#文件末尾追加以下两行代码
http.cors.enabled: true
http.cors.allow-origin: “*”
elasticsearch修改语法
Elasticsearch实际上并没有在原文档进行就地更新。无论何时我们进行更新,Elasticsearch都会删除旧文档,索引一个新文档来立刻替换它。
把刚才新增的请求方式改为PUT,就是修改了。不过修改必须指定id,id对应文档存在,则修改;id对应文档不存在,则新增
请求方式:http://127.0.0.1:9200/索引名称/_doc/ID编号
示例:
PUT /heima/goods/3
{
"title":"超大米手机",
"images":"http://image.leyou.com/12479122.jpg",
"price":3899.00,
"stock": 100,
"saleable":true
}
kibana示例:
• PUT financial_report/_doc/izSZ2IYBNh5IGSpnAVb4/
{
"indicator":"李华"
}
elasticsearch删除语法
-
根据主键删除数据
语法:【DELETE /索引名称/类型名称/主键编号】
示例:Delete 索引名称/文档名称/主键编号
-
根据匹配条件删除数据
描述:如果你想根据条件来删除你的数据,则在Query查询体中组织你的条件就可以了,注意请求方式是Post
示例:
POST 索引名称/文档名称/_delete_by_query { "query":{ "term":{ "_id":100000100 } } }
-
删除所有数据
描述:注意请求方式是Post,只删除数据,不删除表结构
示例:
POST /testindex/testtype/_delete_by_query?pretty { "query": { "match_all": { } } }
-
删除单个索引(结构和数据同时删除)
语法:【DELETE /索引名称】
示例:Delete 索引名称
-
删除多个索引
语法:【DELETE /索引1,索引2】
示例:Delete 索引名称1,索引名称2
elasticsearch新增语法
-
随机生成id添加数据
通过POST请求,可以向一个已经存在的索引库中添加数据
语法:
POST /索引库名/类型名{ "key":"value" }
-
自定义id添加数据
语法:
POST /索引库名/类型/id值 { "key":"value" }
-
添加字段:
新增字段首先查看索引当前结构(GET):GET http://IP:9200/user
查询结果例如:
{ "user": { "aliases": {}, "mappings": { "properties": { "age": { "type": "double" }, "name": { "type": "text" }, "sex": { "type": "text" } } }, "settings": { "index": { "routing": { "allocation": { "include": { "_tier_preference": "data_content" } } }, "number_of_shards": "1", "provided_name": "user", "creation_date": "1668516591325", "number_of_replicas": "1", "uuid": "BArUKCd8RFafjfMSu6oHBw", "version": { "created": "8050099" } } } } }
可以看到当前的user索引具有name、age、sex三个字段,此时则可以进行除三个字段外的字段添加,添加模板如下:
PUT /索引名/_mapping { "properties": { "字段1": { "type": "float" }, "字段2": { "type": "keyword" }, "字段3": { "type": "text" } } }
示例:
#增加索引字段location,为geo_point索引 #PUT /my_index/_mapping/my_type { "properties": { "location": { "type": "geo_point" } } }
elasticsearch数据查询
elasticsearch中查询方式主要分为URI Search和 Body Search:
• URI Search:操作简便,方便通过命令进行测试,但它仅能包含部分查询语法
• Body Search:将搜索条件写在请求体中,可以编写复杂查询
elasticsearch查询语法
查询模板
单条件查询模板如下:
GET /索引库名/_search
{
"query":{
"查询类型":{"查询条件":"查询条件值"}
}
}
查询示例如下:
#GET /resource_scale/_search
{
"query":{
"match":{"month":"1325347200000"}
}
}
这里的query代表一个查询对象,里面可以有不同的查询属性
查询类型:例如:match_all, match,term , range 等等
查询条件:查询条件会根据类型的不同,写法也有差异,后面详细讲解
-
match查询指定条件
match类型查询,会把查询条件进行分词,然后进行查询,多个词条之间是or的关系;例如搜索"小米电视",不仅会查询到电视,而且与小米相关的都会查询到,多个词之间是or的关系。
match查询条件只能作为单条件查询,如果有多个条件,则会报错;
某些情况下,我们需要更精确查找,我们希望这个关系变成and,可以这样做:
#GET /heima/_search { "query":{ "match": { "title": { "query": "小米电视", "operator": "and" } } } }
示例:查询name = “政治经济概况” 的数据
#http://10.254.218.131:9200/sheet_type/_doc/_search { "query":{ "match":{ "name":"政治经济概况" } } }
-
多字段查询(multi_match)
multi_match与match类似,不同的是它可以在多个字段中查询
#GET /heima/_search { "query":{ "multi_match": { "query": "小米", #在title字段和subtitle字段中查询小米这个词 "fields": [ "title", "subTitle" ] } } }
-
bool多条件查询
match单条件查询会报错,此时需要使用复合查询bool,bool查询包含四种操作
• must :对应mysql的 and a=
• must not :对应mysql的 and a!=
• filter :对应mysql的 and a=
• should :对应mysql的 or a=must和filter不同在于must算分,filter不算分;所以filter效率比must高
精确匹配模板:
#精确不分词匹配查找,term/terms,单个匹配是term;多个是terms #存在match搜索可以搜索到但term搜索不到的问题,原因是表中关键词存储的格式是text类型存储的, #text类型数据默认是分词的,所以精确匹配匹配不到。将text类型改成keyword类型即可实现功能。 { "query" : { "bool":{ "must":{"terms" : {"键1": ["值11","值12"]}}, "should":{"term" : {"键2": "值2"} } } } }
条件排除模板:
{ "query" : { "bool":{ "must":{"match" : {"键1": "值1"}}, "must_not":{"match" : {"键2": "值2"}} } } }
或者模板:
#如果没有filter和must查询的话,那么必须满足一个should中的条件,表示或者 { "query" : { "bool":{ "should":{"match" : {"键1": "值1"}}, "should":{"match" : {"键2": "值2"} } } } }
有或没有模板:
#如果包含了must或者filter查询,那么should的查询语句就不是或者的意思了,而是有或者没有都行的含义 { "query" : { "bool":{ "must":{"match" : {"键1": "值1"}}, "should":{"match" : {"键2": "值2"}} } } }
范围查询模板:
#范围过滤:range #大于:gt #小于:lt #大于等于:gae #小于等于:lte { "query" : { "bool":{ "must":{"range" : {"键": {"gt":"值1","lt":"值2"}} } } } }
字段存在查询模板:
{ "query" : { "bool":{ "must":{"exists":{"field": "字段"}} } } } }
过滤条件查询模板:
{ "query" : { "bool": { "must": {"match" : {"键" : "值"}}, "filter": {"range" : {"条件" : { "gt" : 范围 }} } } } }
-
aggs聚合查询
在Elasticsearch中,支持聚合操作,类似SQL中的group by操作
示例:按照年龄进行分组
{ "aggs":{ "all_interests":{ "terms":{ "field":"age" } } } }
返回结果:
{ "took": 61, "timed_out": false, "_shards": { "total": 2, "successful": 2, "skipped": 0, "failed": 0 }, "hits": { "total": { "value": 3, "relation": "eq" }, "max_score": 1, "hits": [ { "_index": "haoke", "_type": "user", "_id": "LgOccnEBK3PERGtN9-n7", "_score": 1, "_source": { "name": "王五", "age": 20, "sex": "女" } }, { "_index": "haoke", "_type": "user", "_id": "LwPYcnEBK3PERGtNAOmn", "_score": 1, "_source": { "id": 1002, "name": "张三", "age": 23, "sex": "男" } }, { "_index": "haoke", "_type": "user", "_id": "MAPYcnEBK3PERGtNx-k7", "_score": 1, "_source": { "id": 1003, "name": "李四", "age": 20, "sex": "女" } } ] }, "aggregations": { "all_interests": { "doc_count_error_upper_bound": 0, "sum_other_doc_count": 0, "buckets": [ { "key": 20, "doc_count": 2 # 可以看出,年龄为20的用户有2个 }, { "key": 23, "doc_count": 1 # 可以看出,年龄为23的用户有1个 } ] } } }
elasticsearch-head
-
elasticsearch-head安装
elasticsearch不同于solr自带图形化界面,开发者可通过安装elasticsearch的head插件,完成图形化界面的效果,完成索引数据的查看。
1. 安装head插件
2. 安装node.js
3.
4.
-
elasticsearch-head界面介绍
-
数据浏览区
数据浏览这里以_下划线开头的表示es内置字段,无法进行修改数据;每点击一条数据都会有json格式的显示
-
es-head查询数据
在查询的框中填写es地址和查询的索引
_search表示查询索引中的所有数据,类型选择GET,最后点击提交请求
-
分词插件
下载地址:https://github.com/medcl/elasticsearch-analysis-ik/releases点击这里
ik分词器提供了两种分词算法 : ik_smart和ik_max_word
其中ik_smart为最小切分,ik_max_word为最细粒度划分
具体测试可用上文的方法测试即可
第三种客户端操作elasticsearch
使用java客户端操作elasticsearch
目标:
- 能够使用Java客户端完成创建,删除索引的操作
- 能够使用java客户端完成文档的增删改查的操作
- 能够使用java客户端完成文档的查询操作
- 能够完成文档的分页操作
- 能够完成文档的高亮查询操作
- 能够搭建springdata elasticsearch的环境
- 能够完成spring data elasticsearch的基本增删改查操作
- 能够掌握基本条件查询的方法命名规则
【1】使用Java客户端管理es
-
创建索引库
-
步骤:
-创建java maven工程
-添加jar包,添加maven坐标
-编写测试方法实现创建索引库- 创建一个Settting对象,相当于是一个配置信息。主要配置集群的名称。
- 创建一个客户端Client对象
- 使用client对象创建一个索引库
- 关闭client对象
-
步骤实现:
-
创建工程及导入坐标
-
编写测试方法
节点名称、ip、端口可在config/elasticsearch.yml中查看
/** * 创建索引库(无mapping) * @throws UnknownHostException */ @Test public void createIndex() throws UnknownHostException { //创建一个Setting对象,相当于是一个配置信息,主要配置集群的名称 Settings settings = Settings.builder().put("cluster.name", "my-application").build(); //创建一个客户端client对象 System.out.println("1"); TransportClient client = new PreBuiltTransportClient(settings); System.out.println("2"); //InetSocketTransportAddress 在es6.*以上版本是没有这个了,但是在5.*中还在用 //如果是springboot项目中,6.8.3是springboot父工程中已配置好的版本,如果读者使用的es版本是es5.*,可在Pom文件中添加版本锁定即可 client.addTransportAddress(new TransportAddress(InetAddress.getByName("192.168.33.100"),9300)); //es是集群部署,假如第一个节点挂了,会自动找接下来的节点进行连接 // client.addTransportAddress(new TransportAddress(InetAddress.getByName("192.168.33.100"),9301)); // client.addTransportAddress(new TransportAddress(InetAddress.getByName("192.168.33.100"),9302)); System.out.println("3"); //使用client对象创建一个索引库 client.admin().indices().prepareCreate("index_java").get(); System.out.println("4"); //关闭client对象 client.close(); System.out.println("success"); }
-
-
-
使用java客户端设置mappings
-
步骤:
-创建一个Setting对象
-创建一个Client对象
-创建一个mapping信息,应该是一个json数据,可以是字符串,也可以是XContextBuilder对象
-使用client同es服务器发送mapping信息
-关闭client对象 -
实现步骤
/** * 设置索引库mapping * @throws UnknownHostException */ @Test public void setMappings() throws IOException { //创建一个setting对象 Settings settings = Settings.builder().put("cluster.name", "my-application").build(); //创建客户端对象 TransportClient client = new PreBuiltTransportClient(settings). addTransportAddress(new TransportAddress(InetAddress.getByName("192.168.33.100"),9300)); System.out.println("1"); XContentBuilder builder = XContentFactory.jsonBuilder() .startObject() .startObject("article") .startObject("properties") .startObject("id") .field("type", "long") .field("store", true) .endObject() .startObject("title") .field("type", "text") .field("store", true) .field("analyzer", "hanlp") .endObject() .startObject("content") .field("type", "text") .field("store", true) .field("analyzer", "hanlp") .endObject() .endObject() .endObject() .endObject(); System.out.println("2"); //使用client把mapping信息设置到索引库中 client.admin().indices() //设置要做mapping的索引 .preparePutMapping("index_java") //设置要做索引的type .setType("article") .setSource(builder) //执行操作 .get(); //关闭连接 client.close(); }
-
elasticsearch-postman
使用Postman向ES发送put请求进行文档的存储/更新:https://ip:9200/索引/类别/文档
使用Postman向ES发送get请求进行文档的获取:https://ip:9200/索引/类别/文档
使用Postman向ES发送head请求进行文档是否存在,200存在,404不存在:https://ip:9200/索引/类别/文档
使用Postman向ES发送delete请求进行文档的删除:https://ip:9200/索引/类别/文档
查询所有文档所有 get方法:https://ip:9200/索引/类别/_search
查询满足条件的文档 get方法:https://ip:9200/索引/类别/_search?q=键:值
使用查询表达式搜索满足条件的文档 post方法:https://ip:9200/索引/类别/_search加上一个表达式的json
-
添加索引【PUT】
http://127.0.0.1:9200/索引名称
-
添加索引附带Mapping【PUT】
http://127.0.0.1:9200/索引名称
{ "mappings":{ "usermessage":{ "properties":{ "id":{ "type":"long", "store":true }, "titile":{ "type":"text", "store":true, "index":true, "analyzer":"standard" }, "content":{ "type":"text", "store":true, "index":true, "analyzer":"standard" } } } } }
-
添加文档并指定ID【POST】
http://127.0.0.1:9200/索引名称/索引类型/自定义ID
-
添加文档不指定ID【POST】
http://127.0.0.1:9200/索引名称/索引类型
-
主键查询【GET】
http://127.0.0.1:9200/索引名称/_doc/ID编号
-
全查询【GET】
http://127.0.0.1:9200/索引名称/_search
-
修改文档【PUT】
http://127.0.0.1:9200/索引名称/_doc/ID编号
-
删除文档【DELETE】
http://127.0.0.1:9200/索引名称/_doc/ID编号
如果重复删除,则result 则返回not_found
-
删除索引
- 通过Postman删除
- 通过elasticsearch-head的复合查询删除
- 通过Postman删除
-
搜索文档方式
- 通过通过postman _search
2. 通过通过postman query_string查询
- 通过通过postman _search
-
通过head图形化插件查询
- 分词器分词效果测试方法:
elasticsearch-Kibana
-
创建索引及文档
#PUT twitter/_doc/1 { "user": "GB", "uid": 1, "city": "Beijing", "province": "Beijing", "country": "China" }
-
修改文档
#POST twitter/_update/1 { "doc": { "city": "成都", "province": "四川" } }
springboot-elasticsearch
Spring Data 的强大之处,就在于你不用写任何DAO处理,自动根据方法名或类的信息进行CRUD操作。只要你定义一个接口,然后继承Repository提供的一些子接口,就能具备各种基本的CRUD功能。
把数据存储到es中,有两种方式一种是 ElasticsearchRepository 接口,另一种是ElasticsearchTemplate接口,今天我们主要分析ElasticsearchRepository接口。
具体流程如下:
-
配置文件增加属性
//端口配置 spring.elasticsearch.rest.uris= http://localhost:9200 //集群名称 spring.elasticsearch.rest.username= xy_elastic
-
接口继承ElasticSearchRepository
@Repository public interface ItemRepository extends ElasticsearchRepository<Item, Long> { }
-
实体类
@Data @ToString /* *indexName : 文档名称 *shards: 分片数量,默认1 */ @Document(indexName = "item", shards = 5) public class Item implements Serializable { @Id private Long id; @Field(type = FieldType.Text , analyzer = "ik_max_word") private String title; @Field(type = FieldType.Long) private Long price; @Field(type = FieldType.Text) private String name; @Field(type = FieldType.Date, format = DateFormat.custom, pattern = "yyyy-MM-dd HH:mm:ss") @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss") private Date date; }
-
ElasticSearchRepository已封装方法(保存/更新/批量保存/删除/查询/查询全部)
{ @Autowired private ItemRepository itemRepository; //实体类 Item item = new Item(); item.setId(1L); item.setTitle("ElasticSearch目前最新的已到7X"); item.setPrice(30000L); //保存 或 更新(区分的依据就是id) itemRepository.save(item); List<Item> list = new ArrayList<>(); list.add(item); //批量保存 itemRepository.saveAll(list); //删除 itemRepository.deleteById(id); /** * 通过id获取信息 * * @param id id * @return {@link Item} */ public Item esGetInfoById(Long id){ Optional<Item> item = itemRepository.findById(id); return item.get(); } /** * 查询全部 * * @param id id * @return {@link Item} */ public Iterable<Item> esGetInfoAll(){ Iterable<Item> items = itemRepository.findAll(); return items; } }
-
ItemRepository自定义方法
Spring Data 的另一个强大功能,是根据方法名称自动实现功能。
比如:你的方法名叫做:findByTitle,那么它就知道你是根据title查询,然后自动帮你完成,无需写实现类。当然,方法名称要符合一定的约定:
Keyword 例子 Elasticsearch Query String And findByNameAndPrice {“bool” : {“must” : [ {“field” : {“name” : “?”}}, {“field” : {“price” : “?”}} ]}} Or findByNameOrPrice {“bool” : {“should” : [ {“field” : {“name” : “?”}}, {“field” : {“price” : “?”}} ]}} Is findByName {“bool” : {“must” : {“field” : {“name” : “?”}}}} Not findByNameNot {“bool” : {“must_not” : {“field” : {“name” : “?”}}}} Between findByPriceBetween {“bool” : {“must” : {“range” : {“price” : {“from” : ?,“to” : ?,“include_lower” : true,“include_upper” : true}}}}} LessThanEqual findByPriceLessThan {“bool” : {“must” : {“range” : {“price” : {“from” : null,“to” : ?,“include_lower” : true,“include_upper” : true}}}}} GreaterThanEqual findByPriceGreaterThan {“bool” : {“must” : {“range” : {“price” : {“from” : ?,“to” : null,“include_lower” : true,“include_upper” : true}}}}} Before findByPriceBefore {“bool” : {“must” : {“range” : {“price” : {“from” : null,“to” : ?,“include_lower” : true,“include_upper” : true}}}}} After findByPriceAfter {“bool” : {“must” : {“range” : {“price” : {“from” : ?,“to” : null,“include_lower” : true,“include_upper” : true}}}}} Like findByNameLike {“bool” : {“must” : {“field” : {“name” : {“query” : “?*”,“analyze_wildcard” : true}}}}} StartingWith findByNameStartingWith {“bool” : {“must” : {“field” : {“name” : {“query” : “?*”,“analyze_wildcard” : true}}}}} EndingWith findByNameEndingWith {“bool” : {“must” : {“field” : {“name” : {“query” : “*?”,“analyze_wildcard” : true}}}}} /** * 获取页面项目 * * @param keyWord 关键字 * @param pageable 可分页 * @return {@link Page<Item>} */ public Page<Item> getPageItems(String keyWord, Pageable pageable) { NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder(); // 构建布尔查询 BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery(); /** * must 多条件 &(并且) * mustNot 多条件 != (非) * should 多条件 || (或) */ // 关键字查询 boolQueryBuilder.must(QueryBuilders.matchQuery("title",keyWord)); nativeSearchQueryBuilder.withQuery(boolQueryBuilder); nativeSearchQueryBuilder.withPageable(pageable); return itemRepository.search(nativeSearchQueryBuilder.build()); }
快捷使用导航
索引查询分类
数据修改分类
数据查询分类
数据删除分类
数据汇总分类
数据类型分类
查看索引user中的数据:GET http://IP:9200/user/_search
当前索引增加字段:
PUT s_person/_mapping/_doc?include_type_name=true
{
"properties": {
"weight": {
"type": "integer"
}
}
}
索引空列数据统一赋值:
POST http://IP:9200/user/_update_by_query
{
"script": {
"lang": "painless",
"inline": "ctx._source.class = '一班'"
}
}
索引空列数据按判断赋值:
POST s_person/_doc/_update_by_query
{
"script":{
"lang":"painless",
"source":"if (ctx._source.weight == null) {ctx._source.weight = 1}"
}
}
索引空列数据赋其他字段的值:
POST http://IP:9200/user/_update_by_query
{
"script": {
"lang": "painless",
"inline": "ctx._source.class = ctx._source.sex"
}
}
索引结构查询
#GET http://IP:9200/user
单字段修改
#POST data_report/_update/DDSLyoYBEypAanHiieNt
{
"doc": {
"status": "IMPORT"
}
}
单字段查询
#GET /resource_scale/_search
{
"query":{
"match":{"month":"1325347200000"}
}
}
多字段查询
#GET data_report/_search
{
"query":{
"bool":{
"must":[
{"match":{"status":"PENDING_REVIEW"}},
{"term":{"province":"新疆"}}
]
}
}
}
全部字段查询
GET /heima/_search
{
"query":{
"match_all": {}
}
}
指定字段查询
GET network_scale/_search?size=30
{
"_source": {
"includes":["dbId","userId"]
},
"query":{
"match_all": {}
}
}
排除字段查询
GET network_scale/_search?size=30
{
"_source": {
"excludes": ["nickName"]
},
"query":{
"match_all": {}
}
}
指定条数返回查询
GET network_scale/_search?size=30
{
"query":{
"bool":{
"must":[
{"match":{"month":"1654012800000"}},
{"term":{"province":"广东"}}
]
}
}
}
分页返回查询
GET network_scale/_search?size=30&from=5
{
"query":{
"bool":{
"must":[
{"match":{"month":"1654012800000"}},
{"term":{"province":"广东"}}
]
}
}
}
多词查询(or)
#title 字段中包含brown 或 dog的都进行查询
#它在内部实际上先执行两次 term 查询,然后将两次查询的结果合并作为最终结果输出,为了做到这点,它将两个 term 查询包入一个 bool 查询中
GET /my_index/my_type/_search
{
"query": {
"match": {
"title": "BROWN DOG!"
}
}
}
多词查询(and)
#match 查询还可以接受 operator 操作符作为输入参数,默认情况下该操作符是 or
GET /my_index/my_type/_search
{
"query": {
"match": {
"title": {
"query": "BROWN DOG!",
"operator": "and"
}
}
}
}
多次字段分组查询:
#GET /你的索引/_search
{
"size": 0,
"aggregations": {
"字段一的结果命名": {
"terms": {
"field": "startTime.keyword"
},
"aggregations": {
"字段二的结果命名": {
"terms": {
"field": "aa.keyword"
}
}
}
}
}
}
单条删除
DELETE /network_scale/_doc/njWj34YBNh5IGSpnWRkp
返回某个字段数值求和
GET network_scale/_search?size=30
{
"_source": [
"fiveGNetworkScale._5GAmount.jiZhanKaiTong"
],
"query":{
"bool":{
"must":[
{"match":{"month":"1654012800000"}},
{"term":{"province":"广东"}}
]
}
},
"aggs" : {
"5GAmountNum" : {
"sum": {
"field" : "fiveGNetworkScale._5GAmount.jiZhanKaiTong"
}
}
}
}
返回某个字段个数汇总
GET network_scale/_search?size=30
{
"query":{
"bool":{
"must":[
{
"match":{
"month":"1651334400000"
}
},
{
"term":{
"province":"新疆"
}
}
]
}
},
"aggs":{
"groupByFactory": {
"terms": {
"field": "factory"
}
}
}
}
返回某个字段个数汇总并数值求和
在这里插入代码片
批量删除修改
#POST planning_data/_delete_by_query
{
"query":{
"bool":{
"must":[
{"match":{"province":"浙江"}},
{"match":{"month":"1693497600000"}}
]
}
}
}
字段类型修改
#前提1:原索引
PUT my_index
{
"mappings": {
"_doc": {
"properties": {
"create_date": {
"type": "date",
"format": "yyyy-MM-dd ||yyyy/MM/dd"
}
}
}
}
}
#修改字段类型步骤1:创建新索引
PUT my_index2
{
"mappings": {
"_doc": {
"properties": {
"create_date": {
"type": "text"
}
}
}
}
}
#修改字段类型步骤2:同步数据
POST _reindex
{
"source": {
"index": "my_index"
},
"dest": {
"index": "my_index2"
}
}
#修改字段类型步骤3:删除原索引
DELETE my_index
#修改字段类型步骤4(拓展):设置别名
POST /_aliases
{
"actions": [
{"add": {"index": "my_index2", "alias": "my_index"}}
]
}
API快捷使用导航
聚合分类
单字段聚合
sql表示语句
--计算每个球队的球员数
select team, count(*) as player_count from player group by team;
es表示语句
public static void main (String[] args){
TermsBuilder teamAgg= AggregationBuilders.terms("player_count ").field("team");
sbuilder.addAggregation(teamAgg);
SearchResponse response = sbuilder.execute().actionGet();
}
多字段聚合
sql表示语句
--计算每个球队每个位置的球员数
select team, position, count(*) as pos_count from player group by team, position;
es表示语句
public static void main (String[] args){
TermsBuilder teamAgg= AggregationBuilders.terms("player_count ").field("team");
TermsBuilder posAgg= AggregationBuilders.terms("pos_count").field("position");
sbuilder.addAggregation(teamAgg.subAggregation(posAgg));
SearchResponse response = sbuilder.execute().actionGet();
}
单字段聚合求最大/最小/平均
sql表示语句
--计算每个球队年龄最大/最小/总/平均的球员年龄
select team, max(age) as max_age from player group by team;
es表示语句
public static void main (String[] args){
TermsBuilder teamAgg= AggregationBuilders.terms("player_count ").field("team");
MaxBuilder ageAgg= AggregationBuilders.max("max_age").field("age");
sbuilder.addAggregation(teamAgg.subAggregation(ageAgg));
SearchResponse response = sbuilder.execute().actionGet();
}
单字段聚合求多字段的最大/最小/平均
sql表示语句
--计算每个球队球员的平均年龄,同时又要计算总年薪
select team, avg(age)as avg_age, sum(salary) as total_salary from player group by team;
es表示语句
public static void main (String[] args){
TermsBuilder teamAgg= AggregationBuilders.terms("team");
AvgBuilder ageAgg= AggregationBuilders.avg("avg_age").field("age");
SumBuilder salaryAgg= AggregationBuilders.avg("total_salary ").field("salary");
sbuilder.addAggregation(teamAgg.subAggregation(ageAgg).subAggregation(salaryAgg));
SearchResponse response = sbuilder.execute().actionGet();
}
单字段聚合汇总并排序
sql表示语句
--计算每个球队总年薪,并按照总年薪倒序排列
select team, sum(salary) as total_salary from player group by team order by total_salary desc;
es表示语句
public static void main (String[] args){
//Order.aggregation函数的第一个参数是aggregation的名字,第二个参数是boolean型,true表示正序,false表示倒序
TermsBuilder teamAgg= AggregationBuilders.terms("team").order(Order.aggregation("total_salary ", false);
SumBuilder salaryAgg= AggregationBuilders.avg("total_salary ").field("salary");
sbuilder.addAggregation(teamAgg.subAggregation(salaryAgg));
SearchResponse response = sbuilder.execute().actionGet();
}
单字段聚合汇总返回所有结果条数
es表示语句
public static void main (String[] args){
//默认情况下,search执行后,仅返回10条聚合结果,如果想返回更多的结果,需要在构建TermsBuilder 时指定size
TermsBuilder teamAgg= AggregationBuilders.terms("team").size(15);
}
常见ES报错情况
[TOO_MANY_REQUESTS/12/disk usage exceeded flood-stage watermark, index has read-only-allow-delete block]
排查及解决思路:
一、排查
1. 分析ES集群日志
[root@localhost ~]# tail -fn 100 /data/elasticsearch/logs/es-cluster.log
从日志信息上可以得到一些信息:一个是请求太多,另一个是磁盘超过阈值;这两个原因都有可能触发ES主动给索引上锁。
2. 查看ES节点磁盘容量
[root@localhost ~]# df -h
二、解决
1. 清理ES节点磁盘空间(进行服务器扩容)
2. Kibana发送重置请求,关闭索引的只读状态
PUT _all/_settings
{
"index.blocks.read_only_allow_delete": null
}
[应急]若来不及给Elasticsearch硬盘扩容,可以先关闭磁盘分配保护,让最后仅有的5%的磁盘空间缓冲一点时间,然后再给硬盘扩容
PUT _cluster/settings
{
"transient": {
"cluster.routing.allocation.disk.threshold_enabled": false
}
}
磁盘扩容完毕后,启用磁盘分配保护
PUT _cluster/settings
{
"transient": {
"cluster.routing.allocation.disk.threshold_enabled": true
}
}