在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简单的实现树莓派的WEB控制
Feb 18 Python
python 队列详解及实例代码
Oct 18 Python
老生常谈Python基础之字符编码
Jun 14 Python
python读取txt文件中特定位置字符的方法
Dec 24 Python
Python使用pydub库对mp3与wav格式进行互转的方法
Jan 10 Python
Python查找最长不包含重复字符的子字符串算法示例
Feb 13 Python
Python利用sqlacodegen自动生成ORM实体类示例
Jun 04 Python
python实现批量修改文件名
Mar 23 Python
python中如何写类
Jun 29 Python
Django ORM判断查询结果是否为空,判断django中的orm为空实例
Jul 09 Python
详解使用python爬取抖音app视频(appium可以操控手机)
Jan 26 Python
将Python代码打包成.exe可执行文件的完整步骤
May 12 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的面试题集,附我的答案和分析(一)
2006/11/19 PHP
POSIX 风格和兼容 Perl 风格两种正则表达式主要函数的类比(preg_match, preg_replace, ereg, ereg_replace)
2010/10/12 PHP
php stripslashes和addslashes的区别
2014/02/03 PHP
使用PHP Socket 编程模拟Http post和get请求
2014/11/25 PHP
PHP CURL 内存泄露问题解决方法
2015/02/12 PHP
PHP使用逆波兰式计算工资的方法
2015/07/29 PHP
PHP中Enum(枚举)用法实例详解
2015/12/07 PHP
ExtJs设置GridPanel表格文本垂直居中示例
2013/07/15 Javascript
收集json解析的四种方法分享
2014/01/17 Javascript
谈谈第三方App接入微信登录 解读
2016/12/27 Javascript
js中获取键盘按下键值event.keyCode、event.charCode和event.which的兼容性详解
2017/03/15 Javascript
详解angular2.x创建项目入门指令
2018/10/11 Javascript
详解axios中封装使用、拦截特定请求、判断所有请求加载完毕)
2019/04/09 Javascript
javascript实现对话框功能警告(alert 消息对话框)确认(confirm 消息对话框)
2019/05/07 Javascript
JavaScript Math对象和调试程序的方法分析
2019/05/13 Javascript
vue实现行列转换的一种方法
2019/08/06 Javascript
vue 翻页组件vue-flip-page效果
2020/02/05 Javascript
浅谈JS for循环中使用break和continue的区别
2020/07/21 Javascript
[47:55]Ti4第二日主赛事败者组 NaVi vs EG 1
2014/07/20 DOTA
13个最常用的Python深度学习库介绍
2017/10/28 Python
python绘制简单折线图代码示例
2017/12/19 Python
Python如何应用cx_Oracle获取oracle中的clob字段问题
2019/08/27 Python
使用NumPy读取MNIST数据的实现代码示例
2019/11/20 Python
让IE可以变相支持CSS3选择器
2010/01/21 HTML / CSS
怀俄明州飞钓:Platte River Fly Shop
2017/12/28 全球购物
芬兰设计商店美国:Finnish Design Shop US
2019/03/25 全球购物
Vrbo英国:预订度假屋
2020/08/19 全球购物
个人贷款授权委托书样本
2014/10/07 职场文书
党的群众路线教育实践活动专题组织生活会发言材料
2014/10/17 职场文书
道德与公民自我评价
2015/03/09 职场文书
父亲节感言
2015/08/03 职场文书
大学社团活动总结怎么写
2019/06/21 职场文书
创业计划书之熟食店
2019/10/16 职场文书
Springboot配置suffix指定mvc视图的后缀方法
2021/07/03 Java/Android
Javascript使用integrity属性进行安全验证
2021/11/07 Javascript
maven 解包依赖项中的文件的解决方法
2022/07/15 Java/Android