flask-elasticsearch使用

###序言

生生被es折腾了2天,才勉强算明白一点python-es的用法,如果有使用或者理解上不对的地方,烦请一定指出!

需求:

小说网站内实现根据小说名字或者小说作者姓名模糊查询出对应的小说信息,使用flask框架和es实现。

为什么要用es?

es的好处百度一搜一大堆,简单来说就是实时搜索,稳定,可靠,快速!

关于es的一些说明:

1、在Elasticsearch中,文档归属于一种**类型(type)**,而这些类型存在于**索引(index)**中,画一个简单的对比图来类比传统关系型数据库:

```python
Relational DB -> Databases -> Tables -> Rows -> Columns
Elasticsearch -> Index -> Types -> Documents -> Fields
```

Elasticsearch集群可以包含多个**索引(Index)**(数据库),每一个索引可以包含多个**类型(types)**(表),每一个类型包含多个**文档(documents)**(行),然后每个文档包含多个**字段(Fields)**(列)

2、下面同步mysql数据到es时就遵循了这种对应关系,并且es内index、type、documents、Fields起名都和mysql相同,例如mysql的数据库叫novel,其中一张表叫tb_novel_detail,那么es中一个索引就叫novel,这个索引下的一个类型就叫tb_novel_detail。



废话不多说,进入正题!

主要步骤:

1、使用docker安装elasticsearch

2、使用mysqlsmom同步 Mysql 数据到 Elasticsearch(全量同步和分析binlog的增量同步)

3、安装python使用的elasticsearch模块

4、查询

###1、docker安装elasticsearch

- 获取镜像,可以通过网络pull

```shell
docker image pull delron/elasticsearch-ik:2.4.6-1.0
```

- 修改elasticsearch的配置文件 elasticsearc-2.4.6/config/elasticsearch.yml第54行,更改ip地址为本机ip地址

```shell
network.host: 172.16.125.139 # 此处为你的ip地址,Linux系统下ifconfig命令就可以查看
```

配置文件下载地址:链接:https://pan.baidu.com/s/1yziQxGZTlkeezFGSq5aHDg 密码:60bq

配置文件修改之后创建容器时需要映射进去

- 创建docker容器运行

```shell
docker run -dti --network=host --name=elasticsearch -v /home/python/elasticsearch-2.4.6/config:/usr/share/elasticsearch/config delron/elasticsearch-ik:2.4.6-1.0
```

注意:此条启动命令mac系统下docker无法正常启动,可能是因为--network=host在mac下无法映射,我是使用ubuntu系统安装的docker启动的

###2、使用mysqlsmom同步 Mysql 数据到 Elasticsearch(全量同步和分析binlog的增量同步)

下载mysqlsmom:https://github.com/m358807551/mysqlsmom/blob/master/docs/source/hello.md

mysqlsmom使用文档:https://mysqlsmom.readthedocs.io/en/latest/hello.html#

注:官方要求:

1、运行时使用python2.7

2、开启redis

3、mysql开启binlog,并且设置binlog-format=ROW(binlog增量同步时需要)

#####以下为我mysqlsmom配置文件设置方法

- 全量同步

设置:init_config.py(以同步tb_novel_detail表为例)

```python
# coding=utf-8

STREAM = "INIT"

# 修改数据库连接
CONNECTION = {
'host': '127.0.0.1',
'port': 3306,
'user': 'root',
'passwd': 'mysql'
}

# 一次同步 BULK_SIZE 条数据到elasticsearch,不设置该配置项默认为1
BULK_SIZE = 1

# 修改elasticsearch节点
NODES = [{"host": "172.16.125.139", "port": 9200}]

TASKS = [
{
"stream": {
"database": "novel", # 在此数据库执行sql语句
"sql": "select * from tb_novel_detail", # 将该sql语句选中的数据同步到 elasticsearch
# "pk": {"field": "id", "type": "char"} # 当主键id的类型是字符串时
},
"jobs": [
{
"actions": ["insert", "update"],
"pipeline": [
{"set_id": {"field": "id"}} # 默认设置 id字段的值 为elasticsearch中的文档id
],
"dest": {
"es": {
"action": "upsert",
"index": "novel", # 设置 index
"type": "tb_novel_detail", # 设置 type
"nodes": NODES
}
}
}
]
}
]

# CUSTOM_ROW_HANDLERS = "./my_handlers.py"
# CUSTOM_ROW_FILTERS = "./my_filters.py"
```

