在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 相关文章推荐
改进Django中的表单的简单方法
Jul 17 Python
从头学Python之编写可执行的.py文件
Nov 28 Python
Python迭代器定义与简单用法分析
Apr 30 Python
Python单元测试实例详解
May 25 Python
python 自动批量打开网页的示例
Feb 21 Python
PyQt5 QTable插入图片并动态更新的实例
Jun 18 Python
jupyter notebook中美观显示矩阵实例
Apr 17 Python
Python读取图像并显示灰度图的实现
Dec 01 Python
Python操作Excel的学习笔记
Feb 18 Python
Scrapy实现模拟登录的示例代码
Feb 21 Python
Python读写Excel表格的方法
Mar 02 Python
Python中Cookies导出某站用户数据的方法
May 17 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之Smarty入门
2007/01/04 PHP
分享ThinkPHP3.2中关联查询解决思路
2015/09/20 PHP
ThinkPHP项目分组配置方法分析
2016/03/23 PHP
浅谈Laravel核心解读之Console内核
2018/12/02 PHP
Laravel框架源码解析之模型Model原理与用法解析
2020/05/14 PHP
javascript编程起步(第六课)
2007/01/10 Javascript
JavaScript 应用类库代码
2008/06/02 Javascript
JavaScript Tips 使用DocumentFragment加快DOM渲染速度
2010/06/28 Javascript
js实现微信分享代码
2020/10/11 Javascript
使用Ajax生成的Excel文件并下载的实例
2016/11/21 Javascript
微信小程序template模板实例详解
2017/10/27 Javascript
karma+webpack搭建vue单元测试环境的方法示例
2018/05/24 Javascript
vue 优化CDN加速的方法示例
2018/09/19 Javascript
实现高性能javascript的注意事项
2019/05/27 Javascript
python实现定制交互式命令行的方法
2014/07/03 Python
使用Python编写类UNIX系统的命令行工具的教程
2015/04/15 Python
使用python绘制常用的图表
2016/08/27 Python
解决pyecharts在jupyter notebook中使用报错问题
2020/04/23 Python
简单了解python字符串前面加r,u的含义
2019/12/26 Python
tensorflow tf.train.batch之数据批量读取方式
2020/01/20 Python
使用Python爬虫库BeautifulSoup遍历文档树并对标签进行操作详解
2020/01/25 Python
使用Python操作ArangoDB的方法步骤
2020/02/02 Python
Python爬虫程序架构和运行流程原理解析
2020/03/09 Python
环境工程毕业生自荐信
2013/11/17 职场文书
仓库理货员岗位职责
2013/12/18 职场文书
员工评语大全
2014/01/19 职场文书
学生党员的自我评价范文
2014/03/01 职场文书
2014年党课学习心得体会
2014/07/08 职场文书
作风建设年活动总结
2014/08/27 职场文书
英语教师个人工作总结
2015/02/09 职场文书
装配车间主任岗位职责
2015/04/08 职场文书
红色经典观后感
2015/06/18 职场文书
师范生见习总结范文
2015/06/23 职场文书
教师节座谈会主持词
2015/07/03 职场文书
Python 类,对象,数据分类,函数参数传递详解
2021/09/25 Python
使用Cargo工具高效创建Rust项目
2022/08/14 Javascript