How to use sunspot_rails gem to search for related articles - ruby-on-rails

I have a mini blog app and i would like user to view articles that relates to what they are reading in the article show page. without the sunspot_rails gem i would do something like this
in my model
def self.related_search(query, join = "AND")
find(:all, :conditions => related_search_conditions(query, join))
end
def self.related_search_conditions(query, join)
query.split(/\s+/).map do |word|
'(' + %w[name description notes].map { |col| "#{col} LIKE #{sanitize('%' + word.to_s + '%')}" }.join(' OR ') + ')'
end.join(" #{join} ")
end
then in my view it would be like this
#article.related_search
but i want to use the sunspot_rails gem to make this way easy. Any help. Thanks

As RocketR mentions, this is a trivial use case for Sunspot.
First, use Sunspot to specify that you have three fields to be indexed as text.
class Article < ActiveRecord::Base
searchable do
text :name
text :description
text :notes
end
end
Then issue a search, likely from within a controller action. The #search object below contains metadata about the search response, including the matching objects under its results method.
#search = Article.search do
keywords query
end
#results = #search.results
To find other documents that are similar to an object you already have loaded, say in a show action, you can call the more_like_this instance method. This is a special kind of search, which uses Solr's "More Like This" functionality, and which returns a search object similar to the above full-text search. You can use its results method to render the results of that search.
<%= render #article.more_like_this.results %>
The more_like_this method also accepts a block with similar options to the search block, so you can have more control over how you're judging similarity.
Hope that helps!

Related

Rails how to use where method for search or return all?

I am trying to do a search with multiple attributes for Address at my Rails API.
I want to search by state, city and/or street. But user doesn't need to send all attributes, he can search only by city if he wants.
So I need something like this: if the condition exists search by condition or return all results of this condition.
Example:
search request: street = 'some street', city = '', state = ''
How can I use rails where method to return all if some condition is nil?
I was trying something like this, but I know that ||:all doesn't work, it's just to illustrate what I have in mind.:
def get_address
address = Adress.where(
state: params[:state] || :all,
city: params[:city] || :all,
street: params[:street] || :all)
end
It's possible to do something like that? Or maybe there is a better way to do it?
This is a more elegant solution using some simple hash manipulation:
def filter_addesses(scope = Adress.all)
# slice takes only the keys we want
# compact removes nil values
filters = params.permit(:state, :city, :street).to_h.compact
scope = scope.where(filters) if filters.any?
scope
end
Once you're passing a column to where, there isn't an option that means "on second thought don't filter by this". Instead, you can construct the relation progressively:
def get_address
addresses = Address.all
addresses = addresses.where(state: params[:state]) if params[:state]
addresses = addresses.where(city: params[:city]) if params[:city]
addresses = addresses.where(street: params[:street]) if params[:street]
addresses
end
I highly recommend using the Searchlight gem. It solves precisely the problem you're describing. Instead of cluttering up your controllers, pass your search params to a Searchlight class. This will DRY up your code and keep your controllers skinny too. You'll not only solve your problem, but you'll have more maintainable code too. Win-win!
So in your case, you'd make an AddressSearch class:
class AddressSearch < Searchlight::Search
# This is the starting point for any chaining we do, and it's what
# will be returned if no search options are passed.
# In this case, it's an ActiveRecord model.
def base_query
Address.all # or `.scoped` for ActiveRecord 3
end
# A search method.
def search_state
query.where(state: options[:state])
end
# Another search method.
def search_city
query.where(city: options[:city])
end
# Another search method.
def search_street
query.where(street: options[:street])
end
end
Then in your controller you just need to search by passing in your search params into the class above:
AddressSearch.new(params).results
One nice thing about this gem is that any extraneous parameters will be scrubbed automatically by Searchlight. Only the State, City, and Street params will be used.

Case statement for STI type column

