Advanced search query parsing with Ruby

rob 23 Dec 2011
Comments Off

Making advanced search forms for your backend kinda sucks, right? I ran into this problem with a project and decided to come up with something similar to how Google handles search queries. No dropdowns, no checkboxes, no radios, none of that stuff. I just wanted to be able to type status:active user:rob [term] and get what I asked for. I came up with a pretty nice utility class that can handle some basic query syntaxes and give you back hashes or arrays and the left over query.

With this class I can make links that perform my basic “advanced” queries, without littering the query string. For example, if I wanted to ‘get a list of all guest users with the name “jake” in NJ, NY, or FL’ I could search for the following: guest:true state:nj,ny,fl jake. I would then take that in the controller’s params and parse it out using terms = SearchTerms.new(params[:q]) which would give me the elements broken down:

>> terms = SearchTerms.new("guest:true state:nj,ny,fl jake")
=> #<SearchTerms:0x007fc239072008 @query="jake", @parts={"guest"=>true, "state"=>["nj", "ny", "fl"]}, @split=true>
>> terms.guest
=> true
>> terms.state
=> ["nj", "ny", "fl"]
>> terms.query
=> "jake"

Then we can use those tokens with ActiveRecord’s scopes.

@user = @user.registered(false) if terms.guest

Heres the gist

# Helper class to help parse out more advanced saerch terms
# from a form query
#
# Note: all hash keys are downcased, so ID:10 == {'id' => 10}
#
# Usage:
# terms = SearchTerms.new('id:10 search terms here')
# => @query="search terms here", @parts={"id"=>"10"}
# => terms.query = 'search terms here'
# => terms['id'] = 10
#
# terms = SearchTerms.new('name:"support for spaces" state:pa')
# => @query="", @parts={"name"=>"support for spaces", "state"=>"pa"}
# => terms.query = ''
# => terms['name'] = 'support for spaces'
#
# terms = SearchTerms.new('state:pa,nj,ca')
# => @query="", @parts={"state"=>["pa","nj","ca"]}
#
# terms = SearchTerms.new('state:pa,nj,ca', false)
# => @query="", @parts={"state"=>"pa,nj,c"}
#
# Useful to drive custom logic in controllers
class SearchTerms
attr_reader :query, :parts

# query:: this is what you want tokenized
# split:: if you'd like to split values on "," then pass true
def initialize(query, split = true)
@query = ''
@parts = {}
@split = split
parse_query!(query)
end

def [](key)
@parts[key]
end

private

def parse_query!(query)
tmp = []
query.scan(/(?:(w+))(?::([w,-]+|(?:"(?:.+|[^"])*")))?/).map do |key,value|
if value.nil?
tmp << key
else
@parts[key.downcase] = clean_value(value)
end
end
@query = tmp.join(' ')
end

def clean_value(value)
return value.tr('"', '') if value.include?('"')
return value.split(',') if @split && value.include?(',')
value
end
end

Comments (0)

Comments are closed.