zoukankan      html  css  js  c++  java
  • 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')
    
    
  • 相关阅读:
    css中的元素旋转
    display:inlineblock的深入理解
    js时间获取。
    长英文自动换行的最终解决方法
    jqery图片展示效果
    链接A引发的思考
    电子邮件制作规范和建议
    overflow与textindent:9999px 字体隐藏及input value偏移
    jQuery load的详解
    转载:前端调试利器DebugBa
  • 原文地址:https://www.cnblogs.com/jia-shu/p/14677332.html
Copyright © 2011-2022 走看看