Ranking results with complex conditions using Rails and Squeel

前端 未结 1 1658
轻奢々
轻奢々 2021-01-29 02:50

I\'m probably doing something silly here - and I\'m open to other ways of doing - but I\'m trying to order my results set based on a computed field:

Cli         


        
相关标签:
1条回答
  • 2021-01-29 03:34

    After a bit of a dance, I was able to calculate the ranking using a series of outer joins to other scopes as follows:

    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
    

    where Client.weigh_<attribute>(client) is another scope that looks like the following:

    def self.weigh_social_insurance_number (client)
      select{[:id]}.where{social_insurance_number == client[:social_insurance_number]}
    end
    

    This allowed me to break out the comparison of the value from the check for nil and so removed the third value in my boolean calculation (TRUE => 1, FALSE => 0).

    Clean? Efficient? Elegant? Maybe not... but working. :)

    EDIT base on new information

    I've refactored this into something much more beautiful, thanks to Bigxiang's answer. Here's what I've come up with:

    First, i replaced the weigh_<attribute>(client) scopes with sifters. I'd previously discovered that you can use sifters in the select{} portion of the scope - which we will be using in a minute.

    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
    

    So, let's create a scope using the sifters to weigh all our criteria:

    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
    

    Now that we can determine if the criteria provided match the column value, we compute our ranking using another sifter:

    sifter :ranking do
      (weight_social_insurance_number * 100 + weight_surname * 10 + weight_date_of_birth * 5 + weight_given_names).as(ranking)
    end
    

    And adding it all together to make our scope that includes all the model attributes and our computed attributes:

    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
    

    So, I can now search for a client and have the results ranked by how closely they match the provided criteria with:

    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
    

    Cleaner, more elegant, and working better!

    0 讨论(0)
提交回复
热议问题