一、概念
全文检索的定义—
全文检索首先将要查询的目标文档中的词提取出来,组成索引,通过查询索引达到搜索目标文档的目的。这种先建立索引,再对索引进行搜索的过程就叫全文检索(Full-text Search)。
Lucene实现全文检索的流程:

Elastic 的底层是开源库 Lucene。但是,你没法直接用 Lucene,必须自己写代码去调用它的 接口。Elastic 是 Lucene 的封装,提供了 REST API 的操作接口,开箱即用。 REST API:天然的跨平台。开源的 Elasticsearch 是目前全文搜索引擎的首选。 它可以快速地储存、搜索和分析海量数据。维基百科、Stack Overflow、Github 都采用它.
二、基础概念
1、index (索引)
动词,相当于MySQL中的insert
名称,相当于 MySQL 中的 Database
2、Type(类型)
在 Index(索引)中,可以定义一个或多个类型。
类似于 MySQL 中的 Table;每一种类型的数据放在一起;
3、Document(文档)
保存在某个索引(Index)下,某种类型(Type)的一个数据(Document),文档是 JSON 格 式的,Document 就像是 MySQL 中的某个 Table 里面的内容;
4、倒排索引机制
取得关键词
由于lucene是基于关键词索引和查询的,首先我们要取得这两篇文章的关键词,通常我们需要如下处理措施:
- a.我们现在有的是文章内容,即一个字符串,我们先要找出字符串中的所有单词,即分词。英文单词由于用空格分隔,比较好处理。中文单词间是连在一起的需要特殊的分词处理。
- b.文章中的”in”, “once” “too”等词没有什么实际意义,中文中的“的”“是”等字通常也无具体含义,这些不代表概念的词可以过滤掉
- c.用户通常希望查“He”时能把含“he”,“HE”的文章也找出来,所以所有单词需要统一大小写。
- d.用户通常希望查“live”时能把含“lives”,“lived”的文章也找出来,所以需要把“lives”,“lived”还原成“live”
- e.文章中的标点符号通常不表示某种概念,也可以过滤掉
1 | 在lucene中以上措施由Analyzer类完成。 经过上面处理后, |
建立倒排索引
有了关键词后,我们就可以建立倒排索引了。上面的对应关系是:“文章号”对“文章中所有关键词”。倒排索引把这个关系倒过来,变成: “关键词”对“拥有该关键词的所有文章号”。
文章1,2经过倒排后变成
| 关键字 | 文章号 |
|---|---|
| guangzhou | 1 |
| he | 2 |
| i | 1 |
| live | 1,2 |
| shanghai | 2 |
| tom | 1 |
通常仅知道关键词在哪些文章中出现还不够,我们还需要知道关键词在文章中出现次数和出现的位置,通常有两种位置:
- a.字符位置,即记录该词是文章中第几个字符(优点是关键词亮显时定位快);
- b.关键词位置,即记录该词是文章中第几个关键词(优点是节约索引空间、词组(phase)查询快),lucene中记录的就是这种位置。
加上“出现频率”和“出现位置”信息后,我们的索引结构变为:
| 关键词 | 文章号[出现频率] | 出现位置 |
|---|---|---|
| guangzhou | 1[2] | 3,6 |
| he | 2[1] | 1 |
| i | 1[1] | 4 |
| live | 1[2] | 2, 5 , |
| 2[1] | 2 | |
| shanghai | 2[1] | 3 |
| tom | 1[1] | 1 |
以live 这行为例我们说明一下该结构:live在文章1中出现了2次,文章2中出现了一次,它的出现位置为“2,5,2”这表示什么呢?我们需要结合文章号和出现频率来分析,文章1中出现了2次,那么“2,5”就表示live在文章1中出现的两个位置,文章2中出现了一次,剩下的“2”就表示live是文章2中第 2个关键字。
三、初步检索
1、_cat
1 | GET /_cat/nodes 查看所有节点 |
2、索引一个文档(保存)
保存一个数据,保存在哪个索引的哪个类型下,指定用哪个唯一标识
在 customer 索引下的 external 类型下保存 1 号数据为 :
1 | PUT customer/external/1 |
PUT 和 POST 都可以:
- POST 新增。如果不指定 id,会自动生成 id。指定 id 就会修改这个数据,并新增版本号,
- PUT 可以新增可以修改。PUT 必须指定 id;由于 PUT 需要指定 id,我们一般都用来做修改操作,不指定 id 会报错。
3、查询文档
1 | GET customer/external/1 |
查询结果:
1 | { |
4、更新文档
1 | POST customer/external/1/_update |
或者
1 | POST customer/external/1 |
或者
1 | PUT customer/external/1 |
不同:POST 操作会对比源文档数据,如果相同不会有什么操作,文档 version 不增加 ; PUT 操作总会将数据重新保存并增加 version 版本; 带_update 对比元数据如果一样就不进行任何操作。 看场景; 对于大并发更新,不带 update; 对于大并发查询偶尔更新,带 update;对比更新,重新计算分配规则。
更新同时增加属性
1
POST customer/external/1/_update{ "doc":{"name":"Jane Doe","age":20}}
5、删除文档&索引
1 | DELETE customer/external/1DELETE customer |
6、bulk批量API
1 |
|
bulk API 以此按顺序执行所有的 action(动作)。如果一个单个的动作因任何原因而失败, 它将继续处理它后面剩余的动作。当 bulk API 返回时,它将提供每个动作的状态(与发送的顺序相同),所以您可以检查是否一个指定的动作是不是失败了。
7、样本测试数据
一份顾客银行账户信息的虚构的 JSON 文档样本。每个文档都有下列的 schema ;
(模式):
1 | { "account_number": 0, "balance": 16623, "firstname": "Bradshaw", "lastname": "Mckenzie", "age": 29, "gender": "F", "address": "244 Columbus Place", "employer": "Euron", "email": "bradshawmckenzie@euron.com", "city": "Hobucken", "state": "CO"} |
https://github.com/elastic/elasticsearch/blob/master/docs/src/test/resources/accounts.json?raw=true 导入测试数据
POST bank/account/_bulk
四、进阶检索
1、SearchAPI
ES 支持两种基本方式检索 :
- 一个是通过使用 REST request URI 发送搜索参数(uri+检索参数)
- 另一个是通过使用 REST request body 来发送它们(uri+请求体)
检索信息
- 一切检索从 _search 开始
1 | GET bank/_search //检索bank下所有信息,包括type和docsGET bank/_search?q=*&sort=account_number:asc // 请求参数方式检索 |
响应结果解释:
1 | took - Elasticsearch 执行搜索的时间(毫秒) time_out - 告诉我们搜索是否超时 _shards - 告诉我们多少个分片被搜索了,以及统计了成功/失败的搜索分片 hits - 搜索结果 hits.total - 搜索结果 hits.hits - 实际的搜索结果数组(默认为前 10 的文档) sort - 结果的排序 key(键)(没有则按 score 排序) score 和 max_score - 相关性得分和最高得分(全文检索用) |
- uri+请求体进行检索
1 | GET bank/_search { "query": { "match_all": {} }, "sort": [ { "account_number": { "order": "desc" } } ]} |
HTTP 客户端工具(POSTMAN),get 请求不能携带请求体,我们变为 post 也是一样的.我们 POST 一个 JSON 风格的查询请求体到 _search API。 需要了解,一旦搜索的结果被返回,Elasticsearch 就完成了这次请求,并且不会维护任何服务端的资源或者结果的 cursor(游标).
2、Query DSL
1)、基础语法格式
Elasticsearch 提供了一个可以执行查询的 Json 风格的 DSL(domain-specific language 领域特定语言)。这个被称为 Query DSL。该查询语言非常全面,并且刚开始的时候感觉有点复杂, 真正学好它的方法是从一些基础的示例开始的。
一个查询语句的典型结构
1
{ QUERY_NAME:{ ARGUMENT:VALUE, ARGUMENT:VALUE,...... }}
如果是针对某个字段,那么它的结构如下
1
{ QUERY_NAME:{ FIELD_NAME:{ ARGUMENT:VALUE, ARGUMENT:VALUE,...... } }}
1 | GET bank/_search{ "query": { "match_all": {} }, "from": 0, "size": 5, "sort": [ { "account_number": { "order": "desc" } } ]} |
- query 定义如何查询,
- match_all 查询类型【代表查询所有的所有】,es 中可以在 query 中组合非常多的查询类型完成复杂查询
- 除了query 参数之外,我们也可以传递其它的参数以改变查询结果。如 sort,size
- from+size 限定,完成分页功能
- sort 排序,多字段排序,会在前序字段相等时后续字段内部排序,否则以前序为准
2)、返回部分字段
1 | GET bank/_search{ "query":{ "match_all":{} }, "from":0, "size":5, "_source":["age","balance"]} |
3)、match[匹配查询]
基础类型(非字符串),精确匹配
1
GET bank/_search{ "query":{ "match":{ "account_number":20 } }}match返回account_number=20的数据
字符串,全文检索
1
GET bank/_search{ "query":{ "match":{ "address":"mill" } }}最终查询出 address 中包含 mill 单词的所有记录 match 当搜索字符串类型的时候,会进行全文检索,并且每条记录有相关性得分。
字符串,多个单词(分词+全文检索)
1
GET bank/_search{ "query":{ "match":{ "address":"mill road" } }}最终查询出 address 中包含 mill 或者 road 或者 mill road 的所有记录,并给出相关性得分
4)、match_phrase[短语匹配]
将需要匹配的值当成一个整体单词(不分词)进行检索
1 | GET bank/_search{ "query":{ "match_phrase":{ "address":"mill road" } }}查出 address 中包含 mill road 的所有记录,并给出相关性得分 |
5)、multi_match[多字段匹配]
1 | GET bank/_search{ "query":{ "multi_match":{ "query":"mill", "fields":["state","address"] } }}state 或者 address 包含 mill |
6)、bool[复合查询]
bool 用来做复合查询: 复合语句可以合并任何其它查询语句,包括复合语句,了解这一点是很重要的。这就意味着,复合语句之间可以互相嵌套,可以表达非常复杂的逻辑。
must: 必须达到must列举的所有条件
1
GET bank/_search{ "query":{ "bool":{ "must":[ {"match":{"address":"mill"}}, {"match":{"gender":"M"}} ] } }}
should:应该达到 should 列举的条件,如果达到会增加相关文档的评分,并不会改变查询的结果。如果 query 中只有 should 且只有一种匹配规则,那么 should 的条件就会被作为默认匹配条件而去改变查询结果
1
GET bank/_search{ "query":{ "bool":{ "must":[ {"match":{"address":"mill"}}, {"match":{"gender":"M"}} ], "should":[ {"match":{"address":"lane"}} ] } }}
must_not必须不是指定的情况
1
GET bank/_search{ "query":{ "bool":{ "must":[ {"match":{"address":"mill"}}, {"match":{"gender":"M"}} ], "should":[ {"match":{"address":"lane"}} ], "must_not":[ {"match":{"email":"neteria.com"}} ] } }}address 包含 mill,并且 gender 是 M,如果 address 里面有 lane 最好不过,但是 email 必 须不包含 baluba.com
7)、filter[结果过滤]
并不是所有的查询都需要产生分数,特别是那些仅用于 “filtering”(过滤)的文档。为了不计算分数 Elasticsearch 会自动检查场景并且优化查询的执行。
1 | GET bank/_search{ "query":{ "bool":{ "must":[ {"match":{"address":"mill"}}, {"match":{"gender":"M"}} ], "filter":{ "range":{ "balance":{ "gte":10000, "lte":20000 } } } } }} |
8)、term
和 match 一样。匹配某个属性的值。全文检索字段用 match,其他非 text 字段匹配用 term。
Avoid using the term query for text fields. By default, Elasticsearch changes the values of text fields as part of analysis. This can make finding exact matches for text field values difficult. To search text field values, use the match query instead.
避免对文本字段使用术语查询。默认情况下,作为分析的一部分,Elasticsearch 会更改文本字段的值。 这会使查找文本字段值的精确匹配变得困难。要搜索文本字段值,请改用匹配查询。
1 | GET bank/_search{ "query":{ "bool":{ "must":[ {"term":{ "age":{ "value":"28" } }}, {"match":{ "address":"990 Mill Road" }} ] } }} |
9)、aggregations(执行聚合)
聚合提供了从数据中分组和提取数据的能力。最简单的聚合方法大致等于 SQL GROUP BY 和 SQL 聚合函数。在 Elasticsearch 中,您有执行搜索返回 hits(命中结果),并且同时返回聚合结果,把一个响应中的所有 hits(命中结果)分隔开的能力。这是非常强大且有效的,您可以执行查询和多个聚合,并且在一次使用中得到各自的(任何一个的)返回结果,使用 一次简洁和简化的 API 来避免网络往返。
- 搜索 address 中包含 mill 的所有人的年龄分布以及平均年龄,但不显示这些人的详情。
1 | GET bank/_search { "query": { "match": { "address": "mill" } }, "aggs": { "group_by_state": { "terms": { "field": "age" } }, "avg_age": { "avg": { "field": "age" } } }, "size": 0} |
1 | size:0 //不显示搜索数据 aggs:执行聚合。聚合语法如下 "aggs": { "aggs_name 这次聚合的名字,方便展示在结果集中": { "AGG_TYPE 聚合的类型(avg,term,terms)": { } } }, |
- 复杂一点: 按照年龄聚合, 并且请求这些年龄段的这些人的平均薪资
1 | GET bank/account/_search{ "query":{ "match_all":{} }, "aggs":{ "age_avg":{ "terms":{ "field":"age", "size":1000 }, "aggs":{ "banlances_avg":{ "avg":{ "field":"balance" } } } } }, "size":1000} |
- 复杂:查出所有年龄分布,并且这些年龄段中 M 的平均薪资和 F 的平均薪资以及这个年龄段的总体平均薪资
1 | GET bank/account/_search{ "query":{ "match_all":{} }, "aggs":{ "age_agg":{ "terms":{ "field":"age", "size":100 }, "aggs":{ "gender_agg":{ "terms":{ "field":"gender.keyword", "size":100 }, "aggs":{ "balance_avg":{ "avg":{ "field":"balance" } } } }, "balance_avg":{ "avg":{ "field":"balance" } } } } }, "size":1000} |
3、Mapping
1)、字段类型

