(十)Flask 學習 —— 全文搜索

全文搜索

回顧

在前面的章節(分頁),咱們已經增強了數據庫查詢,所以可以在頁面上獲取各類查詢。html

今天,咱們會繼續探討數據庫的話題,只是領域不一樣。全部存儲內容的應用程序必須提供搜索能力。python

許多其它類型的網站可能使用了谷歌、必應等索引全部的內容而且提供查詢結果。這個對於大多數靜態頁面的網站,像論壇,是很好用。咱們應用程序 microblog 的基本單元是用戶短小的 blog,不是整個頁面。咱們但願搜索結果是動態的。例如,咱們想要在全部的 blog 中搜索關鍵詞 「dog」。這是顯而易見的,除非是有人搜索這個詞,否則大型的搜索引擎不可能索引搜索結果。所以咱們除了使用本身的搜索是別無選擇的。git

全文搜索引擎的簡介

不幸的是,在關係數據庫中的全文搜索支持沒有獲得很好的規範。每一個數據庫都以本身的方式實現全文搜索,而且 SQLAlchemy 沒有實現全文搜索。github

咱們目前使用了 SQLite 做爲數據庫,所以咱們能夠繞過 SQLAlchemy,使用 SQLite 提供的特性來建立全文文本索引。可是這並非一個好主意,由於若是咱們要更換數據庫的時候,咱們須要重寫全文搜索的代碼。數據庫

所以,相反咱們讓數據庫處理常規數據,咱們將建立一個專門的數據庫,專一服務於文本搜索。django

如今有一些開源的全文搜索引擎。在個人知識範圍內惟一一個用 Python 編寫的 Flask 擴展是 Whoosh。一個純 Python 的搜索引擎的好處就是在 Python 解釋器可用的任何地方可以安裝和運行。缺點也是很顯然的,性能可能比不上 C 或者 C++ 編寫的。個人觀點是最理想的解決方式就是開發一個鏈接不一樣搜索引擎的 Flask 擴展,以某種方式來處理搜索,就像 Flask-SQLAlchemy 同樣。可是目前在 Flask 中暫時沒有這類型的擴展。Django 開發者提供了一個很好的擴展,用來支持不一樣的全文搜索引擎,叫作 django-haystack。也許不久就會有人寫一個相似的 Flask 擴展。flask

若是你暫時沒有在虛擬環境上安裝 Flask-WhooshAlchemy,請安裝它。Windows 用戶應該運行這個:瀏覽器

flask\Scripts\pip install Flask-WhooshAlchemy

其它用戶必須運行這個:session

flask/bin/pip install Flask-WhooshAlchemy

Python 3 兼容性

很是不幸地是,Flask-WhooshAlchemy 這個包在 Python 3 中存在問題。而且 Flask-WhooshAlchemy 不會兼容 Python 3。我爲這個擴展作了一個分支而且作了一些改變以便其兼容 Python 3,所以大家須要卸載官方的版本而且安裝個人分支:app

$ flask/bin/pip uninstall flask-whooshalchemy
$ flask/bin/pip install git+git://github.com/miguelgrinberg/flask-whooshalchemy.git

使人遺憾地這不是惟一的問題。Whoosh 一樣與 Python 3 的兼容存在問題。我在測試中遇到過 這個問題 <https://bitbucket.org/mchaput/whoosh/issue/395/ascii-codec-error-when-performing-query>,可是以個人能力目前也沒法解決這個問題,只能等待官方的修復。目前來講,Python 3 暫時不能徹底地使用這個功能。

配置

配置 Flask-WhooshAlchemy 也是至關簡單。咱們只須要告訴擴展全文搜索數據庫的名稱(文件 config.py):

WHOOSH_BASE = os.path.join(basedir, 'search.db')

模型修改

由於把 Flask-WhooshAlchemy 整合進 Flask-SQLAlchemy,咱們須要在模型的類中指明哪些數據須要創建搜索索引(文件 app/models.py):

from app import app
import sys

if sys.version_info >= (3, 0):
    enable_search = False
    
else:
    enable_search = True
    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.body)

if enable_search:
    whooshalchemy.whoosh_index(app, Post)

模型有一個新的 __searchable__ 字段,這裏麪包含數據庫中的全部能被搜索而且創建索引的字段。在咱們的例子中,咱們只要索引 blog 的 body 字段。

經過調用 whoosh_index 函數,咱們爲這個模型初始化了全文搜索索引。

由於這個改變並不影響到關係數據庫的格式,所以不須要錄製新的遷移腳本。

由於以前存儲在數據庫的 blog 是沒有創建索引的。爲了保持數據庫和全文搜索引擎的同步,咱們須要刪除以前撰寫的 blog:

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

搜索

如今咱們準備開始搜索。首先讓咱們在數據庫中添加些 blog。有兩種方式去添加。咱們能夠運行應用程序,經過瀏覽器像普通用戶同樣添加 blog。另一種就是在 Python 提示符下。

在 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()
>>>

如今咱們在全文索引中有一些 blog,咱們能夠這樣搜索:

>>> 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 支持一個更增強大的 搜索查詢語言

整合全文搜索到應用程序

爲了使得搜索功能在咱們的應用程序中可用,咱們須要添加些修改。

配置

在配置文件中,咱們須要指明搜索結果返回的最大數量(文件 config.py):

MAX_SEARCH_RESULTS = 50

搜索表單

咱們準備在導航欄中添加一個搜索表單。把表單放在導航欄中是有好處的,由於應用程序全部頁都有搜索表單。

首先,咱們添加一個搜索表單類(文件 app/forms.py):

class SearchForm(Form):
    search = StringField('search', validators=[DataRequired()])

接着咱們必須建立一個搜索表單對象而且使得它對全部模版中可用,由於咱們將搜索表單放在導航欄中,導航欄是全部頁面共有的。最容易的方式就是在 before_request 函數中建立這個表單對象,接着把它放在全局變量 g 中(文件 app/views.py):

from forms import SearchForm

@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()

咱們接着添加表單到模板中(文件 app/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 函數僅僅當用戶登陸纔會建立一個表單對象,由於咱們的程序不會對非認證用戶顯示任何內容。

搜索視圖函數

上面的模版中,咱們在 action 字段中設置發送搜索請求到 search 視圖函數。search 視圖函數以下(文件 app/views.py):

@app.route('/search', methods = ['POST'])@login_requireddef 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))

這個函數實際作的事情很少,它只是從查詢表單這可以獲取查詢的內容,並把它做爲參數重定向另一頁。搜索工做不在這裏直接作的緣由仍是擔憂用戶無心中觸發了刷新,這樣會致使表單數據被重複提交。

搜索結果頁

一旦查詢的關鍵字被接收到,search_results 函數就會開始工做(文件 app/views.py):

from config import MAX_SEARCH_RESULTS

@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,而且把最大的結果數也做爲參數傳遞給 Whoosh。

最後一部分就是搜索結果的模版(文件 app/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 %}

結束語

若是你想要節省時間的話,你能夠下載 microblog-0.10.zip

相關文章
相關標籤/搜索