SQL - объединение поисковых запросов вместо нескольких поездок в БД - PullRequest
0 голосов
/ 19 января 2019

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

  • Первый: точное совпадение
  • Второй: начинается с этого термина
  • Третий: содержит слово этого термина
  • Forth: заканчивается этим термином
  • Пятый: содержит термин в любом вопросе

Итак, я впервые получил сущности из БД:

result = entities.Where(e => e.Name.Contains(searchTerm)).ToList();

А потом я переставил их в памяти:

var sortedEntities = result.Where(e => e.Name.ToLower() == searchTerm.ToLower())
    .Union(result.Where(e => e.Name.StartsWith(searchTerm, StringComparison.OrdinalIgnoreCase)))
    .Union(result.Where(e => e.Name.Contains($" {searchTerm} ")))
    .Union(result.Where(e => e.Name.EndsWith(searchTerm, StringComparison.OrdinalIgnoreCase)))
    .Union(result.Where(e => e.Name.Contains(searchTerm)));

Работало нормально, пока я не добавил подкачку. Теперь, если точное совпадение указано на странице 2 (в данных, поступающих из БД), оно не будет отображаться первым.

Единственное решение, которое я могу придумать, - это разделить запросы (в данном случае 5 запросов) и вручную отслеживать размер страницы. У меня вопрос, есть ли способ заставить БД соблюдать этот порядок и получить отсортированные данные за одну поездку БД?

1 Ответ

0 голосов
/ 21 января 2019

Мне потребовалось некоторое время, чтобы понять, что вы используете Union в попытке упорядочить данные по «силе совпадения»: сначала те, которые точно соответствуют, затем те, которые соответствуют другому регистру и т. Д. Когда я вижу Union s с предикатами мой обусловленный Павловым ум переводит его в OR s.Мне пришлось переключиться с быстро думать на медленный .

Так что проблема в том, что нет предсказуемой сортировки.Без сомнения, цепочечные операторы Union действительно создают детерминированный конечный порядок сортировки, но это не обязательно порядок Union с, потому что каждый Union также выполняет неявный Distinct.Общее правило: если вам нужен определенный порядок сортировки, используйте OrderBy методы.

Сказав это, и взяв ...

var result = entities
    .Where(e => e.Name.Contains(searchTerm))
    .Skip((pageNumber - 1) * pageSize)
    .Take(pageSize).ToList();

... желаемый результат кажется доступным для:

var sortedEntities = result
    .OrderByDescending(e => e.Name == searchTerm)
    .ThenByDescending(e => e.Name.ToLower() == searchTerm.ToLower())
    .ThenByDescending(e => e.Name.StartsWith(searchTerm, StringComparison.OrdinalIgnoreCase))
    ... etc.

(по убыванию, потому что false orders before true)

Однако, если совпадений больше, чем pageSizeзаказ будет слишком поздно.Если pageSize = 20 и пункт 21 является первым точным соответствием, этот элемент не будет отображаться на странице 1. Это означает, что заказ должен быть выполнен до подкачки.

Первым шагом будетудалите .ToList() из первого оператора.Если вы удалите его, первый оператор будет выражением IQueryable, и Entity Framework сможет объединить полный оператор в один оператор SQL.Следующим шагом будет перемещение Skip/Take в конец полного оператора, и он также будет частью SQL.

var result = entities.Where(e => e.Name.Contains(searchTerm));

var sortedEntities = result
    .OrderByDescending(e => e.Name == searchTerm)
    .ThenByDescending(e => e.Name.ToLower() == searchTerm.ToLower())
    .ThenByDescending(e => e.Name.StartsWith(searchTerm, StringComparison.OrdinalIgnoreCase))
    ... etc
    .Skip((pageNumber - 1) * pageSize)
    .Take(pageSize).ToList();

Но теперь возникает новая проблема.

Поскольку сравнение строк с StringComparison.OrdinalIgnoreCase не поддерживается, Entity Framework автоматически переключится на оценку на стороне клиента для части оператора.Все отфильтрованные результаты будут возвращены из базы данных, но большая часть упорядочения и вся подкачка будут выполняться в памяти.

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

Конечному результату :

var result = entities.Where(e => e.Name.Contains(searchTerm));
var sortedEntities = result
    .OrderByDescending(e => e.Name == searchTerm)
    .ThenByDescending(e => e.Name.StartsWith(searchTerm))
    .ThenByDescending(e => e.Name.Contains($" {searchTerm} "))
    .ThenByDescending(e => e.Name.EndsWith(searchTerm))
    .ThenByDescending(e => e.Name.Contains(searchTerm))
    .Skip((pageNumber - 1) * pageSize)
    .Take(pageSize).ToList();

Почему «менее изысканный»?Поскольку, согласно вашим комментариям, параметры сортировки базы данных не чувствительны к регистру, поэтому SQL не может различить точные совпадения по регистру без добавления операторов COLLATE.Это то, что мы не можем сделать с LINQ.

...