- binlog的增量同步

```python
# coding=utf-8

STREAM = "BINLOG" # "BINLOG" or "INIT"
SERVER_ID = 99 # mysql的my.cnf文件内的server_id值和这里要相同
SLAVE_UUID = __name__

# 一次同步 BULK_SIZE 条数据到elasticsearch,不设置该配置项默认为1
BULK_SIZE = 1

BINLOG_CONNECTION = {
'host': '127.0.0.1',
'port': 3306,
'user': 'root',
'passwd': 'mysql'
}

# redis存储上次同步位置等信息
REDIS = {
"host": "127.0.0.1",
"port": 6379,
"db": 6,
# "password": "password", # 不需要密码则注释或删掉该行
}


NODES = [{"host": "172.16.125.139", "port": 9200}]

TASKS = [
{
"stream": {
"database": "novel",
"table": "tb_novel_detail"
},
"jobs": [
{
"actions": ["insert", "update"],
"pipeline": [
# {"only_fields": {"fields": ["id", "name", "age"]}},
{"set_id": {"field": "id"}}
],
"dest": {
"es": {
"action": "upsert",
"index": "novel",
"type": "tb_novel_detail",
"nodes": NODES
}
}
},
# 重点在这里,配置删除
{
"actions": ["delete"], # 当读取到 binlog 中该表的删除操作时
"pipeline": [{"set_id": {"field": "id"}}], # 要删除的文档 _id
"dest": {
"es": {
"action": "delete", # 在 es 中执行删除操作
"index": "novel",
"type": "tb_novel_detail",
"nodes": NODES # 与上面的 index 和 type 相同
}
}
}
]
},
{
"stream": {
"database": "novel",
"table": "tb_author"
},
"jobs": [
{
"actions": ["insert", "update"],
"pipeline": [
# {"only_fields": {"fields": ["id", "name", "age"]}},
{"set_id": {"field": "id"}}
],
"dest": {
"es": {
"action": "upsert",
"index": "novel",
"type": "tb_author",
"nodes": NODES
}
}
},
# 重点在这里,配置删除
{
"actions": ["delete"], # 当读取到 binlog 中该表的删除操作时
"pipeline": [{"set_id": {"field": "id"}}], # 要删除的文档 _id
"dest": {
"es": {
"action": "delete", # 在 es 中执行删除操作
"index": "novel",
"type": "tb_author",
"nodes": NODES # 与上面的 index 和 type 相同
}
}
}
]
}
]

# CUSTOM_ROW_HANDLERS = "./my_handlers.py"
# CUSTOM_ROW_FILTERS = "./my_filters.py"
```

mysqlsmom的具体使用方法看使用文档,很简单

### 3、安装python使用的elasticsearch模块

```shell
pip install elasticsearch

from elasticsearch import Elasticsearch
es = Elasticsearch([{'host':'172.16.125.139','port':9200}])
```

###4、查询

实现搜索查询需求的思路:

思路1:

- 通过搜索关键词直接在es内查询后得到所需要的所有数据,拿到es返回的这些数据可直接满足需求,无需再去mysql查询:

所需数据:id(小说id),name(小说名称),ncp_id(此此小说最新章节id),ncp_title(此小说最新章节标题),author(作者名字),words(字数),update_time(更新时间),update_status(更新状态)

- 其中id,name,words,update_time,update_status来自mysql的tb_novel_detail表,也就对应到es内的tb_novel_detail类型(type)

- ncp_id,ncp_title来自tb_chapter表,也就对应到es内的tb_chapter类型(type)

- author来自tb_author表,···

而如何能让es搜索一个关键词时同时返回3个type的信息呢?mysql有连表查询,es是否有呢?答案是有(nested、parent和child关联查询、ES6.X 新类型Join),但是研究了一下这三个,没完全弄明白!而且es的连表查询是否能达到要求,还不可知。放弃···

nested、parent和child关联查询的部分说明:https://blog.csdn.net/tuposky/article/details/80988915

ES6.X 新类型Join部分说明:https://blog.csdn.net/laoyang360/article/details/79774481

- 如果没办法连表查询,是否可以在mysql里把这三张表创建一个视图,然后把这个视图同步到es里作为一个type,如果这样再查询,返回这个type里的所有数据,即满足要求。
- 实际操作发现,确实可以用mysqlsmom全量同步视图的数据到es生成一个type,但是基于binlog的增量同步却无法同步视图内数据的变化,个人分析可能是因为视图并不是真正的表,其内数据变化并不会产生binlog日志,所以此想法也作废。

- 又尝试是否可以修改mysqlsmom代码,在其中自定义函数,以此来实现直接把三张表的数据同步到es时直接合并成一个type,一阵苦思冥想后···放弃···

思路2:

- 既然无法直接在es内查询后得到所需要的所有数据,那么是否可以在es内搜索查询得到所有符合条件的小说id,然后拿着这些小说id到mysql连表查询出所需要的所有数据,答案是可以,我就是这么做来实现需求的,但是这么做的坏处是增加了mysql查询的步骤,如果搜索结果的数据量很大,性能肯定不如直接从es拿到所有数据。



#####以下是es增删改查的一些方法,最后是我实现此需求的最终代码

查询:

```shell
# 常用参数
index - 索引名
q - 查询指定匹配 使用Lucene查询语法
from_ - 查询起始点 默认0
doc_type - 文档类型
size - 指定查询条数 默认10
field - 指定字段 逗号分隔
sort - 排序 字段:asc/desc
body - 使用Query DSL
scroll - 滚动查询
```

#####查询所有数据

```python
body = {
    "query":{
        "match_all":{}
    },
"size":2 # 设置返回2条数据,默认是10条
}
response = es.search(index="novel", doc_type="tb_novel_detail", body=body)
# response是一个字典
```

返回的数据默认显示为十条数据,其中hits[“total”]为查询数量总数

response:

```python
{
'_shards': {
'total': 5,
'failed': 0,
'successful': 5
},
'took': 1,
'timed_out': False,
'hits': {
'total': 135,
'max_score': 1.0,
'hits': [
{
'_index': 'novel',
'_source': {
'id': 48,
'reads': 0,
'words': '410819',
'category_id': 8,
'author_id': 45,
'update_time': '2017-02-09T08:03:32',
'intro': ' 一首家喻户晓的《伤仲永》,让悲催的方仲永同志家喻户晓。穿越历史重重的迷雾,附身方仲永,欢乐在大宋。\n 深挖史料,披露北宋神童文化始末,宋绶,吕夷简,晏殊,范仲淹,苏轼,王安石,司马迁……跟随方仲永的脚步,追看神童背后的权谋纵横。\n 弯弓射酒,用最流氓的手段对待流氓,强军大宋,挥斥方遒,用最风流的态度对待风流,快意人间。\n 从最仆街的废柴神童,到男神级别丈夫堂堂,他融入大宋,改造大宋,一步步踏上自己的逆袭之路。\n作者',
'image': 'https://www.biqukan.com/files/article/image/22/22879/22879s.jpg',
'name': '大宋第一废柴神童',
'update_status': 1
},
'_score': 1.0,
'_id': '48',
'_type': 'tb_novel_detail'
},
{
'_index': 'novel',
'_source': {
'id': 52,
'reads': 3,
'words': '11613006',
'category_id': 9,
'author_id': 49,
'update_time': '2016-03-15T07:20:53',
'intro': ' 一个大山里走出来的绝世高手,一块能预知未来的神秘玉佩……林逸是一名普通学生,不过,他还身负另外一个重任,那就是追校花!而且还是奉校花老爸之命!虽然林逸很不想跟这位难伺候的大小姐打交道,但是长辈之命难违抗,他不得不千里迢迢的转学到了松山市,给大小姐鞍前马后的当跟班……于是,史上最牛B的跟班出现了——大小姐的贴身高手!看这位跟班如何发家致富偷小姐,开始他奉旨泡妞牛X闪闪的人生……本书有点儿纯……也有点儿小暧昧……\n作者',
'image': 'https://www.biqukan.com/images/nocover.jpg',
'name': '校花之贴身高手',
'update_status': 1
},
'_score': 1.0,
'_id': '52',
'_type': 'tb_novel_detail'
}
]
}
}
```

