在Python的Flask框架中实现全文搜索功能


Posted in Python onApril 20, 2015

 全文检索引擎入门

灰常不幸的是,关系型数据库对全文检索的支持没有被标准化。不同的数据库通过它们自己的方式来实现全文检索,而且SQLAlchemy在全文检索上也没有提供一个好的抽象。

我们现在使用SQLite作为我们的数据库,所以我们可以绕开SQLAlchemy而使用SQLite提供的工具来创建一个全文检索索引。但这么做不怎么好,因为如果有一天我们换用别的数据库,那么我们就得重写另一个数据库的全文检索方法。

所以我们的方案是,我们将让我们现有的数据库处理常规数据,然后我们创建一个专门的数据库来解决全文检索。

只有很少的开源的全文检索引擎。据我说知只有一个Whoosh提供了Flask的扩展,它是用Python语言写的全文检索引擎。使用纯Python引擎的优点是它可以运行在任何有Python解释器的地方。缺点就是它的搜索性能没有达到用C或者C++写的搜索引擎那么好。在我的脑子里理想的解决方案是有一个搜索引擎,它提供了Flask的扩展,能连接大多数数据库,而且还要像Flask-SQLAlchemy那样提供一个能自由使用大多数数据库的方法,但现在貌似木有这样的全文检索引擎。Django的开发者有一个非常棒的,支持大多数全文检索引擎的扩展,叫django-haystack。希望有一天某个家伙能为Flask提供一个相似的扩展。

但现在,我们将通过Whoosh实现我们自己的全文检索。我们将使用Flask-WhooshAlchemy扩展,该扩展使得Whoosh数据库和Flask-SQLAlchemy模块结合起来。

如果你还没在你的虚拟环境中安装Flask-WhooshAlchemy扩展,马上安装它。

Windows用户用以下命令安装:
 

flask\Scripts\pip install Flask-WhooshAlchemy

其他用户用以下命令安装:

 

flask/bin/pip install Flask-WhooshAlchemy

配置

配置Flask-WhooshAlchemy灰常简单。我们只需要告诉扩展全文检索数据库的名字即可(fileconfig.py):
 
WHOOSH_BASE = os.path.join(basedir, 'search.db')
修改模块

在将Flask-WhooshAlchemy和Flask-SQLAlchemy结合起来时,我们需要在合适的模块类(fileapp/models.py)指定哪些数据时需要被索引的:
 

from app import app
import flask.ext.whooshalchemy as whooshalchemy
 
class Post(db.Model):
  __searchable__ = ['body']
 
  id = db.Column(db.Integer, primary_key = True)
  body = db.Column(db.String(140))
  timestamp = db.Column(db.DateTime)
  user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
 
  def __repr__(self):
    return '<Post %r>' % (self.text)
 
whooshalchemy.whoosh_index(app, Post)

这个模块有一个新的__searchable__字段,它是一个列表,包括了所有可以被当做搜索索引的数据库字段。在我们的项目里我们只需要所有文章帖子的body字段。
 

在这个模块中,我们也必须通过调用whoosh_index这个方法来初始化全文索引。

这不是一个能影响我们关系型数据库的改变,所以我们没必要换新的数据库。

不幸的是所有的博客文章在添加全文检索引擎之前就已经存在于数据库中了,而且没有被索引。为了保持数据库和全文检索引擎的同步,我们将在数据库中删除所有已经存在的博客文章,然后重新开始。首先我们打开Python解释器。Windows用户为以下内容:
 

flask\Scripts\python

其它操作系统用户:

 

flask/bin/python

然后在Python命令提示符中删除所有博客文章:
 

>>> from app.models import Post
>>> from app import db
>>> for post in Post.query.all():
...  db.session.delete(post)
>>> db.session.commit()

搜索

现在我们开始做搜索。首先,让我们添加几篇博客文章到数据库。我们有两种方法做这个事。我们可以像普通用户一样通过网页打开应用程序添加文章,或者直接在Python命令行里添加。

用一下方法从命令行添加:
 

>>> from app.models import User, Post
>>> from app import db
>>> import datetime
>>> u = User.query.get(1)
>>> p = Post(body='my first post', timestamp=datetime.datetime.utcnow(), author=u)
>>> db.session.add(p)
>>> p = Post(body='my second post', timestamp=datetime.datetime.utcnow(), author=u)
>>> db.session.add(p)
>>> p = Post(body='my third and last post', timestamp=datetime.datetime.utcnow(), author=u)
>>> db.session.add(p)
>>> db.session.commit()

