了解搜索网站大体功能
image.png搜索网站首页
image.png搜索网站搜素结果页
es完成搜索建议-搜索建议字段保存
image.png事实上 elasticsearch 自身是支持搜索建议的,当我们在输入框输入内容的时候,可以通过 elasticsearch 的接口完成智能提示,甚至可以发现,搜索框中输入的是错误的单词, java 写成了 jaav,elasticsearch 甚至完成了纠错,可以智能提示可能需要搜索的内容
elasticsearch 搜索建议自动补全文档:https://www.elastic.co/guide/en/elasticsearch/reference/5.3/search-suggesters-completion.html
image.png查看官方文档
所以首先需要定义的就是这个新增 mapping,在 ArticleSpider 项目中,mapping 是通过 model 去定义的,现在需要解决的首个问题就是如何在 model 中新增这个 字段,并且将其 type 设为 completion
修改之前写的 es_types.py 文件,给
ArticleType
model 增加suggest
字段
# ArticleSpider/models/es_types.py
from datetime import datetime
from elasticsearch_dsl import DocType, Date, Nested, Boolean, \
analyzer, Completion, Keyword, Text, Integer
from elasticsearch_dsl.connections import connections
from elasticsearch_dsl.analysis import CustomAnalyzer as _CustomAnalyzer
# 指明连接的服务器
connections.create_connection(hosts=['localhost'])
class CustomAnalyzer(_CustomAnalyzer):
"""
自定义 CustomAnalysis,解决报错问题
"""
def get_analysis_definition(self):
return {}
# filter=['lowercase'] 参数做大小写转换
ik_analyzer = CustomAnalyzer('ik_max_word', filter=['lowercase'])
class ArticleType(DocType):
"""
伯乐在线文章类型
"""
# 新增 suggest 字段,为 Completion 类型
# 理论上是可以向下面这样写的,在 Completion 内部指明 analyzer
# 但是由于 elasticsearch_dsl 的源码有点问题,直接这样写的话
# 运行程序,init 的时候会报错
# suggest = Completion(analyzer='ik_max_word')
# 现在解决方案是自定义一个 CustomAnalyzer
suggest = Completion(analyzer=ik_analyzer)
title = Text(analyzer='ik_max_word') # 需要进行分词,所以定义成 text
create_date = Date()
url = Keyword() # 无需分词
url_object_id = Keyword()
front_img_url = Keyword()
front_img_path = Keyword()
praise_nums = Integer()
comment_nums = Integer()
fav_nums = Integer()
tags = Text(analyzer='ik_max_word')
content = Text(analyzer='ik_max_word')
class Meta:
index = 'jobbole'
doc_type = 'article'
if __name__ == '__main__':
ArticleType.init() # 可以直接生成 mapping
image.png运行文件
image.png查看 mapping 结果
image.png image.png已经根据 elasticsearch 的文档生成了 mapping,那么爬虫每爬取一条数据,保存到 elasticsearch 的时候,要如何生成
suggest
字段这个值呢?
image.png那么大概知道怎么一回事,这个 input 的分词是如何生成的呢?
事实上,可以通过GET _analyze
这个接口来完成的
所以在爬虫爬取过来数据,在将数据插入 elasticsearch 的时候,就需要对数据进行处理,后续在做搜索建议的时候才有可能成功
所以要在之前定义的 item
JobBoleArticleLoadItem
的save_to_es
方法中来生成 搜索建议值
# ArticleSpider/items.py
from ArticleSpider.models.es_types import ArticleType
# 获取 es 的连接
from elasticsearch_dsl.connections import connections
es = connections.create_connection(ArticleType._doc_type.using)
def gen_suggests(index, info_tuple):
"""
根据传递进来的字符串和权重生成搜索建议数组
Args:
index: 索引名称
info_tuple: 包含 text(内容)、weight(权重) 的元组
Returns:
suggests: 搜索建议数组
"""
# used_words 设定为 set 目的是为了去重
# 假如第一次 传过来一个字符串 "python爬虫",属于 "title" 这个字段
# 第二次传过来同样字符串 "python爬虫",属于 "tags" 这个字段,这两个
# 字段 suggest 的权重是不同的,第一次 title 已经解析了 "python爬虫"
# 这个词,权重为 10,tags 权重为 3,那么第二次 tags 解析 "python爬虫"
# 这个词的时候,如果不去重,就会把之前的权重给改掉
# 所以这里为了只取第一次分析的结果,后面遇到相同的词不进行修改,以第一次为准
used_words = set()
suggests = []
print('-=-=-=-=', info_tuple)
for text, weight in info_tuple:
if text:
# 传递过来非空字符串才进行处理
# 调用 es 的 analyze 接口分析字符串,将字符串分词以及大小写转换
# 返回的 words 就是处理后的数据
words = es.indices.analyze(index=index, analyzer='ik_max_word', params={'filter': ['lowercase']}, body=text)
# len(r['token']) > 1 过滤掉经过分词分成的单个字的词
analyzerd_words = set([r['token'] for r in words['tokens'] if len(r['token']) > 1])
new_words = analyzerd_words - used_words
else:
new_words = set()
if new_words:
suggests.append({'input': list(new_words), 'weight': weight})
return suggests
class JobBoleArticleLoadItem(scrapy.Item):
...
def save_to_es(self):
"""
将数据存入 Elasticsearch
"""
# 将 item 转换为 es 数据
article = ArticleType()
article.title = self['title']
article.create_date = self['create_date']
article.content = remove_tags(self['content'])
article.front_img_url = self['front_img_url']
if 'front_img_path' in self:
article.front_img_path = self['front_img_path']
article.praise_nums = self['praise_nums']
article.fav_nums = self['fav_nums']
article.comment_nums = self['comment_nums']
article.url = self['url']
article.tags = self['tags']
article.meta.id = self['url_object_id']
# article.suggest = [{'input': [], 'weight': 2}]
article.suggest = gen_suggests(ArticleType._doc_type.index, ((article.title, 10), (article.tags, 7)))
article.save()
image.png运行 jobbole spider,断点调试
image.png image.png查看数据
这样,就完成了 搜索建议字段的保存,接下来就可以运行 spider 源源不断的向 es 中写入数据了
django实现elasticsearch的搜索建议
- 准备工作
- 创建虚拟环境
lcv_search
mkvirtualenv lcv_search
- 进入虚拟环境,安装依赖包
pip install Django==1.11
pip install elasticsearch-dsl==5.2.0
image.png创建项目
LcvSearch
image.png打开项目,新建的
search
app 已经自动添加进来了
image.png image.png启动项目,可以正常运行
image.png项目根目录下创建 static/ 目录用于存放静态文件,将 html 文件放到 templates/ 目录下
settings.py 配置
# LcvSearch/settings.py
TEMPLATES = [
{
...
'DIRS': [os.path.join(BASE_DIR, 'templates')],
...
]
...
STATICFILES_DIRS = [
os.path.join(BASE_DIR, 'static')
]
urls.py 配置
# LcvSearch/urls.py
from django.views.generic import TemplateView
urlpatterns = [
url(r'^admin/', admin.site.urls),
url(r'^$', TemplateView.as_view(template_name='index.html'), name='index'),
]
替换 HTML 中静态文件引用
image.png image.png启动项目
- 搜索建议的原理是如何实现的?
image.png打开百度,搜索框搜索关键字就会发现,实际上,当我们输入关键词的时候,前端页面通过 js 已经自动向后端发送了请求,并将搜索结果返回,就实现了搜索建议
- elasticsearch 如何实现模糊搜索
image.png image.png image.png image.pngelasticsearch 为我们提供了 fuzzy 模糊搜索
# 模糊搜索
GET jobbole/_search
{
"query": {
"fuzzy": {"title": "linu"}
},
"_source": ["title"]
}
# 模糊搜索
GET jobbole/_search
{
"query": {
"match": {"title": "linu"}
},
"_source": ["title"]
}
GET _search
{
"query": {
"fuzzy": {
"title": {
"value": "linu",
"fuzziness": 1,
"prefix_length": 0
}
}
}
}
# 其中 fuzziness 指明编辑距离
# 编辑距离(Edit Distance),又称Levenshtein距离,是指两个字串之间,由一个转成另一个所需的最少编辑操作次数。许可的编辑操作包括将一个字符替换成另一个字符,插入一个字符,删除一个字符。一般来说,编辑距离越小,两个串的相似度越大。
# prefix_length 指明需要搜索的字符串前面不需要参与变换的词的长度
image.png明白了什么是 fuzzy 搜索,下面演示如何通过 suggest 完成自动补全
POST jobbole/_search?pretty
{
"suggest": {
"my-suggest" : {
"text" : "linux",
"completion" : {
"field" : "suggest",
"fuzzy" : {
"fuzziness" : 2
}
}
}
}
}
现在将 ArticleSpider 项目中的
ArticleSpider/models/es_types.py
中的代码拷贝到 LcvSearch 项目中的search/models.py
文件,这也是为什么之前在 ArticleSpider 项目中写的时候为什么尽量写成和 Django 的 Model 一样的原因,拿过来即可以使用
# search/models.py
from datetime import datetime
from elasticsearch_dsl import DocType, Date, Nested, Boolean, \
analyzer, Completion, Keyword, Text, Integer
from elasticsearch_dsl.connections import connections
from elasticsearch_dsl.analysis import CustomAnalyzer as _CustomAnalyzer
# 指明连接的服务器
connections.create_connection(hosts=['localhost'])
class CustomAnalyzer(_CustomAnalyzer):
"""
自定义 CustomAnalysis,解决报错问题
"""
def get_analysis_definition(self):
return {}
# filter=['lowercase'] 参数做大小写转换
ik_analyzer = CustomAnalyzer('ik_max_word', filter=['lowercase'])
class ArticleType(DocType):
"""
伯乐在线文章类型
"""
# 新增 suggest 字段,为 Completion 类型
# 理论上是可以向下面这样写的,在 Completion 内部指明 analyzer
# 但是由于 elasticsearch_dsl 的源码有点问题,直接这样写的话
# 运行程序,init 的时候会报错
# suggest = Completion(analyzer='ik_max_word')
# 现在解决方案是自定义一个 CustomAnalyzer
suggest = Completion(analyzer=ik_analyzer)
title = Text(analyzer='ik_max_word') # 需要进行分词,所以定义成 text
create_date = Date()
url = Keyword() # 无需分词
url_object_id = Keyword()
front_img_url = Keyword()
front_img_path = Keyword()
praise_nums = Integer()
comment_nums = Integer()
fav_nums = Integer()
tags = Text(analyzer='ik_max_word')
content = Text(analyzer='ik_max_word')
class Meta:
index = 'jobbole'
doc_type = 'article'
if __name__ == '__main__':
ArticleType.init() # 可以直接生成 mapping
编写 view
# search/views.py
import json
from django.shortcuts import render
from django.http import HttpResponse
from django.views.generic.base import View
from .models import ArticleType
# Create your views here.
class SearchSuggest(View):
def get(self, request):
key_words = request.GET.get('s', '')
re_datas = []
if key_words:
s = ArticleType.search()
s = s.suggest('my-suggest', key_words, completion={
"field": "suggest", "fuzzy": {
"fuzziness": 2
},
"size": 10
})
suggestions = s.execute_suggest()
for match in getattr(suggestions, 'my-suggest')[0].options:
source = match._source
re_datas.append(source['title'])
return HttpResponse(json.dumps(re_datas), content_type='application/json')
配置 urls.py
# LcvSearch/urls.py
from search.views import SearchSuggest
urlpatterns = [
...
# 处理搜索建议
url(r'^suggest/$', SearchSuggest.as_view(), name='suggest'),
]
image.png启动项目
image.png测试搜索建议功能已经实现了
django实现elasticsearch的搜索功能
编写 view
# search/views.py
import json
from datetime import datetime
from django.shortcuts import render
from django.http import HttpResponse
from django.views.generic.base import View
from .models import ArticleType
from elasticsearch import Elasticsearch
# 初始化一个 Elasticsearch 的连接
cline = Elasticsearch(hosts=['127.0.0.1'])
...
class SearchView(View):
def get(self, request):
key_words = request.GET.get('q', '')
page = request.GET.get('p', '1')
try:
page = int(page)
except:
page = 1
start_time = datetime.now()
response = cline.search(
index="jobbole",
body={
"query": {
"multi_match": {
"query": key_words,
"fields": ["tags", "title", "content"]
}
},
"from": (page-1)*10,
"size": 10,
"highlight": {
"pre_tags": ['<span class="keyWord">'],
"post_tags": ['</span>'],
"fields": {
"title": {},
"content": {},
}
}
}
)
end_time = datetime.now()
last_seconds = (end_time - start_time).total_seconds()
# 获取总数量
total_nums = response['hits']['total']
if page % 10 > 0:
page_nums = int(total_nums / 10) + 1
else:
page_nums = int(total_nums / 10)
hit_list = []
for hit in response['hits']['hits']:
hit_dict = {}
if 'title' in hit['highlight']:
hit_dict['title'] = ''.join(hit['highlight']['title'])
else:
hit_dict['title'] = hit['_source']['title']
if 'content' in hit['highlight']:
hit_dict['content'] = ''.join(hit['highlight']['content'])[:500]
else:
hit_dict['content'] = hit['_source']['content'][:500]
hit_dict['create_date'] = hit['_source']['create_date']
hit_dict['url'] = hit['_source']['url']
hit_dict['score'] = hit['_score']
hit_list.append(hit_dict)
return render(request, 'result.html', {
'all_hits': hit_list,
'key_words': key_words,
'total_nums': total_nums,
'page': page,
'page_nums': page_nums,
'last_seconds': last_seconds,
})
配置 urls.py
# LcvSearch/urls.py
from search.views import SearchSuggest, SearchView
urlpatterns = [
...
url(r'^search/$', SearchView.as_view(), name='search'),
]
将前端 HTML 页面中的变量替换为 view 视图中传递过来的变量
image.png image.png启动项目
网友评论