How To Create A Search Function - Ruby On Rails - ruby

Im trying to make a search function for an app. I have a service object called searches and all search logic is in this object.
When the search form is submitted with an about query #results in Search Controller is the following array.
=> [
[0] [],
[1] [
[0] About {
:id => 2,
:title => "About",
:slug => "about",
:meta_description => "",
:meta_keywords => "",
:content => "<p>Lorem ipsum about</p>",
:position => 1,
:published => true,
:on_menu => true,
:created_at => Wed, 13 Jan 2016 00:44:08 UTC +00:00,
:updated_at => Fri, 15 Jan 2016 04:05:52 UTC +00:00
}
],
[2] [],
[3] []
]
Here is my Search object. See how the User attributes are different then the Pages and NewsArticle? Good. Now go look at the view.
class SearchService
attr_accessor :query
def initialize(query)
#query = query
end
# searchable_fields_by_model
CONDITIONS_BY_MODEL = {
Page => [:title, :content],
NewsArticle => [:title, :content]
User => [:first_name, :last_name]
}
def raw_results
ActiveRecord::Base.transaction do
CONDITIONS_BY_MODEL.flat_map do |model, fields|
Array(fields).map do |field|
model.where(["#{field} LIKE ?", "%#{query}%"])
end
end
end
end
end
Below is my view. Currently the view will only display content and title. But my User has first_name and last_name therefore it won't display. I need a way to display the results of all Models and it needs to be clean so if i add another model to search with entirely different attributes it will still display. How would you do this?
.page-header
%h1 Search#index
- #results.each do |results|
- results.each do |r|
%p= r.title
%p= r.content
Search controller
class SearchController < ApplicationController
def index
#results = SearchService.new(params[:q]).results
end
end

How to fix this.
class SearchController < ApplicationController
def index
#results = SearchService.new(params[:q]).results
#page_results = []
#news_article_results = []
#category_results = []
#results.each do |resource|
if resource.class == Page
#page_results << resource
elsif resource.class == NewsArticle
#news_article_results << resource
elsif resource.class == Category
#category_results << resource
else
end
end
end
end
class SearchService
MIN_QUERY_LENGTH = 3
attr_accessor :query
def initialize(query)
raise ArgumentError if query.length < MIN_QUERY_LENGTH
#query = query
end
# searchable_fields_by_model
CONDITIONS_BY_MODEL = {
Page => [:title, :content],
NewsArticle => [:title, :content],
Category => :name,
}
def results
#results ||= ActiveRecord::Base.transaction do
CONDITIONS_BY_MODEL.flat_map do |model, fields|
Array(fields).flat_map do |field|
model.where(["#{field} LIKE ?", "%#{query}%"])
end
end
end
end
end
Here is my view
.page-header
%h1 Search Results
%h2= "For #{params[:q]}"
%br
%h2 Pages
- #page_results.each do |resource|
%p= link_to "#{resource.title}", page_path(resource.slug)
= resource.content.html_safe
%h2 News Article
- #news_article_results.each do |resource|
%p= link_to "#{resource.title}", news_article_path(resource.slug)
= resource.content.html_safe
%h2 Category
- #category_results.each do |resource|
%p= link_to "#{resource.name}", category_path(resource.slug)

Related

Creating a nested hash from ActiveRecord object in ruby

require "active_record"
ActiveRecord::Base.establish_connection(
:adapter => 'mysql2',
:database => '<db_name>',
:username => '<username>',
:password => '<password>',
:host => 'localhost')
ActiveRecord::Base.pluralize_table_names = false
class Location < ActiveRecord::Base
has_many :location_channels
has_many :channels, :through => :location_channels
end
class Channel < ActiveRecord::Base
has_many :location_channels
has_many :locations, :through => :location_channels
end
class LocationChannel < ActiveRecord::Base
belongs_to :location
belongs_to :channel
end
locations = Location.all
hash = {} # hash initialization
locations.each do |location|
hash["location"] = location[:name]
puts "#{location[:name]} has #{location.channels.size} channels:"
location.channels.each do |channel|
puts "--> #{channel[:name]}"
end
puts
end
puts hash
Final goal is to create one JSON file.
So I decided it'll be easier to create JSON from Hash object.
As in the code above, I'm able to access nested documents via JOIN table called LocationChannel class and I'm trying to figure out how to create a Hash object what will look like:
{
["location" => "A", "channels" => {"1","2","3"}],
["location" => "B", "channels" => {"1","2"}],
["location" => "C", "channels" => {"4","5","6"}]
}
where "A", "B" and "C" - locations name and "1", "2", etc. - represents channels name.
And the current code prints out only the last record like:
{"location"=>"A"}
Please correct me how should Hash look like if the sample above is wrong.
UPDATE 1
Thanks to #jonsnow for point out the hash format.
Hash format should be:
{ :locations =>
[
{ name: a, channels: [1,2,3]},
{ name: b, channels: [1,2]},
{ name: c, channels: [4,5,6]}
]
}
Solution for your updated hash,
hash = { locations: [] } # hash initialization
locations.each do |location|
hash[:locations] << { name: location.name,
channels: location.channels.pluck(:name) }
end

