2015-07-13 12 views
11

Ich frage mich, was der beste Weg zum Parsen einer Textabfrage in Rails ist, um dem Benutzer logische Operatoren hinzufügen?Rails Suche ActiveRecord mit logischen Operatoren

ich der Benutzer in der Lage sein möchten, entweder diese oder eine gleichwertige eingeben:

# 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]"] 

Ideal könnten wir noch komplexer bekommen, mit Klammern Reihenfolge der Operationen, um anzuzeigen, aber das ist nicht ein Anforderung.

Gibt es einen Edelstein oder ein Muster, das dies unterstützt?

+0

[Vielleicht hilfreich sein, Titel kann Sie verwirren, aber werfen Sie einen Blick auf die zweite und dritte Option] (http://stackoverflow.com/questions/31096009/hash-notation-for-activerecord-or-query/ 31096106 # 31096106) – potashin

+0

Wenn Sie sagen "dem Benutzer erlauben, logische Operatoren einzubeziehen", meinen Sie, dass es dem Endbenutzer erlaubt ist, Operatoren über eine Web-Schnittstelle zu versorgen? Oder meinst du, wie machst du es mit ActiveRecord's API? – mysmallidea

+0

Sie würden es über einen Eingang eingeben, wie oben gezeigt. – steel

Antwort

7

Dies ist eine mögliche, aber ineffiziente Art und Weise, dies zu tun:

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%' ) 

Wie gesagt, Suchvorgänge wie diese sind jedoch extrem ineffizient. Ich würde empfehlen, dass Sie eine Volltext-Suchmaschine wie Elasticsearch verwenden.

+0

Was ist, wenn mein Suchbegriff 'moore' enthält? –

+0

Hoppla, du hast Recht, ich habe meinen Beitrag bearbeitet, um diesen Fall zu bearbeiten. – mrodrigues

+0

Aber wieder, ich benutze wirklich etwas wie Elasticsearch, es ist viel einfacher und effizienter, solche Suchen zu implementieren. :) – mrodrigues

3

Der einfachste Fall wäre von den Saiten ein Array extrahieren:

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"] 

Dann könnten Sie eine Abfrage-String konstruieren:

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

Dann nutzen Sie eine ilike oder eine like Abfrage holen die Ergebnisse:

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

Natürlich, dass cou Ich bin ein wenig übertrieben. Aber es ist eine einfache Lösung.

+1

So eine Lösung zu rollen scheint ein Rezept für Ärger zu sein, obwohl es auf den ersten Blick einfach aussieht. – steel

+0

Dies würde auch für "moore" fehlschlagen. –

+0

@ KARASZIIstván nur mit den Leerzeichen vor und nach '.split (" OR ")' geteilt. – MurifoX

4

Ich benutze einen solchen Parser in einer Sinatra-App, da die Abfragen dazu neigen, komplex zu sein, erzeuge ich einfach SQL anstelle der Auswahlmethoden für die activerecords. Wenn Sie es verwenden können, fühlen Sie sich frei ..

Sie es wie folgt verwenden, class_name die Activerecord-Klasse ist die Tabelle darstellt, params ein Hash von Strings ist zu analysieren, wird das Ergebnis an den Browser als Json gesendet zB

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 
Verwandte Themen