一 文档查询操作
# 强调:
1.后续查询主要都是 结构化查询 且 匹配模式为match分词查询
2.对于elasticsearch来说,所有的条件都是可插拔的,彼此之间用','分割
3.所有参数的属性值为列表的,都可以实现多个条件并列存在
1 查询的两种方式
# Elasticsearch之查询的两种方式
字符串查询 和 结构化查询
# 文档的查询结果 有项参数 max_score 最大分数
是打分机制,反应查询匹配度 分值越高,匹配度越高
# eg:
查询结果为一条,分数为1.0
查询结果为两条,大概分别为0.5左右
# 1.查询字符串 (query string) # 不常用
请求方式:GET
请求参数:_search?q=字段:值 # 类似:参数在请求路径url中
查询结果:在返回json的 "hits" 中
# eg:
# 查询from字段是gu的所有人
GET lqz/_doc/_search?q=from:gu
# 查询age是22的人
GET lqz/_doc/_search?q=age:22
# 2.结构化查询 (DSL) # 常用
请求方式:GET
请求参数:_search
参数值:以json格式 传递 # 类似:参数在请求体中
查询结果:在返回json的 "hits" 中
# eg:
# 查询from字段是gu的所有人
GET lqz/_doc/_search
{
"query": {
"match": { # 匹配模式
"from": "gu" # 字段: 查询值
}
}
}
2 DSL的匹配模式
# 结构化查询的匹配模式:
match分词查询 和 term精准查询
# match、term和terms的区别 # 面试常问
match: 会对搜索的关键词进行分词,按分词去搜索 # 常用
# eg: 存数据的时候,用了分词 [确实 很 黄,武器 很 长,性格 很 直]
武器很长 ---> 武器 长 有结果
term : 不会对搜索的关键字进行分词,而直接搜索,精准匹配
# eg:
武器很长 ---> 武器很长 搜不到结果
terms : 多个精准词 and连接
# 下面的查询:都是基于 结构化查询的 match匹配模式
2.1 match分词查询
# match系列 常用
match :按条件分词匹配
match_all : 查询所有
match_phrase : 短语查询 phrase n. 短语,词组
match_phrase_prefix : 最左前缀查询 匹配的词组以什么开头
# eg:
# 查询所有
GET t1/_doc/_search
{
"query": {
"match_all": {} # 值为空,表示没有查询条件
}
}
# 短语/词组查询
GET t1/_doc/_search slop v.溢出
{
"query": {
"match_phrase": {
"title": {
"query": "中国世界",
"slop": 2 # 中国和世界之间最多间隔2个字符
}
}
}
}
# slop: 表明该查询的词组 分组后 中间最多有多少个字符间隔
默认为0,即没有该参数时,表明query的值 为整体一个词组
# 最左前缀查询 还有一个max_expanions参数搭配使用 具体再看
GET t1/_doc/_search
{
"query": {
"match_phrase_prefix": {
"desc": "bea"
}
}
}
2.2 term精准查询
# term 系列 不常用
term : 精准查询 不会词条分析处理(分词、标点符号删减、分词后的词条全转小写)
terms : 多个精准词 and连接
# eg:
# term
GET w10/_doc/_search
{
"query": {
"term": {
"t1": "girl"
}
}
}
# terms
GET w10/_doc/_search
{
"query": {
"terms": {
"t1": ["beautiful", "sexy"]
}
}
}
# 注:
不要使用term 对 类型是text的字段 进行查询
因为text类型的字段,会自动分词 且进行标点符号删减 和 全转小写的词条(token)分析处理
3 排序查询
# 不是所有字段都支持排序,只支持数字类型 和时间类型,字符串不支持
# 排序参数: # sort对应列表中,可以加多个排序字段,以','分割
"sort":[
{
"排序字段":{
"order": "desc" 或 "asc" # 降 升
}
}
]
# eg: 降序 desc
GET lqz/_doc/_search
{
"query": {
"match": { "from": "gu" }
},
"sort": [
{
"age": {
"order": "desc"
}
}
]
}
# eg: 升序 asc
GET lqz/_doc/_search
{
"query": {
"match": { "from": "gu" }
},
"sort": [
{
"age": {
"order": "asc"
}
}
]
}
# eg: 升序 查所有 且以age升序
GET lqz/_doc/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"age": {
"order": "asc"
}
}
]
}
4 分页查询
# 分页参数:
"from" : 从第几条开始
"size" : 返回几条结果
# eg: 查询结果从第二条开始,取两条
GET lqz/_doc/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"age": {
"order": "desc"
}
}
],
"from": 2,
"size": 2
}
# 注意:
对于elasticsearch来说,所有的条件都是可插拔的,彼此之间用','分割
# eg: 上述分页查询,可以将排序参数直接去掉
5 布尔查询
# 布尔查询:即多个条件的组合查询 ,或称为 子查询
# 布尔查询的主要功能:
1.多条件的关系查询:与、或、非
2.范围过滤查询:大于、小于 或 什么之间
# 布尔参数:
"bool":{
"关系参数":[
多个查询参数 # 不能直接在match中加多个条件
],
"过滤参数":{
"range": {
"过滤字段": { "属性参数": 值, } # 属性参数可多个 eg: 大于什么,且小于什么
}
}
}
# 关系参数:
must # and 与
should # or 或
must_not # not 非
# 过滤参数:
filter
# 属性参数:
gt : >
gte : >=
lt : <
lte : <=
5.1 多条件的关系查询
# eg: must and条件 并且
GET lqz/_doc/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"from": "gu"
}
},
{
"match": {
"name": "顾老二"
}
}
]
}
}
}
# eg:should or条件 或者
GET lqz/_doc/_search
{
"query": {
"bool": {
"should": [
{
"match": {
"from": "gu"
}
},
{
"match": {
"name": "龙套偏房"
}
}
]
}
}
}
# eg: must_not not条件 都不是
GET lqz/_doc/_search
{
"query": {
"bool": {
"must_not": [
{
"match": {
"from": "gu"
}
},
{
"match": {
"tags": "可爱"
}
},
{
"match": {
"age": 18
}
}
]
}
}
}
5.2 范围过滤查询
# filter过滤条件: 大于、小于 或 什么之间
gt : >
gte : >=
lt : <
lte : <=
# 过滤查询:字段from是gu,且 age<30
GET lqz/_doc/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"from": "gu"
}
}
],
"filter": {
"range": {
"age": {
"lt": 30
}
}
}
}
}
}
# 范围查询:字段from是gu,且25<= age <=30
GET lqz/_doc/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"from": "gu"
}
}
],
"filter": {
"range": {
"age": {
"gte": 25,
"lte": 30
}
}
}
}
}
}
# 注意:
filter需要在bool内部
如果是and条件,需要用must # 建议使用must, should会比较乱
如果是should,会认为是should和filter是"或者"的关系
6 查询结果过滤
# 查询结果过滤:
指的是针对返回的数据,再进行某个字段属性的筛选
eg: 类似于 select name, age from user
# 结果过滤参数:
"_source":[多个字段属性]
# 注意:"_source" 和 "query" 是平级的
# eg: 基本使用
GET lqz/_doc/_search
{
"query": {
"match_all": {
}
},
"_source":["name","age"]
}
# eg:就相当于 mysql中 跟了一堆where之后,前面select再某些字段
GET lqz/_doc/_search
{
"query": {
"bool": {
"must":{
"match":{"from":"gu"}
},
"filter": {
"range": {
"age": {
"lte": 25
}
}
}
}
},
"_source":["name","age"]
}
7 高亮查询
# 高亮查询:
指的是针对返回的结果,将某些字段属性 增加颜色样式 进行突出显示
# 高亮参数:
"highlight": {
# 自定义高亮样式 可不加这两个,有默认高亮样式 注意单引号
"pre_tags": "<b class='key' style='color:red'>", # 在高亮字段前 加标签
"post_tags": "</b>", # 在高亮字段后 加标签
# 需要高亮的字段
"fields": {
"from": {}
}
}
# 注意:"highlight" 与 "query” 同级
# eg:
GET lqz/_doc/_search
{
"query": {
"match": {
"from": "gu"
}
},
"highlight": {
"pre_tags": "<b class='key' style='color:red'>",
"post_tags": "</b>",
"fields": {
"from": {}
}
}
}
8 聚合函数
# 聚合函数: 尽量少ES中使用,因为ES主要是用来做全文检索,使用聚合会影响效率
sum, avg, max ,min
# 聚合或分组参数: 注意:"aggs" 与 "query” 同级
"aggs": {
"结果字段别名":{
"聚合参数":{
"field": "被聚合的具体字段"
}
}
}
# eg: 类似mysql的 select avg(age) as my_avg from 表 where from=gu;
GET lqz/_doc/_search
{
"query": {
"match": {
"from": "gu"
}
},
"aggs": {
"my_avg": {
"avg": {
"field": "age"
}
}
},
"_source": ["name", "age"]
}
# 最大年龄
GET lqz/_doc/_search
{
"query": {
"match": {
"from": "gu"
}
},
"aggs": {
"my_max": {
"max": {
"field": "age"
}
}
},
"_source": ["name", "age"]
}
# 最小年龄
GET lqz/_doc/_search
{
"query": {
"match": {
"from": "gu"
}
},
"aggs": {
"my_min": {
"min": {
"field": "age"
}
}
},
"_source": ["name", "age"]
}
# 总年龄
GET lqz/_doc/_search
{
"query": {
"match": {
"from": "gu"
}
},
"aggs": {
"my_sum": {
"sum": {
"field": "age"
}
}
},
"_source": ["name", "age"]
}
# 分组查询 因ES每个索引,只有一个文档 不存在多表分组的情况
# 查询所有人的年龄段,并且按照`15~20,20~25,25~30`分组,并且算出每组的平均年龄
GET lqz/_doc/_search
{
"size": 0, # 查询结果返回0条 意思是query查出的数据不显示
"query": {
"match_all": {}
},
"aggs": {
"age_group": { # 结果字段别名
"range": { # 以范围分组
"field": "age", # 分组的字段
"ranges": [ # 每个组的范围
{
"from": 15,
"to": 20
},
{
"from": 20,
"to": 25
},
{
"from": 25,
"to": 30
}
]
}
},
"aggs": { # 再对分组后的结果,进行聚合函数计算 这个aggs需要和分组age_group 同级
"my_avg": {
"avg": {"field": "age"}
}
}
}
}
二 ik分词器使用
# IK分词器的下载安装
1.github下载相应版本
https://github.com/medcl/elasticsearch-analysis-ik/releases
再选择?after=v7.5.2
2.解压到es的plugins目录下
3.重启es
# ik_max_word 和 ik_smart 什么区别?
ik_max_word: 会将文本做最细粒度的拆分
eg:会将“中华人民共和国国歌”拆分为
“中华人民共和国,中华人民,中华,华人,人民共和国,人民,人,民,共和国,共和,和,国国,国歌”
会穷尽各种可能的组合,适合 Term 精准查询
ik_smart: 会做最粗粒度的拆分
eg:会将“中华人民共和国国歌”拆分为“中华人民共和国, 国歌”,适合 match_Phrase 短语查询
# 一般:文章标题 用 ik_max_word 文章内容 用 ik_smart
# 若ik分词器,分词效果不太贴切 可修改分词词库
ik分词配置文件:ik文件/config目录 # 自己添加词 或配置
-IKAnalyzer.cfg.xml # 用来配置自定义的词库
-main.dic # ik原生内置的中文词库,大约有27万多条,只要是这些单词,都会被分在一起
-surname.dic # 中国的姓氏
-suffix.dic # 特殊(后缀)名词 例如`乡、江、所、省`等
-preposition.dic # 中文介词 例如`不、也、了、仍`等
-stopword.dic # 英文停用词库 例如`a、an、and、the`等
-quantifier.dic # 单位名词 例如`厘米、件、倍、像素`等
# eg: 简单使用--查看分词器,会对文本分词有哪些结果
GET _analyze
{
"analyzer": "ik_max_word",
"text": "白雪公主和十个小矮人"
}
##### eg: 'ik_max_word'分词
# 准备数据--创建索引
PUT books
{
"mappings": {
"properties":{
"title":{
"type":"text",
"analyzer": "ik_max_word" # 指定哪种分词器分词
},
"price":{
"type":"integer" # 未指定分词器 采用默认standard分析器
},
"addr":{
"type":"keyword"
},
"company":{
"properties":{
"name":{"type":"text"},
"company_addr":{"type":"text"},
"employee_count":{"type":"integer"}
}
},
"publish_date":{"type":"date","format":"yyy-MM-dd"}
}
}
}
# 准备数据--插入文档
PUT books/_doc/1
{
"title":"大头儿子小偷爸爸",
"price":100,
"addr":"北京天安门",
"company":{
"name":"我爱北京天安门",
"company_addr":"我的家在东北松花江傻姑娘",
"employee_count":10
},
"publish_date":"2019-08-19"
}
PUT books/_doc/2
{
"title":"白雪公主和十个小矮人",
"price":"99",
"addr":"黑暗森里",
"company":{
"name":"我的家乡在上海",
"company_addr":"朋友一生一起走",
"employee_count":10
},
"publish_date":"2018-05-19"
}
# 查看索引映射
GET books/_mapping
# 查询文档
GET books/_search
{
"query": {
"match": {
"title": "十"
}
}
}
##### eg: 'ik_smart'分词
# 准备数据--创建索引
PUT books2
{
"mappings": {
"properties":{
"title":{
"type":"text",
"analyzer": "ik_smart" # 指定哪种分词器分词
},
"price":{
"type":"integer"
},
"addr":{
"type":"keyword"
},
"company":{
"properties":{
"name":{"type":"text"},
"company_addr":{"type":"text"},
"employee_count":{"type":"integer"}
}
},
"publish_date":{"type":"date","format":"yyy-MM-dd"}
}
}
}
# 准备数据--插入文档
PUT books2/_doc/1
{
"title":"大头儿子小偷爸爸",
"price":100,
"addr":"北京天安门",
"company":{
"name":"我爱北京天安门",
"company_addr":"我的家在东北松花江傻姑娘",
"employee_count":10
},
"publish_date":"2019-08-19"
}
PUT books2/_doc/2
{
"title":"白雪公主和十个小矮人",
"price":"99",
"addr":"黑暗森里",
"company":{
"name":"我的家乡在上海",
"company_addr":"朋友一生一起走",
"employee_count":10
},
"publish_date":"2018-05-19"
}
# 查询文档
GET books2/_search
{
"query": {
"match": {
"title": "十个"
}
}
}
三 Python中集成ES
1 原生集成
# 原生集成 等同于pymysql 原生操作
Official low-level client for Elasticsearch
# 因为ES是遵守restful规范的Http请求方式,故也可以直接用request模块直接发http请求
import requests
res=requests.put("http://localhost:9200/lqz5")
print(res)
# 安装 elasticsearch
pip3 install elasticsearch
from elasticsearch import Elasticsearch
# 连接Elasticsearch
obj = Elasticsearch("http://localhost:9200") # 默认连接本地elasticsearch
# 创建索引(Index) 并插入数据
result = obj.indices.create(index='user', body={"userid":'1','username':'lqz'},ignore=400)
print(result)
# 删除索引
result = obj.indices.delete(index='user', ignore=[400, 404])
# 插入文档数据
data = {'userid': '1', 'username': 'lqz','password':'123'}
result = obj.create(index='news', doc_type='_doc', id=1, body=data)
print(result)
# 更新文档数据 doc包裹是局部更新
'''
注:不用doc包裹会报错
ActionRequestValidationException[Validation Failed: 1: script or doc is missing
'''
data ={'doc':{'userid': '1', 'username': 'lqz','password':'123ee','test':'test'}}
result = obj.update(index='news', doc_type='_doc', body=data, id=1)
print(result)
# 删除文档数据
result = obj.delete(index='news', doc_type='_doc', id=1)
print(result)
# 查询文档数据
# 查找所有文档
query = {'query': {'match_all': {}}}
# 查找名字叫做jack的所有文档
query = {'query': {'match': {'title': '十个'}}}
# 查找年龄大于11的所有文档
query = {'query': {'range': {'age': {'gt': 11}}}}
allDoc = obj.search(index='books', doc_type='_doc', body=query)
# print(allDoc)
print(allDoc['hits']['hits'][0]['_source'])
2 DSL集成
# 原生集成 等同于ORM
Elasticsearch DSL is a high-level
# 安装 elasticsearch
pip3 install elasticsearch-dsl
from datetime import datetime
# 导入一堆类 包含映射类型、分词器等
from elasticsearch_dsl import Document, Date, Nested, Boolean, \
analyzer, InnerDoc, Completion, Keyword, Text,Integer
from elasticsearch_dsl.connections import connections
# 创建ES连接对象
connections.create_connection(hosts=["localhost"])
# 类似于orm的表模型
class Article(Document):
title = Text(analyzer='ik_max_word')
author = Text()
# 指定表 所属的索引内容 eg:索引名
class Index:
name = 'myindex'
# 自定义save方法 可用于在保存数据之前,额外添加其他操作
def save(self, **kwargs):
# 调用父类的save方法
return super(Article, self).save(**kwargs)
if __name__ == '__main__':
# 调用类的init方法 创建索引
Article.init()
# 新增数据
article = Article()
article.title = "测试测试阿斯顿发送到发斯蒂芬啊啊士大夫阿斯蒂芬"
article.author = "lqz"
article.save() # 数据就保存了
# 查询数据
s = Article.search()
s = s.filter('match', title="测试")
results = s.execute() # 执行 返回是列表 里面是数据对象
print(results[0].title)
# 删除数据
s = Article.search()
s = s.filter('match', title="李清照").delete()
# 修改数据
s = Article().search() # 试试 Article 不加括号
s = s.filter('match', title="测试")
results = s.execute()
print(results[0])
results[0].title="李清照阿斯顿发送到发送阿斯蒂"
results[0].save()
四 集群搭建及脑裂问题
# ES使用两种不同的方式来发现对方:
# 1.广播方式: 在同一个网络中,只要开启多个es实例 一般不用 方便,但过程不可控
只要ES节点能ping通,就自动加入到集群节点中
# 2.单播方式: 指定跟谁谁谁组成集群 unicast n. 单一传播
# eg:4台机器的集群(单播方式)
配置各节点的配置文件:各自ES下的 "\config\elasticsearch.yml"文件
# elasticsearch 1节点
集群名称是my_es1
集群端口是9300 # 集群中 节点相互交互识别的端口 transport.tcp.port
节点名称是node1
监听本地IP和9200端口
可以有权限成为主节点和读写磁盘
# 默认:可成为主节点 和 可进行读写存储
# node.data:false 只能读 不能写
cluster.name: my_es1
node.name: node1
network.host: 127.0.0.1
http.port: 9200
transport.tcp.port: 9300
discovery.zen.ping.unicast.hosts: ["127.0.0.1:9300", "127.0.0.1:9302", "127.0.0.1:9303", "127.0.0.1:9304"]
# elasticsearch 2节点
集群名称是my_es1
集群端口是9302
节点名称是node2
监听本地IP和9202端口
可以有权限成为主节点和读写磁盘
cluster.name: my_es1
node.name: node2
network.host: 127.0.0.1
http.port: 9202
transport.tcp.port: 9302
node.master: true
node.data: true
discovery.zen.ping.unicast.hosts: ["127.0.0.1:9300", "127.0.0.1:9302", "127.0.0.1:9303", "127.0.0.1:9304"]
# elasticsearch 3节点
集群名称是my_es1
集群端口是9303
节点名称是node3
监听本地IP和9203端口
可以有权限成为主节点和读写磁盘
cluster.name: my_es1
node.name: node3
network.host: 127.0.0.1
http.port: 9203
transport.tcp.port: 9303
discovery.zen.ping.unicast.hosts: ["127.0.0.1:9300", "127.0.0.1:9302", "127.0.0.1:9303", "127.0.0.1:9304"]
# elasticsearch4节点
集群名称是my_es1
集群端口是9304
节点名称是node4
监听本地IP和9204端口
仅能读写磁盘而不能被选举为主节点
cluster.name: my_es1
node.name: node4
network.host: 127.0.0.1
http.port: 9204
transport.tcp.port: 9304
node.master: false
node.data: true
discovery.zen.ping.unicast.hosts: ["127.0.0.1:9300", "127.0.0.1:9302", "127.0.0.1:9303", "127.0.0.1:9304"]
# 由上例的配置可以看到:
各节点有一个共同的集群名字my_es1,但由于是本地环境,所以各节点的名字不能一致
分别启动它们,它们通过单播列表相互介绍,发现彼此,然后组成一个my_es1集群。
谁是老大则是要看谁先启动了!
# 脑裂问题:
假设有5个节点,由于网络问题,部分节点失去与主节点的通信
然后部分节点就开始选举新的主节点,继续处理请求
会形成了两个集群
eg:2个节点一组,3个节点一组
# 防止脑裂:设置集群主节点的候补节点的 最小数量:(集群节点总数/2 +1的个数)
discovery.zen.minimum_master_nodes: 3 # 3=5/2 +1
# 除了在配置文件设置,也可以动态设置
PUT /_cluster/settings
{
"persistent":{
"discovery.zen.minimum_master_nodes":2
}
}
# 解决原理:
当2个节点失去与主节点通信后,因节点数量 小于 能够选取主节点的候补节点 最小数量 3,就无法选取新的主节点
五 打分机制
# 打分
确定文档 和 查询匹配度(查询结果和查询参数有多么相关) 的过程 被称为打分
# 打分的运作机制:TF-IDF
1.TF 词频 term frequency
一个词条 在某个文档中 出现的次数
# 出现的次数越多 表示相关度越高 分数应该更高
2.IDF 逆文档频率 inverse document frequency
一个词条 在索引中,多少个的不同文档中出现过
# 出现的数量越多 表示该词越不关键 分数应该更低
# 总结:词频越高、逆文档频率越低 ---> 打分越高
六 如何将mysql的数据同步ES
# 思考:
mysql中一个库有多个表,怎么同步到ES中 # ES7.0之后,每个索引库只含有一个文档('_doc')
# 注意:
1.并不是mysql所有的数据表,都需要同步到ES中
2.只是利用ES的倒排索引来做全文检索,只需要全文检索的字段数据
# 操作步骤:
1.将需要用来全文检索的数据,从mysql中查询出来
2.将需要同步的数据,拼凑成ES的json格式 # 各字段存放在同一个文档即可
3.若在ES中全文检索后,查到了某条数据(但并不详细),此时,再根据数据的主键,去mysql详细查询
# 意义:ES并不是用来代替mysql来数据查询,而只是作为全文检索,来加快整体的数据查询
# eg: 查出'文章标题' 或 '文章内容' 或 '作者' 或 '出版社' 中 有关键字 '你好' 的文章
1.mysql中查出 作者name、文章id、文章title、文章content 等多个表的数据
2.自定义处理拼成一个文档json数据
3.同步插入到ES中
# 后续过程:
前端在ES中查出'你好'后
若前端需要的数据 如文章id、文章内容 ES中有 就直接返回给前端
若前端需要的数据 如文章价格、文章类型等其他字段 ES中没有 再根据文章id或其他主键 去mysql查询 # 这也是回表查询