Flask-WhooshAlchemy这个扩展非常不错,因为它能连接Flask-SQLAlchemy然后自动提交。我们不需要维护全文索引,因为它已经很明显的帮我们做了这件事。

现在我们已经在全文索引中有了一些文章,我们可以搜搜看了:
 

>>> Post.query.whoosh_search('post').all()
[<Post u'my second post'>, <Post u'my first post'>, <Post u'my third and last post'>]
>>> Post.query.whoosh_search('second').all()
[<Post u'my second post'>]
>>> Post.query.whoosh_search('second OR last').all()
[<Post u'my second post'>, <Post u'my third and last post'>]

上面的例子可以看出,查询不需要限制为一个单词。实际上,Whoosh提供了一个漂亮又强大的搜索查询语言(search query language)。
 
整合全文检索到应用程序

为了让我们应用程序的用户能用上搜索功能,我们还需要增加一点小小的改变。
配置

就配置而言,我们仅仅需要指定最大的搜索结果返回数(fileconfig.py):
 

MAX_SEARCH_RESULTS = 50

搜索表单

我们需要在页面顶部的导航栏中增加一个搜索框。把搜索框放到顶部是极好的,因为这样所有页面就都有搜索框了(注:所有页面公用导航栏)。

首先我们增加一个搜索表单类(fileapp/forms.py):
 

class SearchForm(Form):
  search = TextField('search', validators = [Required()])

然后我们需要增加一个搜索表单对象,而且要让它对所有模板可用,这么做是因为我们要将搜索表单放到所有页面的共同的导航栏。完成这个最简单的方法是在before_request handler上创建一个form,然后将它传到Flask的全局变量g(fileapp/views.py):
 

@app.before_request
def before_request():
  g.user = current_user
  if g.user.is_authenticated():
    g.user.last_seen = datetime.utcnow()
    db.session.add(g.user)
    db.session.commit()
    g.search_form = SearchForm()

然后我们添加form到我们的模板(fileapp/templates/base.html):

 

<div>Microblog:
  <a href="{{ url_for('index') }}">Home</a>
  {% if g.user.is_authenticated() %}
  | <a href="{{ url_for('user', nickname = g.user.nickname) }}">Your Profile</a>
  | <form style="display: inline;" action="{{url_for('search')}}" method="post" name="search">{{g.search_form.hidden_tag()}}{{g.search_form.search(size=20)}}<input type="submit" value="Search"></form>
  | <a href="{{ url_for('logout') }}">Logout</a>
  {% endif %}
</div>

注意,我们只是当有用户登录时才会显示这个搜索框。同样的,before_request handler只有在有用户登录时才会创建form,这是因为我们的应用程序不会展示任何内容给没有经过认证的用户。

搜索显示方法(search view funciton)

上面我们已经设置了form的action字段,它会发送所有的搜索请求到search view方法。这就是我们要执行全文检索查询的地方(fileapp/views.py):
 

@app.route('/search', methods = ['POST'])
@login_required
def search():
  if not g.search_form.validate_on_submit():
    return redirect(url_for('index'))
  return redirect(url_for('search_results', query = g.search_form.search.data))

这个方法干的事也不是很多,它只是从表单收集了搜索查询的字段,然后把这些字段作为参数传给查询方法,最后重定向到另一个页面。不在这儿直接做查询的原因是如果一个用户点击了刷新按钮,那么浏览器就会弹出“表单数据将被重新提交”的警告窗口。所以当一个POST请求的响应结果为重定向的时候,这种警告提示就被避免了,因为重定向之后浏览器的刷新按钮将会在重定向的页面被重新载入。

搜索结果页面

一旦一个查询字段被接受,form POST handler就会通过页面重定向把它发送到search_result handler(fileapp/views.py):
 

@app.route('/search_results/<query>')
@login_required
def search_results(query):
  results = Post.query.whoosh_search(query, MAX_SEARCH_RESULTS).all()
  return render_template('search_results.html',
    query = query,
    results = results)

然后搜索结果显示方法会发送这个查询到Whoosh,参数是最大的搜索结果数目,因为我们不想呈现一个很大数目的结果页面,所以我们只显示前50条数据。

最后一部分需要完成的是搜索结果的模板(fileapp/templates/search_results.html):
 

<!-- extend base layout -->
{% extends "base.html" %}
 
{% block content %}
<h1>Search results for "{{query}}":</h1>
{% for post in results %}
  {% include 'post.html' %}
{% endfor %}
{% endblock %}

