13 - 文章搜索1
文章搜索
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> | </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')