6. Полнотекстовый поиск

20 октября 2016 г. 0:03

Как клиент будет находить желаемый продукт в более или менее структурированной коллекции бесчисленных продуктов? Иерархическая навигация часто не работает и занимает слишком много времени. На сегодняшний день большинство посетителей сайта ожидают одного центрального поля поиска где-нибудь рядом с меню.

6.1. API поискового механизма

В Django самый популярный API для полнотекстового поиска - это Haystack. В качестве движка индексации (indexing backend) выбран Elasticsearch, который показал наилучшие результаты по сравнению с Solr и Whoosh. Поэтому эта документация сфокусирована исключительно на Elasticsearch. И так как в djangoSHOP каждый программируемый интерфейс использует REST, то поиск не исключение. К счастью, существует проект под названием drf-haystack, который "REST-фует" наши поисковые результаты, если мы используем специальный класс сериализатора.

В этой документации мы предполагаем, что продавец хочет только индексировать свои продукты, но не произвольное содержание, такое как, например, положения и условия, которое находится за пределами djangoSHOP, но внутри djangoCMS. Последнее, однако, может быть вполне осуществимо.

6.1.1. Настройка

Установите библиотеку Elasticsearch. Текущий Haystack поддерживает только версии ниже чем 2. Затем запусти сервис как демон:

./path/to/elasticsearch-version/bin/elasticsearch -d

Проверьте отвечает ли сервер на HTTP запросы. Адрес http://localhost:9200/ должен возвращать примерно следующее:

$ curl http://localhost:9200/
{
  "status" : 200,
  "name" : "Ape-X",
  "cluster_name" : "elasticsearch",
  "version" : {
    ...
  },
}

В settings.py проверьте, добавлен ли 'haystack' в INSTALLED_APPS и подключён ли сервер приложения с базой данных Elasticsearch:

HAYSTACK_CONNECTIONS = {
    'default': {
        'ENGINE': 'haystack.backends.elasticsearch_backend.ElasticsearchSearchEngine',
        'URL': 'http://localhost:9200/',
        'INDEX_NAME': 'myshop-default',
    },
}

В случае, когда нам нужно индексировать различные языки на нашем сайте, мы должны добавить языки в этот словарь, используя различный INDEX_NAME для каждого из них.

И наконец, настройте сайт, чтобы поисковые запросы направлялись на правильный индекс, используя текущий активный язык:

HAYSTACK_ROUTERS = ('shop.search.routers.LanguageRouter', )

6.2. Индексация продуктов

Прежде чем мы начнем что-то искать, мы должны сначала заполнить его индексы. В Haystack можно создать более одного вида индекса для каждого элемента, который будет добавляется в базу данных поиска.

Каждый тип продукта требует свой индивидуальный индекс-класс. Заметьте, что Haystack делает некоторые автоопределение, поэтому этот класс должен быть добавлен в файл с именем search_indexes.py. Для нашей модели продукта SmartCard этот индекс-класс может выглядеть следующим образом:

#myshop/search_indexes.py

from shop.search.indexes import ProductIndex
from haystack import indexes

class SmartCardIndex(ProductIndex, indexes.Indexable):
    catalog_media = indexes.CharField(stored=True, indexed=False, null=True)
    search_media = indexes.CharField(stored=True, indexed=False, null=True)

    def get_model(self):
        return SmartCard

    # more methods ...

Во время построения индекса Haystack выполняет некоторые подготовительные шаги:

6.2.1. Заполнение базы данных обратными индексами

Базовый класс для поискового индекса объявляется двумя полями для проведения обратных индексов и несколько дополнительных полей для хранения информации об индексируемой сущности продукта:

shop/indexes.py
class ProductIndex(indexes.SearchIndex):
    text = indexes.CharField(document=True,
        indexed=True, use_template=True)
    autocomplete = indexes.EdgeNgramField(indexed=True,
        use_template=True)

    product_name = indexes.CharField(stored=True,
        indexed=False, model_attr='product_name')
    product_url = indexes.CharField(stored=True,
        indexed=False, model_attr='get_absolute_url')

Двва первых индекс-поля требуют шаблон для рендера простого текста, который используется для построения обратного индекса в поисковой базе данных. indexes.CharField используется дя классического обратного текстового индекса, в то время как indexes.EdgeNgramField используется для автодополнения.

Каждый из этих индекс-полей требуют свой собственный шаблон. Они должны быть названы согласно следующим правилам:

search/indexes/myshop/<product-type>_text.txt

и

search/indexes/myshop/<product-type>_autocomplete.txt

и находиться внутри папки template нашего приложения. <product-type> - это имя класса в нижнем регистре данной модели продукта. Создайте два отдельных шаблона для каждого типа продукта, одни для текстового поиска, а другой для автозаполнения.

