Полнотекстовый и фасетный поиск в Django проектах
Сделал для себя очень хорошее открытие - Haystack. Этот пакет для Django позволяет легко и быстро сделать поддержку полнотекстового поиска. Haystack может работать без поисковых движков стороннего производства, а так же поддерживает Solr, Whoosh и Xapian.
Рассматривать работу поиска будем на примере простейшего книжного магазина. У нас есть три модели -это авторы, категории и сами книги.
class Author(models.Model):
first_name = models.CharField(max_length=20)
last_name = models.CharField(max_length=40)
birthday = models.DateField(null=True, blank=True)
@property
def full_name(self):
return u' '.join((self.first_name, self.last_name))
def __unicode__(self):
return self.full_name
class Book(models.Model):
authors = models.ManyToManyField(Author, related_name='books')
title = models.CharField(max_length=300)
publish_year = models.PositiveIntegerField()
isbn = models.CharField(max_length=13)
description = models.TextField(null=True, blank=True)
categories = models.ManyToManyField('Category',
related_name='books')
def __unicode__(self):
return self.title
class Category(models.Model):
title = models.CharField(max_length=100)
def __unicode__(self):
return self.title
Для подключения Haystack к нашему магазину нам нужно добавить 'haystack'
в INSTALLED_APPS
и описать настройки поискового механизма.
Если вам не нужен фасетный поиск - то можно использовать Whoosh, для которого из всех настроек нужно указать только путь к каталогу для хранения индекса.
Теперь нужно описать поисковый индекс в файле search_index.py
нашего приложения(не проекта).
from haystack import indexes
from bookshop.apps.book.models import Book
class BookIndex(indexes.SearchIndex, indexes.Indexable):
authors = indexes.MultiValueField(faceted=True)
title = indexes.CharField(model_attr='title')
year = indexes.IntegerField(model_attr='publish_year',
faceted=True)
text = indexes.CharField(document=True)
categories = indexes.MultiValueField(faceted=True)
def get_model(self):
return Book
def prepare_authors(self, book):
return [a.id for a in book.authors.all()]
def prepare_text(self, book):
return u'\n'.join((
book.title,
u', '.join([a.full_name for a in book.authors.all()]),
book.description,
))
def prepare_categories(self, book):
return [c.id for c in book.categories.all()]
Каждый индексный класс должен содержать поле text
с параметром document=True
. Параметры model_attr
указывают с какого поля модели брать данные, а параметр faceted
указывает что по этому параметру мы хотим строить фасет. Значение для каждого поля можно задавать объявив метод prepare_<имя поля>
который будет возвращать значение.
Теперь нам нужно создать конфигурационную схему для Solr’а, для этого нужно выполнить ./manage build_solr_schema
и xml который нам вернется нужно записать в cхему настройки solr’а. После нужно запустить ./manage.py rebuild_index
что бы пересоздать инндекс. Для обновления индекса нужно выполнять ./manage.py update_index
.
Осталось только написать view для поиска.
from django.shortcuts import render
from haystack.forms import FacetedSearchForm
from haystack.query import SearchQuerySet
def search(request):
results = SearchQuerySet().all().facet('authors').facet('categories')\
.facet('year')
if request.GET.get('q'):
form = FacetedSearchForm(
request.GET,
searchqueryset=results,
selected_facets=request.GET.getlist('selected_facets')
)
if form.is_valid():
results = form.search()
else:
form = FacetedSearchForm()
for facet in request.GET.getlist('selected_facets'):
if not ':' in facet:
continue
field, value = facet.split(':', 1)
if value:
results = results.narrow('%s:"%s"' % (field, value))
full_path = request.get_full_path()
if not '?' in full_path:
full_path += '?'
return render(request, 'search.html', {
'results': results,
'form': form,
'facets': results.facet_counts(),
'full_path': full_path,
})
Стандартная форма к сожалению не рассчитана на отсутствие поискового запроса, поэтому когда у нас его нету мы сами обработаем фасеты (строки 21-26). Также нам нужен шаблон для вывода наших результатов.
{% extends 'base.html' %}
{% load book_extra %}
{% block col_left %}
<div>
<form action="." method="get">
{{ form.as_table }}
<input type="submit" value="Search" />
</form>
</div>
<div>
<h2>
Авторы</h2>
<ul>
{% for author in facets.fields.authors %}
{% if author.1 > 0 %}
<li><a href="https://draft.blogger.com/{{ full_path }}selected_facets=authors_exact:{{ author.0 }}">
{{ author.0|get_author_full_name }} ({{ author.1 }})
</a></li>
{% endif %}
{% endfor %}
</ul>
<h2>
Год издания</h2>
<ul>
{% for year in facets.fields.year %}
{% if year.1 > 0 %}
<li><a href="https://draft.blogger.com/{{ full_path }}&selected_facets=year_exact:{{ year.0 }}">
{{ year.0 }} ({{ year.1 }})
</a></li>
{% endif %}
{% endfor %}
</ul>
<h2>
Категории</h2>
<ul>
{% for category in facets.fields.categories %}
{% if category.1 > 0 %}
<li><a href="https://draft.blogger.com/{{ full_path }}&selected_facets=categories_exact:{{ category.0 }}">
{{ category.0|get_category_title }} ({{ category.1 }})
</a></li>
{% endif %}
{% endfor %}
</ul>
</div>
{% endblock %}
{% block col_right %}
{% for result in results %}
<div class="search_result">
<h3>
<a href="javascript:void(0);">{{ result.object.title }}</a></h3>
<div>
<div>
{% for author in result.object.authors.all %}
{{ author.full_name}}{% if not forloop.last %}, {% endif %}
{% endfor %}
</div>
<div>
{% for category in result.object.categories.all %}
{{ category.title %}{% if not forloop.last %}, {% endif %}
{% endfor %}
</div>
</div>
{{ result.object.description|truncatewords:80 }}</div>
{% empty %}
Sorry, no results found.<br />
{% endfor %}
{% endblock %}