2015-07-13 7 views
11

mi chiedo che cosa il modo migliore per analizzare una query di testo in Rails è, per consentire all'utente di includere operatori logici?Rails ActiveRecord Cerca con operatori logici

mi piacerebbe che l'utente sia in grado di accedere ad una di queste, o qualche equivalente:

# searching partial text in emails, just for example 
# query A 
"jon AND gmail" #=> ["[email protected]"] 

# query B 
"jon OR gmail" #=> ["[email protected]", "[email protected]"] 

# query C 
"jon AND gmail AND smith" #=> ["[email protected]"] 

Idealmente, potremmo ottenere ancora più complessa con parentesi per indicare ordine delle operazioni, ma non è un Requisiti.

C'è una gemma o un modello che supporta questo?

+0

[Forse essere di aiuto, il titolo può confondere, ma un'occhiata al secondo e la terza opzione] (http://stackoverflow.com/questions/31096009/hash-notation-for-activerecord-or-query/ 31096106 # 31096106) – potashin

+0

Quando si dice "consentire all'utente di includere operatori logici", si intende consentire all'utente finale di fornire gli operatori tramite una qualche interfaccia web? O intendi come lo fai con l'API di ActiveRecord? – mysmallidea

+0

Che lo avrebbero inserito tramite un input, come dimostrato sopra. – steel

risposta

7

questo è un modo possibile, ma inefficiente per fare questo:

user_input = "jon myers AND gmail AND smith OR goldberg OR MOORE" 
terms = user_input.split(/(.+?)((?: and | or))/i).reject(&:empty?) 
# => ["jon myers", " AND ", "gmail", " AND ", "smith", " OR ", "goldberg", " OR ", "MOORE"] 

pairs = terms.each_slice(2).map { |text, op| ["column LIKE ? #{op} ", "%#{text}%"] } 
# => [["column LIKE ? AND ", "%jon myers%"], ["column LIKE ? AND ", "%gmail%"], ["column LIKE ? OR ", "%smith%"], ["column LIKE ? OR ", "%goldberg%"], ["column LIKE ? ", "%MOORE%"]] 

query = pairs.reduce([""]) { |acc, terms| acc[0] += terms[0]; acc << terms[1] } 
# => ["column LIKE ? AND column LIKE ? AND column LIKE ? OR column LIKE ? OR column LIKE ? ", "%jon myers%", "%gmail%", "%smith%", "%goldberg%", "%MOORE%"] 

Model.where(query[0], *query[1..-1]).to_sql 
# => SELECT "courses".* FROM "courses" WHERE (column LIKE '%jon myers%' AND column LIKE '%gmail%' AND column LIKE '%smith%' OR column LIKE '%goldberg%' OR column LIKE '%MOORE%' ) 

Tuttavia, come ho detto, ricerche come questa sono estremamente inefficienti. Ti consigliamo di utilizzare un motore di ricerca full-text, ad esempio Elasticsearch.

+0

Cosa succede se il mio termine di ricerca contiene 'moore'? –

+0

Oops, hai ragione, ho modificato il mio post per risolvere questo caso. – mrodrigues

+0

Ma ancora una volta, davvero tu usi qualcosa come Elasticsearch, è molto più facile ed efficiente implementare ricerche come questa. :) – mrodrigues

3

Il caso più semplice sarebbe estrarre una matrice dalle corde:

and_array = "jon AND gmail".split("AND").map{|e| e.strip} 
# ["jon", "gmail"] 
or_array = "jon OR sarah".split("OR").map{|e| e.strip} 
# ["jon", "sarah"] 

allora si potrebbe costruire una stringa di query:

query_string = "" 
and_array.each {|e| query_string += "%e%"} 
# "%jon%%gmail%" 

Poi si utilizza un ilike o una query like a prendere il risultati:

Model.where("column ILIKE ?", query_string) 
# SELECT * FROM model WHERE column ILIKE '%jon%%gmail%' 
# Results: [email protected] 

Naturalmente quel cou Sarebbe un po 'eccessivo. Ma è una soluzione semplice.

+1

Rolling mia soluzione come questa sembra una ricetta per problemi, anche se a prima vista sembra semplice. – steel

+0

Questo fallirebbe anche per 'moore'. –

+0

@ KARASZIIstván diviso con gli spazi bianchi prima e dopo '.split (" OR ")'. – MurifoX

4