2)、映射
Mapping是用来定义一个文档(document),以及它所包含的属性(field)是如何存储和索引的.比如,使用mapping来定义:
哪些字符串属性应该被看做全文本属性(full text fields)。
哪些属性包含数字,日期或者地理位置。
文档中的所有属性是否都能被索引(_all 配置)。
日期的格式。
自定义映射规则来执行动态添加属性。
查看mapping信息
1
GET bank/_mapping
修改mapping信息
https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping.html

3)、新版本改变
Es7 及以上移除了 type 的概念。
- 关系型数据库中两个数据表示是独立的,即使他们里面有相同名称的列也不影响使用,但 ES 中不是这样的。elasticsearch 是基于 Lucene 开发的搜索引擎,而 ES 中不同 type 下名称相同的 filed 最终在 Lucene 中的处理方式是一样的。
- 两个不同 type 下的两个 user_name,在 ES 同一个索引下其实被认为是同一个 filed,你必须在两个不同的 type 中定义相同的 filed 映射。否则,不同 type 中的相同字段名称就会在处理中出现冲突的情况,导致 Lucene 处理效率下降。
- 去掉 type 就是为了提高 ES 处理数据的效率。
Elasticsearch 7.x
- URL 中的 type 参数为可选。比如,索引一个文档不再要求提供文档类型。
Elasticsearch 8.x
- 不再支持 URL 中的 type 参数。
解决:
- 1)、将索引从多类型迁移到单类型,每种类型文档一个独立索引
- 2)、将已存在的索引下的类型数据,全部迁移到指定位置即可。详见数据迁移
1、创建映射
1 | 1、创建索引并指定映射PUT /my-index{ "mappings":{ "properties":{ "age": { "type": "integer" }, "email": { "type": "keyword" }, "name": { "type": "text" } } }} |
2、添加新的字段映射
1 | PUT /my-index/_mapping{ "properties":{ "employeee-id":{ "type":"keyword", "index":false } }} |
3、更新映射
对于已经存在的映射字段,我们不能更新。更新必须创建新的索引进行数据迁移
4、数据迁移
先创建出 new_twitter 的正确映射。然后使用如下方式进行数据迁移
1 | POST _reindex // 固定写法{ "source":{ "index":"twitter" }, "dest":{ "index":"new_twitter" }}// 将旧索引的type下的数据进行迁移POST _reindex{ "source":{ "index":"twitter", "type":"tweet" }, "dest":{ "index":"tweets" }} |
4、分词
一个 tokenizer(分词器)接收一个字符流,将之分割为独立的 tokens(词元,通常是独立的单词),然后输出 tokens 流。
例如,whitespace tokenizer 遇到空白字符时分割文本。它会将文本 “Quick brown fox!“ 分割为 [Quick, brown, fox!]。
该 tokenizer(分词器)还负责记录各个 term(词条)的顺序或 position 位置(用于 phrase 短语和 word proximity 词近邻查询),以及 term(词条)所代表的原始 word(单词)的 start (起始)和 end(结束)的 character offsets(字符偏移量)(用于高亮显示搜索的内容)。
Elasticsearch 提供了很多内置的分词器,可以用来构建 custom analyzers(自定义分词器)。
1)、安装ik 分词器
注意:不能用默认 elasticsearch-plugin install xxx.zip 进行自动安装
https://github.com/medcl/elasticsearch-analysis-ik/releases?after=v6.4.2 对应 es 版本安装
1 | 进入 es 容器内部 plugins 目录 |
2)、测试分词器
1 | POST _analyze |
3)、自定义词库
修改/usr/share/elasticsearch/plugins/ik/config/中的 IKAnalyzer.cfg.xml
/usr/share/elasticsearch/plugins/ik/config
1 |
|
1 | 原来的xml |
按照标红的路径利用 nginx 发布静态资源,按照请求路径,创建对应的文件夹以及文件,放在nginx 的 html 下
然后重启 es 服务器,重启 nginx。 在 kibana 中测试分词效果
更新完成后,es 只会对新增的数据用新词分词。历史数据是不会重新分词的。如果想要历史数据重新分词。需要执行:
1 | POST my_index/_update_by_query?conflicts=proceed |
五、Elasticsearch-Rest-Client (spring整合)
1)、9300:TCP
- spring-data-elasticsearch:transport-api.jar;
- springboot 版本不同, transport-api.jar 不同,不能适配 es 版本
- 7.x 已经不建议使用,8 以后就要废弃
2)、9200:HTTP
- JestClient:非官方,更新慢
- RestTemplate:模拟发 HTTP 请求,ES 很多操作需要自己封装,麻烦
- HttpClient:同上
- Elasticsearch-Rest-Client:官方 RestClient,封装了 ES 操作,API 层次分明,上手简单
最终选择 Elasticsearch-Rest-Client(elasticsearch-rest-high-level-client)
https://www.elastic.co/guide/en/elasticsearch/client/java-rest/current/java-rest-high.html
1、SpringBoot 整合
1 | <dependency> |
2、配置
1 |
|
3、使用
官方文档参考:
1 |
|

If you like this blog or find it useful for you, you are welcome to comment on it. You are also welcome to share this blog, so that more people can participate in it. If the images used in the blog infringe your copyright, please contact the author to delete them. Thank you !