一、ElasticSearch是什么
先看下官网的解释:
Elasticsearch 是一个分布式的、开源的搜索分析引擎,支持各种数据类型,包括文本、数字、地理、结构化、非结构化。 Elasticsearch 是基于 Apache Lucene 的。 Elasticsearch 因其简单的 REST API、分布式特性、告诉、可扩展而闻名。 Elasticsearch 是 Elastic 产品栈的核心,Elastic 产品栈是个开源工具集合,用于数据接收、存储、分析、可视化。
废话一堆,我总结一下:
- 1、
elasticsearch 是一个免费、开源的软件 - 2、
elasticsearch 是一个数据库 - 3、
elasticsearch 是一个搜索引擎 - 4、
elasticsearch 是一个后台服务,使用简单的REST API 调用,提供丰富多样的数据处理能力
二、为什么前端推荐使用ElasticSearch
快速上手
作为前端开发,我们都知道,要开发应用,数据服务是我们永远依赖后台的地方。
对于接口层,自己去写java ,学springboot ,学structs 从0开始学习似乎不太现实。在这个快节奏的软件开发时代,快速学习,快速上手,快速做应用才是王道。试想一下,让你1天之内搭建一个服务器+数据库,如果没有一定的基础,你可以做到吗?我敢说,就连熟悉java 开发的后台同事也不能保证。
所以,ElasticSearch 来了,只需要官网下载一个包,本地使用bat 服务启动,一个现成的后台服务+数据库就完成了。
API适配
传统前端BS 架构,基本都是以Rest 接口方式与后台交互。
fetch("https://leetcode-solution.cn", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
question_id: "1",
lang: "javascript",
code: "console.log(1)",
}),
}).then((res) => {
console.log(res);
});
ElasticSearch ,完全遵循Rest 接口规范,一个根据学生ID查询学生的例子:
fetch("https:192.168.0.1:9200/class/students", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
"query": {
"match_phrase": {
"studentId.keyword": "12345"
}
}
}),
}).then((res) => {
console.log(res);
});
可见。我们只需要修改Body 体内的查询语法,就可以做到不需要后台接口封装,直接调用,对于简单的curl应用,甚至可以做到直接替换。
三、如何使用ElasticSearch
安装和环境
官网下载elasticSearch 和kibana (可视化工具),进入bin 目录,直接执行批处理脚本即可。本文不再赘述。本文es版本和安装地址如下:
如果windows 下安装有问题,可以参考我的这篇博客
索引&类型&文档
大家如果用过sql 。那么index (索引)就像sql 中的库,type (类型)就像sql 中的表,document (文档)就像sql 中的记录。
传统的一个学生表,是如下对应结构:
- 学生表 –
student - 姓名、学号、性别、爱好 –
document
可是es用index+type 表达上述的Table 概念,应该如下数据结构:
- 学生表 –
class/student - 姓名、学号、性别、爱好 –
document
是不是有点混淆?
没错,elasticSearch 这样设计的初衷是为了提高集群管理的效率和能力,但是对于我们以传统表+记录的模式理解来说,是有点牵强。
所以,elasticSearch也做出了改进,在7.0.0开始,正式废除type。
我们今天使用的版本是6.5.4 为了不混淆大家,我们统一约定,一个索引只能有一个类型,索引和类型取一样的名称,后续我们的索引指的就是表。对应关系如下
- 学生表 –
student/student - 姓名、学号、性别、爱好 –
document
四、CURL索引:
新增索引:
纯新增
被动新增
es会在很多情况下为我们新增索引。在系统中没有student表的情况下,执行例如这样的代码
PUT/students/students/1
{
}
注意。上面的代码,会创建一个id为1,但是属性为空的学生。由于没有students表,所以,es会自动为我们创建一个表,并向里面添加一个空文档:
我们查询一下这个id为1的学生:
GET /students/students/_search
{
"query": {
"match_all": {}
}
}
具体的语法我们可以后面再看。这个语法是查询表中所有的文档: 可以看到我们的id为1的学生创建完成了。可是_source字段中没有属性。
表需要有字段,我怎么表达表的字段?例如学生有姓名、年龄等字段。这时候我们就需要引入mapping的概念:
查询表结构:
GET /students/students/_mapping
返回如下:
我的students索引中包含students类型,文档为空,所以字段也为空。接下来我们尝试再添加一个有属性的学生:
POST /students/students/2
{
"age":18,
"name":"李云迪",
"birthday":"1992-09-08"
}
ok。我们继续查所有的学生: 这时候记录就有了。我们来看下mapping如果变化: 我们的students索引自动创建了3个属性:age、birthday、name属性,并且自动推断了类型!
所以mapping默认是可以动态拓展的,新增一条记录,es就会根据你的记录动态添加表字段。
所以,如果我想创建一张属性固定不变的索引,就必须在创建的时候指定mapping:
创建包含age和birthday的techers表:
PUT /teachers/teachers/mapping
{
"teachers" : {
"mappings" : {
"dynamic" : "strict",
"teachers" : {
"properties" : {
"age" : {
"type" : "long"
},
"birthday" : {
"type" : "date"
}
}
}
}
}
}
注意 "dynamic" : "strict" 这个表示我的属性禁止拓展,这时候我们向techers表中添加如下记录:
POST /teachers/techers
{
"name":"李云迪"
}
自然是放不进去:
mapping支持的类型
Elasticsearch 支持如下简单域类型:
- 字符串:
string - 整数 :
byte , short , integer , long - 浮点数:
float ,double - 布尔型:
boolean - 日期:
date
还有复杂类型:
binary 二进制array 数组类型range datatype 数据范围类型,一个字段表示一个范围
integer_range float_range double_range *date_range - i
p_range Nested datatype 嵌套数据类型,用于关联查询。Geo datatypes 地图数据类型。geo_point 地图坐标;存储经纬度。geo_shape datatype 数据类型方便了对任意地理形状(如矩形和多边形)进行索引和搜索。当正在索引的数据或正在执行的查询包含除了点以外的形状时应该使用它。
修改索引:
修改索引,在索引中存在数据时会比较复杂,我们这里暂时不讨论。
索引不存在文档
这种情况下随便改。都没问题
PUT /teachers/teachers/mapping
{
"teachers" : {
"mappings" : {
"dynamic" : "strict",
"teachers" : {
"properties" : {
"age" : {
"type" : "long"
},
"birthday" : {
"type" : "date"
}
}
}
}
}
}
索引存在文档
这种情况下,如果修改已存在字段会被拒绝。操作比较复杂,方法后面会讨论。
查询所有索引:
GET /_cat/indices
返回:
删除索引:
DELETE /teachers/
就是这么简单
返回:
五、CURL文档
创建文档
刚才一节已经说明了创建文档的语法:
POST /students/students/2
{
"age":18,
"name":"李云迪",
"birthday":"1992-09-08"
}
在studetents后面的2 表示的就是文档的唯一id。我也可以不带这个Id,这时候es会为我们随机生成一个:
POST /students/students/
{
"age":18,
"name":"李云迪2",
"birthday":"1992-09-08"
}
返回:
修改文档
我需要把李云迪的年龄改为38:
POST /students/students/1
{
"age":38,
"name":"李云迪",
"birthday":"1992-09-08"
}
1 为第一条记录的id,注意,要带上所有的属性。
删除文档:
DELETE /students/students/1
这样就行了。
查询文档
查询文档我们会在下面作为重点讲解。
六、文档的查询
按照查询语法分类
传统文档的查询,我们一般是通过SQL语句,最常见的如下:
Select * from students
而我们的查询,是以服务形式的查询,与数据库完全不同。ElasticSearch按照查询语法种类,分为轻量查询和表达式查询。
轻量查询的例子
GET /employee/employee/_search?q=last_name:Smith
可以在请求url中评价查询语法。但是这种方式受到url拼接的限制,只能进行比较简单的查询。
表达式查询的例子
Query-string 搜索通过命令非常方便地进行临时性的即席搜索 ,但它有自身的局限性(参见 轻量 搜索 )。Elasticsearch 提供一个丰富灵活的查询语言叫做 查询表达式 , 它支持构建更加复杂和健壮的查询。 领域特定语言 (DSL), 使用 JSON 构造了一个请求。我们可以像这样重写之前的查询所有名为 Smith 的搜索 :
GET /megacorp/employee/_search
{
"query" : {
"match" : {
"last_name" : "Smith"
}
}
}
返回结果与之前的查询一样,但还是可以看到有一些变化。其中之一是,不再使用 query-string 参数,而是一个请求体替代。这个请求使用 JSON 构造,并使用了一个 match 查询(属于查询类型之一,后面将继续介绍)。
按照性能分类
按照查询的性能,我们可以把查询分为query查询(打分制查询)和filter(过滤器查询)。具体的语法我们不用纠结,后面会提到。我们先来看普通查询:
按照语法分类
打分制查询
GET /megacorp/employee/_search
{
"query" : {
"match" : {
"about" : "rock climbing"
}
}
}
返回所有喜欢攀岩的员工: 注意,这里每条查询的记录,都会有一个评分。es 本身就是全文检索搜索引擎,他会根据查询条件对每个文档进行评分。
Elasticsearch 默认按照相关性得分排序,即每个文档跟查询的匹配程度。第一个最高得分的结果很明显:John Smith 的 about 属性清楚地写着 “rock climbing” 。
但为什么Jane Smith 也作为结果返回了呢?原因是她的about 属性里提到了 “rock” 。因为只有 “rock” 而没有 “climbing” ,所以她的相关性得分低于 John 的。
这是一个很好的案例,阐明了 Elasticsearch 如何 在 全文属性上搜索并返回相关性最强的结果。Elasticsearch 中的 相关性 概念非常重要,也是完全区别于传统关系型数据库的一个概念,数据库中的一条记录要么匹配要么不匹配。
可以看到,普通查询(打分制查询主要用于模糊匹配),应用于全文检索等搜索引擎。
过滤器查询
当进行精确值查找时, 我们会使用过滤器(filters )。过滤器很重要,因为它们执行速度非常快,不会计算相关度(直接跳过了整个评分阶段)而且很容易被缓存。我们会在本章后面的 过滤器缓存 中讨论过滤器的性能优势,不过现在只要记住:请尽可能多的使用过滤式查询。
基本语法:
GET /students/students/_search
{
"query" : {
"filter" : {
}
}
}
以一个返回值为例:
"hits" : [
{
"_index" : "my_store",
"_type" : "products",
"_id" : "2",
"_score" : 1.0,
"_source" : {
"price" : 20,
"productID" : "KDKE-B-9947-#kL5"
}
}
]
查询置于 filter 语句内不进行评分或相关度的计算,所以所有的结果都会返回一个默认评分 1 。
所以过滤器一般用于精确查找,不需要关注文档的相关度,所以性能高于普通查询。
普通查询语法
GET /{index}/{type}/_search
{
"query": {
"match_all": { }
}
}
match_all
match_all 查询简单的匹配所有文档。在没有指定查询方式时,它是默认的查询。
match
无论你在任何字段上进行的是全文搜索还是精确查询,match 查询是你可用的标准查询。
如果你在一个全文字段上使用 match 查询,在执行查询前,它将用正确的分析器去分析查询字符串:
{ "match": { "tweet": "About Search" }}
如果在一个精确值的字段上使用它,例如数字、日期、布尔或者一个 not_analyzed 字符串字段,那么它将会精确匹配给定的值:
{ "match": { "age": 26 }}
{ "match": { "date": "2014-09-01" }}
{ "match": { "public": true }}
{ "match": { "tag": "full_text" }}
multi_match
multi_match 查询可以在多个字段上执行相同的 match 查询:
{
"multi_match": {
"query": "full text search",
"fields": [ "title", "body" ]
}
}
range
{
"range": {
"age": {
"gte": 20,
"lt": 30
}
}
}
- gt 大于
- gte 大于等于
- lt 小于
- lte 小于等于
term
term 查询被用于精确值匹配,这些精确值可能是数字、时间、布尔或者那些 not_analyzed 的字符串:
{ "term": { "age": 26 }}
{ "term": { "date": "2014-09-01" }}
{ "term": { "public": true }}
{ "term": { "tag": "full_text" }}
terms
terms 查询和term 查询一样,但它允许你指定多值进行匹配。如果这个字段包含了指定值中的任何一个值,那么这个文档满足条件:
{ "terms": { "tag": [ "search", "full_text", "nosql" ] }}
和term 查询一样,terms 查询对于输入的文本不分析。它查询那些精确匹配的值(包括在大小写、重音、空格等方面的差异)。
exists 查询和missing 查询
exists 查询和 missing 查询被用于查找那些指定字段中有值 (exists ) 或无值 (missing ) 的文档。这与SQL 中的 IS_NULL (missing) 和 NOT IS_NULL (exists) 在本质上具有共性
{
"exists": {
"field": "title"
}
}
过滤器查询语法
GET /{index}/{type}/_search
{
"query": {
{
"filter":{
}
}
}
}
或者:
GET /{index}/{type}/_search
{
"query": {
{
"bool":{
}
}
}
}
可以这么理解。query下面可以放过滤器,filter是普通过滤器,bool是组合过滤器。
关于range 、term 、terms 、exists 、missing 与上面普通查询意义一致,对于bool 过滤器,详细说一下。
bool 组合过滤器
注意,query 里面放嵌套过滤器,就需要使用bool 这是万金油组合查询,用于复杂条件的嵌套查询,基础查询语法如下
{
"bool" : {
"must" : [],
"should" : [],
"must_not" : [],
}
}
must 所有的语句都 必须(must ) 匹配,与AND 等价。must_not 所有的语句都 不能(must not ) 匹配,与NOT 等价。should 至少有一个语句要匹配,与 OR 等价。
就这么简单! 当我们需要多个过滤器时,只须将它们置入 bool 过滤器的不同部分即可。
看个简单的例子:
GET /case_tree/case_tree/_search
{
"query": {
"bool": {
"must": [
{
"exists": {
"field": "review_opinions"
}
},
{
"term": {
"author.keyword": "陈寅莹"
}
}
]
}
}
}
找到寅莹写的存在检视意见的用例。
七、文档的聚合
桶的概念
桶 简单来说就是满足特定条件的文档的集合:
- 一个雇员属于 男性 桶或者 女性 桶
- 奥尔巴尼属于 纽约 桶
- 日期2014-10-28属于 十月 桶
当聚合开始被执行,每个文档里面的值通过计算来决定符合哪个桶的条件。如果匹配到,文档将放入相应的桶并接着进行聚合操作。
桶也可以被嵌套在其他桶里面,提供层次化的或者有条件的划分方案。例如,辛辛那提会被放入俄亥俄州这个桶,而 整个 俄亥俄州桶会被放入美国这个桶。
Elasticsearch 有很多种类型的桶,能让你通过很多种方式来划分文档(时间、最受欢迎的词、年龄区间、地理位置等等)。其实根本上都是通过同样的原理进行操作:基于条件来划分文档。
指标的概念
桶能让我们划分文档到有意义的集合,但是最终我们需要的是对这些桶内的文档进行一些指标的计算。分桶是一种达到目的的手段:它提供了一种给文档分组的方法来让我们可以计算感兴趣的指标。
大多数 指标 是简单的数学运算(例如最小值、平均值、最大值,还有汇总),这些是通过文档的值来计算。在实践中,指标能让你计算像平均薪资、最高出售价格、95%的查询延迟这样的数据。
桶和指标的组合
聚合 是由桶和指标组成的。 聚合可能只有一个桶,可能只有一个指标,或者可能两个都有。也有可能有一些桶嵌套在其他桶里面。例如,我们可以通过所属国家来划分文档(桶),然后计算每个国家的平均薪酬(指标)。
由于桶可以被嵌套,我们可以实现非常多并且非常复杂的聚合:
1.通过国家划分文档(桶)
2.然后通过性别划分每个国家(桶)
3.然后通过年龄区间划分每种性别(桶)
4.最后,为每个年龄区间计算平均薪酬(指标)
最后将告诉你每个 <国家, 性别, 年龄> 组合的平均薪酬。所有的这些都在一个请求内完成并且只遍历一次数据!
实战
看个例子:
GET /cars/transactions/_search
{
"size" : 0,
"aggs": {
"colors": {
"terms": {
"field": "color"
},
"aggs": {
"avg_price": {
"avg": {
"field": "price"
}
}
}
}
}
}
返回值
{
...
"aggregations": {
"colors": {
"buckets": [
{
"key": "red",
"doc_count": 4,
"avg_price": {
"value": 32500
}
},
{
"key": "blue",
"doc_count": 2,
"avg_price": {
"value": 20000
}
},
{
"key": "green",
"doc_count": 2,
"avg_price": {
"value": 21000
}
}
]
}
}
...
}
显然,我们可以根据上面的数据轻易做出树状图,饼图等数据可视化方案。
范围限定的聚合
所有聚合的例子到目前为止,你可能已经注意到,我们的搜索请求省略了一个 query 。 整个请求只不过是一个聚合。
聚合可以与搜索请求同时执行,但是我们需要理解一个新概念: 范围 。 默认情况下,聚合与查询是对同一范围进行操作的,也就是说,聚合是基于我们查询匹配的文档集合进行计算的。
我们可以看到聚合是隔离的。现实中,Elasticsearch 认为 “没有指定查询” 和 “查询所有文档” 是等价的。前面这个查询内部会转化成下面的这个请求:
GET /cars/transactions/_search
{
"size" : 0,
"query" : {
"match_all" : {}
},
"aggs" : {
"colors" : {
"terms" : {
"field" : "color"
}
}
}
}
八、总结
本文介绍的只是elasticsearch的基础概念和语法,实际使用中还会有很多其他语法给大家使用。如果你作为一个前端,需要搭建服务器数据库,再也不需要后台配合了。但是es还有很多应用领域,集群、分片等复杂特性。如果需要系统的学习ElasticSearch,建议大家根据ElasticSearch权威指南学习。
|