美文网首页
django项目--新闻搜索

django项目--新闻搜索

作者: 昆仑草莽 | 来源:发表于2019-08-27 15:08 被阅读0次

    新闻搜索

    一、搜索功能分析

    思考,如果我们要做一个通过关键词搜索文章的功能,需要搜索哪些字段,以及使用什么技术方案呢?

    搜索字段:

    1. 标题

    2. 内容

    3. 作者

    技术方案:

    1. mysql的模糊查询 %like%

      1. 优点:实现起来简单

      2. 缺点:数据量比较大的情况下,查询效率极低

    2. 全文检索引擎

      1. 优点:专业的全文检索引擎,效率高

      2. 缺点:实现起来比较复杂

    本项目选择使用过全文检索引擎。自行实现django框架和全文检索引擎的代码比较麻烦,抱着不重复造轮子的原则,这里我们选用django的第三方包djangohaystack。它支持多种全文检索引擎,本项目选择最流行的全文检索引擎之一elasticsearch

    二、elasticsearch介绍

    elasticsearch 原理;http://developer.51cto.com/art/201904/594615.htm

    三、docker介绍

    lsb_release -a   # 查看系统信息
    uname -a  # 查看位数
    

    ubuntu下安装

    如果是第一次安装,你需要先添加docker的源然后再安装

    1. 更新包

     ```bash
     $ sudo apt-get update</pre>
    
    1. 安装证书
     $ sudo apt-get install apt-transport-https ca-certificates curl gnupg-agent software-properties-common
    
    1. 添加docker的官方GPGkey
     $ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
    
    1. 添加docker源
     $ sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"``
    

    安装 docker ce

    1. 更新包索引
     $ sudo apt-get update
    
    1. 安装docker(这个可能会很久)
    $ sudo apt-get install docker-ce</pre>
    
    1. 检测是否安装成功
    $ 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
    

    运行正常后,重新连接即可。

    四、搜索功能环境搭建

    1.在docker中安装elasticsearch

    1. 获取镜像
       # 注意:因为haystack目前支持的elasticsearch版本为 1.x和2.x
        # 所以这里选择2.4.6
        $ docker pull elasticsearch:2.4.6
    
    1. 安装中文分词插件

      可以创建容器之后再安装插件,为了后面部署方便,我们创建镜像。elasticsearch的中文分词插件是elasticsearch-ik,国人开发,github地址

      根据文档介绍,2.4.6版本对应的ik是1.10.16



      因为直接使用elasticsearch的plugin命令安装会报错,所以通过下载后解压到相应文件夹的方式安装


    2. 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 "xinlan@tanzhou.com"
        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
    
    1. 创建容器

      利用上面创建好的镜像创建一个容器。为了能够进行设置elasticsearch,通过卷挂载的方式创建容器。

      将提供给大家的es配置文件elasticsearch.zip拷贝到家目录下,然后解压

        # 在xshell中使用rz命令将elasticsearch.zip文件传到虚拟机的家目录中
        #然后在家目录中解压
        ~$ unzip elasticsearch.zip
    
    然后运行下面的命令创建容器
    
        # 根据上面创建的镜像创建容器,需要将/home/wcf/elasticsearch/config配置文件路径修改为你自己的路径
        # 将 镜像名xinlan/els-ik:2.4.6改成你的镜像名 
        # docker run -dti --network=host --name es-ik -v /home/pyvip/elasticsearch/config:/usr/share/elasticsearch/config wcfdehao/els-ik:2.4.6
        ~$ docker run -dti --network=host --name es-ik -v /home/wcf/elasticsearch/config:/usr/share/elasticsearch/config xinlan/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
    
    最后运行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"
        }
    

    2.安装djangohaystack

    官方文档

    1. 安装
        # 安装djangohaystack
        # 使用的是当期最新版本 2.8.1
        pip install django-haystack
    
    1. 配置文件
       # 将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'
    
    1. 安装elasticsearch-py

      haystack操作es还需要python的es驱动。兼容性见官网


      根据官网,选择2.4.1版本
    pip install elasticsearch==2.4.1
    

    至此,环境搭建完成。相对应的es,es-ik,haystack,es-python的版本请保持一致。

    五、新闻搜索功能实现

    1.业务流程分析

    • 判断是否传递查询参数q
    • 如果没有传递q,则直接返回热门新闻数据
    • 如果有传递,则返回查询结果
    • 分页

    2. 接口设计

    1. 接口说明:
    类目 说明
    请求方法 POST
    url定义 /news/search/
    参数格式 查询参数
    1. 参数说明:
    参数名 类型 是否必须 描述
    q 字符串 查询的关键字
    page 整数 页码
    1. 返回结果:

      搜索页面html

    3.后端代码

    1. 创建haystack数据模型

      在apps/news/目录下创建search_indexes.py文件,<span style="color:red">注意文件名必须使用search_indexes.py</span>,代码如下:

      # !/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)
      
    2. 创建索引数据模板

      根据上面创建的模型中的第一个text字段中的use_template=True参数,还需要创建一个索引数据模板,用来告诉搜索引擎需要索引哪些字段。

      在templates中创建文件search/indexes/yourappname/modelname_text.txt,所以本项目需要创建search/indexes/news/news_text.txt,文件内容如下:

      {{ object.title }}
      {{ object.digest }}
      {{ object.content }}
      {{ object.author.username }}
      
    3. 创建索引

      按上面的步骤配置好后,就可以运行haystack的命令创建索引了

      ~$ python manage.py rebuild_index
      
    4. 视图代码

      在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):
              query = request.GET.get('q')
              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')
                  paginator = Paginator(hot_news, settings.HAYSTACK_SEARCH_RESULTS_PER_PAGE)
                  try:
                      page = paginator.get_page(int(request.GET.get('page')))
                  except Exception as e:
                      page = paginator.get_page(1)
      
                  return render(request, 'news/search.html', context={
                      'page': page,
                      'paginator': paginator,
                      'query': query
                  })
              else:
                  # 搜索
                  return super().get(request, *args, **kwargs)
      
          def get_context_data(self, *args, **kwargs):
              """
              在context中添加page变量
              :param args: 
              :param kwargs: 
              :return: 
              """
              context = super().get_context_data(*args, **kwargs)
              if context['page_obj']:
                  context['page'] = context['page_obj']
              return context
      
    5. 路由

      在news/urls.py中添加如下路由

          path('news/search/', views.NewsSearchView.as_view(), name='news_search')
      

    4.前端代码

    1. 自定义过滤器

      在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
      
    2. 前端html代码

      {% 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 %}
          <!-- main-contain start  -->
          <div class="main-contain ">
              <!-- search-box start -->
              <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>
                  <!-- 可以用浮动 垂直对齐 以及 flex  -->
              </div>
              <!-- search-box end -->
              <!-- content start -->
              <div class="content">
                  {% if query %}
                      <!-- search-list start -->
                      <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>
      
                      <!-- search-list end -->
                  {% else %}
                      <!-- news-contain start -->
      
                      <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>
      
      
                      <!-- news-contain end -->
                  {% endif %}
                  <!-- Pagination start-->
                  <div class="page-box" id="pages">
                      <div class="pagebar" id="pageBar">
                          <a class="al">{{ page.paginator.count|default:0 }}条</a>
                          <!-- prev page start-->
                          {% 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 %}
                          <!-- prev page end-->
      
                          <!-- page bar start-->
                      {% 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 %}
                          <!-- page bar end-->
      
                          <!-- next page start-->
                          {% 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 %}
                          <!-- next page end-->
      
      
                      </div>
                  </div>
                  <!-- Pagination end-->
              </div>
              <!-- content end -->
          </div>
          <!-- main-contain  end -->
      {% endblock %}
      
    3. css代码

      修改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 ================= */
      

    相关文章

      网友评论

          本文标题:django项目--新闻搜索

          本文链接:https://www.haomeiwen.com/subject/tzjeectx.html