Например:

# search/indexes/smartcard_text.txt

{{ object.product_name }}
{{ object.product_code }}
{{ object.manufacturer }}
{{ object.description|striptags }}
{% for page in object.cms_pages.all %}
{{ page.get_title }}{% endfor %}

Последние два поля используются для хранения информации о содержании продукта рядом с индексируемыми сущностями. Это огромное увеличение производительности, так как эту информацию иначе пришлось бы извлекать из реляционной базы данных, элемент за элементом, а затем рендерить пока подготавливается поисковой результат.

Мы также можем добавить поля в наш индекс-класс, который хранит заранее отрендеренный HTML. В приведенном выше примере, это делается полями catalog_media и search_media. Поскольку мы не предоставляем атрибут модели, мы должны предоставить два метода, которые создает этот контент:

# myshop/search_indexes.py

class SmartCardIndex(ProductIndex, indexes.Indexable):
   # other fields and methods ...

   def prepare_catalog_media(self, product):
        return self.render_html('catalog', product, 'media')

   def prepare_search_media(self, product):
        return self.render_html('search', product, 'media')

Эти методы сами вызывают render_html, кторый принимает продукт и рендерит его, используя шаблоны под названием catalog-product-media.html и search-product-media.html соответственно. Эти шаблоны ищутся в папке myshop/products или, если не найдены там, в папке shop/products. HTML сниппет для catalog-media используется для автозаполнения в поиске, в то время как search-media используется для обычного полнотекстового поискового вызова.

6.2.2. Построение индекса

Чтобы построить индекс в Elasticsearch, вызовите:

./manage.py rebuild_index --noinput

В зависимости от числа продуктов в базе данных это может занять некоторое время.

6.3. Поисковые сериализаторы

Haystack для Django REST Framework это маленькая библиотека, цель которой - упроситить использование Haystack с Django REST Framework. Он принимает результаты поиска, возвращаемые от Haystack, просматривает их подобно джанговским моделям во время сериализации их полей. Сериализатор, который используется для рендера содержимого для этого демо-сайта, может выглядеть так:

# myshop/serializers.py

from rest_framework import serializers
from shop.search.serializers import ProductSearchSerializer as ProductSearchSerializerBase
from .search_indexes import SmartCardIndex, SmartPhoneIndex

class ProductSearchSerializer(ProductSearchSerializerBase):
    media = serializers.SerializerMethodField()

    class Meta(ProductSearchSerializerBase.Meta):
        fields = ProductSearchSerializerBase.Meta.fields + ('media',)
        index_classes = (SmartCardIndex, SmartPhoneIndex)

    def get_media(self, search_result):
        return search_result.search_media

Этот сериализатор является частю демо-проекта, так как мы должны позаботиться об содержании, которое мы хотим показать на нашем сайте, всякий раз, когда посетитель вводит какой-нибудь текст в поле поиска.

6.4. Поисковое представление

В поисковом представлении (Search View) мы связываем сериализатор вместе с djangoCMS apphook. Этот ProductSearchApp может быть добавлен в тот же файл, который мы использовали для объявления ProductsListApp для рендера списка продуктов:

# myshop/cms_apps.py

from cms.app_base import CMSApp
from cms.apphook_pool import apphook_pool

class ProductSearchApp(CMSApp):
    name = _("Search")
    urls = ['myshop.urls.search']

apphook_pool.register(ProductSearchApp)

Как и все apphooks, он тоже требует файл, в котором прописан urlpatterns:

myshop/urls/search.py
from django.conf.urls import url
from shop.search.views import SearchView
from myshop.serializers import ProductSearchSerializer

urlpatterns = [
    url(r'^', SearchView.as_view(
        serializer_class=ProductSearchSerializer,
    )),
]

6.4.1. Вывод поисковых результатов

Как и все другие страницы в djangoSHOP, страница отображения результатов поиска тоже является обычной CMS страницей. Хорошая идея создать эту страницу на корневом уровне дерева страниц.

В качестве названия страницы используйте "Поиск" или что-то другое подходящее по смыслу. Затем мы должны изменить расширенные настройки страницы.

В качестве шаблона используйте шаблон с заполнителем, под которое отведено достаточно места, так как в нём отображаются результаты поиска. Наш шаблон, используемый по умолчанию, прекрасно подойдёт для этого.

В поле id введите shop-search-product. Используйте это жёстко закодированное значение для рендера некоторых по умолчанию HTML сниппетов, которые предназначены для включения в другие шаблоны.

Установите Soft root - эта опция скроет страницу с результатами поиска из списка меню.

В качестве приложения выберите “Search”. Оно выберет apphook, который мы создали в предыдущей секции.

