How to implement a Category filter for searchkick. Basically I have an input field that takes the query term, but I also want a dropdown that users can pick a category to search from or search in ALL categories. There is a many-to-many relationship between posts and categories
My models are:
-- post.rb
class Post < ActiveRecord::Base
has_many :post_categories
has_many :categories, through: :post_categories
searchkick text_start: [:title]
end
-- category.rb
class Category < ActiveRecord::Base
has_many :post_categories
has_many :posts, through: :post_categories
end
--post_category.rb
class PostCategory < ActiveRecord::Base
belongs_to :post
belongs_to :category
end
Now in my posts_controller index action, I have the following, which works so far by returning all posts that match the query parameter, or returns all posts if no query parameter is provided in the search input.
class PostsController < ApplicationController
def index
query = params[:q].presence || "*"
#posts = Post.search (query)
end
end
This works well so far. But I also want to add a category filter in the view, so that the user can choose to search for the query string within a particular category, or search in all categories if no category is selected. Thanks in advance.
As per searchkick documentation, you can add parameters to .search query - see here section Queries, specifically where. Sample from docs:
Product.search "apples", where: {in_stock: true}, limit: 10, offset: 50
Should be smt like
Post.search query, where: {category: [some_array]}
Note - searchkick gem converts conditions from where statement to ElasticSearch filters (see here)
Update - for searching by attributes of related objects (not model itself), you should include its fields into search_index - see sample here
Add the category titles to the search_data method.
class Project < ActiveRecord::Base
def search_data
attributes.merge(
categories_title: categories.map(&:title)
)
end
end
Also this question on related topic
By default search_data of searchkick is return of serializable_hash model call -see sources for reference.
def search_data
respond_to?(:to_hash) ? to_hash : serializable_hash
end unless method_defined?(:search_data)
Which does not include anything of associations by default, unless passed with :include parameter - source
def serializable_hash(options = nil)
...
serializable_add_includes(options) do ..
end
def serializable_add_includes(options = {}) #:nodoc:
return unless includes = options[:include]
...
end
Related
I have a blog with subcategories/main categories and on the main category, I want it to list the posts from all of its child categories. I got it working with using the method .first but I just don't know how to handle this in the way I need it to.
BlogCategory Model:
class BlogCategory < ApplicationRecord
extend FriendlyId
friendly_id :name, use: :slugged
has_many :posts
# This is called a self referential relation. This is where records in a table may point to other records in the same table.
has_many :sub_categories, class_name: "BlogCategory", foreign_key: :parent_id
belongs_to :parent, class_name: 'BlogCategory', foreign_key: :parent_id
# This is a scope to load the top level categories and eager-load their posts, subcategories, and the subcategories' posts too.
scope :top_level, -> { where(parent_id: nil).includes :posts, sub_categories: :posts }
def should_generate_new_friendly_id?
slug.nil? || name_changed?
end
end
blog_categories Controller:
def show
#cat = BlogCategory.friendly.find(params[:id])
#category = #cat.parent
#posts = #cat.posts
#sub_category = #cat.sub_categories.first
unless #sub_category.nil?
#relatives = #sub_category.posts
end
end
private
def cat_params
params.require(:blog_category).permit(:name, :parent_id, :sub_category)
end
def main_cat
#cat = BlogCategory.parent_id.nil?
end
Post Model: belongs_to :blog_category
I have tried a few configurations of .all .each and seen if .collection worked, but these didn't seem to fix my problem.
Thank you I do appreciate it.
You can add a has many association in your Category model like this
has_many :sub_category_posts, through: :sub_categories, source: :posts
In your controller
#relatives = #cat.sub_category_posts
I guess you want all the posts. If a post belongs to a category, that category will be a child of another category, and eventually, you'll have the main category, so, you could do something like:
#posts = Post.where.not(blog_category: nil)
If you have many main categories, one per blog, you need to implement a recursive method.
You could also use https://github.com/collectiveidea/awesome_nested_set and do something like:
#cat.descendants # array of all children, children's children, etc., excluding self
https://github.com/collectiveidea/awesome_nested_set/wiki/Awesome-nested-set-cheat-sheet
I'm trying to build a checkbox filter which will reduce the number of results with each box checked. Each checked box will represent a has_many: through association.
I have an app with three models:
Book
Subject
BookSubject
They are associated like so:
class Book < ActiveRecord::Base
has_many :book_subjects
has_many :subjects, through: :book_subjects
end
class Subject < ActiveRecord::Base
has_many :book_subjects
has_many :books, through: :book_subjects
end
class BookSubject < ActiveRecord::Base
belongs_to :book
belongs_to :subject
end
Through the Books controller, the user can run the Search action, which renders a view page with all of the books shown. By checking boxes in the sidebar (each of which is labeled with the name of a subject), the user should be able to narrow down the number of books which fit their search. This is the search action so far:
def search
if params[:name].nil? || params[:name].empty?
#all_books = Book.all
else
#all_books = Book.joins(:subjects).where(subjects: {name: params[:name].split(",")}).distinct
end
render 'search'
end
Right now, if the user selects "historical" and "epic", they get all books with historical as a subject, and all books with epic as a subject. I would like them to get ONLY the books with both historical AND epic as a subject. I've tried adding .group to the query, but so far it's not working:
def search
if params[:name].nil? || params[:name].empty?
#all_books = Book.all
else
#all_books = Book.joins(:subjects)
.where(subjects: {name: params[:name].split(",")})
.group("books.id")
.having("count(*) >= ?", 1)
end
end
end
How should I modify my query to make filtering possible? Thank you!!!
I think your code is almost fine but why do you have having("count(*) >= ?", 1)?
That condition matches for books which have only one subject from all required list.
Try to change code to the following
def search
#all_books = if params[:name].blank?
Book.all
else
names = params[:name].split(',')
Book.joins(:subjects)
.where(subjects: { name: names })
.group(:id)
.having('count(*) = ?', names.size)
end
end
Example:
I have the following:
class Person < ActiveRecord::Base
has_many :educations
end
class Education < ActiveRecord::Base
belongs_to :school
belongs_to :degree
belongs_to :major
end
class School < ActiveRecord::Base
has_many :educations
# has a :name
end
I want to be able to return all people who went to a specific school so in my PeopleController#index I have
#search = Person.search do
keywords params[:query]
end
#people = #search.results
How do I create the searchable method on the Person model to reach down into school? Do I do something like this:
searchable do
text :school_names do
educations.map { |e| e.school.name }
end
end
which I would eventually have to do with each attribute on education (degree etc) or can I make a searchable method on Education and somehow "call" that from Person.searchable?
Thanks
It would be best if you keep the declaration of all the indexed fields for an specific model in the same place.
Also, you were doing a good job indexing :school_names, just do the same thing for the rest of the associations fields' that you want to index.
class Newsroom < ActiveRecord::Base
has_many :blog_posts
has_many :quote_posts
end
class BlogPost < ActiveRecord::Base
belongs_to :newsroom
end
class QuotePost < ActiveRecord::Base
belongs_to :newsroom
end
I would like to have an instance method, such that I could do #newsroom.posts to get a collection of blog_posts and quote_posts sorted by created_at.
def posts
#posts ||= #load and sort blog_posts, quote_posts, etc
end
What is the best and most efficient way to accomplish this? I have looked into using default_scope, something like:
default_scope :include => [:blog_posts, :quote_posts]
def posts
#posts ||= [blog_posts + quote_posts].flatten.sort{|x,y| x.created_at <=> y.created_at}
end
But I would rather keep the sorting at the database level, if possible. Any suggestions on how to accomplish this? Thanks.
Try something like this:
#app/models/newsroom.rb
scope :ordered_posts, lambda {
includes(:blog_posts,:quote_posts) & BlogPost.order("created_at asc") & QuotePost.order("created_at asc")
}
ARel should be able to handle the ordering of included Quote and Blog Posts. You could clean that up slightly by having scopes in both the BlogPost and QuotePost model that order by created_at and then use those scopes in the Newsroom#ordered_posts method.
I ended up using a polymorphic post model. This seems to give me what I want with the insignificant downside of having an extra model/table. I used delegate to hand off specific attribute getter methods to the correct model.
class Newsroom < ActiveRecord::Base
has_many :posts
end
class Post < ActiveRecord::Base
belong_to :blog_post, :polymorphic => true
delegate :title, :author, :etc, :to => :postable
end
class BlogPost < ActiveRecord::Base
has_one :post, :as => :postable
end
I was wondering if it was possible to use the find method to order the results based on a class's has_many relationship with another class. e.g.
# has the columns id, name
class Dog < ActiveRecord::Base
has_many :dog_tags
end
# has the columns id, color, dog_id
class DogTags < ActiveRecord::Base
belongs_to :dog
end
and I would like to do something like this:
#result = DogTag.find(:all, :order => dog.name)
thank you.
In Rails 4 it should be done this way:
#result = DogTag.joins(:dog).order('dogs.name')
or with scope:
class DogTags < ActiveRecord::Base
belongs_to :dog
scope :ordered_by_dog_name, -> { joins(:dog).order('dogs.name') }
end
#result = DogTags.ordered_by_dog_name
The second is easier to mock in tests as controller doesn't have to know about model details.
You need to join the related table to the request.
#result = DogTag.find(:all, :joins => :dog, :order => 'dogs.name')
Note that dogs is plural in the :order statement.