Грани раздражают. Главным образом потому, что они, как правило, используются относительно редко, но когда они используются, они используются много и многими интересными способами. Поскольку они довольно редко используются, позвольте мне объяснить, что они в первую очередь.
Мы хотим показать пользователям все последние телефоны. Мы можем сделать это, используя следующий запрос:
from phone in session.Query<Phone>() orderby phone.ReleaseDate descending select phone;
Все идет нормально. Но мы хотим добиться большего, чем просто показывать пользователю потенциально большой список. Итак, мы начинаем смотреть на результаты. Мы извлекаем некоторые интересные данные из результатов и позволяем пользователю быстро сузить детали. Типичные аспекты для телефонного запроса:
- марка
- Размер экрана
- камера
- Стоимость
Теперь мы, безусловно, можем дать возможность искать их по отдельности, но тогда вы получите что-то вроде этого:
Это не дружелюбно и не очень полезно. поэтому разработчики UX предложили эту идею:
Это дает пользователю гораздо более простой способ просмотра больших наборов данных и позволяет быстро сузить поиск до того, что он хочет. Это также дает пользователю некоторое представление о реальных данных. Я вижу, как изменение выбора фасета, вероятно, изменит количество результатов, которые у меня есть. Это отличный способ предложить возможности исследования данных. И в любом виде хранилища данных это очень важно. Торговые сайты являются естественным выбором, но системы управления контентом / документами, рабочие места, информационные центры все чаще используют эту функцию.
Прошу прощения за длинное вступление, но я чувствую, что эту функцию необходимо поместить в контекст, чтобы объяснить ее историю и различные итерации, через которые она прошла.
Впервые мы получили эту функцию в сентябре 2011 года. Это был запрос от Мэтта Уоррена, основанный на некотором коде, который я добавил. Я намеренно иду туда, потому что это первый раз, когда мы видим этот код, и он был реализован самым простым способом, который мог бы работать, чтобы продемонстрировать эту функцию и убедиться, что она работает от начала до конца. Базовая реализация выглядела примерно так:
private void HandleTermsFacet(string index, Facet facet, IndexQuery indexQuery, IndexSearcher currentIndexSearcher, Dictionary<string, IEnumerable<FacetValue>> results) { var terms = database.ExecuteGetTermsQuery(index, facet.Name,null, database.Configuration.MaxPageSize); var termResults = new List<FacetValue>(); foreach (var term in terms) { var baseQuery = database.IndexStorage.GetLuceneQuery(index, indexQuery); var termQuery = new TermQuery(new Term(facet.Name, term)); var joinedQuery = new BooleanQuery(); joinedQuery.Add(baseQuery, BooleanClause.Occur.MUST); joinedQuery.Add(termQuery, BooleanClause.Occur.MUST); var topDocs = currentIndexSearcher.Search(joinedQuery, 1); if(topDocs.totalHits > 0) termResults.Add(new FacetValue { Count = topDocs.totalHits, Range = term }); } results[facet.Name] = termResults; }
Другими словами, общая логика была примерно такой:
- Получите граненый запрос со списком фасетов.
- Для каждого аспекта:
- Получить все условия для поля (если это операционная система, то это будет: Android, iOS, BlackBerry, Windows Phone)
- Для каждого из этих терминов выполните исходный запрос и добавьте предложение, ограничивающее результаты текущим термином и значением.
Как вы можете себе представить, это имеет некоторые проблемы с производительностью. Чем больше у нас было аспектов, и чем больше терминов для каждого аспекта, тем дороже это получалось. Удивительно, но этот код почти не изменялся в течение примерно 10 месяцев, и следующим важным изменением было просто обработать все это параллельно.
Вскоре после этого у нас есть больше изменений. Переход от этой тривиальной модели к гораздо более эффективной, где мы выполняем запрос один раз (PR, между прочим, Майкл Вебер) для каждого аспекта, а не один раз для одного аспекта / термина. Динамическое агрегирование в RavenDB также выполняется с помощью фасетов, и это добавило гораздо более общий путь к коду, поэтому в коде фасетов стало происходить больше изменений. В частности, мы начали обрабатывать кэширование, в результате мы значительно сократили время выполнения, реструктурировав методы обработки вещей, чтобы мы выполняли только один запрос, а затем использовали низкоуровневый доступ к индексу, чтобы получить всю другую информацию, которая нам нужна. оптимизированная мода.
Мы получили больше примеров использования клиентов, от парней, у которых было 70000 фасетных запросов, до людей, которые хотели получить фасоли с несколькими миллионами результатов. Все это означало, что мы много думали о том, как мы можем снизить производительность многогранных запросов и, со временем, вещей, которые становятся все более сложными, но гораздо быстрее. В частности, упорядочив последовательность операций для вычисления запроса, добавив поддержку предварительной выборки и в целом значительно уменьшив объем работы, которую нам нужно было сделать, чтобы запрос работал.
Конечным результатом было то, что код фасета выглядел примерно так:
IndexedTerms.ReadEntriesForFields(currentState, fieldsToRead, allCollector.Documents, (term, docId) => { List<Facet> facets; if (!sortedFacets.TryGetValue(term.Field, out facets)) return; foreach (var facet in facets) { switch (facet.Mode) { case FacetMode.Default: var facetValues = facetsByName.GetOrAdd(facet.DisplayName); FacetValue existing; if (facetValues.TryGetValue(term.Text, out existing) == false) { existing = new FacetValue { Range = GetRangeName(term) }; facetValues[term.Text] = existing; } ApplyFacetValueHit(existing, facet, docId, null, currentState, fieldsCrc); break; case FacetMode.Ranges: // removed for brevity break; default: throw new ArgumentOutOfRangeException(); } } }); });
Вокруг этого кода происходит много вещей, но здесь мы фактически вычислили аспекты. Обратите внимание, что это в основном вызывает лямбду один раз за каждый соответствующий термин для указанных полей для каждого документа в запросе.
И производительность была довольно хорошей для наших тестовых случаев, мы проверяли это у нескольких клиентов, которые были активными пользователями, и они увидели заметное улучшение производительности запросов. Но недавно мы получили клиента, который мимоходом упомянул, что фасетные запросы были дорогими. Например, диапазон 5-10 секунд на запрос. Это подняло некоторые красные флажки, мы считаем запросы, которые занимают более 100 мс, подозрительными, поэтому мы запросили дополнительную информацию, и мы поняли, что он использует аспекты немного иначе, чем большинство наших клиентов.
Обычно, фасеты используются для… ну, фасет данных в поиске. Типичные проблемы производительности с аспектами в прошлом были с большим количеством аспектов в результатах поиска (я не шутил с 70000 аспектов). Поскольку большая часть отзывов клиентов была именно о них, мы сосредоточились на этом вопросе в первую очередь, до такой степени, что такие вещи хорошо себя вели. Этот клиент, однако, использовал аспекты не для поиска, а как способ управления самими данными. Это означало, что они использовали много аспектов всего набора данных.
Таким образом, вместо большого количества фасетов в среднем количестве документов мы использовали небольшое количество фасетов, но для всего набора данных. Профилирование привело к тому, что несколько вызовов были выделены:
- GetOrAdd
- TryGetValue
Они сами по себе очень дешевые звонки, но этот фрагмент кода был вызван буквально десятки миллионов раз для этого конкретного сценария. Мы провели некоторое время с профилировщиком и начали играть с кодом, пытаясь понять, сможем ли мы удалить дорогостоящие вызовы и оптимизировать этот путь кода.
Ответы были … нет. На самом деле все, что мы делали, находилось в диапазоне 5-7% (в обоих направлениях, помните)
Это отстой, потому что, возвращаясь к пользователю и говоря: «так оно и есть», нам не понравилось. Мы собрались вместе, закрыли ставни, выключили свет, и исключительно при свете мониторов мы провели большую хакерскую сессию. Мы полностью изменили способ обработки фасетов, чтобы избежать не только проблемы с вызовами TryGetValue или GetOrAdd, но и изменить направление на гораздо более эффективный режим.
Этот пост становится довольно длинным, поэтому я буду обсуждать это в моем следующем посте. На данный момент, вы можете предложить варианты улучшения?