Is there a way of getting the pg_search results from the model and not the indexing table entries? My code works, but I have to loop through the results.
search = PgSearch.multisearch(params[:search])
#items = Array.new
search.find_each do |result|
#items.push(result.searchable)
end
Multisearch will always return PgSearch::Document records.
If you want to query your model directly, you can define a pg_search_scope, e.g.:
pg_search_scope :custom_search, :against => [:title] # Can use multiple fields
And then use it with search = Model.custom_search(params[:search]), which will return Model records.
Also, a cleaner way to write your code above would be:
search = PgSearch.multisearch(params[:search])
#items = search.map(&:searchable)
Related
I am adding search to my rails app with the sunspot gem and I would like to be able to search for transactions by id, amount, or description. Searching by a single attribute works fine, but when I add multiple with or fulltext calls in the search block I get no results returned. I found I can wrap the with calls in a any_of block, but including a fulltext causes a undefined method 'fulltext' for #<Sunspot::DSL::Scope:0x007fb6519c13a0> error.
Search returns the correct results when I search only on 1 attribute, meaning I only have 1 with or 1 fulltext in the any_of block. So I am to search by id, amount, and description invidually. Meaning if there is a transaction with id 213, searching for 213 returns the transaction with id 213. If I search for $4.25, then the results returns every transaction with amount $4.25. If I search for 'Starbucks', then I get every transaction with 'Starbucks' in the description. If I have multiple with or fulltext inside the anyblock I do not get any results returned.
What am I missing?
I have a transaction model like so:
class Transaction < ActiveRecord::Base
...
searchable do
text :description
integer :id
float :amount
end
...
end
And an action in the controller like so:
def search
#search = Transaction.search do
any_of do
with(:amount, params[:search])
with(:id, params[:search])
fulltext(params[:search])
end
end
#transactions = #search.results
end
Sunspot is not intended to search non text fields. Other fields types date/integer/etc.. can be used to scope the search prior to the fulltext search.
As you have posed the question it is not possible with sunspot.
In this example you can see how the float field amount is used to scope prior to the fulltext search.
def search
#search = Transaction.search do
with(:amount).greater_than(params[:amount])
fulltext params[:search] do
fields :description
end
end.results
end
If you wanted to search non text value, you would need to change them to text values first; I can see in some cases where this would be valuable for searching, such as if you had a unique numeric userid.
So bigtunacan is right. It is not possible to search on non text fields. The documentation says:
text fields will be full-text searchable. Other fields (e.g., integer
and string) can be used to scope queries.
But to make this work you can pass a block to the text method and sunspot will index the result of the block. So I pass in all the fields I want to search on in a string.
So in my transaction model I have:
# we have to create an alias because sunspot uses 'id' for something
alias_attribute :transaction_id, :id
searchable do
text :transaction do
"#{description} #{amount} #{transaction_id}"
end
end
And in my transaction controller I have:
def search
search = Transaction.search do
fulltext(params[:search])
end
#transactions = search.results
end
So now I can search by description, id, or amount.
I have a table A(:name, :address, :country_id). I run this query
ids = [1,2,3]
#result = A.where(country_id: ids)
But, I want to get the results in this order : records containing country_id = 2 first, then 1 and then 3.
How should I write this query?
Edit :
#people = []
#result.each do |r|
#people.push([r,r.name])
end
I have an array #people of arrays now and want to sort it. What should be the query now?
Another Edit :
#people.sort_by! {|p| preferred_order.index(p.first.country_id)}
This worked.
You can use sort_by :
ids = [1,2,3]
preferred_order = [2,1,3]
#result = A.where(country_id: ids)
#result.sort_by! { |u| preferred_order.index(u.country_id) }
One option would be adding an extra column
rails g migration add_sortorder_to_A sort_order:integer
Then add a
before_save :update_sort_order
def update_sort_order
if self.country_id == 1
self.update_attributes(sort_order:2)
elsif self.country_id ==2
self.update_attributes(sort_order:1)
........
end
Alternatively, put your sort_order in the countries and sort on that.
Alternatively, create a dynamic field which will give you the sort_order.
There is probably a more elegant way of doing this, but this should work in a quick and dirty way and allow you to use standard activerecord queries and sorting. (i.e. you can just forget about it once you've done it the once)
How can I get collection of attributes for search result?
I.E.
I search through Product model which have many Category:
class Product < ActiveRecord::Base
has_many :categories
Here is my index:
ThinkingSphinx::Index.define :product, with: :active_record do
###
has category(:id), as: :direct_category_id
So, I search through query
products = Product.search params[:query]
categories = Category.find(products.map(&:category_id)) # slow & bad & ugly
But this method is so slow and bad. Is there any way to get all atttributes from search results instead collect?
search = Product.search params[:query]
search.context[:panes] << ThinkingSphinx::Panes::AttributesPane
category_ids = search.collect { |product|
product.sphinx_attributes['direct_category_id']
}
categories = Category.find category_ids
However, keep in mind that if you run this in the console the first line evaluates the search request because IRB renders the result. This means the pane can't be added... so you'll want to add ; '' or similar at the end of the first line (again: only necessary in a Rails console):
search = Product.search params[:query]; ''
okay. I solved by myself. The solution use facets
First of all, we need to add direct_category_id:
has category(:id), as: :direct_category_id, facet: true
After that, we need just to use
category_ids = products.facets[:direct_category_id].keys
categories = Category.where(id: category_ids)
How to paginate results optimally when using a custom search method in model. I've also pushed ordering of the results to elasticsearch and after fetching from db the results are again sorted based on elasticsearch order.
My model search method looks like this:
def self.search query
model_objs = Model.tire.search do
query do
boolean do
should { string "field:#{query}", boost: 10}
should { string "#{query}"}
//other boolean queries
end
end
sort do
by "fieldname"
end
end
ids = model_objs.results.map {|x| x.id.to_i}
model_objs = Model.find(ids)
ids.collect {|id| model_objs.detect {|x| x.id == id}}
end
And in my controller I just have an action to get the results.
def search
search_term = params[:search_term].strip
#model_objs = Model.search search_term
end
I have two goals here, first I want to optimize the number of calls going to elasticsearch or to my database. And I want to paginate the results.
The default pagination mentioned in tire does not work cause I've overridden my search method.
#articles = Article.search params[:q], :page => (params[:page] || 1)
Also using the approach of getting paginated results from elastic search using the from and size would mean I make calls to elasticsearch over and over to fetch results, so I dont want to do something like this.
def self.search query, page_num
model_objs = Model.tire.search do
query do
boolean do
should { string "field:#{query}", boost: 10}
should { string "#{query}"}
//other boolean queries
end
end
sort do
by "fieldname"
end
size 10
from (page_num - 1) * 10
end
ids = model_objs.results.map {|x| x.id.to_i}
model_objs = Model.find(ids)
ids.collect {|id| model_objs.detect {|x| x.id == id}}
end
How can I achieve this with limited network calls?
you can pass page parameter to elastic search
model_objs = Model.tire.search({page: page_num}) do
query do
boolean do
should { string "field:#{query}", boost: 10}
should { string "#{query}"}
//other boolean queries
end
end
no need to pass add 'from'.
Disclaimer: I work with the OP, and this is just to post the solution we took.
Instead of fetching part of the attributes from ES and fetching the rest from the database, the Elastic Search index can be made to hold all the fields that's required for displaying the search results.
This way, query call to elasticsearch is good to display the search result page, without making any extra network calls.
The actual detailed view of the entity can fetch all that is required, but that would be only per entity, which works for us.
I have an ActiveRecord relation of a user's previous "votes"...
#previous_votes = current_user.votes
I need to filter these down to votes only on the current "challenge", so Ruby's select method seemed like the best way to do that...
#previous_votes = current_user.votes.select { |v| v.entry.challenge_id == Entry.find(params[:entry_id]).challenge_id }
But I also need to update the attributes of these records, and the select method turns my relation into an array which can't be updated or saved!
#previous_votes.update_all :ignore => false
# ...
# undefined method `update_all' for #<Array:0x007fed7949a0c0>
How can I filter down my relation like the select method is doing, but not lose the ability to update/save it the items with ActiveRecord?
Poking around the Google it seems like named_scope's appear in all the answers for similar questions, but I can't figure out it they can specifically accomplish what I'm after.
The problem is that select is not an SQL method. It fetches all records and filters them on the Ruby side. Here is a simplified example:
votes = Vote.scoped
votes.select{ |v| v.active? }
# SQL: select * from votes
# Ruby: all.select{ |v| v.active? }
Since update_all is an SQL method you can't use it on a Ruby array. You can stick to performing all operations in Ruby or move some (all) of them into SQL.
votes = Vote.scoped
votes.select{ |v| v.active? }
# N SQL operations (N - number of votes)
votes.each{ |vote| vote.update_attribute :ignore, false }
# or in 1 SQL operation
Vote.where(id: votes.map(&:id)).update_all(ignore: false)
If you don't actually use fetched votes it would be faster to perform the whole select & update on SQL side:
Vote.where(active: true).update_all(ignore: false)
While the previous examples work fine with your select, this one requires you to rewrite it in terms of SQL. If you have set up all relationships in Rails models you can do it roughly like this:
entry = Entry.find(params[:entry_id])
current_user.votes.joins(:challenges).merge(entry.challenge.votes)
# requires following associations:
# Challenge.has_many :votes
# User.has_many :votes
# Vote.has_many :challenges
And Rails will construct the appropriate SQL for you. But you can always fall back to writing the SQL by hand if something doesn't work.
Use collection_select instead of select. collection_select is specifically built on top of select to return ActiveRecord objects and not an array of strings like you get with select.
#previous_votes = current_user.votes.collection_select { |v| v.entry.challenge_id == Entry.find(params[:entry_id]).challenge_id }
This should return #previous_votes as an array of objects
EDIT: Updating this post with another suggested way to return those AR objects in an array
#previous_votes = current_user.votes.collect {|v| records.detect { v.entry.challenge_id == Entry.find(params[:entry_id]).challenge_id}}
A nice approach this is to use scopes. In your case, you can set this up the scope as follows:
class Vote < ActiveRecord::Base
scope :for_challenge, lambda do |challenge_id|
joins(:entry).where("entry.challenge_id = ?", challenge_id)
end
end
Then your code for getting current votes will look like:
challenge_id = Entry.find(params[:entry_id]).challenge_id
#previous_votes = current_user.votes.for_challenge(challenge_id)
I believe you can do something like:
#entry = Entry.find(params[:entry_id])
#previous_votes = Vote.joins(:entry).where(entries: { id: #entry.id, challenge_id: #entry.challenge_id })