Запрос каталога товаров RavenDB магазин для совокупности спецификаций по произвольной коллекции продуктов - PullRequest
5 голосов
/ 04 октября 2011

Это продолжение проекта, изложенного в этом вопросе.

У меня есть следующая модель:

class Product {
  public string Id { get; set; }
  public string[] Specs { get; set; }
  public int CategoryId { get; set; }
}

В массиве "Specs" хранится спецификация продуктапары имя-значение, соединенные специальным символом.Например, если продукт окрашен в синий цвет, строка спецификации будет "Color ~ Blue".Такое представление спецификаций позволяет запрашивать продукты, имеющие несколько значений спецификаций, указанных в запросе.Есть два основных запроса, которые я хотел бы поддержать:

  1. Получить все продукты в данной категории.
  2. Получить все продукты в данной категории, которые имеют набор указанных спецификаций.

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

class CategorySpecGroups {
    public int CategoryId { get; set; }
    public string Spec { get; set; }
    public int Count { get; set; }
}


public class SpecGroups_ByCategoryId : AbstractIndexCreationTask<Product, CategorySpecGroups>
{
    public SpecGroups_ByCategoryId()
    {
        this.Map = products => from product in products
                               where product.Specs != null
                               from spec in product.Specs
                               select new
                               {
                                   CategoryId = product.CategoryId,
                                   Spec = spec,
                                   Count = 1
                               };

        this.Reduce = results => from result in results
                                 group result by new { result.CategoryId, result.Spec } into g
                                 select new
                                 {
                                     CategoryId = g.Key.CategoryId,
                                     Spec = g.Key.Spec,
                                     Count = g.Sum(x => x.Count)
                                 };
    }
}

Затем я могу запросить этот индекс и получить все пары имя-значение спецификации в данной категории.Проблема, с которой я сталкиваюсь, состоит в том, чтобы получить тот же набор результатов, но для запроса, который фильтрует как по категории, так и по набору пар имя-значение спецификации.При использовании SQL этот набор результатов будет получен путем создания группы по набору продуктов, отфильтрованных по категориям и спецификациям.Как правило, этот тип запроса является дорогостоящим, но при фильтрации по категориям и спецификациям наборы продуктов обычно невелики, хотя и не настолько малы, чтобы помещаться на одной странице - они могут содержать до 1000 продуктов.Для справки, MongoDB поддерживает метод group , который можно использовать для достижения того же набора результатов.При этом выполняется серверная группа ad hoc, и производительность приемлема.

Как получить этот тип результатов с помощью RavenDB?

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

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

РЕДАКТИРОВАТЬ

Тем временем я буду пытаться найти решение с использованием Solr, поскольку они поддерживают фасетный поиск из коробки.

РЕДАКТИРОВАТЬ 2

Похоже, что RavenDB также поддерживает фасетный поиск (который изКонечно, имеет смысл, индексы хранятся в Lucene так же, как Solr).Я буду изучать это и публиковать обновления.

РЕДАКТИРОВАТЬ 3

Функциональность граненого поиска RavenDB работает должным образом.Я сохраняю документ настройки фасетов для каждого идентификатора категории, который используется для вычисления фасетов для запроса в данной категории.Проблема, с которой я сталкиваюсь сейчас - это производительность.Для набора из 500 тыс. Продуктов с 4500 различными категориями, в результате чего получается 4500 документов настройки фасетов, запрос по идентификатору категории занимает около 16 секунд при запросе фасетов и около 0,05 секунды при отсутствии запроса фасетов.Конкретная протестированная категория содержит около 6 000 продуктов, 23 различных фасета и 2 000 различных комбинаций имен и диапазонов фасетов.После просмотра кода в FacetedQueryRunner выясняется, что запрос фасетов приведет к запросу Lucene для каждой комбинации имени и значения фасета для получения счетчиков, а также к запросу для каждого имени фасета, чтобы получить условия,Одна проблема с реализацией состоит в том, что она будет извлекать все отдельные термины для данного имени фасета независимо от запроса, что в большинстве случаев значительно сократит количество терминов для фасета и, следовательно, уменьшит количество запросов Lucene.Одним из способов повышения производительности в этом случае было бы сохранение вычисленного набора результатов MapReduce (как показано выше) для каждого документа настройки фасета, который затем можно было бы запросить, чтобы получить все отдельные термины при дальнейшей фильтрации по фасетам.Однако общая производительность может быть слишком медленной.

1 Ответ

3 голосов
/ 11 октября 2011

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

Теперь, учитывая ограничение, я сохраняю документ FacetSetup для каждой конечной категории с идентификатором типа «facets / category_123». Когда документ настройки фасета сохраняется, у меня есть доступ к именам фасетов, а также к значениям фасетов (или диапазонам), которые содержатся в категории. Поэтому я могу сохранить все доступные значения фасетов в коллекции Ranges каждого фасета в документе FacetSetup, однако режим фасетов по-прежнему FacetMode.Default.

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

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

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

Кроме того, было бы неплохо, если бы документы FacetSetup автоматически синхронизировались с коллекцией продуктов, поскольку фактически они являются результатом операции агрегации MapReduce над набором продуктов, первоначально сгруппированных по идентификатору категории, затем имени фасета и затем ценности.

...