Это десятая статья в серии, где я описываю свой опыт написания веб-приложения на Python с использованием микрофреймворка Flask.
Цель данного руководства — разработать довольно функциональное приложение-микроблог, которое я за полным отсутствием оригинальности решил назвать microblog
.
Часть 2: Шаблоны
Часть 3: Формы
Часть 4: База данных
Часть 5: Вход пользователей
Часть 6: Страница профиля и аватары
Часть 7: Unit-тестирование
Часть 8: Подписчики, контакты и друзья
Часть 9: Пагинация
Часть 10: Полнотекстовый поиск(данная статья)
Часть 11: Поддержка e-mail
Часть 12: Реконструкция
Часть 13: Дата и время
Часть 14: I18n and L10n
Часть 15: Ajax
Часть 16: Отладка, тестирование и профилирование
Часть 17: Развертывание на Linux (даже на Raspberry Pi!)
Часть 18: Развертывание на Heroku Cloud
Краткое повторение
В предыдущей статье мы улучшили наши запросы так, чтобы они возвращали посты на страницу.
Сегодня мы продолжим работать с нашей базой данных, но с другой целью. Все приложения, которые хранят контент, должны предоставлять возможность поиска.
Для многих типов веб-сайтов можно просто позволить Google, Bing, и т.п. проиндексировать все и предоставлять результаты поиска. Это хорошо работает с сайтами, которые имеют в основе статические страницы, такие как форум. В нашем маленьком приложении базовая единица контента это короткий пользовательский пост, а не целая страница. Мы хотим более динамичный результат поиска. Для примера, если мы ищем слово «dog» мы хотим видеть все сообщения пользователей, включающие это слово. Очевидно, что страница результата поиска не существует до тех пор, пока никто не проведет поиск, поэтому поисковики не смогут проиндексировать ее.
Введение в системы полнотекстового поиска
К сожалению поддержка полнотекстового поиска в реляционных базах данные не стандартизирована. Каждая база даннные реализует полнотекстовый поиск по-своему, и SQLAlchemy не имеет на этот случай подходящей абстракции.
Мы сейчас используем SQLite для нашей базы, поэтому мы просто могли создать полнотекстовый индекс, используя возможности предоставялемые SQLite, в обход SQLAlchemy. Но это плохая идея, потому что если в один прекрасный день мы решим перейти на другую базу данных, нам придется переписать наш полнотекстовый поиск для другой базы данных.
Вместо этого, мы собираемся оставить нашу базу для работы с обычными данными, и создать специализированную базу для поиска.
Есть несколько систем полнотекстового поиска с открытым исходным кодом. Только одна, насколько мне известно, имеет расширение Flask называемое Whoosh, и движок ее тоже написан на Python. Преимущество использования чистого Python это возможность установить его и запустить везде, где доступен Python. Недостатком является эффективность поиска, которая не сравнится с движками написанными на C или C++. На мой взгляд было бы идеальным решением иметь расширение для Flask которое может соединятся с разными системами и абстрагировать нас от деталей, как это делает Flask-SQLAlchemy освобождая нас от ньюансов различных баз данных, но в области полнотекстового поиска нет пока ничего подобного. Разработчики на Django имеют очень хорошее расширение, которое поддерживает различные системы полнотекстового поиска называемое django-haystack. Может быть в один прекрасный день кто-нибудь создаст аналогичное расширение для Flask.
Но сейчас, мы реализуем наш поиск с помощью Whoosh. Расширение, которое мы собираемся использовать это Flask-WhooshAlchemy, которая объединяет базу Whoosh с моделью Flask-SQLAlchemy.
Если у вас пока нет Flask-WhooshAlchemy в вашем виртуальном окружении, самое время установить его. Пользователи Windows должны сделать так:
flaskScriptspip install Flask-WhooshAlchemy
Все другие могу сделать так:
flask/bin/pip install Flask-WhooshAlchemy
Конфигурация
Конфигурация у Flask-WhooshAlchemy очень простая. Мы просто должны сказать расширению имя нашей базы для полнотекстового поиска (файл config.py
):
WHOOSH_BASE = os.path.join(basedir, 'search.db')
Изменения модели
Поскольку Flask-WhooshAlchemy интегрируется Flask-SQLAlchemy, нам нужно указать какие данные должны быть проиндексированы в каких моделях (файл app/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.body)
whooshalchemy.whoosh_index(app, Post)
Модель теперь имеет новое поле __searchable__
, которое представляет собой массив со всеми полями бд, которые должны попасть в индекс. В нашем случае нам нужен только индекс поля body нашего поста.
Мы также проводим инициализацию полнотекстового индекса для этой модели вызывая функцию whoosh_index
.
Так как мы не меняли формат нашей базы, нам не нужно делать новую миграцию.
К сожалению все посты, которые были в базе до добавления движка полнотекстового поиска, не будут проиндексированы. Чтобы убедиться что база данных и движок поиска синхронизированы мы должны удалить все посты из базы и начать сначала. Сначала запускаем интерпретатор Python. Для пользователей Windows:
flaskScriptspython
Для всех остальных:
flask/bin/python
Этим запросом мы удаляем все посты:
>>> from app.models import Post
>>> from app import db
>>> for post in Post.query.all():
... db.session.delete(post)
>>> db.session.commit()
Поиск
Сейчас мы готовы к поиску. Давайте сначала добавим несколько постов в БД. У нас есть два способа сделать это. Мы можем запустить приложение и добавить посты через web-браузер, как обычный пользователь, или мы можем сделать это через интерпретатор.
Через интерпретатор мы можем сделать это следующим образом:
>>> 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 поддерживает прекрасный язык поисковых запросов.
Интеграция полнотекстового поиска в наше приложение
Чтобы сделать поиск доступным для пользователей нашего приложения мы должны сделать несколько маленьких изменений.
Конфигурация
В конфигурации мы должны указать сколько результатов поиска нужно вернуть (файл config.py
):
MAX_SEARCH_RESULTS = 50
Форма поиска
Мы собираемся добавить форму поиска в строку навигации вверху страницы. Расположнение в верхней части очень удачное, так как поиск будет доступен со всех страниц.
Сначала мы должны добавить класс формы поиска (файл app/forms.py
):
class SearchForm(Form):
search = TextField('search', validators = [Required()])
Затем нам нужно создать объект формы поиска и сделать его доступным для всех шаблонов. Положим его в панель навигации, которая является общей для всех страниц. Простой способ добиться этого это создать форму в обработчике 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
создаст форму только когда пользователь вошел в систему, поскольку наше приложение не показывает никакого контента неавторизованым гостям.
View. Функция Search
Поле action
для нашей формы был установлен выше, чтобы отправлять все запросы в функцию search
нашего представления. Это то место где мы будем выполнять наши полнотекстовые запросы (файл app/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-запрос, тогда при обновлении страницы браузер будет обновлять ту страницу, на которую был редирект, а не сам запрос.
Страница результатов
После того как строка запроса передана формой, обработчик POST передает ее через перенаправление в обработчик 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)
Функция search_result
отправляет запрос в Whoosh, передавая вместе с запросом ограничение по количеству результатов, чтобы защититься от потенциально большого количества результатов поиска.
Поиск завершается в шаблоне search_result (файл 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 %}
И здесь мы снова можем повторно использовать наш post.html
.
Заключительные слова
Мы сейчас завершили еще одну очень важную, хотя и часто упускаемую из вида функцию, которой должно обладать приличное веб-приложение.
Ниже я выкладываю обновленную версию приложения microblog во всеми изменениями, сделанными в этой статье.
Скачать microblog-0.10.zip.
Как всегда, базы данных нет, вы должны создать ее самостоятельно. Если вы следите за этой серией статей, вы знаете как это делать. Если же нет, то вернитесь к статье о базе данных, чтобы узнать.
Я надеюсь, вам понравился этот урок.
Miguel
Автор: NCNecros