Ранжирование результатов по сложным условиям с использованием Rails и Squeel
Я, вероятно, делаю что-то глупое здесь - и я открыт для других способов сделать - но я пытаюсь упорядочить свой набор результатов на основе вычисляемого поля:
Client.select{['clients.*',
(cast((surname == matching_surname).as int) * 10 +
cast((given_names == matching_given_names).as int) +
cast((date_of_birth == matching_date_of_birth).as int).as(ranking)]}.
where{(surname =~ matching_surname) |
(given_names =~ matching_given_names) |
(date_of_birth == matching_date_of_birth)}.
order{`ranking`.desc}
Моя проблема в том что date_of_birth
может быть ноль. Это вызывает cast((...).as int)
вызов, чтобы вернуть три разных значения - 1
если выражение оценивается как true
; 0
если выражение оценивается как false
; а также nil
если значение основного столбца было nil
,
nil
значения из выражений приводят к тому, что весь рейтинг оценивается как NIL
- это означает, что даже если у меня есть запись, которая точно соответствует surname
а также given_names
если date_of_birth
столбец nil
, ranking
для записи nil
,
Я пытался использовать сложное выражение в cast
это проверяет if not nil or the matching_value
, но это не удается с исключением Squeel, используя |
и ruby оценивает его при использовании ||
а также or
,
Я также попытался использовать предикаты в порядке для псевдонимов столбцов:
order{[`ranking` != nil, `ranking`.desc]}
но это бросает ActiveRecord
Исключение жаловаться, что столбец ranking
не существует.
Я в конце моей веревки... есть идеи?
1 ответ
После небольшого танца я смог вычислить ranking
используя серию внешних соединений для других областей следующим образом:
def self.weighted_by_any (client)
scope =
select{[`clients.*`,
[
((cast((`not rank_A.id is null`).as int) * 100) if client[:social_insurance_number].present?),
((cast((`not rank_B.id is null`).as int) * 10) if client[:surname].present?),
((cast((`not rank_C.id is null`).as int) * 1) if client[:given_names].present?),
((cast((`not rank_D.id is null`).as int) * 1) if client[:date_of_birth].present?)
].compact.reduce(:+).as(`ranking`)
]}.by_any(client)
scope = scope.joins{"left join (" + Client.weigh_social_insurance_number(client).to_sql + ") AS rank_A ON rank_A.id = clients.id"} if client[:social_insurance_number].present?
scope = scope.joins{"left join (" + Client.weigh_surname(client).to_sql + ") AS rank_B on rank_B.id = clients.id"} if client[:surname].present?
scope = scope.joins{"left join (" + Client.weigh_given_names(client).to_sql + ") AS rank_C on rank_C.id = clients.id"} if client[:given_names].present?
scope = scope.joins{"left join (" + Client.weigh_date_of_birth(client).to_sql + ") AS rank_D on rank_D.id = clients.id"} if client[:date_of_birth].present?
scope.order{`ranking`.desc}
end
где Client.weigh_<attribute>(client)
это еще одна область, которая выглядит следующим образом:
def self.weigh_social_insurance_number (client)
select{[:id]}.where{social_insurance_number == client[:social_insurance_number]}
end
Это позволило мне вычеркнуть сравнение значения из проверки на ноль и, таким образом, убрало третье значение в моем логическом расчете (TRUE => 1, FALSE => 0).
Чистый? Эффективное? Элегантный? Может и нет... но работает.:)
РЕДАКТИРОВАТЬ базу на новой информации
Я изобразил это во что-то намного более красивое, благодаря ответу Бигсяна. Вот что я придумала:
Во-первых, я заменил weigh_<attribute>(client)
прицелы с ситами. Ранее я обнаружил, что вы можете использовать сита в select{}
часть объема - который мы будем использовать через минуту.
sifter :weigh_social_insurance_number do |token|
# check if the token is present - we don't want to match on nil, but we want the column in the results
# cast the comparison of the token to the column to an integer -> nil = nil, true = 1, false = 0
# use coalesce to replace the nil value with `0` (for no match)
(token.present? ? coalesce(cast((social_insurance_number == token).as int), `0`) : `0`).as(weight_social_insurance_number)
end
sifter :weigh_surname do |token|
(token.present? ? coalesce(cast((surname == token).as int), `0`) :`0`).as(weight_surname)
end
sifter :weigh_given_names do |token|
(token.present? ? coalesce(cast((given_names == token).as int), `0`) : `0`).as(weight_given_names)
end
sifter :weigh_date_of_birth do |token|
(token.present? ? coalesce(cast((date_of_birth == token).as int), `0`) : `0`).as(weight_date_of_birth)
end
Итак, давайте создадим область, используя просеиватели, чтобы взвесить все наши критерии:
def self.weigh_criteria (client)
select{[`*`,
sift(weigh_social_insurance_number, client[:social_insurance_number]),
sift(weigh_surname, client[:surname]),
sift(weigh_given_names, client[:given_names]),
sift(weigh_date_of_birth, client[:date_of_birth])
]}
end
Теперь, когда мы можем определить, соответствуют ли предоставленные критерии значению столбца, мы вычисляем наш рейтинг, используя другой фильтр:
sifter :ranking do
(weight_social_insurance_number * 100 + weight_surname * 10 + weight_date_of_birth * 5 + weight_given_names).as(ranking)
end
И добавляем все это вместе, чтобы сделать нашу область действия, которая включает все атрибуты модели и наши вычисленные атрибуты:
def self.weighted_by_any (client)
# check if the date is valid
begin
client[:date_of_birth] = Date.parse(client[:date_of_birth])
rescue => e
client.delete(:date_of_birth)
end
select{[`*`, sift(ranking)]}.from("(#{weigh_criteria(client).by_any(client).to_sql}) clients").order{`ranking`.desc}
end
Итак, теперь я могу искать клиента и оценивать результаты по тому, насколько близко они соответствуют предоставленным критериям:
irb(main): Client.weighted_by_any(client)
Client Load (8.9ms) SELECT *,
"clients"."weight_social_insurance_number" * 100 +
"clients"."weight_surname" * 10 +
"clients"."weight_date_of_birth" * 5 +
"clients"."weight_given_names" AS ranking
FROM (
SELECT *,
coalesce(cast("clients"."social_insurance_number" = '<sin>' AS int), 0) AS weight_social_insurance_number,
coalesce(cast("clients"."surname" = '<surname>' AS int), 0) AS weight_surname,
coalesce(cast("clients"."given_names" = '<given_names>' AS int), 0) AS weight_given_names, 0 AS weight_date_of_birth
FROM "clients"
WHERE ((("clients"."social_insurance_number" = '<sin>'
OR "clients"."surname" ILIKE '<surname>%')
OR "clients"."given_names" ILIKE '<given_names>%'))
) clients
ORDER BY ranking DESC
Чище, элегантнее и лучше работает!