create ruby object if all attributes are defined

I have a hash that is defining some variables as such:
event_details = {
:title => title(event),
:desc => desc(event),
:url => url(event),
:datetime => datetime(event),
:address => address,
:lat => coords["lat"],
:lng => coords["lng"]
}
and I want to create an "event" object
event = Event.new(event_details)
Now I only want to create the object event if and only if all the attributes are defined and not nil, for example, the lat and lng variables are sometimes nil, and I do not want to create that event object.
I was thinking about having a rescue clause in my event class, but I am not sure how I only create the instance of event, and validate the presence of each attribute. any tips would be greatly appreciated
I suggest:
keys = [:title, :desc, :url, :datetime, :address, :lat, :lng]
event = Event.new(event_details) unless
(keys-event_details.keys].any? || event_details.values.any?(&:nil?)
or
if (keys-event_details.keys].any? || event_details.values.any?(&:nil?)
<..action..>
else
event = Event.new(event_details)
end
You can do:
class Event
attr_accessor :title, :desc
RequiredKeys = [:title, :desc]
def initialize(hash = {})
missing_keys = RequiredKeys - hash.keys
raise "Required keys missing: #{missing_keys.join(', ')}" unless missing_keys.empty?
hash.each do |key, value|
public_send("#{key}=", value);
end
end
end

Show page, show associations

i try to find out how i can show associations deeper than one level.
Show at my FORM, i just done it there:
form do |f|
f.inputs "Details" do
f.input :name
f.input :item_category
f.input :resource
f.input :status
end
f.inputs "Actions" do
f.semantic_errors *f.object.errors.keys
f.has_many :item_actions, :allow_destroy => true, :heading => 'Planets', :new_record => true do |obj|
obj.input :action
obj.input :status
obj.input :_destroy, :as=>:boolean, :required => false, :label=>'Remove'
obj.has_many :item_action_skills, :heading => "Skills" do |ias|
ias.input :skill
ias.input :level
end
end
end
f.actions
end
You can see, i show has_many :item_actions and going one level deeper to item_action.item_action_skills. On this form is works perfect.
Now i'll want it on the show page too. My code:
show do |obj|
attributes_table do
row :name
row :item_category
row(:resource) {|obj| status_tag((obj.resource ? 'yes' : 'no'), (obj.resource ? :ok : :error))}
row(:status) {|obj| status_tag(obj.status_string.first, obj.status_string.last) }
end
panel "Actions" do
table_for obj.item_actions do
column :action
column(:status) {|obj| status_tag(obj.status_string.first, obj.status_string.last) }
end
end
active_admin_comments
end
I write table_for, but how to go now to the next association?
I want the item_action.item_action_skills.
I have no idea. Any idea?
Thanks!
Ruby 1.9.3
Rails 3.2.14
ActiveAdmin 0.6.0
Try this:
panel "Actions" do
table_for obj.item_actions do
column :action
column(:status) {|obj| status_tag(obj.status_string.first, obj.status_string.last) }
column("skills"){|resource|
table_for resource.item_action_skills do
column(:your_column)
end
}
end
end

elastic search object association querying through params

