-
-
Save steveklabnik/7087076 to your computer and use it in GitHub Desktop.
class Foo | |
has_many :tags | |
has_many :states | |
# q is hash: { "tags" => "ruby", "states" => "open" } | |
def self.search(q) | |
# wat do? | |
end | |
end | |
# search should return a relation that scopes everything to the terms | |
# the values 'ruby' and 'open' are in the 'name' attribute of the Tag and State model. |
I assume this is an AND query?
I'd probably define a scope for each of the terms:
scope :tagged, ->(tags) { joins(:tags).where("tag.name IN ?", tags) }
scope :in_state, ->(states) { joins(:states).where("state.name IN ?", states) }
def self.search(q)
tagged(q[:tags]).merge(in_state(q[:states]))
end
It's an AND, yes!
First pass, though it doesn't account for nil values for the tags/states
# q is hash: { "tags" => "ruby", "states" => "open" }
def self.search(q)
joins(:tags, :states).where('tags.name = ? AND states.name = ?', q['tags'], q['states'])
end
I don't think Foo
should be responsible for knowing that it's searching for the name
attribute for Tag
or State
. I do like the idea of Class.search
being a consistent interface across searchable and searched models, though. Something like:
def self.search(value, associated_values = {})
# search self by the value
# call `AssociatedClass.search` for each of the key-value pairs in associated_values
# return the association chain
end
Crucially, each object doesn't need to define what other objects can be queried or which of their parameters get queried. Either that information is passed in via params (e.g. { tags: { name: 'ruby' }, states: { name: 'open' } } }
) or those objects figure it out themselves.
shot in the dark. didn't run it
def search(q)
tags.where(name: q['tags'].split).merge(states.where(name: q['states'].split))
end
We typically avoid joins and just do an additional query (joins are expensive in our data set). Building on the scope example:
scope :by_tag, ->(*tags) {
ids = Tag.where(:name => tags.flatten).uniq.pluck(:foo_id)
where(:id => ids)
}
scope :by_state, ->(*states) {
ids = State.where(:name => states.flatten).uniq.pluck(:foo_id)
where(:id => ids)
}
def self.search(query)
records = all
records = records.by_tags(query["tags"].split(' ')) if query["tags"].present?
records = records.by_states(query["states"].split(' ')) if query["states"].present?
records
end
If joins aren't a concern, then this could be easily tweaked, but I prefer the pluck
pattern.
@coreyword yes, the real search method takes a string, and i do some processing to get it into the hash form. figured i'd just ignore those details here though.
Thanks so much everyone!
I'd love to know what you settle on. In a system of mine I do something like @subdigital's solution...
I prefer what you did @ https://gist.github.com/steveklabnik/6705873
I've tried like 20 different things with search module and stuff in the past year and never was satisfied by it. I think searching would be much easier to build in an abstract way if it would be possible to list the scopes for an ActiveRecord::Base. Is there a reason why it's not available?
Some previous discussion over at https://gist.github.com/steveklabnik/6705873