新闻搜索实现 -- Elasticsearch

1. 需求分析

  • 可以使用数据库的模糊查询(like关键字)来实现,但效率极低
# 模糊查询
# like
# %表示任意多个任意字符
# _表示一个任意字符
SELECT * FROM users WHERE username LIKE '%python%' AND is_delete = 0;
  • 在多个字段中查询,使用like关键字不方便
  • 因此使用搜索引擎来实现全文检索

2. 搜索引擎原理

  • 搜索引擎并不是直接在数据库中进行查询
  • 会对数据库中的数据进行一遍预处理,单独建立一份索引结构数据
  • 类似字典的索引检索页

3. Elasticsearch

  • 开源
  • 搜索引擎首选
  • 底层是开源库Lucene
  • REST API 的操作接口
image.png

搜索引擎在对数据构建索引时,需要进行分词处理。分词是指将一句话拆解成多个单字或词,这些字或词便是这句话的关键词。Elasticsearch 不支持对中文进行分词建立索引,需要配合扩展elasticsearch-analysis-ik来实现中文分词处理。 (结巴,也是专门做中文分词处理的)
whoosh 是另一种搜索引擎,自行了解

4.使用docker安装elasticsearch

a.获取镜像

sudo docker image pull delron/elasticsearch-ik:2.4.6-1.0

b.将百度云盘中的elasticsearch.zip文件传到虚拟机中的家目录,然后unzip解压。在虚拟机中的elasticsearch/config/elasticsearch.yml第54行,更改ip地址为0.0.0.0(IP地址 可以改 命令行输入 ip a 172.17.0.1),端口改为8002,默认端口为9200

# 在xshell中使用rz命令将elasticsearch.zip文件传到虚拟机的家目录中, 然后解压
unzip elasticsearch.zip
cd ~/elasticsearch/config
vim elasticsearch.yml
# network.host: 172.18.168.123
network.host: 0.0.0.0
#
# Set a custom port for HTTP:
#
http.port: 8002(nat模式需要映射)

c.创建docker容器并运行
/home/pyvip/elasticsearch/config 这个是所在目录 pwd 生成的绝对路径不可弄错(只需改这里)

docker run -dti --restart=always --network=host --name=elasticsearch -v /home/pyvip/elasticsearch/config:/usr/share/elasticsearch/config delron/elasticsearch-ik:2.4.6-1.0

当前目录执行 docker container ls -a 查看 elasticsearch Up 即安装ok

d.在执行

curl 127.0.0.1:8002 (ip 0.0.0.0 任意ip可访问, 8002必须与上文设置的端口一致)

# 出现---环境ok

{
  "name" : "Hardshell",
  "cluster_name" : "elasticsearch",
  "cluster_uuid" : "XYrjFhRARKGa4blqNuYZ4A",
  "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"
}

e.进入项目虚拟环境中,安装相关包(django如何与Elasticsearch进行交互)

# 进入项目虚拟环境
pip install django-haystack(换源安装,方法如下)
pip install elasticsearch==2.4.1

f.在settings.py文件中加入如下配置:

INSTALLED_APPS = [
    'haystack',
]
# 子项目只有 一个端口 ,若有多个也可以类似redis数据库进行配置
ELASTICSEARCH_DSL = {  
    'default': {
        'hosts': '127.0.0.1:8002'
    },
}

# Haystack django专门与搜索引擎进行交互的模块 ,只需更改引擎即配置 类似于数据库的配置 即可更换 woosh等其他模块,只需要更改引擎既可作到可插拔的特性,
HAYSTACK_CONNECTIONS = {
    'default': {
        'ENGINE': 'haystack.backends.elasticsearch_backend.ElasticsearchSearchEngine',
        'URL': 'http://127.0.0.1:8002/',  # 此处为elasticsearch运行的服务器ip地址,端口号默认为9200
        'INDEX_NAME': 'dj_pre_class',  # 指定elasticsearch建立的索引库的名称必须小写(新华词典)
    },
}

# 设置每页显示的数据量
HAYSTACK_SEARCH_RESULTS_PER_PAGE = 5
# 当数据库改变时,会自动更新索引(相当于订阅管理)
HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor'

5. 后端功能实现

# 在apps/news/search_indexes.py中创建如下类:(名称固定为search_indexes.py)


from haystack import indexes
# from haystack import site

from .models import News

