• 13


    文章搜索

    Elasticsearch简介

    Elasticsearch 的底层是开源库 Apache Lucene

    Lucene可以被认为是迄今为止最先进、性能最好的、功能最全的搜索引擎库。但是Lucene非常复杂,要使用Lucene则必须了解检索相关知识和Lucene的工作原理才可以。

    Elasticsearch 是 Lucene 的封装,提供了开箱即用,丰富并简单连贯的REST API 的操作接口,让全文搜索变得简单并隐藏Lucene的复杂性。所以,开源的 Elasticsearch 是目前业内实现全文搜索引擎的首选。它可以快速地储存、搜索和分析海量数据。维基百科、Stack Overflow[爆栈]、Github 都采用它。

    官网:https://www.elastic.co/cn/elasticsearch/

    搜索引擎在对数据构建索引时,需要进行分词处理。分词是指将一句话拆解成多个单字或词,这些字或词便是这句话的关键词。如

    我是中国人。
    

    '我'、'是'、'中'、'国'、'人'、'中国'等都可以是这句话的关键词。

    Elasticsearch 不支持对中文进行分词建立索引,需要配合扩展ik分词器[elasticsearch-ik]来实现中文分词处理。

    扩展:https://www.cnblogs.com/leeSmall/p/9189078.html

    docker安装Elasticsearch和ik分词器

    1.拉取镜像

    Elasticsearch 是用Java实现的,所以需要Java虚拟机的支持,在运行之前保证机器上安装了JDK,并且JDK版本不能低于1.7_55。

    
    sudo docker pull bachue/elasticsearch-ik:2.2-1.8
    

    注意: 容器较大,所以可以选择配置国内加速器

    国内的镜像加速器选项较多,如:阿里云,DaoCloud 等。这里我们使用阿里云的docker加速器

    # 配置国内镜像
    sudo mkdir -p /etc/docker
    sudo tee /etc/docker/daemon.json <<-'EOF'
    {
      "registry-mirrors": ["https://2xdmrl8d.mirror.aliyuncs.com"]
    }
    EOF
    sudo systemctl daemon-reload
    sudo systemctl restart docker 
    
    # 再重新拉取镜像
    sudo docker pull bachue/elasticsearch-ik:2.2-1.8
    

    也可以使用笔记里面的素材镜像文件加载到docker中

    sudo docker load -i elasticsearch-ik.tar.gz
    sudo docker image ls
    

    2.创建容器

    拉取了镜像以后,直接创建容器

    vm.max_map_count参数,是允许一个进程在内容中拥有的最大数量(VMA:虚拟内存地址, 一个连续的虚拟地址空间),当进程占用内存超过max_map_count时, 直接GG。所以错误提示:elasticsearch用户拥有的内存权限太小,至少需要262144。

    max_map_count配置文件写在系统中的/proc/sys/vm文件中,但是我们不需要进入docker容器中配置,因为docker使用宿主机的/proc/sys作为只读路径之一。因此我们在Ubuntu系统下设置一下命令即可:

    sudo sysctl -w vm.max_map_count=262144 # 本次服务器,的mvm = 262144,如果服务器关闭了,需要重新设置
    sudo docker run -itd --restart=always --network=host -e ES_JAVA_OPTS="-Xms256m -Xmx256m" --name=esik bachue/elasticsearch-ik:2.2-1.8
    
    

    3.测试

    完成上面操作以后,我们接下来,直接访问浏览器,输入IP:http://127.0.0.1:9200/,出现以下内容则表示elasticsearch安装成功:

    {
      "name" : "Metalhead",
      "cluster_name" : "elasticsearch",
      "version" : {
        "number" : "2.2.0",
        "build_hash" : "8ff36d139e16f8720f2947ef62c8167a888992fe",
        "build_timestamp" : "2016-01-27T13:32:39Z",
        "build_snapshot" : false,
        "lucene_version" : "5.4.1"
      },
      "tagline" : "You Know, for Search"
    }
    

    接下来,我们快速的学习下使用分词器。

    ik分词器的基本使用

    上面的分词器测试中,我们使用了postman发起了如下请求:

    GET请求    http://127.0.0.1:9200/_analyze?pretty
    
    {
      "text": "老男孩python"
    }
    

    这个请求得到的分词结果其实很傻瓜。因为这样会自动把每一个文字都进行了分割。

    所以我们使用postman发起一个新的请求:

    GET    /_analyze?pretty
    
    {
      "analyzer": "ik_smart",
      "text": "老男孩python"
    }
    

    效果:

    {
        "tokens": [
            {
                "token": "老",
                "start_offset": 0,
                "end_offset": 1,
                "type": "CN_CHAR",
                "position": 0
            },
            {
                "token": "男孩",
                "start_offset": 1,
                "end_offset": 3,
                "type": "CN_WORD",
                "position": 1
            },
            {
                "token": "python",
                "start_offset": 3,
                "end_offset": 9,
                "type": "ENGLISH",
                "position": 2
            }
        ]
    }
    

    analyzer表示分词器 ,我们可以理解为分词的算法或者分析器。默认情况下,Elasticsearch内置了很多分词器。

    以下两种举例,又兴趣可以访问文章来深入了解。

    1. standard 标准分词器,单字切分。上面我们测试分词器时候没有声明analyzer参数,则默认调用标准分词器。
    2. simple 简单分词器,按非字母字符来分割文本信息
    

    综合上面的分词器,其实对于中文都不友好,所以我们前面安装的ik分词器就有了用武之地。

    ik分词器在Elasticsearch内置分词器的基础上,新增了2种分词器。

    ik_max_word:会将文本做最细粒度的拆分;尽可能多的拆分出词语
    
    ik_smart:会做最粗粒度的拆分;已被分出的词语将不会再次被其它词语占有
    

    我们使用下ik分词器,在postman中发起请求:

    GET    /_analyze?pretty
    
    {
      "analyzer": "ik_max_word",
      "text": "你好,老男孩python"
    }
    

    效果:

    {
        "tokens": [
            {
                "token": "你好",
                "start_offset": 0,
                "end_offset": 2,
                "type": "CN_WORD",
                "position": 0
            },
            {
                "token": "老",
                "start_offset": 3,
                "end_offset": 4,
                "type": "CN_CHAR",
                "position": 1
            },
            {
                "token": "男孩",
                "start_offset": 4,
                "end_offset": 6,
                "type": "CN_WORD",
                "position": 2
            },
            {
                "token": "python",
                "start_offset": 6,
                "end_offset": 12,
                "type": "ENGLISH",
                "position": 3
            }
        ]
    }
    

    在Django中使用:

    django-haystack 模块:

    专门给 django 提供搜索功能的。 django-haystack 提供了一个统一的API搜索接口,底层可以根据自己需求更换搜索引擎( Solr, Elasticsearch, Whoosh, Xapian 等等),类似于 django 中的 ORM 插件,提供了一个操作数据库接口,但是底层具体使用哪个数据库是可以在配置文件中进行设置的。

    在django中可以通过使用haystack来调用Elasticsearch搜索引擎。而在drf框架中,也有一个对应的drf-haystack模块,是django-haystack进行封装处理的。

    1)安装模块

    pip install drf-haystack          # django框架安装命令: pip install django-haystack
    pip install elasticsearch==2.2.0         # 版本有问题6.0.0,5.0.0,7.5.1,可以装低版本2.2.0
    

    2)注册应用

    settings/dev.py

    INSTALLED_APPS = [
        ...
        'haystack',
        ...
    ]
    

    3)相关配置

    在配置文件中配置haystack使用的搜索引擎后端,settings/dev.py,代码:

    # Haystack
    HAYSTACK_CONNECTIONS = {
        'default': {
            'ENGINE': 'haystack.backends.elasticsearch_backend.ElasticsearchSearchEngine',
            # elasticsearch运行的服务器ip地址,端口号默认为9200
            'URL': 'http://192.168.252.168:9200/',
            # elasticsearch建立的索引库的名称,一般使用项目名作为索引库
            'INDEX_NAME': 'renran',
        },
    }
    
    # 设置在Django运行时,如果有数据产生变化(添加、修改、删除),
    # haystack会自动让Elasticsearch实时生成新数据的索引
    HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor'
    

    4)创建索引类

    通过创建索引类,来指明让搜索引擎对哪些字段建立索引,也就是可以通过哪些字段的关键字来检索数据。

    在article子应用下创建索引类文件search_indexes.py,代码:

    from haystack import indexes
    from .models import Article
    
    class ArticleIndex(indexes.SearchIndex, indexes.Indexable):
        """
        文章索引数据模型类
        """
        text = indexes.CharField(document=True, use_template=True)
        id = indexes.IntegerField(model_attr='id')
        title = indexes.CharField(model_attr='title')
        content = indexes.CharField(model_attr='content')
    
        def get_model(self):
            """返回建立索引的模型类"""
            return Article
    
        def index_queryset(self, using=None):
            """返回要建立索引的数据查询集"""
            return self.get_model().objects.filter(is_public=True)
    

    其中text字段我们声明为document=True,表名该字段是主要进行关键字查询的字段, 该字段的索引值可以由多个数据库模型类字段组成,具体由哪些模型类字段组成,我们用use_template=True表示后续通过模板来指明。其他字段都是通过model_attr选项指明引用数据库模型类的特定字段。

    在REST framework中,索引类的字段会作为查询结果返回数据的来源。

    5)在templates目录中创建text字段使用的模板文件

    配置模板目录,settings/dev.py,代码:

    # 模板引擎
    TEMPLATES = [
        {
            'BACKEND': 'django.template.backends.django.DjangoTemplates',
            'DIRS': [
                os.path.join(BASE_DIR, "templates"),
            ],
            'APP_DIRS': True,
            'OPTIONS': {
                'context_processors': [
                    'django.template.context_processors.debug',
                    'django.template.context_processors.request',
                    'django.contrib.auth.context_processors.auth',
                    'django.contrib.messages.context_processors.messages',
                ],
            },
        },
    ]
    

    接着,在主目录renranapi中创建文件: templates/search/indexes/article/article_text.txt文件中定义,关键字索引查询:

    {{ object.title }}
    {{ object.content }}
    {{ object.id }}
    

    此模板指明当将关键词通过text参数名传递时,可以通过article的title、content、id来进行关键字索引查询。

    6)手动重建索引

    python manage.py rebuild_index
    

    7)创建序列化器

    在article/serializers.py中创建haystack序列化器

    from drf_haystack.serializers import HaystackSerializer
    from .search_indexes import ArticleIndex
    
    class ArticleIndexSerializer(HaystackSerializer):
        """
        文章索引结果数据序列化器
        """
        class Meta:
            index_classes = [ArticleIndex]
            fields = ('text', 'id', 'title', 'content')
    

    注意fields属性的字段名与ArticleIndex类的字段对应。

    8)创建视图

    在article/views.py中创建视图

    from drf_haystack.viewsets import HaystackViewSet
    from .serializers import ArticleIndexSerializer
    from .paginations import ArticleSearchPageNumberPagination
    
    class ArticleSearchViewSet(HaystackViewSet):
        """
        文章搜索
        """
        index_models = [Article]
    
        serializer_class = ArticleIndexSerializer
        pagination_class = ArticleSearchPageNumberPagination
    

    给视图添加分页器

    article/paginations.py,代码:

    from rest_framework.pagination import PageNumberPagination
    class ArticleSearchPageNumberPagination(PageNumberPagination):
        """文章搜索分页器"""
        page_size = 2
        max_page_size = 20
        page_size_query_param = "size"
        page_query_param = "page"
    

    9)定义路由

    通过REST framework的router来定义路由,article/urls.py,代码:

    from django.urls import path,re_path
    from . import views
    # 。。。。
    
    from rest_framework.routers import SimpleRouter
    router = SimpleRouter()
    router.register('search', views.ArticleSearchViewSet, basename='article_search')
    urlpatterns += router.urls
    

    10)测试

    我们可以使用postman进行测试:

    发送get请求,到http://api.renran.com:8000/article/search/?text=搜索数据

    客户端

    客户端提供搜索功能,在头部子组件Header.vie中完善输入搜索内容以后的点击跳转到搜索页面Search.vue效果,

    <template>
      <div class="header">
        <nav class="navbar">
          <div class="width-limit">
            <!-- 左上方 Logo -->
            <a class="logo" href="/"><img src="/static/image/nav-logo.png" /></a>
    
            <!-- 右上角 -->
            <!-- 未登录显示登录/注册/写文章 -->
            <a class="btn write-btn" target="_blank" href="/writer"><img class="icon-write" src="/static/image/write.svg">写文章</a>
            <router-link class="btn sign-up" id="sign_up" to="/register">注册</router-link>
            <router-link class="btn log-in" id="sign_in" to="/login">登录</router-link>
            <div class="container">
              <div class="collapse navbar-collapse" id="menu">
                <ul class="nav navbar-nav">
                  <li class="tab active">
                    <a href="/">
                      <i class="iconfont ic-navigation-discover menu-icon"></i>
                      <span class="menu-text">首页</span>
                    </a>
                  </li>
                  <li class="tab" v-for="(nav_top_value,nav_top_index) in nav_top_list" :key="nav_top_index">
                    <router-link :to="nav_top_value.link" v-if="nav_top_value.is_http">
                      <i class="menu-icon" :class="nav_top_value.icon"></i> <!--图标-->
                      <span class="menu-text">{{ nav_top_value.name }}</span> <!--一级菜单名称-->
                    </router-link>
                    <a :href="nav_top_value.link" v-else>
                      <i class="menu-icon" :class="nav_top_value.icon"></i> <!--图标-->
                      <span class="menu-text">{{ nav_top_value.name }}</span> <!--一级菜单名称-->
                    </a>
                    <ul class="dropdown-menu" v-if="nav_top_value.son_list.length>0">
                      <li v-for="(nav_son_value,nav_son_index) in nav_top_value.son_list" :key="nav_son_index">
                        <router-link :to="nav_son_value.link" v-if="nav_son_value.http">
                          <i :class="nav_son_value.icon"></i>
                          <span>{{nav_son_value.name}}</span>
                        </router-link>
                        <a :href="nav_son_value.link" v-else>
                          <i :class="nav_son_value.icon"></i> <!--图标-->
                          <span>{{nav_son_value.name}}</span> <!--二级菜单名称-->
                        </a>
                      </li>
                    </ul>
                  </li>
                  <li class="search">
                    <form target="_blank" action="/search"  accept-charset="UTF-8" method="get">
                      <input type="text" v-model="search_text" id="q" value="" autocomplete="off" placeholder="搜索" class="search-input">
                      <input type="submit" @click="to_search" class="search-btn" href="javascript:void(0)"></input>
                    </form>
                  </li>
                </ul>
              </div>
            </div>
    
            <!-- 如果用户登录,显示下拉菜单 -->
          </div>
        </nav>
      </div>
    </template>
    
    <script>
        export default {
            name: "Header",
            data(){
              return{
                nav_top_list:[], //导航栏列表
                search_text:'', //搜索内容
              }
            },
           // 页面加载,自动加载数据
            created() {
              this.get_navtop_list();
            },
            methods:{
              // 点击搜索跳转页面
              to_search(){
                // 跳转到搜索页面
                if(this.search_text.length<1){
                    return;
                }
                this.$router.push(`/search?text=${this.search_text}`);
            },
    
              // 获取头部导航栏信息
              get_navtop_list() {
                this.$axios.get(`${this.$settings.host}/home/nav/top/`)
                  .then((res) => {
                    this.nav_top_list = res.data  //响应回来的数据
                  }).catch((error) => {
                  this.$message.error("无法获取头部导航信息");
                })
              },
            },
    
        }
    </script>
    

    在前端创建搜索页面Search.vue,代码如下:

    <template>
      <div class="container search">
        <div class="row">
          <div class="aside">
            <div>
              <ul class="menu">
                <li class="active"><a><div class="setting-icon"><i class="iconfont ic-search-note"></i></div> <span>文章</span></a></li>
                <li class=""><a><div class="setting-icon"><i class="iconfont ic-search-user"></i></div> <span>用户</span></a></li>
                <li class=""><a><div class="setting-icon"><i class="iconfont ic-search-collection"></i></div> <span>专题</span></a></li>
                <li class=""><a><div class="setting-icon"><i class="iconfont ic-search-notebook"></i></div> <span>文集</span></a></li>
              </ul>
            </div>
            <div class="search-recent">
              <div class="search-recent-header clearfix">
                <span>最近搜索</span> <a>清空</a></div>
              <ul class="search-recent-item-wrap">
                <li><a href="" target="_blank"><i class="iconfont ic-search-history"></i> <span>dd</span> <i class="iconfont ic-unfollow"></i></a></li>
                <li><a href="" target="_blank"><i class="iconfont ic-search-history"></i> <span>2020</span> <i class="iconfont ic-unfollow"></i></a></li>
              </ul>
            </div>
          </div>
          <div class="col-xs-16 col-xs-offset-8 main">
            <div class="search-content">
              <div class="sort-type">
                <a class="active">综合排序 · </a>
                <a class="">热门文章 ·</a>
                <a class="">最新发布 ·</a>
                <a class="">最新评论</a>
                <span>&nbsp;&nbsp;|&nbsp;</span>
                <div class="v-select-wrap">
                  <div class="v-select-submit-wrap"><svg viewBox="0 0 10 6" aria-hidden="true"><path d="M8.716.217L5.002 4 1.285.218C.99-.072.514-.072.22.218c-.294.29-.294.76 0 1.052l4.25 4.512c.292.29.77.29 1.063 0L9.78 1.27c.293-.29.293-.76 0-1.052-.295-.29-.77-.29-1.063 0z"></path></svg>
                  </div>
                </div>
              </div>
              <div class="result">16743 个结果</div>
              <ul class="note-list">
                <li v-for="(search_article_vlaue,search_article_index) in search_article_list" :key="search_article_index">
                  <div class="content">
                    <div class="author"><a href="" target="_blank" class="avatar"><img :src="search_article_vlaue.author_avatar"></a> <div class="info"><a href="" class="nickname">{{search_article_vlaue.author_name}}</a><span class="time">{{search_article_vlaue.pub_data}}</span>
                    </div>
                  </div>
                  <router-link :to="`/article/${search_article_vlaue.id}`"  target="_blank" class="title" >{{search_article_vlaue.title}}</router-link>
                  <p class="abstract">{{search_article_vlaue.content}}...</p>
                    <div class="meta">
                      <a href="" target="_blank"><i class="iconfont ic-list-read"></i>{{search_article_vlaue.read_count }}</a>
                      <a href="" target="_blank"><i class="iconfont ic-list-comments"></i> {{search_article_vlaue.comment_count}}</a>
                      <span><i class="iconfont ic-list-like"></i> {{search_article_vlaue.like_count}}</span>
                      <span><i class="iconfont ic-list-money"></i> {{search_article_vlaue.reward_count}}</span>
                    </div>
                  </div>
                </li>
              </ul>
              <div>
                <ul class="pagination">
                  <li><a href="" class="active">1</a></li>
                  <li><a>2</a></li>
                  <li><a>3</a></li>
                  <li><a>4</a></li>
                  <li><a>下一页</a></li>
                  <router-link to="/login">baidu</router-link>
                </ul>
              </div>
            </div>
          </div>
        </div>
      </div>
    </template>
    
    <script>
        export default {
          name: "Search",
          data(){
            return{
              search_article_list:[], // 搜索文章列表
              search_text:'', // 索引内容
              search_count:0, // 搜索文章数量
            }
          },
          created() {
            this.search_text = this.$route.query.text;
            this.get_search_article_list()
          },
          methods:{
            // 获取搜索的文章
            get_search_article_list(){
              this.$axios.get(`${this.$settings.host}/article/search`,{
                params:{
                  text:this.search_text
                }
              }).then((res)=>{
                this.search_article_list = res.data.results
                this.search_count = res.data.count
              }).catch((error)=>{
                this.$message.error('获取搜索内容失败!')
              })
            },
          },
        }
    </script>
    
    <style scoped>
        /* 这里的css在笔记的素材中找到Search.vue复制进去 */
    </style>
    

    路由,代码:router/index.js

    import Vue from 'vue'
    import Router from 'vue-router'
    
    Vue.use(Router);
    
    // ....
    
    import Search from "@/components/Search"
    
    
    export default new Router({
      mode: "history",
      routes: [
        /// ...
          {
           name:"Search",
           path:"/search",
           component: Search,
         },
      ]
    })
    
    

    服务端

    在搜索页面加载完成以后,对api数据进行搜索请求,因为客户端需要更多的返回搜索字段,所以我们重新调整api视图接口,返回用户信息和点赞等记录数值。

    模型增加两个字段,代码,article/models.py,代码:

    class Article(BaseModel):
        """文章模型"""
        title = models.CharField(max_length=200, verbose_name="文章标题")
        content = models.TextField(null=True, blank=True, verbose_name="文章内容")
        render = models.TextField(null=True, blank=True, verbose_name="文章内容(处理标签内容)")
        user = models.ForeignKey(User, on_delete=models.DO_NOTHING, verbose_name="用户")
        collection = models.ForeignKey(ArticleCollection, on_delete=models.CASCADE, verbose_name="文集")
        pub_date = models.DateTimeField(null=True, default=None, verbose_name="发布时间")
        access_pwd = models.CharField(max_length=15,null=True, blank=True, verbose_name="访问密码")
        read_count = models.IntegerField(default=0, null=True, blank=True, verbose_name="阅读量")
        like_count = models.IntegerField(default=0, null=True, blank=True, verbose_name="点赞量")
        collect_count = models.IntegerField(default=0, null=True, blank=True, verbose_name="收藏量")
        comment_count = models.IntegerField(default=0, null=True, blank=True, verbose_name="评论量")
        reward_count = models.IntegerField(default=0, null=True, blank=True, verbose_name="赞赏量")
        is_public = models.BooleanField(default=False, verbose_name="是否公开")
        class Meta:
            db_table = "rr_article"
            verbose_name = "文章"
            verbose_name_plural = verbose_name
    
        def __str__(self):
            return self.title
    
        # 新增字段
        @property
        def user_nickname(self):
            return self.user.nickname
    
        @property
        def user_avatar(self):
            try:
                image_url = self.user.avatar.url
                return image_url
            except:
                return ""
    

    索引类代码,article/search_indexes.py,代码:

    from haystack import indexes
    from .models import Article
    
    class ArticleIndex(indexes.SearchIndex, indexes.Indexable):
        """
        文章索引数据模型类
        """
        # 全局索引,文档字段,这个字段不属于模型的,可以通过这个索引字段,到数据库中进行多个字段的搜索匹配
        text = indexes.CharField(document=True, use_template=True)
        id = indexes.IntegerField(model_attr='id')
        title = indexes.CharField(model_attr='title')
        content = indexes.CharField(model_attr='content')
    
        read_count = indexes.IntegerField(model_attr='read_count')
        like_count = indexes.IntegerField(model_attr='like_count')
        comment_count = indexes.IntegerField(model_attr='comment_count')
        reward_count = indexes.IntegerField(model_attr='reward_count')
        author_id = indexes.IntegerField(model_attr="user_id")
        author_name = indexes.CharField(model_attr="user_nickname")
        author_avatar = indexes.CharField(model_attr="user_avatar")
        pub_date = indexes.DateTimeField(model_attr="pub_date",null=True)
    
        def get_model(self):
            """返回建立索引的模型类"""
            return Article
    
        def index_queryset(self, using=None):
            """返回要建立索引的数据查询集"""
            return self.get_model().objects.filter(is_public=True)
    

    序列化器,增加多个返回字段,article/serializers.py,代码:

    # 文章索引结果数据序列化器
    class ArticleIndexSerializer(HaystackSerializer):
        """
        文章索引结果数据序列化器
        """
        class Meta:
            index_classes = [ArticleIndex]
            # 注意fields属性的字段名与ArticleIndex类的字段相对应
            fields = ('text','id', 'title', 'content', "author_id", 'author_name', "author_avatar", 'read_count','like_count','comment_count','reward_count','pub_date')
    
    
  • 相关阅读:
    盒子垂直水平居中
    Sahi (2) —— https/SSL配置(102 Tutorial)
    Sahi (1) —— 快速入门(101 Tutorial)
    组织分析(1)——介绍
    Java Servlet (1) —— Filter过滤请求与响应
    CAS (8) —— Mac下配置CAS到JBoss EAP 6.4(6.x)的Standalone模式(服务端)
    JBoss Wildfly (1) —— 7.2.0.Final编译
    CAS (7) —— Mac下配置CAS 4.x的JPATicketRegistry(服务端)
    CAS (6) —— Nginx代理模式下浏览器访问CAS服务器网络顺序图详解
    CAS (5) —— Nginx代理模式下浏览器访问CAS服务器配置详解
  • 原文地址:https://www.cnblogs.com/jia-shu/p/14677332.html
Copyright © 2020-2023  润新知