创建一个 autocomplete 输入系统 - 前端 + 后端
文章转载自:https://mp.weixin.qq.com/s/uqchdrkhdFsof0ZFtECujg
我们经常在网站搜索输入时,会帮我们提醒自动完成的功能,比如:
图片
当我们在百度上搜索 Elasticsearch 时,它会自动弹出一些可以让我们进行搜索的条目。在很多的情况下,用户可能直接选择其中的一个进行输入,而不需要打入全部的文字。
在我之前的文章里,有关 autocomplete,也即自动补全的内容,我有几篇文章可以供大家来进行参考:
Elasticsearch:Search-as-you-type 字段类型
Elasticsearch:使用 search_analyzer 及 edge ngram 来实现 Search-As-You-Type
Elasticsearch:定制分词器(analyzer)及相关性
在今天的文章中,我将使用几种方法来展示自动完成是如何实现的。为了方便大家理解下面的代码,请在 github 上下载我的代码:
git clone https://github.com/liu-xiao-guo/AutoComplete-Input-Elastic-Search-Python
$ pwd
/Users/liuxg/python/AutoComplete-Input-Elastic-Search-Python
$ tree -L 2
.
├── Backend
│ └── api.py
├── Frontend
│ ├── app.py
│ └── templates
├── README.md
└── games.json
整个项目的代码非常简单。
Backend:处理前端发送来的请求,并转发至 Elasticsearch
Frontend:处理网页发送的搜索请求
games.json:这是一个实验的数据
准备数据
我们首先把 games.json 这个 JSON 数据摄入到 Elasticsearch 中:
图片
图片
我们接下来选择下载的 games.json 文件:
图片
我们输入索引的名称为 games:
图片
在上面,我们需要修改 mappings 为:
{
"properties": {
"critic_score": {
"type": "long"
},
"developer": {
"type": "text"
},
"genre": {
"type": "keyword"
},
"global_sales": {
"type": "double"
},
"id": {
"type": "keyword"
},
"image_url": {
"type": "keyword"
},
"name": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword"
}
}
},
"platform": {
"type": "keyword"
},
"publisher": {
"type": "keyword"
},
"user_score": {
"type": "long"
},
"year": {
"type": "long"
}
}
}
把 name 字段修改为一个 multi-field 的字段。点击上面的 Import。这样就完成了我们的 games 索引的摄入。
图片
运行 Backend
我们接下来运行 Backend 应用。这是一个基于 Flask 的 Python 应用。我们需要安装它所需要的 Python 包:
pip3 install flask
pip3 install flask_restful
pip3 install Api
pip3 install reqparse
pip3 install Elasticsearch
我们的 api.py 的设计非常简单:
api.py
try:
from flask import app,Flask
from flask_restful import Resource, Api, reqparse
import elasticsearch
from elasticsearch import Elasticsearch
import datetime
import concurrent.futures
import requests
import json
except Exception as e:
print("Modules Missing {}".format(e))
app = Flask(__name__)
api = Api(app)
#------------------------------------------------------------------------------------------------------------
INDEX_NAME = 'games'
es = Elasticsearch([{'host': 'localhost', 'port': 9200, 'http_auth':('elastic', 'password')}])
#------------------------------------------------------------------------------------------------------------
class Controller(Resource):
def __init__(self):
self.query = parser.parse_args().get("query", None)
self.baseQuery ={
"_source": [],
"size": 0,
"min_score": 0.5,
"query": {
"bool": {
"must": [
{
"match_phrase_prefix": {
"name": {
"query": "{}".format(self.query)
}
}
}
],
"filter": [],
"should": [],
"must_not": []
}
},
"aggs": {
"auto_complete": {
"terms": {
"field": "name.keyword",
"order": {
"_count": "desc"
},
"size": 25
}
}
}
}
def get(self):
res = es.search(index=INDEX_NAME, size=0, body=self.baseQuery)
return res
parser = reqparse.RequestParser()
parser.add_argument("query", type=str, required=True, help="query parameter is Required ")
api.add_resource(Controller, '/autocomplete')
if __name__ == '__main__':
app.run(debug=True, port=4000)
在上面,我的集群的访问用户名及密码为:elastic/password。在上面,它做了一个很简单的 match_phrase_prefix 搜索:
GET games/_search
{
"size": 0,
"query": {
"bool": {
"must": [
{
"match_phrase_prefix": {
"name": "final fan"
}
}
],
"must_not": [],
"filter": [],
"should": []
}
},
"aggs": {
"auto_complete": {
"terms": {
"field": "name.keyword",
"size": 25
}
}
}
}
它的返回值为:
{
"took" : 3,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 11,
"relation" : "eq"
},
"max_score" : null,
"hits" : [ ]
},
"aggregations" : {
"auto_complete" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "Crisis Core: Final Fantasy VII",
"doc_count" : 1
},
{
"key" : "Dissidia: Final Fantasy",
"doc_count" : 1
},
{
"key" : "Final Fantasy IX",
"doc_count" : 1
},
{
"key" : "Final Fantasy Tactics",
"doc_count" : 1
},
{
"key" : "Final Fantasy VII",
"doc_count" : 1
},
{
"key" : "Final Fantasy VIII",
"doc_count" : 1
},
{
"key" : "Final Fantasy X",
"doc_count" : 1
},
{
"key" : "Final Fantasy X-2",
"doc_count" : 1
},
{
"key" : "Final Fantasy XII",
"doc_count" : 1
},
{
"key" : "Final Fantasy XIII",
"doc_count" : 1
},
{
"key" : "Final Fantasy XIII-2",
"doc_count" : 1
}
]
}
}
}
从上面的结果中,我们可以看出来搜索的结果。
我们使用如下的命令来运行 Backend 的应用:
$ pwd
/Users/liuxg/python/AutoComplete-Input-Elastic-Search-Python/Backend
$ python api.py
* Serving Flask app "api" (lazy loading)
* Environment: production
WARNING: This is a development server. Do not use it in a production deployment.
Use a production WSGI server instead.
* Debug mode: on
* Running on http://127.0.0.1:4000/ (Press CTRL+C to quit)
* Restarting with fsevents reloader
* Debugger is active!
* Debugger PIN: 119-780-958
这样我们的 Backend 就运行起来了。我们在下面来运行 Frontend 的应用。
运行 Frontend
我们进入到 Frondend 的子目录中,并使用如下的命令来进行运行:
$ pwd
/Users/liuxg/python/AutoComplete-Input-Elastic-Search-Python/Frontend
$ ls
app.py templates
$ python app.py
* Serving Flask app "app" (lazy loading)
* Environment: production
WARNING: This is a development server. Do not use it in a production deployment.
Use a production WSGI server instead.
* Debug mode: on
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
* Restarting with fsevents reloader
* Debugger is active!
* Debugger PIN: 119-780-958
如上所示,前端运行于地址 http://127.0.0.1:5000/。我们在浏览器中打开它:
图片
图片
图片
当我们在输入框中打入 final 时,我们可以看到候选的名单出现了。它可以让我们很方便地进行输入。我们甚至可以选择其中的一个进行输入:
图片
在客户端的设计中,它使用了 ajax 技术。当我们的输入发生改变时自动补全窗口的里的名单也会自动发生变化。
更进一步改进
在某种程度上,上面的设计还是不错的。它满足了许多情况下的需求。接下来,我们来使用 search-as-you-type 字段类型来完成我们的设计。我们可以参考我之前的文章 “ Elasticsearch:Search-as-you-type 字段类型”。我们首先来删除之前导入的 games 索引以及被创建的索引模式 games*。然后,我们在摄入数据时:
图片
点击当前页面的 Import 按钮,并完成 games 索引的创建:
图片
这样我们的 name 字段为 search_as_you_type 类型。由于一些原因,search_as_you_type 类型的数据目前还不能定义 multi-field,我们不能为这个字段添加 keyword 字段。
https://github.com/elastic/elasticsearch/issues/56326issue 里,有一个解决方案就是把 search_as_you_type 作为一个 multi-field,而把 keyword 作为一个主要的字段。在本文章中就不做展开了。留给开发者自己研究。
图片
请注意在下面的练习中,我使用的不是这个含有 multi-field 的定义。
我们需要修改我们的 Backend 才能使得它起作用。我们修改 api.py 如下:
Backend/api.py
try:
from flask import app,Flask
from flask_restful import Resource, Api, reqparse
import elasticsearch
from elasticsearch import Elasticsearch
import datetime
import concurrent.futures
import requests
import json
except Exception as e:
print("Modules Missing {}".format(e))
app = Flask(__name__)
api = Api(app)
#------------------------------------------------------------------------------------------------------------
INDEX_NAME = 'games'
es = Elasticsearch([{'host': 'localhost', 'port': 9200, 'http_auth':('elastic', 'password')}])
#------------------------------------------------------------------------------------------------------------
class Controller(Resource):
def __init__(self):
self.query = parser.parse_args().get("query", None)
print(self.query)
self.baseQuery ={
# "_source": [],
"size": 10,
"min_score": 0.5,
"query": {
"bool": {
"must": [
{
"match_phrase_prefix": {
"name": {
"query": "{}".format(self.query)
}
}
}
],
"filter": [],
"should": [],
"must_not": []
}
}
}
def get(self):
res = es.search(index=INDEX_NAME, size=25, body=self.baseQuery)
return res
parser = reqparse.RequestParser()
parser.add_argument("query", type=str, required=True, help="query parameter is Required ")
api.add_resource(Controller, '/autocomplete')
if __name__ == '__main__':
app.run(debug=True, port=4000)
在上面,我们使用了 match_phrase_prefix 来完成我们的搜索。它相当于如下的搜索:
GET games/_search
{
"query": {
"match_phrase_prefix": {
"name": "final fan"
}
}
}
由于我们没有使用 aggs 来返回结果,取而代之的是搜索的文档,那么我们需要修改相应的 home.html 文档:
Frontend/templates/home.html
我们把 typeHandler 修改为:
const typeHandler = function(e) {
$result.innerHTML = e.target.value;
console.log(e.target.value);
$.ajax({
url: "/pipe",
type : 'POST',
cache: false,
data:{'data': e.target.value},
success: function(html)
{
console.log(html)
var data = html.hits.hits
var _ = []
$.each(data, (index, value)=>{
_.push(value._source.name)
});
console.log(_)
$( "#source" ).autocomplete({
source: _
});
}
});
}
这个是由于我们的响应格式的变化:
图片
我们重新运行 Backend 和 Frontend,那么我们可以看到和之前一模一样的结果:
图片
你是不是觉得把 name 字段的类型修改后也没有什么特别的,对吧? 但是我们可以尝试一下如下的搜索:
图片
在上面,我们输入 fi 及 fan,我们没有看到任何的结果。我们没有充分利用 search_as_you_type 给我们带来的好处。
我们重新修改 Backend 中的 api.py 为如下的代码:
Backend/api.py
try:
from flask import app,Flask
from flask_restful import Resource, Api, reqparse
import elasticsearch
from elasticsearch import Elasticsearch
import datetime
import concurrent.futures
import requests
import json
except Exception as e:
print("Modules Missing {}".format(e))
app = Flask(__name__)
api = Api(app)
#------------------------------------------------------------------------------------------------------------
INDEX_NAME = 'games'
es = Elasticsearch([{'host': 'localhost', 'port': 9200, 'http_auth':('elastic', 'password')}])
#------------------------------------------------------------------------------------------------------------
class Controller(Resource):
def __init__(self):
self.query = parser.parse_args().get("query", None)
print(self.query)
self.baseQuery ={
"_source": [],
"size": 0,
"min_score": 0.5,
"query": {
"bool": {
"must": [
{
"multi_match": {
"query": "{}".format(self.query),
"type": "bool_prefix",
"operator": "or",
"fields": [
"name",
"name._2gram",
"name._3gram"
]
}
}
],
"filter": [],
"should": [],
"must_not": []
}
}
}
def get(self):
res = es.search(index=INDEX_NAME, size=25, body=self.baseQuery)
return res
parser = reqparse.RequestParser()
parser.add_argument("query", type=str, required=True, help="query parameter is Required ")
api.add_resource(Controller, '/autocomplete')
if __name__ == '__main__':
app.run(debug=True, port=4000)
在上面,我使用了 multi-match。上面的搜索相当于这样的命令:
GET games/_search
{
"query": {
"multi_match": {
"query": "fi fan",
"type": "bool_prefix",
"operator": "or",
"fields": [
"name",
"name._2gram",
"name._3gram"
]
}
}
}
上面的命令,可以搜索出来前缀为 fi 及 fan 的文档。
我们也同时把 typeHandler 修改为:
const typeHandler = function(e) {
$result.innerHTML = e.target.value;
console.log(e.target.value);
$.ajax({
url: "/pipe",
type : 'POST',
cache: false,
data:{'data': e.target.value},
success: function(html)
{
console.log(html)
var data = html.hits.hits
var _ = []
console.log("nice")
$.each(data, (index, value)=>{
_.push(value._source.name)
});
console.log("list:")
console.log(_)
$( "#source" ).autocomplete({
source: _
});
$( "#result" ).text(_)
}
});
}
在上面,我们使用 result 来显示结果。我们重新运行 Backend 及 Frontend:
图片
尽管画面不是很美,但是,当我们输入诸如 "fi fan" 这样的词,我们可以看到我们想要的搜索的结果。
创建一个 autocomplete 输入系统 - 前端 + 后端的更多相关文章
- Spring Boot+Jpa(MYSQL)做一个登陆注册系统(前后端数据库一站式编程)
Spring Boot最好的学习方法就是实战训练,今天我们用很短的时间启动我们第一个Spring Boot应用,并且连接我们的MySQL数据库. 我将假设读者为几乎零基础,在实战讲解中会渗透Sprin ...
- 动手实践记录(利用django创建一个博客系统)
1.添加一个分类的标签,和主表的关系是 外键 class Category(models.Model): """ 分类 """ name = ...
- ASP.NET Core模块化前后端分离快速开发框架介绍之2、快速创建一个业务模块
源码地址 GitHub:https://github.com/iamoldli/NetModular 演示地址 地址:https://nm.iamoldli.com 账户:admin 密码:admin ...
- 利用BitLocker和vhdx创建一个有加密的Win10系统
如果电脑不支持TPM加密BitLocker,就无法对系统盘进行全盘加密. 可以采用一个变通的方法:创建一个vhdx,将这个虚拟磁盘进行BitLocker加密,然后在这个盘里安装操作系统,最后把vhdx ...
- UE4编程之C++创建一个FPS工程(一)创建模式&角色&处理输入
转自:http://blog.csdn.net/u011707076/article/details/44180951 从今天开始,我们一起来学习一下,如何使用C++将一个不带有任何初学者内容的空模板 ...
- 基于gulp编写的一个简单实用的前端开发环境好了,安装完Gulp后,接下来是你大展身手的时候了,在你自己的电脑上面随便哪个地方建一个目录,打开命令行,然后进入创建好的目录里面,开始撸代码,关于生成的json文件请点击这里https://docs.npmjs.com/files/package.json,打开的速度看你的网速了注意:以下是为了演示 ,我建的一个目录结构,你自己可以根据项目需求自己建目
自从Node.js出现以来,基于其的前端开发的工具框架也越来越多了,从Grunt到Gulp再到现在很火的WebPack,所有的这些新的东西的出现都极大的解放了我们在前端领域的开发,作为一个在前端领域里 ...
- springmvc在处理请求过程中出现异常信息交由异常处理器进行处理,自定义异常处理器可以实现一个系统的异常处理逻辑。为了区别不同的异常通常根据异常类型自定义异常类,这里我们创建一个自定义系统异常,如果controller、service、dao抛出此类异常说明是系统预期处理的异常信息。
springmvc在处理请求过程中出现异常信息交由异常处理器进行处理,自定义异常处理器可以实现一个系统的异常处理逻辑. 1.1 异常处理思路 系统中异常包括两类:预期异常和运行时异常RuntimeEx ...
- 10.4 android输入系统_框架、编写一个万能模拟输入驱动程序、reader/dispatcher线程启动过程源码分析
1. 输入系统框架 android输入系统官方文档 // 需FQhttp://source.android.com/devices/input/index.html <深入理解Android 卷 ...
- 无废话Android之listview入门,自定义的数据适配器、采用layoutInflater打气筒创建一个view对象、常用数据适配器ArrayAdapter、SimpleAdapter、使用ContentProvider(内容提供者)共享数据、短信的备份、插入一条记录到系统短信应用(3)
1.listview入门,自定义的数据适配器 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/and ...
随机推荐
- java-数据输入,分支结构
数据输入 1.Scanner使用的基本步骤" 导包:import java.util.Scanner;(导包的动作必须出现在类定义的上边) 创建对象:Scanner sc = new Sca ...
- 下载Chrome离线安装包
https://www.google.com/chrome/thankyou.html?standalone=1&platform=mac&installdataindex=defau ...
- 自定义nginx的日志格式存储到Filebeat和Logstash
vim /etc/nginx/nginx.conf log_format main '$remote_addr - $remote_user [$time_local] ' '"$reque ...
- day04 缓冲字符流__异常处理
缓冲字符流 缓冲字符输入流:java.io.BufferedReader 是一个高级的字符流,特点是块读文本数据,并且可以按行读取字符串. package io; import java.io.*; ...
- Solution -「Luogu 4135」作诗
写在前面 & 前置芝士 好像是好久没有打理 blog 了.感觉上学期是有点颓.嘶,初三了好好冲一次吧. 那么回到这道题目.你会分块就能看懂. 题目大意 先挂个来自洛谷的 link. ...
- 使用Three.js实现炫酷的赛博朋克风格3D数字地球大屏 🌐
声明:本文涉及图文和模型素材仅用于个人学习.研究和欣赏,请勿二次修改.非法传播.转载.出版.商用.及进行其他获利行为. 背景 近期工作有涉及到数字大屏的需求,于是利用业余时间,结合 Three.js ...
- vscode problem
1.Inconsistent use of tabs and spaces in indentation 原因:tab和空格键不能同时使用 vs code按住ctrl + p,输入以下内容 >c ...
- 小白之Python基础(一)
一.数字类型: 1.整形 十进制:默认为十进制:(如:99,100.......) 十六进制: 0x,0X开头的表示16进制数 二进制:0b,0B开头的表示2进制数 八进制: 0o,0O开头的表示8进 ...
- 【原创】Auto.js get和post 案例
本文所有教程及源码.软件仅为技术研究.不涉及计算机信息系统功能的删除.修改.增加.干扰,更不会影响计算机信息系统的正常运行.不得将代码用于非法用途,如侵立删! Auto.js get和post 案例 ...
- 自定义spring boot starter 初尝试
自定义简单spring boot starter 步骤 从几篇博客中了解了如何自定义starter,大概分为以下几个步骤: 1 引入相关依赖: 2 生成属性配置类: 3 生成核心服务类: 4 生成自动 ...