I'm having some difficulty with Elastic Search and Tire not returning any results. I'm using Ruby 1.9.3 and Rails 3.2.11.
In my controller I'm calling:
#location_id = 1
#listings = Listing.search(params.merge!(location_id: #location_id))
In my listing model I have
mapping do
indexes :id, type: 'integer'
...
indexes :author do
indexes :location_id, :type => 'integer', :index => :not_analyzed
...
end
def self.search(params={})
tire.search(load: true, page: params[:page], per_page: 20) do |search|
search.query { string params[:query], :default_operator => "AND" } if params[:query].present?
search.filter :range, posted_at: {lte: DateTime.now}
search.filter :term, "author.location_id" => params[:location_id]
end
I have 300 results which all have the location_id of 1 in the database so I can't seem to figure out why it's returning a nil set? If I comment out the author.location_id search filter line it returns all other results as expected?
There are several things which needs to be adressed in a situation like yours. Let's start with a fully working code:
require 'active_record'
require 'tire'
require 'logger'
# Tire.configure { logger STDERR }
# ActiveRecord::Base.logger = Logger.new(STDERR)
Tire.index('articles').delete
ActiveRecord::Base.establish_connection( adapter: 'sqlite3', database: ":memory:" )
ActiveRecord::Schema.define(version: 1) do
create_table :articles do |t|
t.string :title
t.integer :author_id
t.date :posted_at
t.timestamps
end
create_table :authors do |t|
t.string :name
t.integer :number, :location_id
t.timestamps
end
add_index(:articles, :author_id)
add_index(:authors, :location_id)
end
class Article < ActiveRecord::Base
belongs_to :author, touch: true
self.include_root_in_json = false
include Tire::Model::Search
include Tire::Model::Callbacks
mapping do
indexes :title
indexes :author do
indexes :location_id, type: 'integer'
end
end
def self.search(params={})
tire.search load: {include: 'author'} do |search|
search.query do |query|
query.filtered do |f|
f.query { params[:query].present? ? match([:title], params[:query], operator: 'and') : match_all }
f.filter :range, 'posted_at' => { lte: DateTime.now }
f.filter :term, 'author.location_id' => params[:location_id]
end
end
end
end
def to_indexed_json
to_json( only: ['title', 'posted_at'], include: { author: { only: [:location_id] } } )
end
end
class Author < ActiveRecord::Base
has_many :articles
after_touch do
articles.each { |a| a.tire.update_index }
end
end
# -----
Author.create id: 1, name: 'John', location_id: 1
Author.create id: 2, name: 'Mary', location_id: 1
Author.create id: 3, name: 'Abby', location_id: 2
Article.create title: 'Test A', author: Author.find(1), posted_at: 2.days.ago
Article.create title: 'Test B', author: Author.find(2), posted_at: 1.day.ago
Article.create title: 'Test C', author: Author.find(3), posted_at: 1.day.ago
Article.create title: 'Test D', author: Author.find(3), posted_at: 1.day.from_now
Article.index.refresh
# -----
articles = Article.search query: 'test', location_id: 1
puts "", "Documents with location:1", '-'*80
articles.results.each { |a| puts "* TITLE: #{a.title}, LOCATION: #{a.author.location_id}, DATE: #{a.posted_at}" }
articles = Article.search query: 'test', location_id: 2
puts "", "Documents with location:2", '-'*80
articles.results.each { |a| puts "* TITLE: #{a.title}, LOCATION: #{a.author.location_id}, DATE: #{a.posted_at}" }
puts "(NOTE: 'D' is missing, because is not yet posted)"
articles = Article.search query: 'test b', location_id: 1
puts "", "Documents with query:B and location:1", '-'*80
articles.results.each { |a| puts "* TITLE: #{a.title}, LOCATION: #{a.author.location_id}, DATE: #{a.posted_at}" }
First, it's usually a good idea to create an isolated, extracted case like this.
In your example code, I assume you have a relationship Listing belongs_to :author. You need to properly define the mapping and serialization, which I again assume you did.
As for the query itself:
Unless you're using faceted navigation, use the filtered query, not top level filters, as in my example code.
Do not use the string query, unless you really want to expose all the power (and fragility!) of the Lucene query string query to your users.
Use the match query, as your "generic purpose" query -- Tire sprinkles some sugar on top of it, allowing to easily create multi_match queries, etc
The filter syntax in your example is correct. When the filter method is called multiple times in Tire, it creates and and filter.
Uncomment the Tire logging configuration (and possibly also the ActiveRecord logging), to see what the code is doing.

Link_to with additional variable

I want to create a simple link_to (rails 3) with two additional variables:
= link_to 'Try', new_try_path(:k => users.collect{|m| m.user.username}, :h=> users2.collect{|m| m.user2.username2}, :proof => true)
The problem is if users2 is blank, this html code is generated: &k=[1]&&proof=true
I tried something like this. Can you help me please?
= link_to 'Try', new_try_path(:k => users.collect{|m| m.user.username}, :h=> users2.collect{|m| m.user2.username2} if users2.blank?, :proof => true)
Thank you!
Things like this should definitely be refactored into a helper, such as
# view
= try_link(users, users2)
# helper
def try_link(users, users2)
options = { :k => users.collect { |m| m.user.username }, :proof => true }
unless users2.blank?
options[:h] = users2.collect { |m| m.user2.username2 }
end
link_to 'Try', new_try_path(options)
end
This is about the bare minimum you can do to make the view code less horrible.
You might also want to consider putting the whole collect thing into the model.
Also Hash#merge might be helpful in cases like this, where you can do
a = { :foo => 1 }
b = { :bar => 2 }
puts a.merge(b) # => { :foo => 1, :bar => 2 }
Not very elegant, but should work:
- options = { :k => users.map{ |m| m.user.username }, :proof => true }
-# add :h parameter only if users2 is not empty
- options[:h] = users2.map{ |m| m.user2.username2 } unless users2.blank?
= link_to 'Try, new_try_path(options)
If users2 is blank h parameter will be omitted from generated URL.
As alternative you can filter out blank values from options hash:
# for ruby 1.9 (select only non-blank values)
options.select! { |k, v| v.present? }
# for ruby 1.8 (delete blank values)
options.delete_if { |k, v| v.blank? }

Resources