I have a User model that can have many Websites. For example, I will be able to call User.websites and it will pull up an ActiveRecord array of all of the websites that the User owns. But each website uses Single Table Inheritance (STI) and a type column that will determine the type of the website, like commercial, advertising, or forum. My problem is, that I was to make a case/when statement that runs a function if the the User has a website that is of a certain type. for example:
#user = User.first
case #user.websites
when includes?(Commercial)
'This user has a commercial website'
when includes?(Forum)
'This user has a forum website'
when include?(Ad)
'This user has a advertisement website'
end
Does anyone have a clue on the best way to do such a thing?
Shortest way to do this is using a the map method. Just one line!
#user.websites.select(:type).uniq.map{
|x| puts "This user has a #{x.type.downcase} website"
}
Or if you want to have these strings in an array.
#user.websites.select(:type).uniq.map{|x| x.type.downcase}
If you want to run functions. I would advice naming the functions in a friendly way such as commercial_cost, forum_cost, etc.. so that you can use the send method which makes your code compact.
#user.websites.select(:type).uniq.map{ |x| #user.send(x.type.downcase + "_cost") }
def types
'This user has following websites: ' +
websites.pluck(:type).map(&:downcase).join(', ')
end
#user.rb
def return_data(*types)
types.each do |t|
if Website.where(:user_id => self.id, :type=> t).length>0
send "method_for_owners_of_#{t}_site"
end
end
end
def method_for_owners_of_Commercial_site
end
#...
#user.return_data(["Commercial"])
#user.return_data(["Commercial", "Ad"])
...
According to Rails API
When you do Firm.create(name: "37signals"), this record will be saved
in the companies table with type = “Firm”.

Indexing and ordering by dynamic field with sunspot

So I have many items that can be part of many different pages. So here is the simplified models:
class Page
#we just need the id for this question
end
class Item
embeds_many :page_usages
end
class PageUsage
field :position, :default => 0
embedded_in :item
belongs_to :page
end
So the page_usage is holding the position of the items on every page. I want to put that into solr so it can pull up the right items and in the right order for me.
I've looked into dynamic fields and ended up with something like this but not really sure. I want the field to basically be the page id pointing to the position of the item:
searchable do
dynamic_integer :page_usages do
page_usages.inject({}) do |hash, page_usage|
hash.merge(page_usage.page_id => page_usage.position)
end
end
end
And in my controller I have something like this:
Item.search do
dynamic :page_usages do
#i have #page.id but not sure how to get all items with the #page.id
end
end
I need something that will check if the item exist on the page and find out how to use order_by with the position. Is this possible this way or do I have to find another solution?
Solved it after lots of trial and error.
searchable do
dynamic_integer :page_usages do
page_usages.inject({}) do |hash, page_usage|
hash.merge( ("page" + page_usage.page_id.to_s).to_sym => page_usage.position)
end
end
end
So I first had to store the key as a symbol which is important. But the problem I ran into was that the symbol couldn't have quotes in it. So if you call to_sym on the id, it would look something like :"123456789" which will give you a "wrong constant name" error later on. So I threw on a string before the id to create the new symbol which looks like :page123456789.
Next step was to create the search block:
Item.search do
dynamic :page_usages do
with ("page" + page.id.to_s).to_sym ).greater_than(-1)
order_by(("page" + page.id.to_s).to_sym, :asc)
end
end
By using that page id, I was able to pull up all the right items in the right order. I used greater than -1 because by default my positions start at 0 and goes up from there.

Simple Search and Rails

I have a simple search in Rails:
def self.search(search)
# if search is not empty
if search
find(:all, :conditions => ["name LIKE ?", "%#{search}%"])
# if search is empty return all
else
find(:all)
end
The view:
<% if #registries.empty? %>
Can't find the registry. Please try a differnet name.
<% else %>
<% #registries.each do |registry| %>
......etc.
How can I code it to show "Nothing Found" instead of find(:all) if it could not find a query?
I tries a few things, but nothing works. Even if I take out the else it still shows all queries if it can't find the one searching for.
Thanks in advance
When you mean "not empty" you must test against that specifically. Remember in Ruby that there are only two values that evaluate as false: false and nil.
You probably intended:
if (search.present?)
where("name like ?", "%#{search}%").all
else
all
end
Use of the Rails 3 style where clause makes your methods a lot easier to understand. Using find with :conditions is the old Rails 1 and 2 style.

Rails: Previous and Next record from previous query

My app has photos, and users can search for photos that meet certain criteria. Let's say a user searches for photos by tag, and we get something like this:
#results = Photo.tagged_with('mountain')
Now, #results is going to be a standard activerecord query with multiple records. These would be shown in a grid, and a user can then click on a photo. This would take the users to the photos#show action.
So, lets say the user searches for something and the app finds 5 records, [1,2,3,4,5], and the user clicks on photo #3.
On the photo#show page I'd like to be able to show a "Next Photo", "Previous Photo", and "Back to Search".
The only other constraint is, if the user browses to a photo directly (via another page or a bookmark etc) there wouldn't be a logical "next" and "previous" photo since there wasn't a query that led them to that photo, so in that case the template shouldn't render the query-related content at all.
So, I have been thinking about how to do this kind of thing and I don't really have a lot of good ideas. I suppose I could do something like store the query in session to be able to go back to it, but I don't know how to find the photos that would have shown up to the left and right of the selected photo.
Does anyone have any examples of how to do this kind of thing?
So, after much trial and error, here is what I came up with:
In my Photo model:
# NEXT / PREVIOUS FUNCTIONALITY
def previous(query)
unless query.nil?
index = query.find_index(self.id)
prev_id = query[index-1] unless index.zero?
self.class.find_by_id(prev_id)
end
end
def next(query)
unless query.nil?
index = query.find_index(self.id)
next_id = query[index+1] unless index == query.size
self.class.find_by_id(next_id)
end
end
This method returns the next and previous record from a search or a particular folder view by accepting an array of those records ids. I generate that ID in any controller view that creates a query view (ie the search page and the browse by folders page):
So, for instance, my search controller contains:
def search
#search = #collection.photos.search(params[:search])
#photos = #search.page(params[:page]).per(20)
session[:query] = #photos.map(&:id)
end
And then the photo#show action contains:
if session[:query]
#next_photo = #photo.next(session[:query])
#prev_photo = #photo.previous(session[:query])
end
And lastly, my view contains:
- if #prev_photo || #next_photo
#navigation
.header Related Photos
.prev
= link_to image_tag( #prev_photo.file.url :tenth ), collection_photo_path(#collection, #prev_photo) if #prev_photo
- if #prev_photo
%span Previous
.next
= link_to image_tag( #next_photo.file.url :tenth ), collection_photo_path(#collection, #next_photo) if #next_photo
- if #next_photo
%span Next
Now it turns out this works great in regular browsing situations -- but there is one gotcha that I have not yet fixed:
Theoretically, if a user searches a view, then jumps to a photo they've generated a query in session. If, for some reason, they then browse directly (via URL or bookmark) to another photo that was part of the previous query, the query will persist in session and the related photos links will still be visible on the second photo -- even though they shouldn't be on a photo someone loaded via bookmark.
However, in real life use cases this situation has actually been pretty difficult to recreate, and the code is working very well for the moment. At some point when I come up with a good fix for that one remaining gotcha I'll post it, but for now if anyone uses this idea just be aware that possibility exists.
Andrew, your method not universal and dont give guaranteed right result. There is better way to do this.
In your model:
def previous
Photo.where('photos.id < ?', self.id).first
end
def next
Photo.where('photos.id > ?', self.id).last
end
And in views:
- if #photo.previous
= link_to 'Previous', #photo.previous
- if #photo.next
= link_to 'Next', #photo.next
A gem I wrote called Nexter does it for you.
You pass it an AR Scope combination (aka ActiveRelation) plus the current Object/Record and Nexter will inspect the order clause to build the sql that will fetch the before/previous and after/next records.
Basically it looks at the ActiveRelation#order_values in order(a, b, c) and comes out with :
# pseudo code
where(a = value_of a AND b = value of b AND c > value of c).or
where(a = value_of a AND b > value of b).or
where(a > value of a)
That's only the gist of it. It also works with association values and is clever with finding the inverse values for the previous part. To keep the state of your search (or scope combination) you can use another lib like siphon, ransack, has_scope etc...
Here's a working example from the README
The model :
class Book
def nexter=(relation)
#nexter = Nexter.wrap(relation, self)
end
def next
#nexter.next
end
def previous
#nexter.previous
end
end
The controller
class BookController
before_filter :resource, except: :index
def resource
#book_search = BookSearch.new(params[:book_search])
#book ||= Book.includes([:author]).find(params[:id]).tap do |book|
book.nexter = siphon(Book.scoped).scope(#book_search)
end
end
end
The view :
<%= link_to "previous", book_path(#book.previous, book_search: params[:book_search]) %>
<%= link_to "collection", book_path(book_search: params[:book_search]) %>
<%= link_to "next", book_path(#book.next, book_search: params[:book_search])
```
You could take a look at what done in ActsAsAdjacent:
named_scope :previous, lambda { |i| {:conditions => ["#{self.table_name}.id < ?", i.id], :order => "#{self.table_name}.id DESC"} }
named_scope :next, lambda { |i| {:conditions => ["#{self.table_name}.id > ?", i.id], :order => "#{self.table_name}.id ASC"} }
Essentially, they're scopes(pre Rails 3 syntax) to retrieve records that have IDs lesser/greater than the ID of the record you passed in.
Since they're scopes, you can chain previous with .first to get the first item created before the current item, and .tagged_with('mountain').first to get the first such item tagged with 'mountain'.

Resources