聚焦Python分布式爬虫必学框架 Scrapy 打造搜索引擎
elasticsearch介绍
image.png我们建设了一个网站或者程序之后,希望添加搜索功能,发现搜索功能的工作很难,主要有以下几点
image.pngelasticsearch 百度介绍
可以看到官网上列出了使用 elasticsearch 的公司有哪些:https://www.elastic.co/use-cases
事实上,除了 elasticsearch ,全文搜索引擎还有很多,如 solr、sphinx 等。近年来随着 ELK 日志分析系统的流行,elasticsearch 才逐渐被大家所认可
ELK 基于 JAVA 开发,Lucene 是一个非常有名的底层搜索接口,但是虽然 Lucene 很出名且很好用,但是使用难度还是比较高,所以 elasticsearch 对 Lucene 进行了封装,让其对开发者更友好,很多开源的搜索引擎都是基于 Lucene 来完成的
关于搜索,我们好像可以通过 like 语句去数据库中查询,那问什么还会有这么多独立出来的搜索引擎呢?
通过 like 语句或者正则表达式可以满足基本需求,但是对于一些复杂的需求就无法满足了
关系型数据库搜索缺点:
- 无法打分:无法对搜索结果进行排序
- 无分布式:传统数据库做分布式是比较麻烦的,对开发者要求较高
- 无法解析搜索请求:比如搜索 'Python',这种搜索请求是比较简单的,但搜索 '我想学习 Python',这种搜索就无法解析,搜索不到结果,但是在百度上搜索 '我想学习 Python',就会进行分词,关系型数据库是没法完成这些功能的,或者需要我们自己完成,这就加大了开发难度
- 效率低:当数据库中数据上亿、几十亿的时候,关系型数据库单库就无法满足要求,如果做分布式,开发成本大大提高
- 分词:英语用单词表达意思,对于中文单个字很难有其具体意思,英文分词只需要按照空格和标点来就行了,但是,中文分词是一个比较有技术含量的课题,不过现在市面上分词库非常多,直接配置拿过来就可用
NoSQL 数据库:
文档数据库,与关系数据库差别很大
MongoDB、Redis、Elasticsearch 都是 NoSQL
MongoDB 也是 NoSQL,我们为什么不用它来做搜索以及存储呢
NoSQL 和 关系型数据库应用场景不一样,并不存在谁优于谁,在某些情况下是可以将 Elasticsearch 当作 MongoDB 来使用的,比如不会频繁的 update,因为 Elasticsearch 的更新操作实际上是比较慢的,远低于 MongoDB,MongoDB 的更新实际上又慢于 MySQL 等关系型数据库的更新操作,但是 MongoDB 在插入和查询在绝大多数情况下是优于 MySQL 的,Elasticsearch 虽然可以当作 NoSQL 来使用,但是并不能完全取代 MongoDB 或者 MySQL,Elasticsearch 目前主要用途还是搜索引擎,实际上它既有数据的存储,又有数据的分析,当我们提到搜索功能的时候,其他所有数据库在 Elasticsearch 面前几乎就可以当作一个玩笑(我不是针对谁,我是说在座的各位,都是垃圾...)Elasticsearch 更注重搜索
elasticsearch安装
- 安装 elasticsearch-rtf
elasticsearch-rtf 是国内一位大神在 elasticsearch 上进行了一系列的插件安装之后的发型版本,更原版 elasticsearch 实际上是一样的,只是增加了插件
image.pngelasticsearch 基于 JAVA 开发,所以要先安装 JAVA,也就是 JDK,elasticsearch 5 以及以上版本需要安装 JAVA 8 以上
JAVA 下载地址:http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html
选择对应版本下载安装即可
image.png命令行下
java -version
查看 JAVA 版本
elasticsearch 官网下载地址:https://www.elastic.co/downloads/elasticsearch
image.png由于 elasticsearch 官方版本插件不多,所以在 GitHub 上面下载 elasticsearch-rtf,地址:https://github.com/medcl/elasticsearch-rtf
image.pngelasticsearch-rtf 目录总览
image.pngbin/ 目录存放了很多可执行文件,包括 Linux 和 Windows 中的一些可执行文件,
elasticsearch
用于启动 elasticsearch,elasticsearch-plugin
用来安装插件
image.pngconfig/ 目录,其中有一个
yml
格式文件elasticsearch.yml
,现在很多工具采用yml
格式文件来作为配置文件
image.pnglib/ 目录存放的都是 elasticsearch 依赖的
jar
包
image.pngmodules/ 目录存放 elasticsearch 的模块
image.pngplugins/ 目录中存放的就是 elasticsearch-rtf 里面装的所有插件,如果安装原版 elasticsearch 的话这些插件就要自己去装
在 elasticsearch 的 bin/ 目录下运行
elasticsearch
或者elasticsearch.bat
就可以启动 elasticsearch 了
image.png但是我这里报了一个错,原因是 JAVA 没有加到环境变量中
image.png新建一个用户变量,变量名
JAVA_HOME
,变量值为 JAVA 安装目录
image.png重新打开命令行窗口,运行
elasticsearch
来启动 elasticsearch
image.png启动成功,控制台输出可以看到分别对 9300 和 9200 端口进行了监听,其中 9300 是 JAVA 的调用接口,9200 是我们访问 elasticsearch 的接口
image.png浏览器中访问 http://127.0.0.1:9200/ 会看到有 JSON 数据输出
有上图这样的输出,就代表 elasticsearch 安装完成,
cluster_name
是 elasticsearch 集群名称,默认是elasticsearch
image.png启动 elasticsearch 后,目录下会多出 data/ 目录和 logs/ 目录,分别用于存放数据以及 log 日志
- elasticsearch 的 head 插件和 kibana 的安装
head 插件是个什么东西?可以简单的类比成 navicat。安装了 MySQL 以后,要对数据进行管理,想看到数据需要安装一个 navicat 来管理数据库,head 插件是一个基于浏览器的一个插件,可以完成一种类似于 navicat 的功能,可以对 elasticsearch 里面的数据进行管理,可以执行查询语句等
elasticsearch 是一个 NoSQL 数据库,看到它的数据比较不方便,就需要 head 插件来浏览存在 elasticsearch 当中的数据
elasticsearch-head 的 GitHub 地址:https://github.com/mobz/elasticsearch-head
image.png安装方式
关于 npm:
Node.js 官方下载地址:https://nodejs.org/en/
image.png官网下载后一路下一步安装即可
image.png安装后 cmd 下测试
npm
命令能否运行
image.png image.png image.png由于 npm 下载依赖包的地址都是国外服务器,可以使用 cnpm 来管理依赖包的下载,cnpm 是 淘宝 NPM 镜像,地址:http://npm.taobao.org/
cnpm 安装好后,以后所有需要用到 npm 命令的地方都可以使用 cnpm 来代替
image.png先用 git 将 elasticsearch-head clone 下来
image.png先 cd 到 elasticsearch-head 目录下,然后运行
cnpm install
image.png运行
cnpm install
以后,elasticsearch-head 所需依赖就安装完成,发现在 elasticsearch-head 目录下多了一个 node_modules/ 目录,这个目录就是用来存放 npm 所安装的依赖的
image.png通过
npm run start
命令启动服务,会监听 9100 端口(启动之前先启动 elasticsearch)
image.png image.png浏览器访问 9100 端口,会提示
集群健康值: 未连接
,意思就是连接不到 9200 端口,但是在浏览器中访问是可以正常返回结果的
elasticsearch-head 无法连接到 elasticsearch 的 9200 端口而浏览器直接访问是可以的,原因是,elasticsearch 为了安全考虑,默认情况下不允许使用第三方的服务。elasticsearch-head 就相当于一个第三方的代理,用这个代理连接 9200 端口并进行操作,默认情况下 elasticsearch 是不允许的,代理没有权限对其进行访问,为了满足 elasticsearch-head 可以连接 elasticsearch,在启动 elasticsearch 的时候需要做一些配置
这些配置是放在 elasticsearch-rtf-master/config/elasticsearch.yml 文件中的
# 自定义配置
# 默认情况下不允许通过第三方插件来访问,配置好后第三方插件既可以访问
http.cors.enabled: true
http.cors.allow-origin: "*"
http.cors.allow-methods: OPTIONS, HEAD, GET, POST, PUT, DELETE
http.cors.allow-headers: "X-Requested-With, Content-Type, Content-Length, X-User"
image.png
image.png重启 elasticsearch
image.png刷新浏览器 http://127.0.0.1:9100/ 页面,现在可以访问了,
集群健康值: green (0 of 0)
kibana 严格来说是一个项目,他并不是一个 elasticsearch 的插件,这里主要使用 kibana 里的 sense 插件,sense 类似于 rest 接口的一个插件,sense 专门用来调用 elasticsearch 交互用的,很强大。
实际上 kibana 也是 elasticsearch 产品线中的一个产品,也就是之前介绍的 ELK 当中的 K,kibana 实际上是一个 HTML 页面,这个页面完成了和 elasticsearch 的交互,让我们操作 elasticsearch 更加简单
kibana 地址:https://www.elastic.co/downloads/kibana
下载版本一定要同之前下载的 elasticsearch 版本相一致,这里下载 kibana-5.1.1 版本
地址:https://www.elastic.co/downloads/past-releases/kibana-5-1-1
image.pngkibana 目录结构
image.pngkibana 的 bin/ 目录
kibana.bat 用来运行 kibana
kibana-plugin.bat 用来安装 kibana 插件
image.png运行 kibana,会在 5601 端口监听
image.png浏览器打开 http://127.0.0.1:5601
image.pngDev Tools 就是之前说所的 sense,在之前的版本他就叫 sense
image.png image.png点击
Get to work
,出现的页面就是我们需要用到的页面
elasticsearch的基本概念
通常这些概念在一个分布式系统中都会出现
- 集群
一个或多个节点组织在一起就叫做集群
elasticsearch 是一个分布式搜索引擎,既然是分布式,那么就会可能同时会有多个 elasticsearch 实例存在,比如有 3 台服务器,这 3 台加在一起就是一个集群
- 节点
一个节点就是集群中的一个服务器,比如有 3 台服务器,这 3 台服务器都部署了一个 elasticsearch,那么这 3 台服务器都叫节点,3 个节点加在一起就是一个集群
节点是有名称的,每一个节点由一个名字来标识,默认是一个随机的漫画角色的名字
- 分片
将索引划分成多份的能力,允许水平分割和扩展容量,多个分片响应请求,提高性能和吞吐量
这里所说的
索引
类似于数据库,不要把 elasticsearch 中的索引当作普通数据库中的索引来理解,它们是不一样的
当数据越来越多,索引就会越来越大,索引越来越大,如果放在一台服务器上,性能就会受影响,所以 elasticsearch 可以将索引分成多个,比如一个索引分成 5 份,分别放在不同的地方,这样的话它的水平扩展能力就很容易了。数据有可能存在多个分片上,比如 A 这个数据是放在第 1 个 和 第 2 个 分片上的,这个时候,当我们去请求 A 这个数据的时候,elasticsearch 会自动将这个请求的查询信息路由到对应这个数据的分片上面,所以它的分片之间是有一个路由关系的,分片实际上也提高了 elasticsearch 的性能以及吞吐量
- 副本
创建分片的一份或多份的能力,在一个节点失败其余节点可以顶上
我们可以吧
副本
理解为数据的备份,比如说我们设置某一个索引(或者理解为某一个数据库)的时候,我们给他设置一个值作为它的副本,比如这个值设为 2,后期它在保存数据的时候就会自动保存两份
副本的好处就是在某一个节点(服务器)失效的时候,可以顶上,加大了分布式系统的可靠性
分片
和副本
不要搞混了,分片是将一个过大的数据分成多分,放到不同的地方,副本是有多少分复制的数据,比如说给定一个副本的值是 2,那么这个数据就会存两份。
- Elasticsearch 对比 MySQL 概念理解
Elasticsearch | MySQL |
---|---|
index(索引) | 数据库 |
type(类型) | 表 |
documents(文档) | 行 |
fields | 列 |
elasticsearch 是一个搜索引擎,我们不是通过 elasticsearch 去分析放在数据库中的数据,而是 elasticsearch 为了达到自己的搜索的目的,它的数据是自己来保存的,它不是一个中间库,它是集合了数据保存以及数据分析的一个搜索引擎
其中,
索引
有两层含义,当作名词理解时,就是一个数据库,当作为一个动词来理解时就不再是数据库的概念,比如我们对一个 documents 进行索引,这句话的意思可以理解为在数据库中进行一个插入的操作(insert),也就是将这个 documents 插入到这个索引的 type 当中[话有点绕,但就是这么回事...]
- HTTP 方法
HTTP 1.0 定义了三种请求方式:GET、POST、HEAD 方法
后面又新增了五中方法:OPTIONS、PUT、DELETE、TRACE、CONNECT
正是由于 HTTP 提供了这些方法,我们才有可能实现 REST 接口,恰好
elasticsearch 是基于 REST 接口来完成的
elasticsearch 常用 HTTP 方法的含义
方法 | 描述 |
---|---|
GET | 请求指定的页面信息,并返回实体主体 |
POST | 向指定的资源提交数据进行处理请求。数据被包含在请求体中。POST 请求可能会导致新的资源建立 和 / 或 已有的资源修改 |
PUT | 向服务器传送的数据取代指定的文档的内容 |
DELETE | 请求服务器删除指定的页面 |
倒排索引
image.png目前的搜索引擎当中,底层的索引存储都采用的是 倒排索引
倒排索引是底层索引存储的最基本的一种方式,也是搜索引擎区别于已有的关系数据库或者说其余的 NoSQL 数据库的核心
image.png假设现在有 A、B、C 三个 documents
我们现在要查询有哪些 documents 包含
python
这个关键词
有这种查询或者搜索需求的话,如果不通过倒排索引,按照正常的思维来理解,我么要如何来做?
正常的思维方式就是:
先对文件进行遍历,分别遍历 A、B、C 三个文件内的所有内容,才能判断每个文件中到底有没有python
这个关键词
这样,为了查询一个关键词,将所有的 documents 全部遍历一遍,那么效率就太低了,如果文件上亿个,每次查询都去遍历上亿个,这肯定是不可能的
image.png所以说就出现了倒排
倒排就是在文件进行存储之前,先对文件进行分析,比如存储 A 文件,就要先对其进行分析,对其内容全部遍历,遍历完之后进行分析,B、C 文件同理,也就是内个文件在插入前都需要对其进行遍历并分析,这个分析就包括了分词
,所以,分析后倒排数据的结构如下图
这里把
Python 写各大聊天系统的屏蔽脏话功能原理
当作一个 elasticsearch 的 documents,把这句话进行倒排索引处理之后就是表格中的样子,上面表就是一个 倒排索引 非常基础的一个表结构,可以简单的看为一个 dict,左边 关键词 为 key,右边是关键词出现的 文章 为 value。
左侧关键词部分都是对这句话进行分词之后出现的关键词。然后给Python
这个词后面加一个列表,比如说存放Python
这个词出现的 文章,然后就可以得出,Python
这个关键词在 文章1 和 文章3 中出现过,同样聊天
这个词在 文章2 当中出现过,依次往下分析,就能够直到每个词所出现的 documents 有哪些。这样的话就可以把这个表格数据看成一个 dict,key 为 关键词,value 是一个 list,list 里面存放的是这个关键词所在的文章
当然分词有很多,比如
Python 写各大聊天系统的屏蔽脏话功能原理
这句话里面的的
要不要放到 关键词里面,这种词实际上没有什么含义,当然最后要不要放进来是不需要我们去关心的,这些 elasticsearch 都帮我们做好了
这是最简单的一个倒排索引的模型,但是,这样就可以了吗?貌似可以,但实际上这样是不完善的,因为这个关键词有可能在文章中出现多次,比如
Python
这个关键词在 文章1 中出现 30 次,在 文章3 中出现过 1 次,如果我们不把它出现过的频率记录下来,后期就没法对文本进行打分。因为如果一个文本当中,它的某一个关键词出现频率越高,这个词权重可能就会比较高,如果按照现在的结构来存储的话,就没法存储关键词的权重
image.png所以倒排索引还有更加精确的存储结构
左侧依旧存储关键词,后面结构就变了,首先还是会存储文章,也就是关键词所出现的文章,比如文章1,又加了一些数据,第二个数据尖括号中
<2,10>
存储的是Python
这个关键词在 文章1 当中出现的位置,在第 2 个单词 和 第 10 个单词,这个位置信息在分析的时候对 elasticsearch 打分来说都是很重要的信息,最后存储的数字 2,代表Python
关键词在
文章1 中出现的频率
其中关键词出现次数(也叫词频)对 elasticsearch 打分来说也是很重要的信息,我们可以把它叫做
TF
有了这样一个倒排索引的结构,搜索就变得非常高效了,我们可以对其打分
如果想了解 elasticsearch 到底是如何打分的,可以去了解
TF-IDF
这个概念,数据挖掘以及机器学习也会遇到这个概念
image.png这里分析比较简单,真正做好倒排索引是有很多问题需要解决的
这里面的所有问题 elasticsearch 都帮我们去完成了
elasticsearch 基本的索引和文档CRUD操作
- 索引初始化操作
编写创建命令后运行
# es 的文档、索引的 CRUD 操作
# 索引初始化操作
# 指定分片(默认值5)和副本(默认值1)的数量
# shards 一旦设置不能修改
PUT lagou
{
"settings": {
"index": {
"number_of_shards": 5,
"number_of_replicas": 1
}
}
}
image.png
image.png刷新 elasticsearch-head 已成功创建索引
lagou
image.png可以查看索引信息
image.png image.png image.png image.png image.png image.png image.png上面的
lagou
索引是通过 REST 接口来添加的,实际上还可以在 elasticsearch-head 中添加,而且图形化,类似 navicat
注意:一旦索引创建完成,分片数量(number_of_shards)是不能修改的,副本数量(number_of_replicas)是可以修改的
获取索引 settings 信息
# 获取索引 settings 信息
# 获取全部索引信息
GET _all/_settings
GET _settings
# 获取单个索引 settings 信息
GET lagou/_settings
# 获取多个索引 settings 信息
# 多个索引之间用逗号作分割,且不能有空格
GET .kibana,lagou/_settings
image.png
更新 settings 信息
# 更新 settings 信息
# 更新副本数量
PUT lagou/_settings
{
"number_of_replicas": 2
}
# 尝试更新切片数量,会产生异常
PUT lagou/_settings
{
"number_of_shards": 3
}
image.png
image.png
image.png
获取索引信息
# 获取索引信息
# 获取所有索引信息
GET _all
# 获取一个索引信息
GET lagou
image.png
向索引中添加文档
在关系型数据库当中,要存储数据到数据库当中,是要先创建表,再向表中插入数据。在 elasticsearch 中(或者很多 NoSQL 数据库中)实际上是不用先新建表(elasticsearch 中为 type)的,可以直接向索引中插入数据。type 中我们是可以设定字段以及属性的,不过不设置也是可以的,它会默认去猜插入的值是什么
# 向索引中添加文档
# job 表示 type,1 是这条数据的 id
PUT lagou/job/1
{
"title":"python分布式爬虫开发",
"salary_min":15000,
"city":"北京",
"company":{
"name":"百度",
"company_addr":"北京市软件园"
},
"publish_date":"2017-4-16",
"comments":15
}
image.png
image.png在 elasticsearch-head 中查看数据
image.png image.png保存文档的时候如果不指明 id 值,elasticsearch 会自动生成 id(UUID)
获取文档
# 获取文档
# 获取文档全部字段
GET lagou/job/1
GET lagou/job/1?_source
# 获取文档指定字段
GET lagou/job/1?_source=title
GET lagou/job/1?_source=title,city
image.png
image.png
image.png
image.png
修改文档
# 修改文档
# 这种方式是一种覆盖的方式,会全部覆盖掉原来的内容
PUT lagou/job/1
{
"title":"python分布式爬虫开发",
"salary_min":15000,
"company":{
"name":"百度",
"company_addr":"北京市软件园"
},
"publish_date":"2017-4-16",
"comments":15
}
# 增量修改方式,通过POST方法,只修改comments字段(推荐的方式)
POST lagou/job/1/_update
{
"doc": {
"comments":20
}
}
image.png
image.png
image.png
image.png
删除操作
# 删除
# 删除documents
DELETE lagou/job/1
# 删除type,无法删除成功,会报错, es5已经不支持这里通DELETE来删除type
DELETE lagou/job
# 删除index
DELETE lagou
image.png
image.png
image.png
image.png
以上所有
CURD
操作
# es 的文档、索引的 CRUD 操作
# 索引初始化操作
# 指定分片和副本的数量
# shards 一旦设置不能修改
PUT lagou
{
"settings": {
"index": {
"number_of_shards": 5,
"number_of_replicas": 1
}
}
}
# 获取 settings 信息
# 获取全部索引信息
GET _all/_settings
GET _settings
# 获取单个索引 settings 信息
GET lagou/_settings
# 获取多个索引 settings 信息
# 多个索引之间用逗号作分割,且不能有空格
GET .kibana,lagou/_settings
# 更新 settings 信息
# 更新副本数量
PUT lagou/_settings
{
"number_of_replicas": 2
}
# 尝试更新切片数量,会产生异常
PUT lagou/_settings
{
"number_of_shards": 3
}
# 获取索引信息
# 获取所有索引信息
GET _all
# 获取一个索引信息
GET lagou
# 向索引中添加文档
PUT lagou/job/1
{
"title":"python分布式爬虫开发",
"salary_min":15000,
"city":"北京",
"company":{
"name":"百度",
"company_addr":"北京市软件园"
},
"publish_date":"2017-4-16",
"comments":15
}
# 不指明 id
POST lagou/job/
{
"title":"python django 开发工程师",
"salary_min":30000,
"city":"上海",
"company":{
"name":"美团科技",
"company_addr":"北京软件园A区"
},
"publish_date":"2018-3-30",
"comments":20
}
# 获取文档
# 获取文档全部字段
GET lagou/job/1
GET lagou/job/1?_source
# 获取文档指定字段
GET lagou/job/1?_source=title
GET lagou/job/1?_source=title,city
# 修改文档
# 这种方式是一种覆盖的方式,会全部覆盖掉原来的内容
PUT lagou/job/1
{
"title":"python分布式爬虫开发",
"salary_min":15000,
"company":{
"name":"百度",
"company_addr":"北京市软件园"
},
"publish_date":"2017-4-16",
"comments":15
}
# 增量修改方式,通过POST方法,只修改comments字段(推荐方式)
POST lagou/job/1/_update
{
"doc": {
"comments":20
}
}
# 删除
# 删除documents
DELETE lagou/job/1
# 删除type,无法删除成功,会报错, es5已经不支持这里通DELETE来删除type
DELETE lagou/job
# 删除index
DELETE lagou
elasticsearch的mget和bulk批量操作
- mget 批量获取
为什么会有 mget 这样一个批量操作命令存在?使用 GET 方法获取某一个 documents 的时候,虽然可以获取,但是当要查询的数量过多的时候,效率是比较低的。因为每次操作都需要建立一个 HTTP 的连接,然后再去获取,这样的话开销是比较大的,HTTP 每次建立都要经过 3 次握手协议,没查询一条数据,都进行 3 次握手,查询效率太低。所以 elasticsearch 提供了
_mget
命令,可以让我们一次性查询多条记录
image.png测试数据
# 批量获取
# 查询job1 type中id为1的数据以及job2 type中id为2的数据
GET _mget
{
"docs":[
{
"_index":"testdb",
"_type":"job1",
"_id":1
},
{
"_index":"testdb",
"_type":"job2",
"_id":2
}
]
}
# 查询同一个index下面不同的type还有更简单的方式
GET testdb/_mget
{
"docs":[
{
"_type":"job1",
"_id":1
},
{
"_type":"job2",
"_id":2
}
]
}
# 查询同一个index和type中只有id不一样的数据
GET testdb/job1/_mget
{
"docs":[
{
"_id":1
},
{
"_id":2
}
]
}
# 上面查询语句的简写方式
GET testdb/job1/_mget
{
"ids":[1,2]
}
image.png
image.png
image.png
- bulk 批量操作
bulk 批量操作可以合并多个操作,比如有多个 index、delete、update、create 操作,bulk 是支持这 4 种操作的,它可以让多个这 4 种类型的操作做一次提交,让 elasticsearch 一次执行完成。它还可以让我们从一个索引导入到另外一个索引当中
当我们单个进行操作的时候实际上效率是比较低的,比如说有上千个操作,如果提交上千次请求,HTTP 建立的过程就会比较慢,所以 elasticsearch 给我们提供了一个
_bulk
批量操作,让我们可以一次提交多个操作
官方给了一个 bulk 批量操作的格式
action_and_meta_data\n
optional_source\n
action_and_meta_data\n
optional_source\n
...
action_and_meta_data\n
optional_source\n
其中
action_and_meta_data\n
和optional_source\n
这两行指明的是一个操作,action_and_meta_data\n
指明的是它的操作,optional_source\n
指明的是这次操作的数据。将多个操作放到一起,作为一个请求体,通过_bulk
一次性发送给 elasticsearch 并执行
image.png需要注意的是:每一条数据都由两行构成(delete 除外),其他命令比如 index 、create 都是由元信息行和数据行组成,update 比较特殊,它的数据行可能是 doc 可能是 upsert 或者 script
# bulk操作
POST _bulk
{"index":{"_index":"lagou","_type":"job","_id":"1"}}
{"title":"python分布式爬虫开发","salary_min":15000,"city":"北京","company":{"name":"百度","company_addr":"北京软件园"},"publish_date":"2017-4-16","comments":15}
{"index":{"_index":"lagou","_type":"job2","_id":"2"}}
{"title":"python django 开发工程师","salary_min":30000,"city":"上海","company":{"name":"美团科技","company_addr":"北京软件园A区"},"publish_date":"2018-3-30","comments":20}
image.png
image.png
特别注意:bulk 操作中,第 2 行的数据行一定不能做美化,也就是不能换行,只能所有数据都在一行,不能写成美化后的 JSON 格式
# bulk 常见操作
{"index":{"_index":"test","_type":"type1","_id":"1"}}
{"field1":"value1"}
{"delete":{"_index":"test","_type":"type1","_id":"2"}}
{"create":{"_index":"test","_type":"type1","_id":"3"}}
{"field1":"value3"}
{"update":{"_id":"1","_type":"type1","_index":"index1"}}
{"doc":{"field2":"value2"}}
# bulk 操作都是由两行组成,第 1 行是 action,第 2 行指明了操作的数据,这样两行构成一个操作
# 实际上 bulk 中有一个特例,就是 delete 操作,它只有一行,没有后面的数据行,这种情况 elasticsearch 会去自动解析
# bulk 是一次性提交很多命令,它会把这些数据都发送到一个节点,然后由这个节点解析元数据
# 元数据就是 {"_index":"test","_type":"type1","_id":"1"} 这些数据,这个元数据指明了
# elasticsearch 的一些元信息,这个节点解析元数据后分发给其他节点的分片,进行操作
# 这些操作全部执行完成后在返回结果,如果数据量比较大,就可能会造成一定的延迟,所以说
# 我们也不能一次性提交过多数据
elasticsearch的mapping映射管理
映射就是当我们创建索引的时候,可以预先定义字段的类型以及相关属性
实际上映射是创建在 type 上边的,所以映射我们可以把它理解为定义数据表的时候每一个字段所定义的类型。比如 ArticleSpider 爬虫定义字段的时候,comments 字段的类型是 int,对于 elasticsearch 同理,在放置每一条数据的时候,可以对每一个字段定义一种类型。比如之前插入文档操作的时候,
{"title":"python django 开发工程师","salary_min":30000,"city":"上海","company":{"name":"美团科技","company_addr":"北京软件园A区"},"publish_date":"2018-3-30","comments":20}
,title 就是字符串类型,salary_min 就是数字类型。我们实际上是可以先定义这里面每一个字段的类型的,以及这个类型有什么属性,这个属性要比 MySQL 这种关系型数据库里面的属性要丰富,数据库中最多可以定义字段属性是否可以为 NULL,但是对于 elasticsearch 来说,它的相关属性会更加丰富。
之前插入文档的时候并没有定义类型,elasticsearch 是如何识别这些类型的呢?实际上 elasticsearch 是会去动态的识别,根据 JSON 里面传入进来的数据,它来判断是什么类型
Elasticsearch 会根据 JSON 源数据的基础类型猜测你想要的字段映射。将输入的数据转变成可搜索的索引项。Mapping 就是我们自己定义的字段数据类型,同时告诉 Elasticsearch 如何索引数据(通过设置相关属性来告诉 Elasticsearch)以及是否可以被搜索
我们设置了 Elasticsearch 的 Mapping 之后的作用是:会让索引建立的更加细致和完善
实际上,对于大多数简单的文本来说,我们自己是不用去建立 Mapping 的,因为 Elasticsearch 大部分情况都可以通过自动猜测猜出来,但是当某些情况之下,自己要去定义的时候,自定义类型以及相关属性就变得丰富了。
映射类型:静态映射、动态映射
Elasticsearch 内置类型
string类型 | text,keyword(还有一个string类型,但是在es5中开始被废弃) |
数字类型 | long,integer,short,byte,double,float |
日期类型 | date |
bool类型 | boolean |
binary类型 | binary |
复杂类型 | object,nested |
geo类型 | geo-point,geo-shape |
专业类型 | ip,competion |
image.png
- string 类型中的 text 和 keyword 的区别。如果将某一个 string 类型的数据设置为 text,这个字段传递过来的数据就会被分析器进行分析(包括分词、抽取词干、去除无意义的词等),如果将某一个 string 类型的数据设置为 keyword,就不会做分析操作,不进行倒排索引,只会当作一个字符串来存取,所以这种情况下如果想要查询这个 string 就必须完全匹配
- 日期类型 date 不止可以代表日期,还可以解析 datetime
- bool类型解析传递过来的值,这个值比较丰富,比如 True、False、Yes、No 等都会被 es 识别并转换成 bool类型
- binary类型存放二进制,但是二进制数据是不会被进行检索的
- geo类型是地理位置,geo-point 通过经纬度来标识一个位置,geo-shape 通过多个点标识一片区域
- 专业类型中 ip 就是 ip 地址,competion 是用来做搜索建议的
- 复杂类型包括 object 和 nested
image.png常用属性以及试用类型
创建索引
# 创建索引
PUT lagou
{
"mappings": {
"job":{
"properties": {
"title":{
"type": "text"
},
"salary_min":{
"type": "integer"
},
"city":{
"type": "keyword"
},
"company":{
"properties": {
"name":{
"type":"text"
},
"company_addr":{
"type":"text"
},
"employee_count":{
"type":"integer"
}
}
},
"publish_date":{
"type": "date",
"format": "yyyy-MM-dd"
},
"comments":{
"type": "integer"
}
}
}
}
}
image.png
image.png
添加数据
# 放入数据
PUT lagou/job/1
{
"title":"python分布式爬虫开发",
"salary_min":15000,
"company":{
"name":"百度",
"company_addr":"北京市软件园",
"employee_count":50
},
"publish_date":"2018-7-10",
"comments":15
}
image.png
image.png
image.png image.png尝试添加一条错误的数据
image.png可以发现 elasticsearch 自动将 string 类型的 salary_min 转换成了 integer
获取 mapping
# 获取mapping
GET lagou/_mapping
GET lagou/_mapping/job
GET _all/_mapping
GET _all/_mapping/job
image.png
注意:索引中一旦创建好了类型,就不能再修改了。这是和关系型数据库很大不同的一点,关系型数据库创建好后还是可以修改字段类型的,对于 elasticsearch 来说一旦设置了某一列的类型,就再也无法修改,可惜新增字段,但是已有字段无法修改,如果想要修改,只能删除索引,新建索引,新建索引之后再将以前数据导入。所以在新建索引前一定要想好类型,后期数据越来越多,新建索引就会很麻烦
elasticsearch的简单查询
image.pngelasticsearch 是功能强大的搜索引擎,使用它的目的就是为了快速的查询到需要的数据
添加映射
# 添加映射
PUT lagou
{
"mappings": {
"job":{
"properties": {
"title":{
"store": true,
"type": "text",
"analyzer": "ik_max_word"
},
"company_name":{
"store": true,
"type": "keyword"
},
"desc":{
"type": "text"
},
"comments":{
"type": "integer"
},
"add_time":{
"type": "date",
"format": "yyyy-MM-dd"
}
}
}
}
}
image.png
向索引中添加几条数据
POST lagou/job/
{
"title":"python scrapy redis分布式爬虫基本",
"company_name":"百度科技有限公司",
"desc":"对scrapy的概念熟悉, 熟悉redis的基本操作",
"comments":5,
"add_time":"2018-7-2"
}
POST lagou/job/
{
"title":"elasticsearch打造搜索引擎",
"company_name":"阿里巴巴科技有限公司",
"desc":"熟悉数据结构算法, 熟悉python的基本开发",
"comments":15,
"add_time":"2018-6-20"
}
POST lagou/job/
{
"title":"python打造推荐引擎系统",
"company_name":"阿里巴巴科技有限公司",
"desc":"熟悉推荐引擎的原理以及算法,掌握C语言",
"comments":60,
"add_time":"2017-10-20"
}
image.png
查询
image.png image.png image.png image.png image.pngmatch 查询
image.png image.png image.png image.pngterm 查询
image.pngterms查询
image.png控制查询的返回数量
image.pngmatch_all查询
image.png image.png image.png image.pngmatch_phrase查询
image.png image.pngmulti_match查询
image.png image.png image.png指定返回字段
image.png通过sort对结果进行排序
image.png image.png范围查询
image.pngwildcard查询
查询操作
# 查询
# match查询
# match查询会对输入进行分词,会去找 match 中指名的字段里面有没有这个词
GET lagou/job/_search
{
"query": {
"match": {
"title": "取"
}
}
}
# term查询
# term查询不会对输入进行分词,会直接拿着输入的数据进行查询(类似keyword一样)
GET lagou/job/_search
{
"query": {
"term": {
"company_name": "阿里巴巴科技有限公司"
}
}
}
# terms查询
# terms查询传入的是一个数组,这里面只要有一个值匹配就会返回结果
GET lagou/job/_search
{
"query": {
"terms": {
"title": ["工程师","django","系统"]
}
}
}
# 控制查询的返回数量
# 可以用来做分页,from是指从第几个开始查询,size是指查询几个
GET lagou/_search
{
"query": {
"match": {
"title": "python"
}
},
"from":1,
"size": 2
}
# match_all查询
GET lagou/_search
{
"query": {
"match_all": {}
}
}
# match_phrase查询
# 短语查询
GET lagou/_search
{
"query": {
"match_phrase": {
"title":{
"query": "python系统",
"slop":5
}
}
}
}
# multi_match查询
# 比如可以指明多个字段
# 比如查询title和desc这两个字段里面包含python关键词文档
GET lagou/_search
{
"query": {
"multi_match": {
"query": "python",
"fields": ["title^3","desc"]
}
}
}
# 指定返回字段
GET lagou/_search
{
"stored_fields": ["title","company_name","desc"],
"query": {
"match": {
"title": "python"
}
}
}
# 通过sort对结果进行排序
GET lagou/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"comments": {
"order": "desc"
}
}
]
}
# range查询
# 查询范围
GET lagou/_search
{
"query": {
"range": {
"comments": {
"gte": 10,
"lte": 20,
"boost": 2.0
}
}
}
}
# 对时间做了一个range查询
GET lagou/_search
{
"query": {
"range": {
"add_time": {
"gte": "2018-04-01",
"lt": "now"
}
}
}
}
# wildcard查询
# 可以简单的理解为模糊查询
GET lagou/_search
{
"query": {
"wildcard": {
"title": {
"value": "pyth*n",
"boost": 2
}
}
}
}
elasticsearch的bool组合查询
bool 查询
# bool查询
# 老版本的filtered已经在es5以后被bool替代
# bool 包括 must、should 、must_not、filter
# 使用方法如下
#当查询条件有多个的时候,后面跟数组
#bool:{
# "filter":[],
# "must":[],
# "should":[],
# "must_not":[],
#}
#当查询条件只有一个的时候,后面跟字典
#bool:{
# "filter":{},
# "must":{},
# "should":{},
# "must_not":{},
#}
# filter 是对字段进行过滤,且不参与打分
# must 数组里面的所有查询都必须满足,如果有3个查询,这3个必须同时满足
# should 数组里面的所有查询条件只要满足其中一个或多个就可以
# must_not 与 must 相反,数组里面的所有查询都不能满足
image.png image.png image.png image.png image.png image.png image.png image.png image.png image.png image.png建立测试数据
image.pngbool 过滤查询
image.png嵌套查询
# bool查询
# 老版本的filtered已经在es5以后被bool替代
# bool 包括 must、should 、must_not、filter
# 使用方法如下
#当查询条件有多个的时候,后面跟数组
#bool:{
# "filter":[],
# "must":[],
# "should":[],
# "must_not":[],
#}
#当查询条件只有一个的时候,后面跟字典
#bool:{
# "filter":{},
# "must":{},
# "should":{},
# "must_not":{},
#}
# filter 是对字段进行过滤,且不参与打分
# must 数组里面的所有查询都必须满足,如果有3个查询,这3个必须同时满足
# should 数组里面的所有查询条件只要满足其中一个或多个就可以
# must_not 与 must 相反,数组里面的所有查询都不能满足
# 建立测试数据
POST lagou/testjob/_bulk
{"index":{"_id":1}}
{"salary":10,"title":"Python"}
{"index":{"_id":2}}
{"salary":20,"title":"Scrapy"}
{"index":{"_id":3}}
{"salary":30,"title":"Django"}
{"index":{"_id":4}}
{"salary":30,"title":"Elasticsearch"}
# 简单过滤查询
# 查询薪资为20k的工作
# 对照 SQL 语句写法:select * from testjob where salary=20
GET lagou/testjob/_search
{
"query": {
"bool": {
"must": {
"match_all":{}
},
"filter": {
"match": {
"salary": 20
}
}
}
}
}
# 可以指定多个值
GET lagou/testjob/_search
{
"query": {
"bool": {
"must": {
"match_all":{}
},
"filter": {
"terms": {
"salary": [10,20]
}
}
}
}
}
# select * from testjob where title="Python"
GET lagou/testjob/_search
{
"query": {
"bool": {
"must": {
"match_all":{}
},
"filter": {
"term": {
"title": "python"
}
}
}
}
}
# 查看分析器解析的结果
GET _analyze
{
"analyzer": "ik_max_word",
"text": "python网络开发工程师"
}
GET _analyze
{
"analyzer": "ik_smart",
"text": "python网络开发工程师"
}
# bool 过滤查询,可以做组合过滤查询
# select * from testjob where (salary=20 or title=python) and (salary!=30)
# 查询薪资等于20k或者工作为python的工作,排除价格为30k的
GET lagou/testjob/_search
{
"query": {
"bool": {
"should": [
{"term": {"salary": 20}},
{"term": {"title": "python"}}
],
"must_not": [
{"term":{"salary":30}}
]
}
}
}
# 嵌套查询
# select * from testjob where title="python" or (title="django" and salary=30)
GET lagou/testjob/_search
{
"query": {
"bool": {
"should": [
{"term":{"title":"python"}},
{"bool": {
"must": [
{"term":{"title":"django"}},
{"term":{"salary":30}}
]
}}
]
}
}
}
# 过滤空和非空
# 建立测试数据
POST lagou/testjob2/_bulk
{"index":{"_id":"1"}}
{"tags":["search"]}
{"index":{"_id":"2"}}
{"tags":["search","python"]}
{"index":{"_id":"3"}}
{"other_field":["some data"]}
{"index":{"_id":"4"}}
{"tags":null}
{"index":{"_id":"5"}}
{"tags":["search",null]}
# 处理null空值的方法
# select tags from testjob2 where tags is not NULL
GET lagou/testjob2/_search
{
"query": {
"bool": {
"filter": {
"exists": {
"field": "tags"
}
}
}
}
}
# 查询为空值的字段
GET lagou/testjob2/_search
{
"query": {
"bool": {
"must_not": {
"exists":{
"field":"tags"
}
}
}
}
}
scrapy写入数据到elasticsearch中
将数据写入到 elasticsearch 中,需要用到一个 elasticsearch 官方提供的 Python 接口
elasticsearch-dsl
GitHub 地址:https://github.com/elastic/elasticsearch-dsl-py
文档:http://elasticsearch-dsl.readthedocs.io/en/latest/
需要通过 pip 安装
pip install elasticsearch-dsl==5.2.0
image.png查看文档示例可以发现与 Django 的 Models 极为相似,很明显是借鉴了 Django
image.png为了和 Django 更加接近,在 ArticleSpider/ 目录下新建名为 models/ 的 python package,在内部新建一个 es_types.py 的模块
# ArticleSpider/models/es_types.py
from datetime import datetime
from elasticsearch_dsl import DocType, Date, Nested, Boolean, \
analyzer, Completion, Keyword, Text, Integer
from elasticsearch_dsl.connections import connections
# 指明连接的服务器
connections.create_connection(hosts=['localhost'])
class ArticleType(DocType):
"""
伯乐在线文章类型
"""
title = Text(analyzer='ik_max_word') # 需要进行分词,所以定义成 text
create_date = Date()
url = Keyword() # 无需分词
url_object_id = Keyword()
front_img_url = Keyword()
front_img_path = Keyword()
praise_nums = Integer()
comment_nums = Integer()
fav_nums = Integer()
tags = Text(analyzer='ik_max_word')
content = Text(analyzer='ik_max_word')
class Meta:
index = 'jobbole'
doc_type = 'article'
if __name__ == '__main__':
ArticleType.init() # 可以直接生成 mapping
image.png image.png运行
es_types.py
文件,没有报错,说明 mapping 创建成功
以上
es_types.py
文件中的语法都是基于 es5 的,与目前最新版 es6 语法上略有不同,在 es6 无法运行,具体可查看官方文档
编写 管道文件,定义
ElasticsearchPipeline
ITEM 中间件 用来将数据存入 Elasticsearch
# ArticleSpider/pipelines.py
from w3lib.html import remove_tags
from ArticleSpider.models.es_types import ArticleType
class ElasticsearchPipeline(object):
"""
将数据写入 Elasticsearch
"""
def process_item(self, item, spider):
# 将 item 转换为 es 数据
article = ArticleType()
article.title = item['title']
article.create_date = item['create_date']
article.content = remove_tags(item['content'])
article.front_img_url = item['front_img_url']
if 'front_img_path' in item:
article.front_img_path = item['front_img_path']
article.praise_nums = item['praise_nums']
article.fav_nums = item['fav_nums']
article.comment_nums = item['comment_nums']
article.url = item['url']
article.tags = item['tags']
article.meta.id = item['url_object_id']
article.save()
return item
将
ElasticsearchPipeline
配置到 settings 中
# ArticleSpider/settings.py
ITEM_PIPELINES = {
'ArticleSpider.pipelines.ElasticsearchPipeline': 1,
}
image.png运行 jobbole spider 调试代码
image.png数据可以正常写入 Elasticsearch
image.png可以正常查询数据
现在有个问题,就是传递过来的 item 如果不是 jobbole 的 item,而是其他 item,如 lagou item,这样的话,不同 spider 的 item 字段是不一样的,所以不能在一个 pipeline 中统一处理所有的 spider,所以可以跟之前存入 MySQL 数据库时候的做法一样,就是将这些逻辑放到 item 类本身中
将逻辑迁移到 items.py 中
# ArticleSpider/items.py
from w3lib.html import remove_tags
from ArticleSpider.models.es_types import ArticleType
class JobBoleArticleLoadItem(scrapy.Item):
...
def save_to_es(self):
"""
将数据存入 Elasticsearch
"""
# 将 item 转换为 es 数据
article = ArticleType()
article.title = self['title']
article.create_date = self['create_date']
article.content = remove_tags(self['content'])
article.front_img_url = self['front_img_url']
if 'front_img_path' in self:
article.front_img_path = self['front_img_path']
article.praise_nums = self['praise_nums']
article.fav_nums = self['fav_nums']
article.comment_nums = self['comment_nums']
article.url = self['url']
article.tags = self['tags']
article.meta.id = self['url_object_id']
article.save()
pipeline 中只需要调用
save_to_es
方法即可
# ArticleSpider/pipelines.py
class ElasticsearchPipeline(object):
"""
将数据写入 Elasticsearch
"""
def process_item(self, item, spider):
# 将 item 转换为 es 数据
item.save_to_es()
return item
image.png再次调试,数据依旧可以正常写入