赞
踩
我们生活中的数据总体分为两种:结构化数据和非结构化数据。
结构化数据:指具有固定格式或有限长度的数据,如数据库,元数据等。
非结构化数据:指不定长或无固定格式的数据,如邮件,word 文档等磁盘上的文件
常见的结构化数据也就是数据库中的数据。在数据库中搜索很容易实现,通常都是使用 sql
语句进行查询,而且能很快的得到查询结果。
为什么数据库搜索很容易?
因为数据库中的数据存储是有规律的,有行有列而且数据格式、数据长度都是固定的。
(1 ) 顺序扫描法(Serial Scanning)
所谓顺序扫描,比如要找内容包含某一个字符串的文件,就是一个文档一个文档的看,对
于每一个文档,从头看到尾,如果此文档包含此字符串,则此文档为我们要找的文件,接着看下
一个文件,直到扫描完所有的文件。如利用 windows 的搜索也可以搜索文件内容,只是相当的
慢。
(2 ) 全文检索(Full-text Search)
将非结构化数据中的一部分信息提取出来,重新组织,使其变得有一定结构,然后对此有
一定结构的数据进行搜索,从而达到搜索相对较快的目的。这部分从非结构化数据中提取出的然
后重新组织的信息,我们称之 索引。
例如:字典。字典的拼音表和部首检字表就相当于字典的索引,对每一个字的解释是非结
构化的,如果字典没有音节表和部首检字表,在茫茫辞海中找一个字只能顺序扫描。然而字的某
些信息可以提取出来进行结构化处理,比如读音,就比较结构化,分声母和韵母,分别只有几种
可以一一列举,于是将读音拿出来按一定的顺序排列,每一项读音都指向此字的详细解释的页数。
我们搜索时按结构化的拼音搜到读音,然后按其指向的页数,便可找到我们的非结构化数据——
也即对字的解释。
这种先建立索引,再对索引进行搜索的过程就叫全文检索(Full-text Search) 。
虽然创建索引的过程也是非常耗时的,但是索引一旦创建就可以多次使用,全文检索主要处
理的是查询,所以耗时间创建索引是值得的。
可以使用 Lucene 实现全文检索。Lucene 是 apache 下的一个开放源代码的全文检索引擎工具
包。提供了完整的查询引擎和索引引擎,部分文本分析引擎。Lucene 的目的是为软件开发
人员提供一个简单易用的工具包,以方便的在目标系统中实现全文检索的功能。
对于数据量大、数据结构不固定的数据可采用全文检索方式搜索,比如百度、Google 等搜
索引擎、论坛站内搜索、电商网站站内搜索等。
1、绿色表示索引过程,对要搜索的原始内容进行索引构建一个索引库,索引过程包括:
确定原始内容即要搜索的内容–>采集文档–>创建文档–>分析文档–>索引文档
2、红色表示搜索过程,从索引库中搜索内容,搜索过程包括:
用户通过搜索界面–>创建查询–>执行搜索,从索引库搜索–>渲染搜索结果
我们以一个招聘网站的搜索为例,比如说智联招聘,在网站上输入关键字搜索显示的内容不是直接从数据库中来的,而是从索引库中获取的,网站的索引数据需要提前创建的。以下是创建的过程:
第一步:获得原始文档:就是从mysql数据库中通过sql语句查询需要创建索引的数据
第二步:创建文档对象,把查询的内容构建成lucene能识别的Document对象
获取原始内容的目的是为了索引,在索引前需要将原始内容创建成文档(Document),
文档中包括一个一个的域(Field),这个域对应就是表中的列。
注意:每个 Document 可以有多个 Field,不同的 Document 可以有不同的 Field,同一个
Document 可以有相同的 Field(域名和域值都相同)
每个文档都有一个唯一的编号,就是文档 id。
第三步:分析文档
将原始内容创建为包含域(Field)的文档(document),需要再对域中的内容进行分析,
分析的过程是经过对原始文档提取单词、将字母转为小写、去除标点符号、去除停用词等过
程生成最终的语汇单元,可以将语汇单元理解为一个一个的单词。
查询索引也是搜索的过程。搜索就是用户输入关键字,从索引(index)中进行搜索的
过程。根据关键字搜索索引,根据索引找到对应的文档
第一步:创建用户接口:用户输入关键字的地方
把表中的数据放入到索引库中
先把数据导入到mysql 中(随便放到一个database就行),数据如下
第一步:创建一个maven工程 ,刚学过SpringBoot,我们就创建一个SpringBoot项目
第二步:导入依赖
<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>cn.itcast</groupId> <artifactId>lucene_demo</artifactId> <version>1.0-SNAPSHOT</version> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.3.RELEASE</version> </parent> <dependencies> <!--springboot mvc--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--数据库依赖--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.47</version> </dependency> <!--通用mapper--> <dependency> <groupId>tk.mybatis</groupId> <artifactId>mapper-spring-boot-starter</artifactId> <version>2.1.5</version> </dependency> <!--springboot 测试--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> </dependency> <!--lucene依赖--> <dependency> <groupId>org.apache.lucene</groupId> <artifactId>lucene-core</artifactId> <version>4.10.3</version> </dependency> <!--lucene的分词器--> <dependency> <groupId>org.apache.lucene</groupId> <artifactId>lucene-analyzers-common</artifactId> <version>4.10.3</version> </dependency> <!--lombok--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> </dependencies></project>
第三步:创建引导类
package cn.itcast;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import tk.mybatis.spring.annotation.MapperScan;/** * @author: mryhl * @date: Created in 2020/11/6 9:51 * @description: 引导类 */@SpringBootApplication@MapperScan("cn.itcast.mapper")public class LuceneApplication { public static void main(String[] args) { SpringApplication.run(LuceneApplication.class,args); }}
第四步:配置yml文件
# 服务名称spring: application: name: lucene-demo# 数据库驱动 datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql:///lucene_db username: root password: root# 日志输出logging: level: cn.itcast: debug
第五步:创建实体类、mapper、service
实体类
package cn.itcast.pojo;import lombok.Data;import tk.mybatis.mapper.annotation.KeySql;import javax.persistence.Column;import javax.persistence.Id;import javax.persistence.Table;/** * @author: mryhl * @date: Created in 2020/11/6 11:59 * @description: 实体类 */@Data@Table(name = "job_info")public class JobInfo { @Id //使用主键自增策略 @KeySql(useGeneratedKeys = true) private Long id; private String companyName; private String companyAddr; private String jobName; private String jobAddr; //列表和属性名的对应 @Column(name = "salary_min") private Integer salary; private String url;}
mapper
package cn.itcast.mapper;import cn.itcast.pojo.JobInfo;import tk.mybatis.mapper.common.BaseMapper;/** * @author: mryhl * @date: Created in 2020/11/6 12:00 * @description: dao层,继承BaseMapper 通用mapper */public interface JobInfoMapper extends BaseMapper<JobInfo> {}
service (只是测试,这个可以省略)
package cn.itcast.service.impl;import cn.itcast.mapper.JobInfoMapper;import cn.itcast.pojo.JobInfo;import cn.itcast.service.JobInfoService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;import java.util.List;/** * @author: mryhl * @date: Created in 2020/11/6 12:02 * @description: service实现类 */@Servicepublic class JobInfoServiceImpl implements JobInfoService { @Autowired private JobInfoMapper jobInfoMapper; @Override public List<JobInfo> findAll() { return jobInfoMapper.selectAll(); }}
功能测试
package cn.itcast;import cn.itcast.pojo.JobInfo;import cn.itcast.service.JobInfoService;import org.junit.Test;import org.junit.runner.RunWith;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.context.SpringBootTest;import org.springframework.test.context.junit4.SpringRunner;import java.util.List;@RunWith(SpringRunner.class)@SpringBootTest(classes = LuceneApplication.class)@Slf4jpublic class LuceneTest { @Autowired private JobInfoService jobInfoService; @Test public void testFindAll() throws Exception{ List<JobInfo> list = jobInfoService.findAll(); log.debug("====yhl.log====查询到的数据条数"+ list.size()); } }
运行结果:
在test下创建一个包cn.itcast
package cn.itcast;import cn.itcast.pojo.JobInfo;import cn.itcast.service.JobInfoService;import org.apache.lucene.analysis.standard.StandardAnalyzer;import org.apache.lucene.document.*;import org.apache.lucene.index.IndexWriter;import org.apache.lucene.index.IndexWriterConfig;import org.apache.lucene.store.FSDirectory;import org.apache.lucene.util.Version;import org.junit.Test;import org.junit.runner.RunWith;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.context.SpringBootTest;import org.springframework.test.context.junit4.SpringRunner;import java.io.File;import java.util.List;@RunWith(SpringRunner.class)@SpringBootTest(classes = LuceneApplication.class)public class LuceneTest { @Autowired private JobInfoService jobInfoService; @Test public void testFindAll() throws Exception{ List<JobInfo> list = jobInfoService.findAll(); System.out.println(list.size()); }/** * @author mryhl * 基于Lucene创建索引 */ @Test public void testCreateIndex() throws Exception { // 指定索引库目录 FSDirectory directory = FSDirectory.open(new File("E:\\project\\luceneindex\\java116")); // 索引写入对象的配置信息对象 // 指定分词器 StandardAnalyzer analyzer = new StandardAnalyzer(); IndexWriterConfig config = new IndexWriterConfig(Version.LATEST, analyzer); // 创建索引 写入对象 IndexWriter indexWriter = new IndexWriter(directory, config); // 准备原始数据(数据库中的数据) List<JobInfo> list = jobInfoService.findAll(); // 遍历原始数据,创建文档对象 for (JobInfo jobInfo : list) { // 创建文档对象 Document document = new Document(); //指定文档对象中存储的内容 document.add(new LongField("id",jobInfo.getId(), Field.Store.YES)); //TextField 代表字符串类型,并且是可以分次的字符串 document.add(new TextField("companyName",jobInfo.getCompanyName(), Field.Store.YES)); document.add(new TextField("companyAddr",jobInfo.getCompanyAddr(), Field.Store.YES)); document.add(new TextField("jobName",jobInfo.getJobName(), Field.Store.YES)); document.add(new TextField("jobAddr",jobInfo.getJobAddr(), Field.Store.YES)); document.add(new IntField("salary",jobInfo.getSalary(), Field.Store.YES)); //StringField 代表字符串类型,并且不分词 document.add(new StringField("url",jobInfo.getUrl(), Field.Store.YES)); //添加到索引库写对象 indexWriter.addDocument(document); } indexWriter.close(); }}
@Test public void testQuery() throws Exception{ //1、指定索引库目录 FSDirectory directory = FSDirectory.open(new File("E:\\project\\luceneindex\\java116")); //2、创建读取索引库对象 DirectoryReader reader = DirectoryReader.open(directory); //3、创建查询索引库对象 IndexSearcher searcher = new IndexSearcher(reader); //4、指定查询对象 TermQuery query = new TermQuery(new Term("companyName","北京")); //4、执行查询 参数一:查询对象 参数二:展示的数据条数 TopDocs topDocs = searcher.search(query, 10); log.debug("====mryhl==== 满足条件的数据个数"+topDocs.totalHits); //5、展示查询结果集 ScoreDoc[] scoreDocs = topDocs.scoreDocs; for (ScoreDoc scoreDoc : scoreDocs) { //获取文档id int docId = scoreDoc.doc; //获取文档对象 Document document = searcher.doc(docId); log.debug( "====mryhl==== id:"+ document.get("id")); log.debug( "====mryhl==== companyName:"+ document.get("companyName")); log.debug( "====mryhl==== companyAddr:"+ document.get("companyAddr")); log.debug( "====mryhl==== salary:"+ document.get("salary")); log.debug( "====mryhl==== url:"+ document.get("url")); } }
查看结果你会发现,居然没有数据,如果把查询的关键字“北京”那里改为“北”或“京”就可以,原因是因为中文会一个字一个字的分词,显然是不合适的,所以我们需要使用可以合理分词的分词器,其中最有名的是IKAnalyzer分词器
使用方式:
第一步:导依赖
<!--导入依赖,ik中文分词器--><dependency> <groupId>com.janeluo</groupId> <artifactId>ikanalyzer</artifactId> <version>2012_u6</version></dependency>
第二步:可以添加配置文件
放入到resources文件夹中。
加载扩展词和停用词:
扩展词:随着语言发展,产生的新词就是扩展词。 例如:奥利给
停用词:不能用于搜索的词条。 例如:语气助词 的 呢
第三步 创建索引时使用IKanalyzer
@Testpublic void testCreateIndex() throws Exception { // 指定索引库目录 FSDirectory directory = FSDirectory.open(new File("E:\\project\\luceneindex\\java116")); // 索引写入对象的配置信息对象 // 指定分词器 // StandardAnalyzer analyzer = new StandardAnalyzer(); IKAnalyzer analyzer = new IKAnalyzer(); IndexWriterConfig config = new IndexWriterConfig(Version.LATEST, analyzer); // 创建索引 写入对象 IndexWriter indexWriter = new IndexWriter(directory, config); // 删除数据 indexWriter.deleteAll(); // 准备原始数据(数据库中的数据) List<JobInfo> list = jobInfoService.findAll(); // 遍历原始数据,创建文档对象 for (JobInfo jobInfo : list) { // 创建文档对象 Document document = new Document(); //指定文档对象中存储的内容 document.add(new LongField("id",jobInfo.getId(), Field.Store.YES)); //TextField 代表字符串类型,并且是可以分次的字符串 document.add(new TextField("companyName",jobInfo.getCompanyName(), Field.Store.YES)); document.add(new TextField("companyAddr",jobInfo.getCompanyAddr(), Field.Store.YES)); document.add(new TextField("jobName",jobInfo.getJobName(), Field.Store.YES)); document.add(new TextField("jobAddr",jobInfo.getJobAddr(), Field.Store.YES)); document.add(new IntField("salary",jobInfo.getSalary(), Field.Store.YES)); //StringField 代表字符串类型,并且不分词 document.add(new StringField("url",jobInfo.getUrl(), Field.Store.YES)); //添加到索引库写对象 indexWriter.addDocument(document); } indexWriter.close();}
把原来的索引数据删除,再重新生成索引文件,再使用关键字“北京”就可以查询到结果了
考虑一个问题:一个大型网站中的索引数据会很庞大的,所以使用lucene这种原生的写代码的方式就不合适了,所以需要借助一个成熟的项目来实现,目前比较有名是solr和elasticSearch,我们今天学习elasticSearch的使用。
Elasticsearch是一个需要安装配置的软件。
Elastic官网:https://www.elastic.co/cn/
Elastic有一条完整的产品线:Elasticsearch、Logstash、Kibana等,前面说的三个就是大家常说的ELK技术栈。
Elasticsearch官网:https://www.elastic.co/cn/products/elasticsearch
如上所述,Elasticsearch具备以下特点:
目前Elasticsearch最新的版本是6.2.4,我们就使用的是2019年4月份的版本
需要JDK1.8及以上
总结:elasticsearch搜索服务器。是在Lucene技术的基础上进行封装, 完成==大数据==量数据搜索功能。
为了快速看到效果我们直接在本地window下安装Elasticsearch。环境要求:JDK8及以上版本
第一步:把今天资料文件夹中准备好的软件放到一个没有中文没有空格的位置,解压即可
第二步:修改配置文件
1、修改索引数据和日志数据存储的路径
第33行和37行,修改完记得把注释打开
# ----------------------------------- Paths ------------------------------------## Path to directory where to store the data (separate multiple locations by comma):#path.data: D:\develop\elasticsearch-6.2.4\data## Path to log files:#path.logs: D:\develop\elasticsearch-6.2.4\logs#
第三步:进入bin目录中直接双击 图下的命令文件。
如果启动失败(估计好多同学都会启动失败的),需要修改虚拟机内存的大小
找到jvm.options文件 如图修改
启动后台输出如下
可以看到绑定了两个端口:
9300:集群节点间通讯接口,接收tcp协议
9200:客户端访问接口,接收Http协议
我们在浏览器中访问:http://127.0.0.1:9200
自己安装一下谷歌elasticsearch head 插件
Kibana是一个基于Node.js的Elasticsearch索引库数据统计工具,可以利用Elasticsearch的聚合功能,生成各种图表,如柱形图,线状图,饼图等。
而且还提供了操作Elasticsearch索引数据的控制台,并且提供了一定的API提示,非常有利于我们学习Elasticsearch的语法。
因为Kibana依赖于node,需要在windows下先安装Node.js,双击运行课前资料提供的node.js的安装包:
一路下一步即可安装成功,然后在任意黑窗口输入名:
node -v
可以查看到node版本,如下:
然后安装kibana,最新版本与elasticsearch保持一致,也是6.2.4
解压即可:
配置
进入安装目录下的config目录,修改kibana.yml文件:
修改elasticsearch服务器的地址:
elasticsearch.url: "http://127.0.0.1:9200"
运行
进入安装目录下的bin目录:
双击运行:
发现kibana的监听端口是5601
选择左侧的DevTools菜单,即可进入控制台页面:
在页面右侧,我们就可以输入请求,访问Elasticsearch了。
Lucene的IK分词器早在2012年已经没有维护了,现在我们要使用的是在其基础上维护升级的版本,并且开发为Elasticsearch的集成插件了,与Elasticsearch一起维护升级,版本也保持一致,最新版本:6.2.4
https://github.com/medcl/elasticsearch-analysis-ik
大家先不管语法,我们先测试一波。
在kibana控制台输入下面的请求:
GET _analyze{ "analyzer": "ik_max_word", "text": "我爱傻丹丹,嘿嘿"}
运行得到结果:
{ "tokens": [ { "token": "我", "start_offset": 0, "end_offset": 1, "type": "CN_CHAR", "position": 0 }, { "token": "爱", "start_offset": 1, "end_offset": 2, "type": "CN_CHAR", "position": 1 }, { "token": "傻", "start_offset": 2, "end_offset": 3, "type": "CN_CHAR", "position": 2 }, { "token": "丹丹", "start_offset": 3, "end_offset": 5, "type": "CN_WORD", "position": 3 }, { "token": "嘿嘿", "start_offset": 6, "end_offset": 8, "type": "CN_WORD", "position": 4 } ]}
Elasticsearch提供了Rest风格的API,即http请求接口,而且也提供了各种语言的客户端API
文档地址:https://www.elastic.co/guide/en/elasticsearch/reference/current/getting-started.html
从官网中查找方式如下:
Elasticsearch支持的客户端非常多:https://www.elastic.co/guide/en/elasticsearch/client/index.html
点击Java Rest Client后,你会发现又有两个:
Low Level Rest Client是低级别封装,提供一些基础功能,但更灵活
High Level Rest Client,是在Low Level Rest Client基础上进行的高级别封装,功能更丰富和完善,而且API会变的简单
建议先学习Rest风格API,了解发起请求的底层实现,请求体格式等。
Elasticsearch也是基于Lucene的全文检索库,本质也是存储数据,很多概念与MySQL类似的。
对比关系:
索引库(indices)---------------------------------Database 数据库 类型(type)----------------------------------Table 数据表 文档(Document)---------------------------------Row 行 域字段(Field)--------------------------------Columns 列 映射配置(mappings)-------------------------------每个列的约束(类型、长度)
详细说明:
概念 | 说明 |
---|---|
索引库(indices) | indices是index的复数,代表许多的索引, |
类型(type) | 类型是模拟mysql中的table概念,一个索引库下可以有不同类型的索引,类似数据库中的表概念。数据库表中有表结构,也就是表中每个字段的约束信息;索引库的类型中对应表结构的叫做映射(mapping) ,用来定义每个字段的约束。 |
文档(document) | 存入索引库原始的数据。比如每一条商品信息,就是一个文档 |
字段(field) | 文档中的属性 |
映射配置(mappings) | 字段的数据类型、属性、是否索引、是否存储等特性 |
Elasticsearch采用Rest风格API,因此其API就是一次http请求,你可以用任何工具发起http请求
请求方式:PUT
请求路径:/索引库名
请求参数:json格式:
{ "settings": { "属性名": "属性值" }}
settings:就是索引库设置,其中可以定义索引库的各种属性,目前我们可以不设置,都走默认。
kibana的控制台,可以对http请求进行简化,示例:
相当于是省去了elasticsearch的服务器地址
而且还有语法提示,非常舒服。
语法
Get请求可以帮我们查看索引信息,格式:
GET /索引库名
删除索引使用DELETE请求
语法
DELETE /索引库名
再次查看mryhl:
当然,我们也可以用HEAD请求,查看索引是否存在:
有了索引库
,等于有了数据库中的database
。接下来就需要索引库中的类型
了,也就是数据库中的表
。创建数据库表需要设置字段约束,索引库也一样,在创建索引库的类型时,需要知道这个类型下有哪些字段,每个字段有哪些约束信息,这就叫做字段映射(mapping)
字段的约束我们在学习Lucene中我们都见到过,包括到不限于:
我们一起来看下创建的语法。
语法
请求方式依然是PUT
PUT /索引库名/_mapping/类型名称{ "properties": { "字段名": { "type": "类型", "index": true, "store": true, "analyzer": "分词器" } }}
类型名称:就是前面将的type的概念,类似于数据库中的表
字段名:任意填写,下面指定许多属性,例如:
type:类型,可以是text、keyword、long、short、date、integer、object等
text:字符串类型,可以分词
keyword:字符串类型,不分词
index:是否索引,默认为true
store:是否存储,默认为false
analyzer:分词器,这里的ik_max_word
即使用ik分词器
示例
发起请求:
PUT mryhlPUT mryhl/_mapping/goods{ "properties": { "title": { "type": "text", "analyzer": "ik_max_word" }, "images": { "type": "keyword", "index": false }, "price": { "type": "float" } }}
注意:需要先建立索引库,在建映射类型
响应结果:
{ "acknowledged": true}
上述案例中,就给heima这个索引库添加了一个名为goods
的类型,并且在类型中设置了3个字段:
并且给这些字段设置了一些属性,至于这些属性对应的含义,我们在后续会详细介绍。
语法:
GET /索引库名/_mapping
查看某个索引库中的所有类型的映射。如果要查看某个类型映射,可以再路径后面跟上类型名称。即:
GET /索引库名/_mapping/映射名
示例:
# select mryhl/mappingGET /mryhl/_mapping
响应:
{ "mryhl": { "mappings": { "goods": { "properties": { "images": { "type": "keyword", "index": false }, "price": { "type": "float" }, "title": { "type": "text", "analyzer": "ik_max_word" } } } } }}
Elasticsearch中支持的数据类型非常丰富:
我们说几个关键的:
String类型,又分两种:
Numerical:数值类型,分两类
Date:日期类型
elasticsearch可以对日期格式化为字符串存储,但是建议我们存储为毫秒值,存储为long,节省空间。
Array:数组类型
Object:对象
{ name:"Jack", age:21, girl:{ name: "Rose", age:21 }}
如果存储到索引库的是对象类型,例如上面的girl,会把girl编程两个字段:girl.name和girl.age
index影响字段的索引情况。
index的默认值就是true,也就是说你不进行任何配置,所有字段都会被索引。
但是有些字段是我们不希望被索引的,比如商品的图片信息,就需要手动设置index为false。
是否将数据进行额外存储。
在学习lucene时,我们知道如果一个字段的store设置为false,那么在文档列表中就不会有这个字段的值,用户的搜索结果中不会显示出来。
但是在Elasticsearch中,即便store设置为false,也可以搜索到结果。
原因是Elasticsearch在创建文档索引时,会将文档中的原始数据备份,保存到一个叫做_source
的属性中。而且我们可以通过过滤_source
来选择哪些要显示,哪些不显示。
而如果设置store为true,就会在_source
以外额外存储一份数据,多余,因此一般我们都会将store设置为false,事实上,store的默认值就是false。
刚才 的案例中我们是把创建索引库和类型分开来做,其实也可以在创建索引库的同时,直接制定索引库中的类型,基本语法:
put /索引库名{ "settings":{ "索引库属性名":"索引库属性值" }, "mappings":{ "类型名":{ "properties":{ "字段名":{ "映射属性名":"映射属性值" } } } }}
来试一下吧:
PUT mryhl2{ "settings": {}, "mappings": { "goods":{ "properties": { "images":{ "type": "keyword", "index": false }, "price":{ "type": "float" }, "title":{ "type": "text", "analyzer": "ik_max_word" } } } }}
结果:
{ "acknowledged": true, "shards_acknowledged": true, "index": "mryhl2"}
然后我们查看下映射:
文档,即索引库中某个类型下的数据,会根据规则创建索引,将来用来搜索。可以类比做数据库中的每一行数据。
通过POST请求,可以向一个已经存在的索引库中添加文档数据。
语法:
POST /索引库名/类型名{ "key":"value"}
示例:
POST /mryhl/goods{ "title":"huawei手机", "images":"http://image.leyou.com/12479122.jpg", "price":2699.00}
响应:
{ "_index": "mryhl", "_type": "goods", "_id": "ndWxnXUBkrLBtf3LN9ip", "_version": 1, "result": "created", "_shards": { "total": 2, "successful": 1, "failed": 0 }, "_seq_no": 0, "_primary_term": 1}
可以看到结果显示为:created
,应该是创建成功了。
另外,需要注意的是,在响应结果中有个_id
字段,这个就是这条文档数据的唯一标示
,以后的增删改查都依赖这个id作为唯一标示。
可以看到id的值为:ndWxnXUBkrLBtf3LN9ip
,这里我们新增时没有指定id,所以是ES帮我们随机生成的id。
根据rest风格,新增是post,查询应该是get,不过查询一般都需要条件,这里我们把刚刚生成数据的id带上。
通过kibana查看数据:
GET mryhl/goods/ndWxnXUBkrLBtf3LN9ip
查看结果:
{ "_index": "mryhl", "_type": "goods", "_id": "ndWxnXUBkrLBtf3LN9ip", "_version": 1, "found": true, "_source": { "title": "huawei手机", "images": "http://image.leyou.com/12479122.jpg", "price": 2699 }}
_source
:源文档信息,所有的数据都在里面。_id
:这条文档的唯一标示如果我们想要自己新增的时候指定id,可以这么做:
POST /索引库名/类型/id值{ ...}
示例:
POST mryhl/goods/001{ "title":"HONOR手机", "images":"http://image.leyou.com/12479122.jpg", "price":3699.00}
得到的数据:
{ "_index": "mryhl", "_type": "goods", "_id": "001", "_version": 1, "result": "created", "_shards": { "total": 2, "successful": 1, "failed": 0 }, "_seq_no": 0, "_primary_term": 1}
把刚才新增的请求方式改为PUT,就是修改了。不过修改必须指定id,
比如,我们把使用id为3,不存在,则应该是新增:
PUT mryhl/goods/002{ "title":"HONOR手机", "images":"http://image.leyou.com/12479122.jpg", "price":3699.00}
结果:
{ "_index": "mryhl", "_type": "goods", "_id": "002", "_version": 1, "result": "created", "_shards": { "total": 2, "successful": 1, "failed": 0 }, "_seq_no": 1, "_primary_term": 1}
可以看到是created
,是新增。
我们再次执行刚才的请求,不过把数据改一下:
PUT mryhl/goods/001{ "title":"HUAWEI手机", "images":"http://image.leyou.com/12479122.jpg", "price":3699.00}
查看结果:
{ "_index": "mryhl", "_type": "goods", "_id": "001", "_version": 2, "result": "updated", "_shards": { "total": 2, "successful": 1, "failed": 0 }, "_seq_no": 2, "_primary_term": 1}
可以看到结果是:updated
,显然是更新数据
删除使用DELETE请求,同样,需要根据id进行删除:
语法
DELETE /索引库名/类型名/id值
示例:
刚刚我们在新增数据时,添加的字段都是提前在类型中定义过的,如果我们添加的字段并没有提前定义过,能够成功吗?
事实上Elasticsearch非常智能,你不需要给索引库设置任何mapping映射,它也可以根据你输入的数据来判断类型,动态添加数据映射。
测试一下:
POST /mryhl/goods/3{ "title":"HUAWEI MATE XS", "images":"http://image.leyou.com/12479122.jpg", "price":3299.00, "stock": 200, "saleable":true, "subTitle":"折叠屏"}
我们额外添加了stock库存,saleable是否上架,subtitle副标题、3个字段。
来看结果:
{ "_index": "mryhl", "_type": "goods", "_id": "3", "_version": 1, "result": "created", "_shards": { "total": 2, "successful": 1, "failed": 0 }, "_seq_no": 4, "_primary_term": 1}
成功了!在看下索引库的映射关系:
GET /mryhl/goods/_mapping
{ "mryhl": { "mappings": { "goods": { "properties": { "images": { "type": "keyword", "index": false }, "price": { "type": "float" }, "saleable": { "type": "boolean" }, "stock": { "type": "long" }, "subTitle": { "type": "text", "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } } }, "title": { "type": "text", "analyzer": "ik_max_word" } } } } }}
stock、saleable、subtitle都被成功映射了。
subtitle是String类型数据,ES无法智能判断,它就会存入两个字段。例如:
这种智能映射,底层原理是动态模板映射,如果我们想修改这种智能映射的规则,其实只要修改动态模板即可!
动态模板的语法:
1)模板名称,随便起
2)匹配条件,凡是符合条件的未定义字段,都会按照这个规则来映射
3)映射规则,匹配成功后的映射规则
举例,我们可以把所有未映射的string类型数据自动映射为keyword类型:
PUT mryhl3{ "mappings": { "goods": { "properties": { "title": { "type": "text", "analyzer": "ik_max_word" } }, "dynamic_templates": [ { "strings": { "match_mapping_type": "string", "mapping": { "type": "keyword" } } } ] } }}// 结果{ "acknowledged": true, "shards_acknowledged": true, "index": "mryhl3"}
在这个案例中,我们把做了两个映射配置:
这样,未知的string类型数据就不会被映射为text和keyword并存,而是统一以keyword来处理!
我们试试看新增一个数据:
POST /mryhl/goods/001{ "title":"HUAWEI手机", "images":"http://image.leyou.com/12479122.jpg", "price":3299.00}
我们只对title做了配置,现在来看看images和price会被映射为什么类型呢:
GET /mryhl3/_mapping
结果:
{ "mryhl3": { "mappings": { "goods": { "dynamic_templates": [ { "strings": { "match_mapping_type": "string", "mapping": { "type": "keyword" } } } ], "properties": { "title": { "type": "text", "analyzer": "ik_max_word" } } } } }}
可以看到images被映射成了keyword,而非之前的text和keyword并存,说明我们的动态模板生效了!
我们从4块来讲查询:
_source
过滤基本语法
GET /索引库名/_search{ "query":{ "查询类型":{ "查询条件":"查询条件值" } }}
这里的query代表一个查询对象,里面可以有不同的查询属性
match_all
, match
,term
, range
等等示例:
GET /mryhl/_search{ "query": { "match_all": {} }}
query
:代表查询对象
match_all
:代表查询所有
结果:
{ "took": 2, "timed_out": false, "_shards": { "total": 5, "successful": 5, "skipped": 0, "failed": 0 }, "hits": { "total": 3, "max_score": 1, "hits": [ { "_index": "mryhl", "_type": "goods", "_id": "ndWxnXUBkrLBtf3LN9ip", "_score": 1, "_source": { "title": "huawei手机", "images": "http://image.leyou.com/12479122.jpg", "price": 2699 } }, { "_index": "mryhl", "_type": "goods", "_id": "3", "_score": 1, "_source": { "title": "HUAWEI MATE XS", "images": "http://image.leyou.com/12479122.jpg", "price": 3299, "stock": 200, "saleable": true, "subTitle": "折叠屏" } }, { "_index": "mryhl", "_type": "goods", "_id": "001", "_score": 1, "_source": { "title": "HUAWEI手机", "images": "http://image.leyou.com/12479122.jpg", "price": 3299 } } ] }}
我们先加入一条数据,便于测试:
PUT /mryhl/goods/3{ "title":"荣耀智慧屏", "images":"http://image.leyou.com/12479122.jpg", "price":3899.00}
现在,索引库中有2部手机,1台电视:
match
类型查询,会把查询条件进行分词,然后进行查询,多个词条之间是or的关系
GET mryhl/_search{ "query": { "match": { "title": "荣耀手机" } }}
结果:
在上面的案例中,不仅会查询到电视,而且与小米相关的都会查询到,多个词之间是or
的关系。
某些情况下,我们需要更精确查找,我们希望这个关系变成and
,可以这样做:
GET mryhl/_search{ "query": { "match": { "title": { "query": "荣耀手机", "operator": "and" } } }}
结果:
本例中,只有同时包含小米
和电视
的词条才会被搜索到。
term
查询被用于精确值 匹配,这些精确值可能是数字、时间、布尔或者那些未分词的字符串
GET mryhl/_search{ "query": { "term": { "title": { "value": "荣耀" } } }}
结果:
{ "took": 1, "timed_out": false, "_shards": { "total": 5, "successful": 5, "skipped": 0, "failed": 0 }, "hits": { "total": 2, "max_score": 0.6931472, "hits": [ { "_index": "mryhl", "_type": "goods", "_id": "4", "_score": 0.6931472, "_source": { "title": "荣耀手机", "images": "http://image.leyou.com/12479122.jpg", "price": 3899 } }, { "_index": "mryhl", "_type": "goods", "_id": "3", "_score": 0.64072424, "_source": { "title": "荣耀智慧屏", "images": "http://image.leyou.com/12479122.jpg", "price": 3899 } } ] }}
bool
把各种其它查询通过must
(与)、must_not
(非)、should
(或)的方式进行组合
GET mryhl/_search{ "query": { "bool": { "must":{ "match": { "title": "荣耀" }}, "must_not": { "match": { "title": "电视" }}, "should": { "match": { "title": "手机" }} } }}
结果:
{ "took": 1, "timed_out": false, "_shards": { "total": 5, "successful": 5, "skipped": 0, "failed": 0 }, "hits": { "total": 2, "max_score": 0.87546873, "hits": [ { "_index": "mryhl", "_type": "goods", "_id": "4", "_score": 0.87546873, "_source": { "title": "荣耀手机", "images": "http://image.leyou.com/12479122.jpg", "price": 3899 } }, { "_index": "mryhl", "_type": "goods", "_id": "3", "_score": 0.64072424, "_source": { "title": "荣耀智慧屏", "images": "http://image.leyou.com/12479122.jpg", "price": 3899 } } ] }}
range
查询找出那些落在指定区间内的数字或者时间
GET mryhl/_search{ "query": { "range": { "price": { "gte": 2000, "lte": 3000 } } }}
range
查询允许以下字符:
操作符 | 说明 |
---|---|
gt | 大于 |
gte | 大于等于 |
lt | 小于 |
lte | 小于等于 |
{ "took": 18, "timed_out": false, "_shards": { "total": 5, "successful": 5, "skipped": 0, "failed": 0 }, "hits": { "total": 1, "max_score": 1, "hits": [ { "_index": "mryhl", "_type": "goods", "_id": "ndWxnXUBkrLBtf3LN9ip", "_score": 1, "_source": { "title": "huawei手机", "images": "http://image.leyou.com/12479122.jpg", "price": 2699 } } ] }}
默认情况下,elasticsearch在搜索的结果中,会把文档中保存在_source
的所有字段都返回。
如果我们只想获取其中的部分字段,我们可以添加_source
的过滤
示例:
GET mryhl/_search{ "query": { "match_all": {} }, "_source": ["title","price"]}
返回的结果:
{ "took": 15, "timed_out": false, "_shards": { "total": 5, "successful": 5, "skipped": 0, "failed": 0 }, "hits": { "total": 4, "max_score": 1, "hits": [ { "_index": "mryhl", "_type": "goods", "_id": "ndWxnXUBkrLBtf3LN9ip", "_score": 1, "_source": { "price": 2699, "title": "huawei手机" } }, { "_index": "mryhl", "_type": "goods", "_id": "4", "_score": 1, "_source": { "price": 3899, "title": "荣耀手机" } }, { "_index": "mryhl", "_type": "goods", "_id": "001", "_score": 1, "_source": { "price": 3299, "title": "HUAWEI手机" } }, { "_index": "mryhl", "_type": "goods", "_id": "3", "_score": 1, "_source": { "price": 3899, "title": "荣耀智慧屏" } } ] }}
我们也可以通过:
二者都是可选的。
示例:
GET mryhl/_search{ "query": { "match_all": {} }, "_source": { "includes": ["title","price"] }}
与下面的结果将是一样的:
{ "took": 1, "timed_out": false, "_shards": { "total": 5, "successful": 5, "skipped": 0, "failed": 0 }, "hits": { "total": 4, "max_score": 1, "hits": [ { "_index": "mryhl", "_type": "goods", "_id": "ndWxnXUBkrLBtf3LN9ip", "_score": 1, "_source": { "price": 2699, "title": "huawei手机" } }, { "_index": "mryhl", "_type": "goods", "_id": "4", "_score": 1, "_source": { "price": 3899, "title": "荣耀手机" } }, { "_index": "mryhl", "_type": "goods", "_id": "001", "_score": 1, "_source": { "price": 3299, "title": "HUAWEI手机" } }, { "_index": "mryhl", "_type": "goods", "_id": "3", "_score": 1, "_source": { "price": 3899, "title": "荣耀智慧屏" } } ] }}
条件查询中进行过滤
所有的查询都会影响到文档的评分及排名。如果我们需要在查询结果中进行过滤,并且不希望过滤条件影响评分,那么就不要把过滤条件作为查询条件来用。而是使用filter
方式:
GET mryhl/_search{ "query": { "bool": { "must":{ "match": { "title": "荣耀手机" }}, "filter":{ "range":{"price":{"gt":2000.00,"lt":3800.00}} } } }}
{ "took": 17, "timed_out": false, "_shards": { "total": 5, "successful": 5, "skipped": 0, "failed": 0 }, "hits": { "total": 2, "max_score": 0.7549128, "hits": [ { "_index": "mryhl", "_type": "goods", "_id": "001", "_score": 0.7549128, "_source": { "title": "HUAWEI手机", "images": "http://image.leyou.com/12479122.jpg", "price": 3299 } }, { "_index": "mryhl", "_type": "goods", "_id": "ndWxnXUBkrLBtf3LN9ip", "_score": 0.18232156, "_source": { "title": "huawei手机", "images": "http://image.leyou.com/12479122.jpg", "price": 2699 } } ] }}
sort
可以让我们按照不同的字段进行排序,并且通过order
指定排序的方式
GET mryhl/_search{ "query": { "match_all": {} }, "sort": [ { "price": { "order": "desc" } } ]}
{ "took": 22, "timed_out": false, "_shards": { "total": 5, "successful": 5, "skipped": 0, "failed": 0 }, "hits": { "total": 4, "max_score": null, "hits": [ { "_index": "mryhl", "_type": "goods", "_id": "4", "_score": null, "_source": { "title": "荣耀手机", "images": "http://image.leyou.com/12479122.jpg", "price": 3899 }, "sort": [ 3899 ] }, { "_index": "mryhl", "_type": "goods", "_id": "3", "_score": null, "_source": { "title": "荣耀智慧屏", "images": "http://image.leyou.com/12479122.jpg", "price": 3899 }, "sort": [ 3899 ] }, { "_index": "mryhl", "_type": "goods", "_id": "001", "_score": null, "_source": { "title": "HUAWEI手机", "images": "http://image.leyou.com/12479122.jpg", "price": 3299 }, "sort": [ 3299 ] }, { "_index": "mryhl", "_type": "goods", "_id": "ndWxnXUBkrLBtf3LN9ip", "_score": null, "_source": { "title": "huawei手机", "images": "http://image.leyou.com/12479122.jpg", "price": 2699 }, "sort": [ 2699 ] } ] }}
假定我们想要结合使用 price和 _score(得分) 进行查询,并且匹配的结果首先按照价格排序,然后按照相关性得分排序:
GET /mryhl/_search{ "query":{ "bool":{ "must":{ "match": { "title": "荣耀手机" }}, "filter":{ "range":{"price":{"gt":2000,"lt":3000}} } } }, "sort": [ { "price": { "order": "desc" }}, { "_score": { "order": "desc" }} ]}
结果
{ "took": 13, "timed_out": false, "_shards": { "total": 5, "successful": 5, "skipped": 0, "failed": 0 }, "hits": { "total": 1, "max_score": null, "hits": [ { "_index": "mryhl", "_type": "goods", "_id": "ndWxnXUBkrLBtf3LN9ip", "_score": 0.18232156, "_source": { "title": "huawei手机", "images": "http://image.leyou.com/12479122.jpg", "price": 2699 }, "sort": [ 2699, 0.18232156 ] } ] }}
elasticsearch的分页与mysql数据库非常相似,都是指定两个值:
GET /mryhl/_search{ "query": { "match_all": {} }, "sort": [ { "price": { "order": "asc" } } ], "from": 0, "size": 2}
结果:
{ "took": 0, "timed_out": false, "_shards": { "total": 5, "successful": 5, "skipped": 0, "failed": 0 }, "hits": { "total": 4, "max_score": null, "hits": [ { "_index": "mryhl", "_type": "goods", "_id": "ndWxnXUBkrLBtf3LN9ip", "_score": null, "_source": { "title": "huawei手机", "images": "http://image.leyou.com/12479122.jpg", "price": 2699 }, "sort": [ 2699 ] }, { "_index": "mryhl", "_type": "goods", "_id": "001", "_score": null, "_source": { "title": "HUAWEI手机", "images": "http://image.leyou.com/12479122.jpg", "price": 3299 }, "sort": [ 3299 ] } ] }}
高亮原理:
elasticsearch中实现高亮的语法比较简单:
GET /mryhl/_search{ "query": { "match": { "title": "荣耀" } }, "highlight": { "pre_tags": "<font color='red'>", "post_tags": "</font>", "fields": { "title": {} } }}
在使用match查询的同时,加上一个highlight属性:
结果:
{ "took": 52, "timed_out": false, "_shards": { "total": 5, "successful": 5, "skipped": 0, "failed": 0 }, "hits": { "total": 2, "max_score": 0.6931472, "hits": [ { "_index": "mryhl", "_type": "goods", "_id": "4", "_score": 0.6931472, "_source": { "title": "荣耀手机", "images": "http://image.leyou.com/12479122.jpg", "price": 3899 }, "highlight": { "title": [ "<font color='red'>荣耀</font>手机" ] } }, { "_index": "mryhl", "_type": "goods", "_id": "3", "_score": 0.64072424, "_source": { "title": "荣耀智慧屏", "images": "http://image.leyou.com/12479122.jpg", "price": 3899 }, "highlight": { "title": [ "<font color='red'>荣耀</font>智慧屏" ] } } ] }}
mysql聚合函数 一般都是和分组配合使用的 所以mysql中聚合函数又叫组函数 group by
聚合可以让我们极其方便的实现对数据的统计、分析。例如:
实现这些统计功能的比数据库的sql要方便的多,而且查询速度非常快,可以实现近实时搜索效果。
Elasticsearch中的聚合,包含多种类型,最常用的两种,一个叫桶
,一个叫度量
:
*桶(bucket) * 类似于 group by
桶的作用,是按照某种方式对数据进行分组,每一组数据在ES中称为一个桶
,例如我们根据国籍对人划分,可以得到中国桶
、英国桶
,日本桶
……或者我们按照年龄段对人进行划分:010,1020,2030,3040等。
Elasticsearch中提供的划分桶的方式有很多:
综上所述,我们发现bucket aggregations 只负责对数据进行分组,并不进行计算,因此往往bucket中往往会嵌套另一种聚合:metrics aggregations即度量
度量(metrics) 相当于聚合的结果
分组完成以后,我们一般会对组中的数据进行聚合运算,例如求平均值、最大、最小、求和等,这些在ES中称为度量
比较常用的一些度量聚合方式:
为了测试聚合,我们先批量导入一些数据
创建索引:
PUT /car{ "mappings": { "orders": { "properties": { "color": { "type": "keyword" }, "make": { "type": "keyword" } } } }}
注意:在ES中,需要进行聚合、排序、过滤的字段其处理方式比较特殊,因此不能被分词,必须使用keyword
或数值类型
。这里我们将color和make这两个文字类型的字段设置为keyword类型,这个类型不会被分词,将来就可以参与聚合
导入数据,这里是采用批处理的API,大家直接复制到kibana运行即可:
POST /car/orders/_bulk{ "index": {}}{ "price" : 10000, "color" : "红", "make" : "本田", "sold" : "2014-10-28" }{ "index": {}}{ "price" : 20000, "color" : "红", "make" : "本田", "sold" : "2014-11-05" }{ "index": {}}{ "price" : 30000, "color" : "绿", "make" : "福特", "sold" : "2014-05-18" }{ "index": {}}{ "price" : 15000, "color" : "蓝", "make" : "丰田", "sold" : "2014-07-02" }{ "index": {}}{ "price" : 12000, "color" : "绿", "make" : "丰田", "sold" : "2014-08-19" }{ "index": {}}{ "price" : 20000, "color" : "红", "make" : "本田", "sold" : "2014-11-05" }{ "index": {}}{ "price" : 80000, "color" : "红", "make" : "宝马", "sold" : "2014-01-01" }{ "index": {}}{ "price" : 25000, "color" : "蓝", "make" : "福特", "sold" : "2014-02-12" }
首先,我们按照 汽车的颜色color来
划分桶
,按照颜色分桶,最好是使用TermAggregation类型,按照颜色的名称来分桶。
GET car/_search{ "query": { "match_all": {} }, "size": 0, "aggs": { "popular_colors": { "terms": { "field": "color", "size": 10 } } }}
结果:
{ "took": 16, "timed_out": false, "_shards": { "total": 5, "successful": 5, "skipped": 0, "failed": 0 }, "hits": { "total": 8, "max_score": 0, "hits": [] }, "aggregations": { "popular_colors": { "doc_count_error_upper_bound": 0, "sum_other_doc_count": 0, "buckets": [ { "key": "红", "doc_count": 4 }, { "key": "绿", "doc_count": 2 }, { "key": "蓝", "doc_count": 2 } ] } }}
通过聚合的结果我们发现,目前红色的小车比较畅销!
前面的例子告诉我们每个桶里面的文档数量,这很有用。 但通常,我们的应用需要提供更复杂的文档度量。 例如,每种颜色汽车的平均价格是多少?
因此,我们需要告诉Elasticsearch使用哪个字段
,使用何种度量方式
进行运算,这些信息要嵌套在桶
内,度量
的运算会基于桶
内的文档进行
现在,我们为刚刚的聚合结果添加 求价格平均值的度量:
GET car/_search{ "query": { "match_all": {} }, "size": 0, "aggs": { "popular_colors": { "terms": { "field": "color", "size": 10 }, "aggs": { "avg_price": { "avg": { "field": "price" } } } } }}
结果:
{ "took": 7, "timed_out": false, "_shards": { "total": 5, "successful": 5, "skipped": 0, "failed": 0 }, "hits": { "total": 8, "max_score": 0, "hits": [] }, "aggregations": { "popular_colors": { "doc_count_error_upper_bound": 0, "sum_other_doc_count": 0, "buckets": [ { "key": "红", "doc_count": 4, "avg_price": { "value": 32500 } }, { "key": "绿", "doc_count": 2, "avg_price": { "value": 21000 } }, { "key": "蓝", "doc_count": 2, "avg_price": { "value": 20000 } } ] } }}
可以看到每个桶中都有自己的avg_price
字段,这是度量聚合的结果
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。