Io uso un tale parser in un'app Sinatra, dal momento che le query tendono ad essere complessa produco SQL pianura invece di utilizzare i metodi di selezione activerecords. Se è possibile utilizzarlo, non esitate ..

Si utilizza in questo modo, class_name è la classe ActiveRecord che rappresenta la tabella, params è un hash di stringhe per analizzare, il risultato viene inviato al browser come JSON esempio

generic_data_getter (Person, {age: ">30",name: "=John", date: ">=1/1/2014 <1/1/2015"})

def generic_data_getter (class_name, params, start=0, limit=300, sort='id', dir='ASC') 
    selection = build_selection(class_name, params) 
    data = class_name.where(selection).offset(start).limit(limit).order("#{sort} #{dir}") 
    {:success => true, :totalCount => data.except(:offset, :limit, :order).count, :result => data.as_json} 
    end 

def build_selection class_name, params 
    field_names = class_name.column_names 
    selection = [] 
    params.each do |k,v| 
    if field_names.include? k 
     type_of_field = class_name.columns_hash[k].type.to_s 
     case 
     when (['leeg','empty','nil','null'].include? v.downcase) then selection << "#{k} is null" 
     when (['niet leeg','not empty','!nil','not null'].include? v.downcase) then selection << "#{k} is not null" 
     when type_of_field == 'string' then 
     selection << string_selector(k, v) 
     when type_of_field == 'integer' then 
     selection << integer_selector(k, v) 
     when type_of_field == 'date' then 
     selection << date_selector(k, v) 
     end 
    end 
    end 
    selection.join(' and ') 
end 

def string_selector(k, v) 
    case 
    when v[/\|/] 
    v.scan(/([^\|]+)(\|)([^\|]+)/).map {|p| "lower(#{k}) LIKE '%#{p.first.downcase}%' or lower(#{k}) LIKE '%#{p.last.downcase}%'"} 
    when v[/[<>=]/] 
    v.scan(/(<=?|>=?|=)([^<>=]+)/).map { |part| "#{k} #{part.first} '#{part.last.strip}'"} 
    else 
    "lower(#{k}) LIKE '%#{v.downcase}%'" 
    end 
end 

def integer_selector(k, v) 
    case 
    when v[/\||,/] 
    v.scan(/([^\|]+)([\|,])([^\|]+)/).map {|p|p p; "#{k} IN (#{p.first}, #{p.last})"} 
    when v[/\-/] 
    v.scan(/([^-]+)([\-])([^-]+)/).map {|p|p p; "#{k} BETWEEN #{p.first} and #{p.last}"} 
    when v[/[<>=]/] 
    v.scan(/(<=?|>=?|=)([^<>=]+)/).map { |part| p part; "#{k} #{part.first} #{part.last}"} 
    else 
    "#{k} = #{v}" 
    end 
end 

def date_selector(k, v) 
    eurodate = /^(\d{1,2})[-\/](\d{1,2})[-\/](\d{1,4})$/ 
    case 
    when v[/\|/] 
    v.scan(/([^\|]+)([\|])([^\|]+)/).map {|p|p p; "#{k} IN (DATE('#{p.first.gsub(eurodate,'\3-\2-\1')}'), DATE('#{p.last.gsub(eurodate,'\3-\2-\1')}'))"} 
    when v[/\-/] 
    v.scan(/([^-]+)([\-])([^-]+)/).map {|p|p p; "#{k} BETWEEN DATE('#{p.first.gsub(eurodate,'\3-\2-\1')}')' and DATE('#{p.last.gsub(eurodate,'\3-\2-\1')}')"} 
    when v[/<|>|=/] 
    parts = v.scan(/(<=?|>=?|=)(\d{1,2}[\/-]\d{1,2}[\/-]\d{2,4})/) 
    selection = parts.map do |part| 
     operator = part.first ||= "=" 
     date = Date.parse(part.last.gsub(eurodate,'\3-\2-\1')) 
     "#{k} #{operator} DATE('#{date}')" 
    end 
    when v[/^(\d{1,2})[-\/](\d{1,4})$/] 
    "#{k} >= DATE('#{$2}-#{$1}-01') and #{k} <= DATE('#{$2}-#{$1}-31')" 
    else 
    date = Date.parse(v.gsub(eurodate,'\3-\2-\1')) 
    "#{k} = DATE('#{date}')" 
    end 
end