本节我们来完成新闻搜索功能, 首先让我们来思考一下,要做一个通过关键词搜索文章的功能,需要搜索哪些字段,以及使用什么技术方案呢?
既然我们是准备做新闻博客网站, 那我们就可以拿同类型网站的做一下对比, 例如CSDN, 简书, 博客园等, 这些常见的博客网站其主要覆盖的搜索字段有:
实现这些字段检索的技术方案有以下两种:
%like%
本项目选择使用过全文检索引擎。自行实现django框架和全文检索引擎的代码比较麻烦,抱着不重复造轮子的原则,这里我们选用django的第三方包djangohaystack
。它支持多种全文检索引擎,本项目选择最流行的全文检索引擎之一elasticsearch
。
elasticsearch 原理:http://developer.51cto.com/art/201904/594615.htm
工作原理示意图
全文搜索
的请求发送至django, 即输入搜索内容分词
和模糊查询
, 这些操作在mysql中也可以使用, 但如果遇到数据量大的项目, 效率会很低, 因此, 就需要借助搜索引擎elasticsearch
mysql
和elasticsearch
:
mysqlclient
django-haystack
, 以及python的es驱动获取数据
, 然后进行索引
, 并储存到它自己那里架构
docker 对象
docker Hub
安装
官方安装文档
lsb_release -a # 查看系统信息
uname -a # 查看位数
如果是第一次安装,你需要先添加docker的源然后再安装
sources.list
cd /etc/apt/
sources.list
, 使用命令: sudo cp sources.list sources.dbk.list
sudo rm sources.list
, 再将你刚才创建的sources.list
拷贝到/etc/apt/
sudo apt-get update
, 刷新源sudo apt-get upgrade
更新系统, 然后再刷新源 $ sudo apt-get install apt-transport-https ca-certificates curl gnupg-agent software-properties-common
$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
$ sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
$ sudo apt-get update
(这个可能会很久)
$ sudo apt-get install docker-ce
$ sudo docker run hello-world
安装成功会出现如下输出:
Hello from Docker!
This message shows that your installation appears to be working correctly.
To generate this message, Docker took the following steps:
1. The Docker client contacted the Docker daemon.
2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
(amd64)
3. The Docker daemon created a new container from that image which runs the
executable that produces the output you are currently reading.
4. The Docker daemon streamed that output to the Docker client, which sent it
to your terminal.
To try something more ambitious, you can run an Ubuntu container with:
$ docker run -it ubuntu bash
Share images, automate workflows, and more with a free Docker ID:
https://hub.docker.com/
For more examples and ideas, visit:
https://docs.docker.com/get-started/
为了方便使用,不用sudo就可以运行docker命令,安装好docker后再命令行输入如下命令:
$ sudo usermod -aG docker $USER
运行正常后,重新连接即可。
# 注意:因为haystack目前支持的elasticsearch版本为 1.x和2.x
# 所以这里选择2.4.6
$ docker pull elasticsearch:2.4.6
可以创建容器之后再安装插件,为了后面部署方便,我们创建镜像。elasticsearch的中文分词插件是elasticsearch-ik
,国人开发,github地址。
根据文档介绍,2.4.6版本对应的ik是1.10.16
因为直接使用elasticsearch的plugin命令安装会报错,所以通过下载后解压到相应文件夹的方式安装。
)]
a.下载es-ik后,将其解压到名为ik的文件夹
~$ unzip elasticsearch-analysis-ik-1.10.6.zip -d ./ik
b.在ik所在文件下创建名为Dockerfile
的文件,内容如下
# dockerfile
FROM elasticsearch:2.4.6
MAINTAINER Fisher "[email protected]"
ADD ./ik/ /usr/share/elasticsearch/plugins/ik/
然后运行命令
~$ sudo docker build -t xinlan/els-ik:2.4.6 .
如果出现下面的错误是因为没有带sudo
~$ docker build -t xinlan/els-ik:2.4.6 .
error checking context: 'no permission to read from '/home/wcf/.viminfo''.
运行成功后,会在你的docker中创建一个新的镜像
~$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
xinlan/els-ik 2.4.6 ecf93deefe2b 26 minutes ago 489MB
elasticsearch 2.4.6 5e9d896dc62c 10 months ago 479MB
如果上面的步骤搞不定请直接下载如下镜像
$ sudo docker image pull wcfdehao/els-ik:2.4.6
利用上面创建好的镜像创建一个容器。为了能够进行设置elasticsearch,通过卷挂载的方式创建容器。
将提供给大家的es配置文件elasticsearch.zip
拷贝到家目录下,然后解压
# 在xshell中使用rz命令将elasticsearch.zip文件传到虚拟机的家目录中
#然后在家目录中解压
~$ unzip elasticsearch.zip
然后运行下面的命令创建容器
# 根据上面创建的镜像创建容器,需要将/home/wcf/elasticsearch/config配置文件路径修改为你自己的路径
~$ docker run -dti --network=host --name es-ik -v /home/pyvip/elasticsearch/config:/usr/share/elasticsearch/config wcfdehao/els-ik:2.4.6
# 查看当前运行的容器
~$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
61c42c36a8f2 xinlan/els-ik:2.4.6 "/docker-entrypoint.…" 29 minutes ago Up 28 minutes es-ik
你也可以在创建时携带restart参数, 让容器可以自启, 具体可以参考本文的后半段
最后运行curl命令检测es是否正常
~$ curl http://127.0.0.1:9200
{
"name" : "Shard",
"cluster_name" : "elasticsearch",
"cluster_uuid" : "Pq6BQQhTQN6q6ML6ThPlbw",
"version" : {
"number" : "2.4.6",
"build_hash" : "5376dca9f70f3abef96a77f4bb22720ace8240fd",
"build_timestamp" : "2017-07-18T12:17:44Z",
"build_snapshot" : false,
"lucene_version" : "5.5.4"
},
"tagline" : "You Know, for Search"
}
官方文档
# 安装djangohaystack
# 使用的是当期最新版本 2.8.1
pip install django-haystack
# 将Haystack添加到`INSTALLED_APPS`中
# settings.py
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'haystack',
'user',
'news',
'doc',
'course',
'verification'
]
# 配置搜索引擎
# 在settings.py中添加如下设置
# 全文搜索引擎haystack 配置
# 不同的搜索引擎,配置不同,详情见官方文档
HAYSTACK_CONNECTIONS = {
'default': {
'ENGINE': 'haystack.backends.elasticsearch_backend.ElasticsearchSearchEngine',
'URL': 'http://127.0.0.1:9200/', # 此处为elasticsearch运行的服务器ip地址和端口
'INDEX_NAME': 'tzpython', # 指定elasticserach建立的索引库名称
},
}
# 搜索结果每页显示数量
HAYSTACK_SEARCH_RESULTS_PER_PAGE = 5
# 实时更新index
HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor'
haystack操作es还需要python的es驱动。兼容性见官网
根据官网,选择2.4.1版本
pip install elasticsearch==2.4.1
至此,环境搭建完成。相对应的es,es-ik,haystack,es-python的版本请保持一致。
设查询文章时输入的参数为q
q
q
,则直接返回热门新闻数据类目 | 说明 |
---|---|
请求方法 | GET |
url定义 | /news/search/ |
参数格式 | 查询参数 |
参数名 | 类型 | 是否必须 | 描述 |
---|---|---|---|
q | 字符串 | 否 | 查询的关键字 |
page | 整数 | 否 | 页码 |
返回结果:
搜索页面html
在apps/news/目录下创建search_indexes.py
文件,注意文件名必须使用search_indexes.py,代码如下:
# !/usr/bin/env python
# -*- coding:utf-8 -*-
# create_time: 2019/7/13
# Author = '心蓝'
from haystack import indexes
from .models import News
class NewsIndex(indexes.SearchIndex, indexes.Indexable):
"""
这个模型的作用类似于django的模型,它告诉haystack哪些数据会被
放进查询返回的模型对象中,以及通过哪些字段进行索引和查询
"""
# 这字段必须这么写,用来告诉haystack和搜索引擎要索引哪些字段
text = indexes.CharField(document=True, use_template=True)
# 模型字段,打包数据
id = indexes.CharField(model_attr='id')
title = indexes.CharField(model_attr='title')
digest = indexes.CharField(model_attr='digest')
content = indexes.CharField(model_attr='content')
image_url = indexes.CharField(model_attr='image_url')
def get_model(self):
"""
返回建立索引的模型
:return:
"""
return News
def index_queryset(self, using=None):
"""
返回要建立索引的数据查询集
:param using:
:return:
"""
# 这种写法遵从官方文档的指引
return self.get_model().objects.filter(is_delete=False)
根据上面创建的模型中的第一个text
字段中的use_template=True
参数,还需要创建一个索引数据模板,用来告诉搜索引擎需要索引哪些字段。
在templates中创建文件search/indexes/
,
所以本项目需要创建search/indexes/news/news_text.txt
,文件内容如下:
{{ object.title }}
{{ object.digest }}
{{ object.content }}
{{ object.author.username }}
按上面的步骤配置好后,就可以运行haystack的命令创建索引了
~$ python manage.py rebuild_index # 看到如下信息,说明运行成功
WARNING: This will irreparably remove EVERYTHING from your search index in connection 'default'.
Your choices after this are to restore from backups or rebuild via the `rebuild_index` command.
Are you sure you wish to continue? [y/N] y
Removing all documents from your index because you said so.
All documents removed.
Indexing 889 新闻文章
GET /tzpython/_mapping [status:404 request:0.005s]
如果出现了get 404错误, 没关系不用管它
在news/views.py中添加如下视图
from haystack.generic_views import SearchView
class NewsSearchView(SearchView):
"""
新闻搜索视图
"""
# 设置搜索模板文件
template_name = 'news/search.html'
# 重写get请求,如果请求参数q为空,返回模型News的热门新闻数据
# 否则根据参数q搜索相关数据
def get(self, request, *args, **kwargs):
# 1. 获取查询参数
query = request.GET.get('q')
# 2. 如果没有查询参数
if not query:
# 则返回热门新闻
# 获取热门新闻对象, 包含外键标签, 查询数据并做筛选和排序
hot_news = HotNews.objects.select_related('news__tag').only('news__title', 'news__image_url', 'news_id', 'news__tag__name').filter(is_delete=False).order_by('priority', '-news__clicks')
# 分页, 从配置文件中拿到haystack参数
paginator = Paginator(hot_news, settings.HAYSTACK_SEARCH_RESULTS_PER_PAGE)
try:
# 拿到前端传递的page,
page = paginator.get_page(int(request.GET.get('page')))
except Exception as e:
# 如果出错则返回第一页,保证容错性
page = paginator.get_page(1)
return render(request, self.template_name, context={
'page': page,
#'paginator': paginator,
'query': query
})
# 3. 如果有查询参数
else:
# 则执行搜索
return super().get(request, *args, **kwargs)
def get_context_data(self, *args, **kwargs):
"""
在context中添加page变量
"""
context = super().get_context_data(*args, **kwargs)
if context['page_obj']:
# 捕获page_obj,将其赋值到page
context['page'] = context['page_obj']
return context
在news/urls.py中添加如下路由
path('news/search/', views.NewsSearchView.as_view(), name='news_search')
在news/templatetags/news_template_filters.py中定义一个处理分页的过滤器
# !/usr/bin/env python
# -*- coding:utf-8 -*-
# create_time: 2019/7/14
# Author = '心蓝'
from django import template
register = template.Library()
@register.filter
def page_bar(page):
page_list = []
if page.number != 1:
page_list.append(1)
if page.number - 3 > 1:
page_list.append('...')
if page.number - 2 > 1:
page_list.append(page.number - 2)
if page.number - 1 > 1:
page_list.append(page.number - 1)
page_list.append(page.number)
if page.paginator.num_pages > page.number + 1:
page_list.append(page.number + 1)
if page.paginator.num_pages > page.number + 2:
page_list.append(page.number + 2)
if page.paginator.num_pages > page.number + 3:
page_list.append('...')
if page.paginator.num_pages != page.number:
page_list.append(page.paginator.num_pages)
return page_list
{% extends 'base/base.html' %}
{% load static %}
{% load news_customer_filters %}
{% block title %}新闻搜索{% endblock %}
{% block link %}
<link rel="stylesheet" href="{% static 'css/news/search.css' %}">
{% endblock %}
{% block main_contain %}
<div class="main-contain ">
<div class="search-box">
<form action="" style="display: inline-flex;">
<input type="search" placeholder="请输入要搜索的内容" name="q" class="search-control">
<input type="submit" value="搜索" class="search-btn">
form>
div>
<div class="content">
{% if query %}
<div class="search-result-list">
<h2 class="search-result-title">搜索结果 <span>{{ page.paginator.num_pages|default:0 }}span> 页h2>
<ul class="news-list">
{% load highlight %}
{% for news in page.object_list %}
<li class="news-item clearfix">
<a href="{% url 'news:news_detail' news.id %}" class="news-thumbnail" target="_blank"><img src="{{ news.image_url }}" alt="">a>
<div class="news-content">
<h4 class="news-title">
<a href="{% url 'news:news_detail' news.id %}">{% highlight news.title with query %}a>
h4>
<p class="news-details">{{ news.digest }}p>
<div class="news-other">
<span class="news-type">{{ news.object.tag.name }}span>
<span class="news-time">{{ news.object.update_time }}span>
<span class="news-author">{% highlight news.object.author.username with query %}span>
div>
div>
li>
{% empty %}
<li class="news-item clearfix">
<p>没有找到你想要的找的内容.p>
li>
{% endfor %}
ul>
div>
{% else %}
<div class="news-contain">
<div class="hot-recommend-list">
<h2 class="hot-recommend-title">热门推荐h2>
<ul class="news-list">
{% for hotnews in page %}
<li class="news-item clearfix">
<a href="#" class="news-thumbnail">
<img src="{{ hotnews.news.image_url }}">
a>
<div class="news-content">
<h4 class="news-title">
<a href="{% url 'news:news_detail' hotnews.news_id %}">{{ hotnews.news.title }}a>
h4>
<p class="news-details">{{ hotnews.news.digest }}p>
<div class="news-other">
<span class="news-type">{{ hotnews.news.tag.name }}span>
<span class="news-time">{{ hotnews.update_time }}span>
<span class="news-author">{{ hotnews.news.author.username }}span>
div>
div>
li>
{% endfor %}
ul>
div>
div>
{% endif %}
<div class="page-box" id="pages">
<div class="pagebar" id="pageBar">
<a class="al">{{ page.paginator.count|default:0 }}条a>
{% if page.has_previous %}
{% if query %}
<a href="{% url 'news:news_search' %}?q={{ query }}&page={{ page.previous_page_number }}"
class="prev">上一页a>
{% else %}
<a href="{% url 'news:news_search' %}?page={{ page.previous_page_number }}"
class="prev">上一页a>
{% endif %}
{% endif %}
{% if page.has_previous or page.has_next %}
{% for n in page|page_bar %}
{% if query %}
{% if n == '...' %}
<span class="point">{{ n }}span>
{% else %}
{% if n == page.number %}
<span class="sel">{{ n }}span>
{% else %}
<a href="{% url 'news:news_search' %}?page={{ n }}&q={{ query }}">{{ n }}a>
{% endif %}
{% endif %}
{% else %}
{% if n == '...' %}
<span class="point">{{ n }}span>
{% else %}
{% if n == page.number %}
<span class="sel">{{ n }}span>
{% else %}
<a href="{% url 'news:news_search' %}?page={{ n }}">{{ n }}a>
{% endif %}
{% endif %}
{% endif %}
{% endfor %}
{% endif %}
{% if page.has_next %}
{% if query %}
<a href="{% url 'news:news_search' %}?q={{ query }}&page={{ page.next_page_number }}"
class="prev">下一页a>
{% else %}
<a href="{% url 'news:news_search' %}?page={{ page.next_page_number }}"
class="prev">下一页a>
{% endif %}
{% endif %}
div>
div>
div>
div>
{% endblock %}
修改static/css/news/search.css如下:
/* ================= main start ================= */
#main {
margin-top: 25px;
min-height: 700px;
}
/* ========= main-contain start ============ */
#main .main-contain {
width: 800px;
float: left;
background: #fff;
}
/* === search-box start === */
.main-contain .search-box {
padding: 40px 50px;
width: 700px;
box-shadow: 1px 2px rgba(0,0,0,.1);
display: inline-flex;
}
.main-contain .search-box .search-control {
width: 600px;
height: 40px;
border-radius: 20px 0 0 20px;
border: 1px solid #ddd;
border-right: none;
padding-left: 0.88em;
font-size: 20px;
}
.main-contain .search-box .search-btn {
width: 100px;
height: 40px;
border: 1px solid red;
background: red;
color: #fff;
font-size: 20px;
border-radius: 0 20px 20px 0;
cursor: pointer;
}
/* === search-box end === */
/* === content start === */
/* == search-list start == */
.content .search-result-list {
padding-top: 20px;
}
.content .search-result-list .search-result-title {
padding-left: 20px;
font-size: 20px;
line-height: 26px;
}
.content .search-result-list .search-result-title span {
font-weight: 700;
color: #ff6620;
}
/* == search-list end == */
/* == news-contain start == */
.content .news-contain .hot-recommend-list {
padding-top: 20px;
}
.hot-recommend-list .hot-recommend-title {
padding-left: 20px;
font-size: 20px;
line-height: 26px;
}
.content .news-contain li {
border-bottom: 1px solid #ededed;
}
.news-list .news-item {
padding: 20px;
}
.news-list .news-item .news-thumbnail {
float: left;
width: 224px;
height: 160px;
margin-right: 30px;
overflow: hidden;
}
.news-item .news-thumbnail img {
width: 100%;
height: 100%;
transition: all 0.3s ease-out;
}
.news-item .news-thumbnail:hover img {
transform: scale(1.1);
transition: all 0.3s ease-in;
}
.news-list .news-item .news-content {
width: 500px;
height: 170px;
float: right;
color: #878787;
font-size: 14px;
}
.news-item .news-content .news-title{
color: #212121;
font-size: 22px;
height: 52px;
line-height: 26px;
transition:all 0.3s ease-out;
}
.news-item .news-content .news-title:hover {
color: #5b86db;
transition:all 0.3s ease-in;
}
.news-item .news-content .news-details {
height: 44px;
line-height: 22px;
margin-top: 19px;
text-align: justify;
}
.news-item .news-content .news-other {
margin-top: 30px;
}
.news-content .news-other .news-type {
color: #5b86db;
}
.news-content .news-other .news-author {
float: right;
margin-right: 15px;
}
.news-content .news-other .news-time {
float: right;
}
/* === current index start === */
#pages {
padding: 32px 0 10px;
}
.page-box {
text-align: center;
/*font-size: 14px;*/
}
#pages a.prev, a.next {
width: 56px;
padding: 0
}
#pages a {
display: inline-block;
height: 26px;
line-height: 26px;
background: #fff;
border: 1px solid #e3e3e3;
text-align: center;
color: #333;
padding: 0 10px
}
#pages .sel {
display: inline-block;
height: 26px;
line-height: 26px;
background: #0093E9;
border: 1px solid #0093E9;
color: #fff;
text-align: center;
padding: 0 10px
}
#pages .point {
display: inline-block;
height: 26px;
line-height: 26px;
background: #fff;
border: 1px solid #e3e3e3;
text-align: center;
color: #333;
padding: 0 10px
}
.highlighted {
font-weight: 700;
color: #ff6620;
}
/* === current index end === */
/* === content end === */
/* ================= main end ================= */
搜索页面的js代码于新闻页面的雷同, 仅仅只需要改一些参数即可
$(() => {
let iPage = 1; // 当前页面页数
let iTotalPage = 1; // 总页数
let bIsLoadData =false; // 是否正在加载
fn_load_docs(); // 加载文件列表
// 页面滚动加载
$(window).scroll(function () {
// 浏览器窗口高度
let showHeight = $(window).height();
// 整个网页高度
let pageHeight = $(document).height();
//页面可以滚动的距离
let canScrollHeight = pageHeight - showHeight;
// 页面滚动了多少, 整个是随着页面滚动实时变化的
let nowScroll = $(document).scrollTop();
if ((canScrollHeight - nowScroll) < 100){
if(!bIsLoadData){
bIsLoadData = true;
//判断页数,去更新新闻,小于总数才加载
if(iPage < iTotalPage){
iPage += 1;
fn_load_docs();
}else {
message.showInfo('已全部加载,没有更多内容!');
$('a.btn-more').html('已全部加载,没有更多内容!')
}
}
}
});
// 获取docs信息
function fn_load_docs() {
$
.ajax({
url: '/doc/docs',
type: 'GET',
data: {page: iPage},
dataType: 'json'
})
.done((res) => {
if (res.errno === '0') {
iTotalPage = res.data.total_page;
res.data.docs.forEach((doc) => {
let content = `
${ doc.image_url } " alt="" class="pay-img doc">
`;
$('.pay-list').append(content);
bIsLoadData = false;
$('a.btn-more').html('滚动加载更多');
})
} else {
message.showError(res.errmsg)
}
})
.fail(() => {
message.showError('服务器超时,请重试!')
})
}
});
编写完上述代码后就可以运行搜索了,如果运行过程中出现搜索不到结果的问题,请先去检查docker容器的运行状态,到控制台中输入:curl http://127.0.0.1:9200
查看状态,若连接失败并且输入docker ps
时没有内容,那么说明docker没有运行,使用以下方法可以设置docker容器的启动
docker 是安装在ubuntu中的,所以如果你重启了ubuntu,创建好的elasticsearch容器当然也会关闭。那怎么打开关闭的容器呢?
先查询容器名, 运行命令:
docker ps -a
找到你要启动的容器名,然后运行命令
docker start 容器名
即可启动容器
如果希望docker容器能够遇到错误自动重启, 则可以在创建容器时携带参数:
docker run --restart=on-failure
如果已经创建了docker容器, 则可以通过update来更新设置:
docker update --restart=on-failure <CONTAINER ID>
其他参数的参考如下:
Flag | Description |
---|---|
no | 不自动重启容器. (默认value) |
on-failure | 容器发生error而退出(容器退出状态不为0)重启容器 |
unless-stopped | 在容器已经stop掉或Docker stoped/restarted的时候才重启容器 |
always | 在容器已经stop掉或Docker stoped/restarted的时候才重启容器 |
如果想要docker容器开机自启, 可以参考如下设置方法:reboot 后 Docker服务及容器自动启动设置
项目源码: https://gitee.com/hao4875/newssite