-
-
Save IceDragon200/004e73bc57598d6254ec to your computer and use it in GitHub Desktop.
Search term parsing : http://proccli.com/advanced-search-query-parsing-ruby
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Search term parser from https://gist.github.com/1477730 | |
# Modified to allow periods (and other non-letter chars) in unquoted field values | |
# and field names. | |
# | |
# 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} | |
# you can also access all keys with methods e.g.: terms.id = terms['id'] = 10 | |
# this doesn't work with query as thats reserved for the left-over pieces | |
# | |
# 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.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 | |
module Parser | |
# regex scanner for the parser | |
SCANNER = %r{ | |
(?: | |
([\w\.]+) # look for any word | |
) | |
(?: # check if it has a value attached | |
: # find the value delimiter | |
( | |
[\w,\-]+ # match any word-like values | |
| # -or- | |
(?:"(?:.+|[^\"])*") # match any quoted values | |
) | |
)? | |
}x | |
class << self | |
private def clean_value(value, **options) | |
return value.tr('"', '') if value.include?('"') | |
return value.split(',') if options.fetch(:split, true) && value.include?(',') | |
return true if value == 'true' | |
return false if value == 'false' | |
return value.to_i if value =~ /^[1-9][0-9]*$/ | |
value | |
end | |
def parse(query, **options) | |
keywords = [] | |
queries = [] | |
parts = {} | |
query.scan(SCANNER).map do |key, value| | |
if value.nil? | |
keywords << key | |
queries << [:keyword, key] | |
else | |
k = key.downcase | |
v = clean_value(value, options) | |
parts[k] = v | |
queries << [:key_value, { key: k, value: v }] | |
end | |
end | |
{ | |
original_query: query, | |
query: keywords.join(' '), | |
keywords: keywords, | |
queries: queries, | |
parts: parts | |
} | |
end | |
end | |
end | |
# @return [String] Original query string passed in | |
attr_reader :original_query | |
# @return [Array<Array>] In case you need the actual order of query parameters | |
# The sub arrays are of the following structure: | |
# [Symbol, Object] | |
# Where Symbol is the kind of query parameter, and Object is the value | |
# Symbol can be: | |
# :keyword - the value is a String keyword | |
# :key_value - the value is a `key` `value` pair as a Hash with keys of the same name | |
attr_reader :queries | |
# @return [String] the reconstructed query string | |
attr_reader :query | |
# @return [Array<String>] all keywords, things that didn't make into the parts | |
attr_reader :keywords | |
# @return [Hash<String, Object>] the parsed parameters | |
attr_reader :parts | |
# @param [Hash<Symbol, Object>] options | |
# @option [Array<Array>] :queries | |
# @option [Array<String>] :keywords | |
# @option [String] :original_query | |
# @option [String] :query | |
# @option [Hash<String, Object>] :parts | |
def initialize(**options) | |
@queries = options.fetch(:queries, []) | |
@keywords = options.fetch(:keywords, []) | |
@original_query = options.fetch(:original_query, '') | |
@query = options.fetch(:query, @original_query) | |
@parts = options.fetch(:parts) | |
end | |
# @param [String, Symbol] key | |
# @return [Object] value | |
def [](key) | |
@parts[key.to_s] | |
end | |
# Slices the parts into a new Hash | |
# | |
# @param [Array<String, Symbol>] keys | |
# @return [SearchTerms] sliced search terms | |
def slice(*keys) | |
SearchTerms.new( | |
queries: @queries, | |
keywords: @keywords, | |
original_query: @original_query, | |
query: @query, | |
parts: @parts.slice(*keys.map(&:to_s)) | |
) | |
end | |
# @return [Hash<String, Object>] hash | |
def to_hash | |
@parts.to_hash | |
end | |
# @overload each_query | |
# @return [Enumerator] | |
# @overload each_query { |type, value| } | |
# @yieldparam [Symbol] type | |
# @yieldparam [Object] value | |
def each_query | |
return to_enum :each_query unless block_given? | |
@queries.each do |row| | |
yield(*row) | |
end | |
end | |
# @param [String] query | |
# @param [Hash<Symbol, Object>] options | |
# @option [Boolean] :split should array like values be split? | |
def self.parse(query, **options) | |
new SearchTerms::Parser.parse(query, options) | |
end | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require 'spec_helper' | |
describe SearchTerms do | |
# A rather crude spec but hey it works | |
context '.parse' do | |
it 'should parse a query' do | |
q = 'to:Eggman from:Sonic meta:1,2,3,4 IAmAKeyword' | |
st = described_class.parse(q) | |
expected_parts = { 'to' => 'Eggman', 'from' => 'Sonic', 'meta' => %w[1 2 3 4] } | |
expect(st).to be_instance_of described_class | |
expect(st.original_query).to eq(q) | |
expect(st.keywords).to eq(['IAmAKeyword']) | |
expect(st.parts).to eq(expected_parts) | |
expect(st.to_hash).to eq(expected_parts) | |
expect(st['to']).to eq('Eggman') | |
expect(st[:meta]).to eq(%w[1 2 3 4]) | |
expect(st.each_query).to be_instance_of(Enumerator) | |
expect(st.each_query.to_a).to eq([ | |
[:key_value, { key: 'to', value: 'Eggman' }], | |
[:key_value, { key: 'from', value: 'Sonic' }], | |
[:key_value, { key: 'meta', value: %w[1 2 3 4] }], | |
[:keyword, 'IAmAKeyword'] | |
]) | |
st2 = st.slice('from', :to) | |
expect(st2.parts).to eq({ 'to' => 'Eggman', 'from' => 'Sonic' }) | |
end | |
end | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# basic usage to search users from your #index action | |
class UsersController < ApplicationController | |
def index | |
if params[:q] | |
terms = SearchTerms.parse(params[:q]) | |
if terms['id'] | |
return redirect_to user_path(terms['id']) | |
else | |
@users = @users.search_by_name(terms.query) unless terms.query.blank? | |
@users = @users.with_role(terms['role']) if terms['role'] | |
@users = @users.registered(false) if terms['guest'] | |
end | |
end | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment