Indexing and ordering by dynamic field with sunspot - ruby-on-rails

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.

Related

building a simple search form in Rails?

I'm trying to build a simple search form in Ruby on Rails, my form is simple enough basically you select fields from a series of options and then all the events matching the fields are shown. The problem comes when I leave any field blank.
Here is the code responsible for filtering the parameters
Event.joins(:eventdates).joins(:categories).where
("eventdates.start_date = ? AND city = ? AND categories.name = ?",
params[:event][:date], params[:event][:city], params[:event][:category]).all
From what I get it's that it looks for events with any empty field, but since all of them have them not empty, it wont match unless all 3 are filled, another problem arises when I try to say, look events inside a range or array of dates, I'm clueless on how to pass multiple days into the search.
I'm pretty new to making search forms in general, so I don't even know if this is the best approach, also I'm trying to keep the searches without the need of a secialized model.
Below is probably what you are looking for. (Note: If all fields all blank, it shows all data in the events table linkable with eventdates and categories.)
events = Event.joins(:eventdates).joins(:categories)
if params[:event]
# includes below where condition to query only if params[:event][:date] has a value
events = events.where("eventdates.start_date = ?", params[:event][:date]) if params[:event][:date].present?
# includes below where condition to query only if params[:event][:city] has a value
events = events.where("city = ?", params[:event][:city]) if params[:event][:city].present?
# includes below where condition to query only if params[:event][:city] has a value
events = events.where("categories.name = ?", params[:event][:category]) if params[:event][:category].present?
end
To search using multiple days:
# params[:event][:dates] is expected to be array of dates.
# Below query gets converted into an 'IN' operation in SQL, something like "where eventdates.start_date IN ['date1', 'date2']"
events = events.where("eventdates.start_date = ?", params[:event][:dates]) if params[:event][:dates].present?
It will be more easy and optimised . If you use concern for filter data.
Make one concern in Model.
filterable.rb
module Filterable
extend ActiveSupport::Concern
module ClassMethods
def filter(filtering_params)
results = self.where(nil)
filtering_params.each do |key, value|
if column_type(key) == :date || column_type(key) ==
:datetime
results = results.where("DATE(#{column(key)}) = ?",
Date.strptime(value, "%m/%d/%Y")) if
value.present?
else
results = results.where("#{column(key)} Like ? ", "%#{value}%") if
value.present?
end
end
results
end
def resource_name
self.table_name
end
def column(key)
return key if key.split(".").count > 1
return "#{resource_name}.#{key}"
end
def column_type(key)
self.columns_hash[key].type
end
end
end
Include this concern in model file that you want to filter.
Model.rb
include Filterable
In your controller Add this methods
def search
#resources = Model.filter(class_search_params)
render 'index'
end
def class_search_params
params.slice(:id,:name) #Your field names
end
So, It is global solution. You dont need to use query for filter. just add this concern in your model file.
That's it.

Get specific result by checking a simple variable inside a double array

I have a #customers variable in which each customer has multiple contracts. Each of those contract has multiple users.
What I would like to do is to get all the customers that have a specific user
I tried using the find method but I don't understand how to do that inside a what seems to be double array.
Currently I have this:
cust.each do |c|
if c.contracts.where(users.find(session[:login]))
#customers << c
end
end
What am I missing here?
I think you have to mention user field in where clause, Can you try below code
cust.each do |c|
if c.contracts.where(user: users.find(session[:login]))
#customers << c
end
end

Unit Testing Tire (Elastic Search) - Filtering Results with Method from to_indexed_json

I am testing my Tire / ElasticSearch queries and am having a problem with a custom method I'm including in to_indexed_json. For some reason, it doesn't look like it's getting indexed properly - or at least I cannot filter with it.
In my development environment, my filters and facets work fine and I am get the expected results. However in my tests, I continuously see zero results.. I cannot figure out where I'm going wrong.
I have the following:
def to_indexed_json
to_json methods: [:user_tags, :location_users]
end
For which my user_tags method looks as follows:
def user_tags
tags.map(&:content) if tags.present?
end
Tags is a polymorphic relationship with my user model:
has_many :tags, :as => :tagable
My search block looks like this:
def self.online_sales(params)
s = Tire.search('users') { query { string '*' }}
filter = []
filter << { :range => { :created_at => { :from => params[:start], :to => params[:end] } } }
filter << { :terms => { :user_tags => ['online'] }}
s.facet('online_sales') do
date :created_at, interval: 'day'
facet_filter :and, filter
end
end
end
I have checked the user_tags are included using User.last.to_indexed_json:
{"id":2,"username":"testusername", ... "user_tags":["online"] }
In my development environment, if I run the following query, I get a per day list of online sales for my users:
#sales = User.online_sales(start_date: Date.today - 100.days).results.facets["online_sales"]
"_type"=>"date_histogram", "entries"=>[{"time"=>1350950400000, "count"=>1, "min"=>6.0, "max"=>6.0, "total"=>6.0, "total_count"=>1, "mean"=>6.0}, {"time"=>1361836800000, "count"=>7, "min"=>3.0, "max"=>9.0, "total"=>39.0, "total_count"=>7, "mean"=>#<BigDecimal:7fabc07348f8,'0.5571428571 428571E1',27(27)>}....
In my unit tests, I get zero results unless I remove the facet filter..
{"online_sales"=>{"_type"=>"date_histogram", "entries"=>[]}}
My test looks like this:
it "should test the online sales facets", focus: true do
User.index.delete
User.create_elasticsearch_index
user = User.create(username: 'testusername', value: 'pass', location_id: #location.id)
user.tags.create content: 'online'
user.tags.first.content.should eq 'online'
user.index.refresh
ws = User.online_sales(start: (Date.today - 10.days), :end => Date.today)
puts ws.results.facets["online_sales"]
end
Is there something I'm missing, doing wrong or have just misunderstood to get this to pass? Thanks in advance.
-- EDIT --
It appears to be something to do with the tags relationship. I have another method, ** location_users ** which is a has_many through relationship. This is updated on index using:
def location_users
location.users.map(&:id)
end
I can see an array of location_users in the results when searching. Doesn't make sense to me why the other polymorphic relationship wouldn't work..
-- EDIT 2 --
I have fixed this by putting this in my test:
User.index.import User.all
sleep 1
Which is silly. And, I don't really understand why this works. Why?!
Elastic search by default updates it's indexes once per second.
This is a performance thing because committing your changes to Lucene (which ES uses under the hood) can be quite an expensive operation.
If you need it to update immediately include refresh=true in the URL when inserting documents. You normally don't want this since committing every time when inserting lots of documents is expensive, but unit testing is one of those cases where you do want to use it.
From the documentation:
refresh
To refresh the index immediately after the operation occurs, so that the document appears in search results immediately, the refresh parameter can be set to true. Setting this option to true should ONLY be done after careful thought and verification that it does not lead to poor performance, both from an indexing and a search standpoint. Note, getting a document using the get API is completely realtime.

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'.

How do I populate a table in rails from a fixture?

Quick summary:
I have a Rails app that is a personal checklist / to-do list. Basically, you can log in and manage your to-do list.
My Question:
When a user creates a new account, I want to populate their checklist with 20-30 default to-do items. I know I could say:
wash_the_car = ChecklistItem.new
wash_the_car.name = 'Wash and wax the Ford F650.'
wash_the_car.user = #new_user
wash_the_car.save!
...repeat 20 times...
However, I have 20 ChecklistItem rows to populate, so that would be 60 lines of very damp (aka not DRY) code. There's gotta be a better way.
So I want to use seed the ChecklistItems table from a YAML file when the account is created. The YAML file can have all of my ChecklistItem objects to be populated. When a new user is created -- bam! -- the preset to-do items are in their list.
How do I do this?
Thanks!
(PS: For those of you wondering WHY I am doing this: I am making a client login for my web design company. I have a set of 20 steps (first meeting, design, validate, test, etc.) that I go through with each web client. These 20 steps are the 20 checklist items that I want to populate for each new client. However, while everyone starts with the same 20 items, I normally customize the steps I'll take based on the project (and hence my vanilla to-do list implementation and desire to populate the rows programatically). If you have questions, I can explain further.
Just write a function:
def add_data(data, user)
wash_the_car = ChecklistItem.new
wash_the_car.name = data
wash_the_car.user = user
wash_the_car.save!
end
add_data('Wash and wax the Ford F650.', #user)
I agree with the other answerers suggesting you just do it in code. But it doesn't have to be as verbose as suggested. It's already a one liner if you want it to be:
#new_user.checklist_items.create! :name => 'Wash and wax the Ford F650.'
Throw that in a loop of items that you read from a file, or store in your class, or wherever:
class ChecklistItem < AR::Base
DEFAULTS = ['do one thing', 'do another']
...
end
class User < AR::Base
after_create :create_default_checklist_items
protected
def create_default_checklist_items
ChecklistItem::DEFAULTS.each do |x|
#new_user.checklist_items.create! :name => x
end
end
end
or if your items increase in complexity, replace the array of strings with an array of hashes...
# ChecklistItem...
DEFAULTS = [
{ :name => 'do one thing', :other_thing => 'asdf' },
{ :name => 'do another', :other_thing => 'jkl' },
]
# User.rb in after_create hook:
ChecklistItem::DEFAULTS.each do |x|
#new_user.checklist_items.create! x
end
But I'm not really suggesting you throw all the defaults in a constant inside ChecklistItem. I just described it that way so that you could see the structure of the Ruby object. Instead, throw them in a YAML file that you read in once and cache:
class ChecklistItem < AR::Base
def self.defaults
##defaults ||= YAML.read ...
end
end
Or if you wand administrators to be able to manage the default options on the fly, put them in the database:
class ChecklistItem < AR::Base
named_scope :defaults, :conditions => { :is_default => true }
end
# User.rb in after_create hook:
ChecklistItem.defaults.each do |x|
#new_user.checklist_items.create! :name => x.name
end
Lots of options.
A Rails Fixture is used to populate test-data for unit tests ; Dont think it's meant to be used in the scenario you mentioned.
I'd say just Extract a new method add_checklist_item and be done with it.
def on_user_create
add_checklist_item 'Wash and wax the Ford F650.', #user
# 19 more invocations to go
end
If you want more flexibility
def on_user_create( new_user_template_filename )
#read each line from file and call add_checklist_item
end
The file can be a simple text file where each line corresponds to a task description like "Wash and wax the Ford F650.". Should be pretty easy to write in Ruby,

Resources