广泛匹配某个字段

```python
body = {
"query": {
"match": {
"name": "三"
}
}
}
Match默认匹配某个字段
response = es.search(index="novel",doc_type="tb_novel_detail",body=body)
```

匹配多个字段

```python
body = {
"query": {
"bool": {
"should": [
{"match": {"name": "大"}},
{"match": {"intro": "大"}}
],
}
}
}
Should或匹配可以匹配某个字段也可以匹配所有字段,其中至少有一个语句要匹配,与 OR 等价
response = es.search(index="novel", doc_type="tb_novel_detail",body=body,scroll='5s')
```

匹配所有字段

```python
body = {
"query": {
"bool": {
"must": [
{"match": {"name": "大"}},
{"match": {"intro": "大"}},
],
}
}
}
Must必须匹配所有需要查询的字段
response = obj.search(index="novel", doc_type="tb_novel_detail",body=body,scroll='5s')
```

注意:

- match查询会先分词,例如查询:三国演义,那么先会分词:三国、演绎、三、国、演、义等,只要有一个词匹配上,即输出结果。
- 如果想输出的结果必须包含三国演义四个字,就不要用match,用match_phrase



分别在type`tb_novel_detail`的name字段和type`tb_author`的author字段里查询搜索的关键词,注意!返回response里包含的是各个type里的数据。

```python
body = {
"query": {
"bool": {
"should": [
{"match_phrase": {"name": 三国}},
{"match_phrase": {"author": content}},
],
}
},
"from": 0, # 从第0条数据开始
"size": 10000 # 获取10000条数据
}
# es可以用from和size配合来实现翻页功能,from设置从第几条开始,size设置每页返回数据数量,默认是10。

response = es.search(index="novel", doc_type=["tb_novel_detail", "tb_author"], body=body)
```

response:

```python
{
'_shards': {
'failed': 0,
'total': 5,
'successful': 5
},
'took': 4,
'timed_out': False,
'hits': {
'max_score': 4.087193,
'total': 2,
'hits': [
{
'_index': 'novel',
'_source': {
'update_status': 1,
'update_time': '2016-12-23T12:57:47',
'name': '三界狂徒',
'author_id': 37,
'id': 40,
'intro': ' 别人是群穿,本书是群死……\n 作为一名有抱负的资深混混,天生就注定不能安分守己,只要有兄弟,无论上揽穹顶落黄泉,我一样能搅个天翻地覆!\n 一部主角在第一章就狗带的书,且看小混混如何掀翻阎罗殿,跳出阴冥逆而伐天,为你揭开三界遗留的那些辛秘。\n 喂,天帝老儿,这个月的保护费是不是得交了啊?!\n ………………\n 不要在意作者的名字,我妈说我五行缺水\n作者',
'words': '817518',
'category_id': 5,
'reads': 0,
'image': 'https://www.biqukan.com/files/article/image/24/24125/24125s.jpg'
},
'_score': 4.087193,
'_type': 'tb_novel_detail',
'_id': '40'
},
{
'_index': 'novel',
'_source': {
'author': '耳根',
'id': 8
},
'_score': 1.7255187,
'_type': 'tb_author',
'_id': '8'
}
]
}
}
```

还有一些我觉得有帮助的帖子:

https://www.cnblogs.com/yjf512/p/4897294.html

https://blog.csdn.net/u011587401/article/details/77476858

https://blog.csdn.net/m_z_g_y/article/details/82628972

