elasticsearch教程
概念
数据库性能对比
数据库操作 | 性能对比 |
---|---|
update | elasticsearch < mongodb < mysql |
insert, select | mongodb > mysql |
elasticsearch
集合了数据存储和分析的功能,主要用途是作为搜索引擎,搜索性能远远大于其他的数据库
术语
elasticsearch | mysql |
---|---|
index(索引)(做为名词时) | 数据库 |
type(类型) | 表 |
document(文档) | 行(一条数据) |
fields | 列 |
index(索引)做为动词时代表插入的意思
HTTP方法
HTTP1.0定义了三种请求方法: GET, POST, HEAD方法
HTTP1.1新增了五种方法: OPTIONS, PUT, DELETE, TRACE, CONNECT
方法 | 描述 | |
---|---|---|
GET | 请求指定的页面信息,并返回实体主体 | 查询 |
POST | 向指定资源提交数据进行处理请求。数据被包含在请求体中 | 增加和修改 |
PUT | 向服务器传送的数据取代指定的文档的内容 | |
DELETE | 请求服务器删除指定的页面 | 删除 |
倒排索引(inverted index)
搜索引擎底层索引存储都采用倒排索引,是区别于关系型数据库和NoSql
数据库的核心。
将数据存储前,进行内容分词,将所有的分词和该数据进行记录。例如:
关键词 | 文章 | 倒排列表(文章,位置, 次数等) |
---|---|---|
python | 文章1, 文章3 | (文章1, ❤️,10>, 2) |
聊天 | 文章2 | (文章2, <12,25,100>, 3) |
系统 | 文章3, 文章4 | (文章3, <10>, 1) |
屏蔽脏话 | 文章5 | (文章5, <50,60>, 2) |
功能原理 | 文章6, 文章7,文章8 | (文章6, <56,57,58> , 3) |
其他技术问题:
- 大小写转换问题,PYTHON应该转换为小写
- 词干抽取, looking和look应该处理为一个词
- 分词,若屏蔽系统 应该分词为"屏蔽、系统"还是应该为"屏蔽系统"
- 倒排索引文件过大 - 压缩编码
安装
安装JDK8
下载列表
确定安装java安装版本是不是1.8:
java -version
配置JAVA_HOME
环境变量:
JAVA_HOME
环境变量是指向jdk
的安装目录C:\Program Files\Java\jdk1.8.0_172
安装elasticsearch-rtf
elasticsearch
中文发行版,针对中文集成了相关插件。与elasticsearch
没有区别,只是多了很多插件。elasticsearch-rtf
elasticsearch
官网下载安装:官网下载列表
启动
E:\gitProject\elasticsearch-rtf\bin> elasticsearch.bat
安装插件
nodejs
插件依赖(下载)
cnpm
淘宝npm
镜像(安装)
npm install -g cnpm --registry=https://registry.npm.taobao.org
head
插件
基于浏览器的elasticsearch
数据库客户端(下载)
克隆
head
插件git clone git://github.com/mobz/elasticsearch-head.git
进入插件根目录
cd elasticsearch-head
安装依赖包
cnpm install
启动服务
cnpm run start
打开服务
解决未连接问题(默认情况不允许插件连接服务)
>>> vim E:\gitProject\elasticsearch-rtf\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"
ik
插件
中文的分析器,elasticsearch-rtf
已经集成了IK,不需要安装
kibana ——Dev Tools
专用于Elasticsearch
的REST接口交互插件 kibana下载
注意: 版本号必须要于elasticsearch
版本对应
启动:kibana-5.1.2-windows-x86/bin/kibana.bat
打开: http://127.0.0.1:5601
操作
创建和修改
# 索引创建操作
# 指定分片和副本的数量(shards一旦设置不可修改)
PUT lagou
{
"settings":{
"index": {
"number_of_shards": 5, # 默认是5
"number_of_replicas": 1 # 默认是1
}
}
}
# 更新settings
PUT lagou/_settings
{
"number_of_replicas": 2
}
# 与很多NoSql数据库一样不需要先定义表
# 修改文档
# 在lagou数据库job表中id为1的位置修改文档(如果没有数据就新建,如果有数据就删除源数据后覆盖)
PUT lagou/job/1
{
"title": "python分布式爬虫开发",
"salary_min": 15000,
"city": "北京",
"company": {
"name": "百度",
"company_addr": "北京市软件园"
},
"publish_date": "2017-4-16",
"comments": 15
}
# 新建文档
POST lagou/job/ # (可以不指定id,会自动生成一个UUID)
{
"title": "python web 后端工程师",
"salary_min": 30000,
"city": "上海",
"company": {
"name": "美团",
"company_addr": "北京市软件园A区"
},
"publish_date": "2017-4-17",
"comments": 20
}
# 增量修改文档
POST lagou/job/1/_update
{
"doc": {
"comments": 20 # 只修改这个字段
}
}
查找
# 获取索引的settings值的几种方式
GET lagou/_settings
GET _all/_settings
GET .kibana,lagou/_settings
GET _settings
# 获取所有索引
GET _all
# 获取拉钩网站的索引信息
GET lagou
# 获取文档
GET lagou/job/1
# 获取文档某个字段内容
GET lagou/job/1?_source=title,city
GET lagou/job/1?_source
删除
# 删除lagou数据库job表的id为1的数据
DELETE lagou/job/1
# 删除lagou索引
DELETE lagou
批量操作
批量导入可以合并多个操作,比如index, delete, update, create等。也可以帮助从一个索引导入到另一个索引。
- 每一条数据都由两行构成(delete除外),不可以格式化成多行。
- 命令都是由元信息行和数据行组成
- update的数据可能是doc也可能是upsert或者script
# 在testdb数据库testtable表的id为1的位置创建一条数据
POST _bulk
{"index": {"_index": "testdb", "_type":"testtable", "_id": "1"}}
{"field1": "value1"}
# delete只有一行,没有数据行
{"delete": {"_index": "testdb", "_type": "testtable", "_id": "2"}}
{"craete": {"_index": "testdb", "_type": "testtable", "_id": "3"}}
{"field1": "value1"}
{"update": {"_index": "testdb", "_type": "testtable", "_id": "3"}}
{"doc": {"field1": "value1"}}
批量查询
# 查询不同数据库中的数据
GET _mget
{
"docs":[
{
"_index": "lagou",
"_type": "job",
"_id": 1
},
{
"_index": "testdb",
"_type": "job2",
"_id": 2
}
]
}
# 查询相同数据库中不同表的数据
GET testdb/_mget
{
"docs":[
{
"_type": "job1",
"_id": 1
},
{
"_type": "job2",
"_id": 2
}
]
}
# 查询同一张表下的不同数据
GET testdb/job1/_mget
{
"docs": [
{
"_id": 1
},
{
"_id": 2
}
]
}
# 查询同一张表下的不同数据(简写)
GET testdb/job1/_mget
{
"ids": [1,2]
}
映射(mapping)
创建索引的时候,可以预先定义字段的类型以及相关属性
Elasticsearch会根据JSON源数据的基础类型猜测你想要的字段映射,将输入的数据转变成可搜索的索引项。
Mapping就是我们自己定义的字段的数据类型,同时配置如果索引数据以及是否可以被搜索。
作用: 会让索引建立的更加细致和完善
类型: 静态映射和动态映射
内置类型
内置类型 | |
---|---|
string类型 | text(会被分析), keyword(不会被分析和建立倒排索引,需要完全匹配) |
数字类型 | long(长整数), integer(整数), short(短整数), byte, double, float |
日期类型 | date(包括时分秒) |
bool类型 | boolean(True, False, Yes, No等) |
binary类型 | binary(二进制类型不会被检索) |
复杂类型 | object(字典类型), nested(字典类型的一个数组) |
geo类型 | geo-point(通过经纬度标识地理位置), geo-shape(通过多个点标识片区) |
专业类型 | ip(ip地址), competion(搜索建议) |
常用属性
属性 | 描述 | 适合类型 |
---|---|---|
store | 值为yes表示存储,为no表示不存储,默认为no | all |
index | yes表示分析,no表示不分析,默认为true | text (其他字段一律no) |
null_value | 如果字段为空,可以设置一个默认值 | all |
analyzer | 设置索引和搜索时的分析器,默认为standard,中文常见的是IK | all |
include_in_all | 默认为_all(让文档每一个字段都被搜索到)。可以在文档字段上加上include_in_all=false 不让某个字段被搜索到 | all |
format | 时间格式字符串的模式 | date |
# 参考数据
PUT lagou/job/1
{
"title": "python分布式爬虫开发",
"salary_min": 15000,
"city": "北京",
"company": {
"name": "百度",
"company_addr": "北京市软件园"
},
"publish_date": "2017-4-16",
"comments": 15
}
# 创建索引
PUT lagou
{
"mappings": { # 固定写法
"job": { # 表
"properties": { # 固定写法
"title": { # 键
"store": true, # 将字段保存起来
"type": "text", # 键的数据类型
"analyzer": "ik_max_word" # text字段会加入倒排索引,需要分析器分析
},
"salary_min": {
"type": "integer"
},
"city": {
"type": "keyword" # 不会分析和分词,必须是原样匹配
},
"company": { # object类
"properties": {
"name": {
"type": "text" #如果没有设置分析器,会使用内置分析器对中文分词,效果一般
},
"company_addr": {
"type": "text"
},
"employee_count": {
"type": "integer"
}
}
},
"publish_date": {
"type": "date",
"format": "yyyy-MM-dd"
},
"comments": {
"type": "integer"
}
}
}
}
}
# 获取mapping信息
GET lagou/_mapping/job
GET _all/_mapping/job
# 获取所有mapping信息
GET _all/_mapping
# 字段一旦设置了类型就不能修改
查询
Elasticsearch是功能非常强大的搜索引擎,使用它的目的就是为了快速的查找到需要的数据。
查询分类
- 基本查询:使用Elasticsearch内置查询条件进行查询
- 组合查询:把多个查询组合在一起进行复合查询
- 过滤:查询同时,通过filter条件在不影响打分的情况下筛选数据
match查询
#GET lagou/_search # job可不写
GET lagou/job/_search
{
"query": {
"match": {
"title": "python网站" # 分词器会在分词后再去匹配
}
}
}
term查询
取确定的值
GET lagou/_search
{
"query": {
"term": {
"title": "python网站" # 不会分词,直接字符串完全匹配,也不是包含关系,是等于关系
}
}
}
terms查询
GET lagou/_search
{
"query": {
"terms": {
"title": ["python", "web"] # 任何一个满足都会返回结果
}
}
}
match_all查询
GET lagou/job/_search
{
"query": {
"match_all": {} # 返回所有数据
}
}
控制查询的返回数量
#GET lagou/_search # job可不写
GET lagou/job/_search
{
"query": {
"match": {
"title": "python网站" # 分词器会在分词后再去匹配
}
},
"from": 0, # 从查询结构的第几个开始
"size": 2 # 取几个数据
}
match_phrase查询
# 短语查询
GET lagou/_search
{
"query": {
"match_phrase": {
"title": {
"query": "python系统", # 分词后同时都需要匹配后的结果
"slop": 6 # 词之间的最小距离
}
}
}
}
multi_match查询
可以指定多个字段
# 查询title和desc这两个字段里面任意一个字段包含python的关键词文档
GET lagou/_search
{
"query": {
"multi_match": {
"query": "python",
"fields": ["title^3", "desc"] # title的权重设置成3倍的
}
}
}
指定返回的字段
GET lagou/_search
{
"stored_fields": ["title", "company_name"], # 指定返回的存储字段(store=false的字段不会被返回)
"query": {
"match": {
"title": "python"
}
}
}
通过sort把结果排序
GET lagou/_search
{
"query": {
"match_all": {}
},
"sort": [{
"comments": {
"order": "desc" # 降序排序
}
}]
}
查询范围
GET lagou/_search
{
"query": {
"range": {
"comments": {
"gte": 10, # 大于等于
"lte": 20, # 小于等于
"boost": 2.0 # 权重是多少
}
}
}
}
GET lagou/_search
{
"query": {
"range": {
"add_time": {
"gt": "2017-04-01", # 大于
"lt": "now" # 小于
}
}
}
}
wildcard查询
支持通配符的模糊查询
GET lagou/_search
{
"query": {
"wildcard": {
"title": {
"value": "pyth*n", # *为通配符
"boost": 2.0
}
}
}
}
bool查询
查询格式如下:
bool:{
"filter": [], # 对字段过滤,不参与打分
"must": [], # 必须同时满足条件(and)
"should": [], # 只需满足一个(or)
"must_not": {}, # 一个都不能满足(not)
}
最简单的filter查询
# select * from testjob where salary=20
GET lagou/testjob/_search
{
"query": {
"bool": {
"must": {
"match_all": {} # 取出所有数据。默认获取所有数据,这个可以不要。
},
"filter":{
"term":{
"salay": 20 # salay=20
}
}
}
}
}
查询多个值
GET lagou/testjob/_search
{
"query": {
"bool": {
"filter":{
"terms":{
"salay": [10,20] # salay=20 or salay=10
}
}
}
}
}
# select * from testjob where title="Python“
# term是完全匹配,但是分析器已经在倒排索引中转换为小写,可能查不到数据。match查询可以查到
GET lagou/testjob/_search
{
"query": {
"bool": {
"filter":{
"term":{
"title": "Python"
}
}
}
}
}
组合过滤查询
# select * from testjob where (salary=20 or title=Python) and (salary != 30)
GET lagou/testjob/_search
{
"query": {
"bool": {
"should": [
{"term":{"salary":20}},
{"term":{"title":"python"}} # "p"不能大写
],
"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": "elasticsearch"}},
{"term": {"salary": 30}}
]
}}
],
}
}
}
处理null空值的方法
# select tags from testjob2 where tags is not NULL
GET lagou/testjob2/_search
{
"query":{
"bool":{
"filter": {
"exists": { # 存在数据的字段(不为空)
"fields": "tags" # "exists","fields"是固定关键字
}
}
}
}
}
# select tags from testjob2 where tags is NULL
GET lagou/testjob2/_search
{
"query":{
"bool":{
"filter": {
"mush_not": { # 不存在数据的字段(为空或没有这个字段)
"fields": "tags" # "exists","fields"是固定关键字
}
}
}
}
}
fuzzy搜索
简单用法
GET jobbole/_search
{
"query": {
"fuzzy": {"title": "linx"} # 模糊搜索title字段,有一定的纠错性
},
"_source": ["title"] # 只显示title字段
}
GET /_search
{
"query": {
"fuzzy": {
"title": {
"value": "linx",
"fuzziness": 2, # 最小编辑距离
"prefix_length": 0 # 前缀长度(前缀不会被编辑)
}
}
},
"_source": ["title"]
}
编辑距离是一种字符串之间相似程度的计算方法,即两个字符串之间的编辑距离等于使一个字符串变成另外一个字符串而进行的插入,删除,替换,相邻字符交换位置而进行操作的最小次数。
比如:
ed("recoginze", "recognize") == 1
(需要交换两个相邻字符"i"和"n"的位置)
ed("sailn", "failing") == 3
(需要将"s"换成"f",在字母"l"后边插入"i","n"后面插入"g")关于编辑距离的求法,普遍的采用的是动态规划方法
suggest建议搜索
POST jobbole/_search?pretty
{
"suggest":{
"my-suggest": { # 变量名称,可以任意指定
"text": "linux", # 需要搜索的字符串
"completion": {
"field": "suggest", # 搜索suggest字段
"fuzzy": {
"fuzziness": 1 # 最小编辑距离
}
}
}
},
"_source": "title" # 只显示title字段
}
python用法
from django.views.generic.base import View
from search.models import ArticleType
class SearchSuggest(View):
def get(self, request):
text = request.GET.get('s','')
re_datas = []
if text:
s = ArticleType.search()
s = s.suggest(
'my_suggest', # # 变量名称,可以任意指定
text, # # 需要搜索的字符串
completion={"field":"suggest", "fuzzy":{"fuzziness":2},}
)
suggestions = s.execute_suggest()
for match in suggestions.my_suggest[0].options:
source = match._source
re_datas.append(source["title"])
return HttpResponse(json.dumps(re_datas), content_type="application/json")
查看分析器解析的结果
GET _analyze
{
"analyzer": "ik_max_word", # 最大分解
"text": "python网络"
}
GET _analyze
{
"analyzer": "ik_smart ", # 最小分解
"text": "python网络"
}
scrapy
写入数据
Elasticsearch的python驱动: elasticsearch-dsl-py
最新版也可以使用pip
安装:pip install elasticsearch-dsl
elasticsearch-dsl-py
官方英文文档: Elasticsearch DSL documentation
建立数据模型
>>> vim ArticleSpider/models/es_types.py
from datetime import datetime
from elasticsearch_dsl import DocType, Date, Integer, Keyword, Text
from elasticsearch_dsl.connections import connections
from elasticsearch_dsl import Completion
from elasticsearch_dsl.analysis import CustomAnalyzer as _CustomAnalyzer
# 定义一个默认的 Elasticsearch 客户端
connections.create_connection(hosts=['localhost'])
# 自定义分析器,避免BUG报错问题
class CustomAnalyzer(_CustomAnalyzer):
def get_analysis_definition(self):
return {}
# lowercase是做大小写转换
ik_analyzer = CustomAnalyzer('ik_max_word',filter=['lowercase'])
class ArticleType(DocType):
# 伯乐在线网站文章类型
# 搜索建议
#suggest = Completion(analyzer="ik_max_word") # 源码BUG,init可能会报错
suggest = Completion(analyzer=ik_analyzer, search_analyzer=ik_analyzer)
title = Text(analyzer='ik_max_word', search_analyzer="ik_max_word", fields={'title': Keyword()})
id = Text()
url = Text()
front_image_url = Text()
front_image_path = Text()
create_date = Date()
praise_nums = Integer()
comment_nums = Integer()
fav_nums = Integer()
tags = Text(analyzer='ik_max_word', fields={'tags': Keyword()})
content = Text(analyzer='ik_max_word')
class Meta:
index = 'jobbole' # 所属的数据库
doc_type = 'article' # 所属的表
#
# def save(self, ** kwargs):
# self.lines = len(self.body.split())
# return super(Article, self).save(** kwargs)
# def is_published(self):
# return datetime.now() < self.published_from
if __name__ == "__main__":
ArticleType.init() # 向数据库初始化数据
将数据保存到数据库
>>> vim ArticleSpider/pipelines.py
from models.es_types import ArticleType
from w3lib.html import remove_tags # 去掉html的标签
class ElasticSearchPipeline(object):
# 写入数据到es中
def analyze_tokens(self, text):
from models.es_types import connections
#es = connections.create_connection(ArticleType._doc_type.using)
es = connections.get_connection(ArticleType._doc_type.using)
index = ArticleType._doc_type.index
if not text:
return []
global used_words
# 调用es的analyze接口分析字符串
result = es.indices.analyze(index=index, analyzer='ik_max_word',
params={'filter': ['lowercase']}, body=text)
# 过滤掉一个字的
words = set([r['token'] for r in result['tokens'] if len(r['token']) > 1])
# 过滤掉已经存在的单词,set做减法
new_words = words.difference(used_words)
used_words.update(words)
return new_words
@classmethod
def from_settings(cls, settings):
dbparms = dict(
host=settings["MYSQL_HOST"],
db=settings["MYSQL_DBNAME"],
user=settings["MYSQL_USER"],
passwd=settings["MYSQL_PASSWORD"],
charset='utf8',
cursorclass=MySQLdb.cursors.DictCursor,
use_unicode=True,
)
dbpool = adbapi.ConnectionPool("MySQLdb", **dbparms)
return cls(dbpool)
def gen_suggests(self, title, tags):
# 根据字符串生成搜索建议数组
global used_words
used_words = set() # 去重
suggests = [] # 需要返回的数组
# 字符串和权重
for item, weight in ((title, 10), (tags, 3)):
item = self.analyze_tokens(item) # 分词
if item:
suggests.append({'input': list(item), 'weight': weight})
return suggests
#return [{"input": [],"weight": 2}]
@classmethod
def from_crawler(cls, crawler):
ext = cls()
ext.settings = crawler.settings
Article.init()
return ext
def process_item(self, item, spider):
article = ArticleType()
article.title = item["title"] # 赋值
article.create_date = item["create_date"]
article.content = remove_tags(item["content"]).strip().replace("\r\n","").replace("\t","")
article.front_image_url = item["front_image_url"]
if "front_image_path" in item:
article.front_image_path = item["front_image_path"]
article.praise_nums = item["praise_nums"]
article.comment_nums = item["comment_nums"]
article.fav_nums = item["fav_nums"]
article.url = item["url"]
article.tags = item["tags"]
article.meta.id = item["url_object_id"] # 设置id
# 生成搜索建议词
suggest = self.gen_suggests(article.title, article.tags)
article.suggest = suggest
article.save() # 保存数据
return item
配置pipeline
>>> vim ArticleSpider/settings.py
ITEM_PIPELINES = {
'ArticleSpider.pipelines.ElasticsearchPipeline': 300,
}
django中的使用
from elasticsearch import Elasticsearch # 是elasticsearch-dsl的底层操作接口
client = Elasticsearch(hosts=["127.0.0.1"])
class SearchView(View):
def get(self, request):
key_words = request.GET.get("q","")
s_type = request.GET.get("s_type", "article")
redis_cli.zincrby("search_keywords_set", key_words)
topn_search = redis_cli.zrevrangebyscore("search_keywords_set", "+inf", "-inf", start=0, num=5)
page = request.GET.get("p", "1")
try:
page = int(page)
except:
page = 1
jobbole_count = redis_cli.get("jobbole_count")
start_time = datetime.now()
response = client.search(
index= "jobbole",
body={
"query":{
"multi_match":{
"query":key_words,
"fields":["tags", "title", "content"]
}
},
"from":(page-1)*10,
"size":10,
"highlight": { # 高亮
"pre_tags": ['<span class="keyWord">'], # 自定义高亮的标签
"post_tags": ['</span>'], # 自定义高亮的结尾标签
"fields": { # 需要被高亮的字段
"title": {},
"content": {},
}
}
}
)
end_time = datetime.now()
last_seconds = (end_time-start_time).total_seconds()
total_nums = response["hits"]["total"]
if (page%10) > 0:
page_nums = int(total_nums/10) +1
else:
page_nums = int(total_nums/10)
hit_list = []
for hit in response["hits"]["hits"]:
hit_dict = {}
if "title" in hit["highlight"]:
hit_dict["title"] = "".join(hit["highlight"]["title"])
else:
hit_dict["title"] = hit["_source"]["title"]
if "content" in hit["highlight"]:
hit_dict["content"] = "".join(hit["highlight"]["content"])[:500]
else:
hit_dict["content"] = hit["_source"]["content"][:500]
hit_dict["create_date"] = hit["_source"]["create_date"]
hit_dict["url"] = hit["_source"]["url"]
hit_dict["score"] = hit["_score"]
hit_list.append(hit_dict)
return render(request, "result.html", {"page":page,
"all_hits":hit_list,
"key_words":key_words,
"total_nums":total_nums,
"page_nums":page_nums,
"last_seconds":last_seconds,
"jobbole_count":jobbole_count,
"topn_search":topn_search})