# 命名规则 模型名Index
class NewsIndex(indexes.SearchIndex, indexes.Indexable):
    """
    News索引数据模型类
    """
    # 首先要定义text固定字段, haystack与elsticsearch交互字段
    text = indexes.CharField(document=True, use_template=True) # document=True, use_template=True 格式固定
    # 指定搜索引擎字段与模型字段匹配
    id = indexes.IntegerField(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')
    # comments = indexes.IntegerField(model_attr='comments')

    # 方法名固定
    def get_model(self):
        """返回建立索引的模型类
        """
        return News

    # 方法名固定
    def index_queryset(self, using=None):
        """返回要建立索引的数据查询集
        """
        # 会出现延时错误
        # return self.get_model().objects.filter(is_delete=False,)
        # 第一次查询用tag_id__in指定tag类型 (本项目tag为[1-6])
        # return self.get_model().objects.filter(is_delete=False, tag_id__in=[1, 2, 3, 4, 5, 6])
        # 若还出现异常,可以先创建小数量的索引 ,然后再加大数量级 update 就好
        return self.get_model().objects.filter(is_delete=False, tag_id=1)
# views.py
from haystack.views import SearchView as _SearchView

# 方法名称与属性名称固定(所有名称名都不可更改,与父类有关)
class NewsSearchView(_SearchView):
    # 模版文件
    template = 'news/search.html'

    # 重写响应方式,如果请求参数q为空,返回模型News的热门新闻数据,否则根据参数q搜索相关数据
    def create_response(self):
        # search/q=python&page=10
        kw = self.request.GET.get('q', '')
        # kw为空,没有参数
        if not kw:
            # 定义标记 是否显示热门新闻或者查询之后的数据
            show_all = True
            hot_news = models.HotNews.objects.select_related('news'). \
                only('news__title', 'news__image_url', 'news__id'). \
                filter(is_delete=False).order_by('priority', '-news__clicks')

            paginator = Paginator(hot_news, settings.HAYSTACK_SEARCH_RESULTS_PER_PAGE)
            try:
                page = paginator.page(int(self.request.GET.get('page', 1)))
            except PageNotAnInteger:
                # 如果参数page的数据类型不是整型,则返回第一页数据
                page = paginator.page(1)
            except EmptyPage:
                # 用户访问的页数大于实际页数,则返回最后一页的数据
                page = paginator.page(paginator.num_pages)
            if page == 0:
                page = paginator.page(1)
            return render(self.request, self.template, locals())
        # kw存在,返回查询结果
        else:
            show_all = False
            qs = super(NewsSearchView, self).create_response()
            return qs
# 创建templates/search/indexes/news/news_text.txt文件(文件名为:应用名_text.txt)
# 创建搜索模板 格式要求 
# 第一级 search
# 第二级 indexes
# 第三级 当前应用名(即创建每个应用所针对的索引类)
# 第四级 model模型名的小写_text.txt(模板)
# 内容 {{ object.需搜索字段名 }}

{{ object.title }}
{{ object.digest }}
{{ object.content }}
# 在apps/news/urls.py中

urlpatterns = [
    path('search/', views.NewsSearchView(), name='news_search'),

]
# 在虚拟机中执行如下命令,生成索引
python manage.py rebuild_index
python manage.py update_index

# 指令详情
python manage.py --help
[haystack]
    build_solr_schema  
    clear_index   清除索引
    haystack_info  haystack相关信息
    rebuild_index  创建索引
    update_index  需要你第一次手动更新
    
python manage.py rebuild_index --help(查询命令使用方法)
    内含 并行处理索引表 
    -k WORKERS, --workers WORKERS
      Allows for the use multiple workers to parallelize
          indexing. Requires multiprocessing.

cat /proc/cpuinfo | grep processor | wc -l 查询系统核心数
6. 前端代码(注:分页页码仅提供前十页,后续补充)

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

{% block title %}
  searchPage
{% endblock %}

<!-- css link start -->
{% block link %}
  <link rel="stylesheet" href="{% static 'css/news/search.css' %}">
{% endblock %}
<!-- css link end -->


<!-- main-contain start  -->
{% block main_contain %}

<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">
                       <!-- search-list start -->
                        {% if not show_all %}
                          <div class="search-result-list">
                            <h2 class="search-result-title">
                              搜索结果 <span style="font-weight: 700;color: #ff6620;">{{ paginator.num_pages }}</span>页
                            </h2>
                            <ul class="news-list">
                              {# 导入自带高亮功能 #}
                              {% load highlight %}
{# page.object_list page是分页的查询集  .object_list转为的列表,存放数据库模型实例 #}
                              {% for one_news in page.object_list %}
                                <li class="news-item clearfix">
                                  <a href="{% url 'news:news_detail' one_news.id %}" class="news-thumbnail" target="_blank">
                          {#  必须加 .object.  #}
                                  <img src="{{ one_news.object.image_url }}">
                                  </a>
                                  <div class="news-content">
                                    <h4 class="news-title">
         {# 'news:news_detail' one_news.id  中间一定要有空格#}
                                      <a href="{% url 'news:news_detail' one_news.id %}">
                {# 高亮处理格式 highlight xxx with query 这里 因为已经再 search_indexes.py 中加载了,所以不用加 .object. #}
                                        {% highlight one_news.title with query %}
                                      </a>
                                    </h4>
                                    <p class="news-details">{% highlight one_news.digest with query %}</p>
                                    <div class="news-other">
                                      <span class="news-type">{{ one_news.object.tag.name }}</span>
                                      <span class="news-time">{{ one_news.object.update_time }}</span>
                                      <span
                                          class="news-author">{% highlight one_news.object.author.username with query %}

                                      </span>
                                    </div>
                                  </div>
                                </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 one_hotnews in page.object_list %}

                                  <li class="news-item clearfix">
                                   {# 这里就不需要 object 了 自己创建的分页,并没有经过elistcsearch模块处理#}
                                      <a href="#" class="news-thumbnail">
                                      <img src="{{ one_hotnews.news.image_url }}">
                                    </a>
                                    <div class="news-content">
                                      <h4 class="news-title">
                                        <a href="{% url 'news:news_detail' one_hotnews.news.id %}">{{ one_hotnews.news.title }}</a>
                                      </h4>
                                      <p class="news-details">{{ one_hotnews.news.digest }}</p>
                                      <div class="news-other">
                                        <span class="news-type">{{ one_hotnews.news.tag.name }}</span>
                                        <span class="news-time">{{ one_hotnews.update_time }}</span>
                                        <span class="news-author">{{ one_hotnews.news.author.username }}</span>
                                      </div>
                                    </div>
                                  </li>

                                {% endfor %}


                              </ul>
                            </div>
                          </div>

                        {% endif %}

                       <!-- search-list end -->
                       <!-- news-contain start -->

                    {# 分页导航 #}
                     <div class="page-box" id="pages">
                       <div class="pagebar" id="pageBar">
                          <a class="a1">{{ page.paginator.count }}条</a>
                         {# 上一页的URL地址 #}
                           {# 判断是否有上一页 #}
                         {% if page.has_previous %}
                           {% if query %}
                           {# page 必须与 views.py 中一致 #}
                             <a href="{% url 'news:news_search' %}?q={{ query }}&amp;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 %}
                         {# 列出所有的URL地址 #}
{# page.paginator.page_range|slice:":10"  搜寻页数范围|过滤器 #}
                         {% for num in page.paginator.page_range|slice:":10" %}
                           {% if num == page.number %}
                             <span class="sel">{{ page.number }}</span>
                           {% else %}
                             {% if query %}
                               <a href="{% url 'news:news_search' %}?q={{ query }}&amp;page={{ num }}"
                                  target="_self">{{ num }}</a>
                             {% else %}
                               <a href="{% url 'news:news_search' %}?page={{ num }}" target="_self">{{ num }}</a>
                             {% endif %}
                           {% endif %}
                         {% endfor %}

                        {# 如果页数大于10,则打两点 #}
                         {% if page.paginator.num_pages > 10 %}
                           ..

                           {% if query %}
                             <a href="{% url 'news:news_search' %}?q={{ query }}&amp;page={{ page.paginator.num_pages }}"
                                target="_self">{{ page.paginator.num_pages }}</a>
                           {% else %}
                             <a href="{% url 'news:news_search' %}?page={{ page.paginator.num_pages }}"
                                target="_self">{{ page.paginator.num_pages }}</a>
                           {% endif %}

                         {% endif %}

                         {# 下一页的URL地址 #}
                         {% if page.has_next %}
                           {% if query %}
                             <a href="{% url 'news:news_search' %}?q={{ query }}&amp;page={{ page.next_page_number }}"
                                class="next">下一页</a>
                           {% else %}
                             <a href="{% url 'news:news_search' %}?page={{ page.next_page_number }}" class="next">下一页</a>
                           {% endif %}
                         {% endif %}
                       </div>
                     </div>

                     <!-- news-contain end -->
                   </div>
                   <!-- content end -->
               </div>
{% endblock %}
<!-- main-contain  end -->


{% block script %}

  <script src="{% static 'js/common.js' %}"></script>
{% endblock %}



/* 在static/css/news/search.css中加入如下代码: */

/* === 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
}

.highlighted {
    color: coral;
    font-weight: bold;
}
/* === current index end === */
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,013评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,205评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,370评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,168评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,153评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,954评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,271评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,916评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,382评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,877评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,989评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,624评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,209评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,199评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,418评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,401评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,700评论 2 345

推荐阅读更多精彩内容