https://blog.csdn.net/laoyang360/article/details/79048455

es权威指南中文版:https://es.xiaoleilu.com/010_Intro/30_Tutorial_Search.html

Python Elasticsearch Client:https://elasticsearch-py.readthedocs.io/en/master/

Elasticsearch: 权威指南:https://www.elastic.co/guide/cn/elasticsearch/guide/current/index.html



以下是我在falsk里具体实现需求的代码

```python
from . import es_blue
from flask import g, render_template, request, redirect, url_for

from utils.common import user_login_data
from .utils import es_select


@es_blue.route('/search', methods=['get'])
@user_login_data
def search():
user = g.user
# 获取参数
content = request.args.get("q", None)
current_page = request.args.get("p", None)
# 校验参数
if not content: # 如果没有输入搜索内容,则返回首页
return redirect(url_for("index.index"))
try:
current_page = int(current_page)
except:
current_page = 1
if current_page < 0: # 如果current_page不是整数或是负数,则current_page=1
current_page = 1
# 调用es进行搜索查询
novel_detail_list, total_page = es_select(content, current_page)
if len(novel_detail_list) == 1: # 如果搜索结果只有一本小说,直接重定向到此小说的章节详情页
return redirect(url_for("novel_detail.novel_detail", novel_id=novel_detail_list[0].id))
data = {
"user": user,
"novel_detail_list": novel_detail_list,
"content": content,
"current_page": current_page,
"total_page": total_page
}
return render_template('search_ret.html', data=data)
```

```python
from config import SEARCH_PAGE_COUNT, es
from utils.dbutils import *
from utils.models import NovelDetail
import math


def es_select(content, current_page):
body = {
"query": {
"bool": {
"should": [
{"match_phrase": {"name": content}},
{"match_phrase": {"author": content}},
],
}
},
"from": 0, # 从第0条数据开始
"size": 10000 # 获取10000条数据
}
response = es.search(index="novel", doc_type=["tb_novel_detail", "tb_author"], body=body)
 # resopnse是字典,结构样式见最下方注释
novel_detail_list = []
for item in response["hits"]["hits"]:
if item["_type"] == "tb_author": # 如果搜索内容是作为作者名查询到的,那么返回的是type(tb_author)的数据,可以拿到author_id,然后通过author_id查询对的一部或多部小说相关信息
author_id = item["_source"]["id"]
sql_str = "select * from tb_novel_all_date where au_id=%d" % author_id

else: # 如果搜索内容是作为小说名查询到的,那么返回的是type(tb_novel_detail)的数据,可以拿到小说id,然后通过小说id查询此小说相关信息
novel_id = item["_source"]["id"]
sql_str = "select * from tb_novel_all_date where id=%d" % novel_id
rets = selectsqls(sql_str)
for i in rets:
model = NovelDetail()
model.id = i[0]
model.name = i[1]
model.newest_chapter_id = i[2]
model.newest_chapter = i[3]
model.author = i[5]
model.words = i[6]
model.update_time = i[7].strftime("%Y-%m-%d")
model.update_status = i[8]
novel_detail_list.append(model)
# 分页的总页数
total_page = math.ceil(len(novel_detail_list) / SEARCH_PAGE_COUNT)
# 对列表进行切片,以达到分页的目的
# 不用es的from和size分页在于,如果搜索内容是通过tb_autor类型查到的,虽然只返回一条author_id数据,但是此author_id可能对应多本小说,而浏览器是逐条展示每本小说的信息,所以无法用es分页
novel_detail_list = novel_detail_list[(current_page - 1) * SEARCH_PAGE_COUNT: current_page * SEARCH_PAGE_COUNT]
return novel_detail_list, total_page

# response数据样式,例:
# {
# "took": 8,
# "timed_out": False,
# "_shards": {
# "total": 5,
# "failed": 0,
# "successful": 5
# },
# "hits": {
# "total": 2,
# "hits": [
# {
# "_index": "novel",
# "_id": "11",
# "_score": 5.021118,
# "_source": {
# "name": "三界独尊",
# "id": 11,
# "reads": 2,
# "update_time": "2016-02-29T05:16:08",
# "image": "https://www.biqukan.com/files/article/image/0/973/973s.jpg",
# "author_id": 5,
# "words": "6899498",
# "update_status": 0,
# "intro": " 天帝之子江尘,转生在一个被人欺凌的诸侯少年身上,从此踏上一段轰杀各种天才的逆袭之路。\n 在江尘面前,谁也没资格自称天才,因为,没有哪一个天才,能比天帝之子更懂天。\n 天才?顺我者天,逆我者渣!\n =============================\n 犁天代表作品",
# "category_id": 5
# },
# "_type": "tb_novel_detail"
# },
# {
# "_index": "novel",
# "_id": "8",
# "_score": 1.8044125,
# "_source": {
# "id": 8,
# "author": "耳根"
# },
# "_type": "tb_author"
# }
# ],
# "max_score": 5.021118
# }
# }
```