Затем сохраните страницу, перейдите в режим Структура и найдите заполнитель названный Main Content. Добавьте плагин Bootstrap Container, в него добавьте Row, а затем Column. В Column добавьть плагин Search Results из раздела Интернет-магазин.

И наконец, опубликуйте страницу и введите некторый текст в поле поиска - должны отобразиться результаты найденный продуктов.

6.5. Автозаполнение в представлении списка продуктов

Как мы уже видели в предыдущем примере, представление списка продуктов подходит для поиска любого элемента в базе данных. Однако, иногда может сложится такая ситуация, что посетитель сайта захочет просто уточнить список отображаемых элементов по каталогу в текущей категории. Здесь загрузка новой страницы, использующей совершенно другой макет, может быть неуместно.

Вместо этого, когда кто-то вводит некоторый текст в поле поиска djangoSHOP начинает сужать список элементов в представлении списка продуктов по набору набираемого набора слов. Это особенно полезно в ситуациях, когда сотни продуктов отображаются на одной странице, и клиенту нужно выбрать желаемый, введя условия поиска.

Чтобы расширить существующее представление списка продуктов для автозаполнения, перейдите в файл, содержащий urlpatterns, который используется для apphook ProductsListApp. При возникновении сомнений обратитесь к файлу myshop/cms_app.py.

В файл с urlpatterns добавьте следующее:

from django.conf.urls import url
from shop.search.views import SearchView
from myshop.serializers import CatalogSearchSerializer

urlpatterns = [
    # previous patterns
    url(r'^search-catalog$', SearchView.as_view(
        serializer_class=CatalogSearchSerializer,
    )),
    # other patterns
]

Замечание:

Будьте осторожны с регулярным выражением ^search-catalog$, находящееся перед шаблоном, которое используется представлением карточки товара; он обычно выглядет так: ^(?P<slug>[\w-]+)$.

CatalogSearchSerializer, использованный здесь, похож на ProductSearchSerializer, который мы видели в предыдущем разделе. Разница лишь в том, что вместо поля search_media используется поле catalog_media, которое рендерит media элементов в макете, подходящем для представления списка каталога. Поэтому этот вид поиска обычно используется в сочетации с автозаполнением, потому что здесь мы переиспользуем такой же шаблон для представление списка продуктов.

6.5.1. Клиентская сторона

Для облегчения размещения поискового поля ввода, djangoSHOP поставляется с многоразовой директивой AngularJS shopProductSearch, которая объявлена ​​внутри модуля shop/js/search-form.js.

HTML сниппет с формой представления, использующая эту директиву, может быть найдена в templates/shop/navbar/search-form.html приложения shop. Если вы переопределяете его, то будьте уверены, что элементы формы используют директиву shop-product-search как атрибут.

<form shop-product-search method="get" action="/url-of-page-rendering-the-search-results">
  <input name="q" ng-model="searchQuery" ng-change="autocomplete()" type="text" />
</form>

Если вы не используете подготовленный HTML сниппет, то убедитесь, что модуль инициализирован перед во время начальной загрузки нашего Angular приложения:

angular.module('myShop', [..., 'django.shop.search', ...]);

Оцените статью

0 из 5 (всего 0 оценок)

Поля, отмеченные звёздочкой ( * ) , являются обязательными.

Спасибо за ваш отзыв!

После нажатия кнопки "Отправить" ваше сообщение будет доставлено мне на почту.

Автор перевода

Права на использование материала, расположенного на этой странице http://vivazzi.pro/django-shop/search/:

Разрешается копировать материал с указанием её автора и ссылки на оригинал без использования параметра rel="nofollow" в теге <a>. Использование:

Автор перевода: Мальцев Артём
Ссылка на перевод статьи: <a href="http://vivazzi.pro/django-shop/search/">http://vivazzi.pro/django-shop/search/</a>

Подробнее: Правила использования сайта

Комментариев: 2

Гость
Гость

10.03.2018 1:56 #

Спасибо за статью! А как мы настраивали русский стемминг? Чтобы кот, коты, котов, котам искались по базовому слово кот?

Ответить

Артём Мальцев
Артём Мальцев автор

11.03.2018 22:17 #

Elasticsearch и будет искать таким образом. Он должен по умолчанию быть так настроен.

Ответить

Вы можете оставить комментарий как незарегистрированный пользователь. Но, зарегистрировавшись, вы сможете получать оповещения об ответах, а также иметь доступ к своему личному аккаунту для просмотра своих комментариев.

Чтобы оставить комментарий от своего имени войдите или зарегистрируйтесь обычным способом или через социальные сети:

Отправить

На данный момент нет специального поиска, поэтому я предлагаю воспользоваться обычной поисковой системой, например, Google, добавив "vivazzi" после своего запроса.

Попробуйте