这儿,我们又可以重新使用我们的post.html页面,所以我们不用担心替换一个新的页面或者其他格式的页面元素,因为所有这些在sub-template中都是通用的方法。

后记

我们现在就有了一个完整的、非常重要的、也是经常被忽视的功能,这也是任何一个优秀的web应用必须具备的功能。

这个时刻更新的微博客应用(换气中···)的源码你可以从这里找到:

microblog-0.10.zip

Python 相关文章推荐
使用Python写个小监控
Jan 27 Python
Python并发之多进程的方法实例代码
Aug 15 Python
[原创]Python入门教程2. 字符串基本操作【运算、格式化输出、常用函数】
Oct 29 Python
python的re模块使用方法详解
Jul 26 Python
python中字典按键或键值排序的实现代码
Aug 27 Python
Python-Flask:动态创建表的示例详解
Nov 22 Python
简单介绍django提供的加密算法
Dec 18 Python
Python实现图像去噪方式(中值去噪和均值去噪)
Dec 18 Python
python使用OpenCV模块实现图像的融合示例代码
Apr 10 Python
Python本地及虚拟解释器配置过程解析
Oct 13 Python
windows系统Tensorflow2.x简单安装记录(图文)
Jan 18 Python
Python生成九宫格图片的示例代码
Apr 14 Python
Python的Flask框架中实现分页功能的教程
Apr 20 #Python
在Python的Flask框架中实现单元测试的教程
Apr 20 #Python
Python的Flask框架中实现登录用户的个人资料和头像的教程
Apr 20 #Python
Python的Flask框架中实现简单的登录功能的教程
Apr 20 #Python
Python的Flask框架与数据库连接的教程
Apr 20 #Python
Python的Flask框架中web表单的教程
Apr 20 #Python
在Python的Flask框架中使用模版的入门教程
Apr 20 #Python
You might like
PHP中全面阻止SQL注入式攻击分析小结
2012/01/30 PHP
浅析php学习的路线图
2013/07/10 PHP
PHP获取浏览器信息类和客户端地理位置的2个方法
2014/04/24 PHP
PHP实现图片裁剪、添加水印效果代码
2014/10/01 PHP
php将access数据库转换到mysql数据库的方法
2014/12/24 PHP
PHP中的随机性 你觉得自己幸运吗?
2016/01/22 PHP
浅析PHP反序列化中过滤函数使用不当导致的对象注入问题
2020/02/15 PHP
jquery随机展示头像代码
2011/12/21 Javascript
JavaScript实现简单图片滚动附源码下载
2014/06/17 Javascript
bootstrap table 服务器端分页例子分享
2015/02/10 Javascript
jQuery控制网页打印指定区域的方法
2015/04/07 Javascript
setTimeout内不支持jquery的选择器的解决方案
2015/04/28 Javascript
在JavaScript中访问字符串的子串
2015/07/07 Javascript
TypeOf这些知识点你了解吗
2016/02/21 Javascript
使用jQuery实现Web页面换肤功能的要点解析
2016/05/12 Javascript
JQuery 设置checkbox值二次无效的解决方法
2016/07/22 Javascript
JavaScript基本类型值-Undefined、Null、Boolean
2017/02/23 Javascript
bootstrap精简教程_动力节点Java学院整理
2017/07/14 Javascript
es6学习之解构时应该注意的点
2017/08/29 Javascript
jQuery中 DOM节点操作方法大全
2017/10/12 jQuery
js 数组详细操作方法及解析合集
2018/06/01 Javascript
vue仿element实现分页器效果
2018/09/13 Javascript
python中将字典形式的数据循环插入Excel
2018/01/16 Python
Python实现通过继承覆盖方法示例
2018/07/02 Python
Python控制台实现交互式环境执行
2020/06/09 Python
Matplotlib配色之Colormap详解
2021/01/05 Python
CSS3的RGBA中关于整数和百分比值的转换
2015/08/04 HTML / CSS
电子商务网站的创业计划书
2014/01/05 职场文书
大学生创业策划书
2014/02/02 职场文书
卫生安全检查制度
2014/02/04 职场文书
合同协议书格式
2014/04/18 职场文书
经典演讲稿汇总
2014/05/19 职场文书
典型事迹材料范文
2014/12/29 职场文书
捐助感谢信
2015/01/22 职场文书
2015庆祝七一建党节94周年活动总结
2015/03/20 职场文书
高中物理教学反思
2016/02/19 职场文书