前端html代码:

```html
{% extends 'base.html' %}

{% block css_block %}
<link href="/css/jquery.pagination.css" rel="stylesheet" type="text/css"/>
{% endblock %}

{% block detail_block %}
<div id="main">

<div id="content">

<table class="grid" width="980px" align="center">

{% if data.novel_detail_list %}
<caption>搜索结果</caption>

<tr align="center" style="height:30px;">

<th width="20%">文章名称</th>

<th width="40%">最新章节</th>

<th width="15%">作者</th>

<th width="9%">字数</th>

<th width="10%">更新</th>

<th width="6%">状态</th>

</tr>
{% for foo in data.novel_detail_list %}
<tr id="nr">

<td class="odd"><a href="/book/{{ foo.id }}">{{ foo.name }}</a></td>

<td class="even"><a href="/books/{{ foo.id }}/{{ foo.newest_chapter_id }}.html" target="_blank"> {{ foo.newest_chapter }}</a></td>

<td class="odd">{{ foo.author }}</td>

<td class="even">{{ foo.words }}</td>

<td class="odd" align="center">{{ foo.update_time }}</td>

<td class="even" align="center">{% if foo.update_status == 0 %}完结{% else %}连载{% endif %}</td>

</tr>
{% endfor %}
{% else %}
<div id="tips" class="tips">

抱歉,搜索没有结果^_^

</div>
{% endif %}



</table>




<div class="box">
<div id="pagination" class="page"></div>
</div>


<style>

.pagelink {
text-align: center;
margin-top: 10px;
}

.pagelink a {
border: 1px solid #C3DFEA;
padding: 3px 7px;
margin: 2px;
background-color: #E1ECED
}

kbd {
display: none
}

em {
border: 1px solid #C3DFEA;
padding: 3px 7px;
margin: 2px;
background-color: #E1ECED
}

.first, .last, strong {
border: 1px solid #C3DFEA;
padding: 3px 7px;
margin: 2px;
background-color: #E1ECED
}

strong {
background-color: #88C6E5
}

.tips {
text-align: center;
border: 1px solid #C3DFEA;
padding: 10px 0px;
margin-top: 10px;
margin-bottom: 20px;
background-color: #E1ECED;
font-weight: bold
}

</style>

<script type="text/javascript" src="/xxgg/jquery.pagination.min.js"></script>
<script>
$(function() {
$("#pagination").pagination({
currentPage: {{ data.current_page }},
totalPage: {{ data.total_page }},
callback: function(current) {
window.location = "/search?p=" + current + "&q=" + '{{ data.content }}'
}
});
});
</script>
</div>

</div>

</div>
{% endblock %}

{% block foot_block %}
<div class="footer">

<hr>

<p>本站所有小说为转载作品,所有章节均由网友上传,转载至本站只是为了宣传本书让更多读者欣赏。</p>

<p>Copyright &#169; 趣看小说 All Rights Reserved. </p>

</div>

</body>

</html>
{% endblock %}
```




posted on 2019-01-22 15:29  A-Way  阅读(376)  评论(0)    收藏  举报

导航