Searchkick - Autocomplete with multiple models & fields
I am struggling to implement the autocomplete functionality for multiple models associated to my Post model. The search functionality works fine and returns the expected data. My autocomplete method also works fine if I implement it the way it is done in the documentation (only for posts' title however).
I also tried this answer and this one by switching Post.index.name to Post.searchkick_index.name but the autocomplete functionality does not display.
This is the code I wrote in posts_controller.rb:
def autocomplete
render json: Post.search(params[:query],
index_name: [
Post.searchkick_index.name,
Tag.searchkick_index.name,
User.searchkick_index.name
],
limit: 10,
load: false,
misspellings: { below: 5 })
end
I also tried:
def autocomplete
render json: Searchkick.search(params[:query],
models: [Post, Tag, User],
limit: 10,
load: false,
misspellings: { below: 5 })
end
I get no errors with the above code, but the autocomplete functionality does not work either.
In post.rb:
class Post < ApplicationRecord
belongs_to :user
has_many :comments, dependent: :destroy
has_many :posts_tags, dependent: :destroy
has_many :tags, through: :posts_tags
searchkick word_start: %i[title]
def search_data
{
title: title,
description: description,
user: user.full_name
}.merge(
tag: tags.map(&:title),
comments: comments.map(&:description)
)
end
end
As suggested in the answer section, I also tried the following:
def autocomplete
posts = Post.search(params[:query], execute: false)
tags = Tag.search(params[:query], execute: false)
users = User.search(params[:query], execute: false)
render json: Searchkick.multi_search([posts, tags, users])
end
This returns the following error: fatal - exception reentered.
I want to be able to autocomplete Post's title, Tag's title & User's full_name. How should I change my code?
Thank you in advance!
From multi_search of searchkick:
posts = Post.search(params[:query], execute: false)
tags = Tag.search(params[:query], execute: false)
users = User.search(params[:query], execute: false)
Searchkick.multi_search([posts, tags, users])
Related
Using Rails 6 I am designing an application to manage police fines. A user can violate many articles, an article can have many letters and a letter can have many commas.
This is my implementation:
#models/fine.rb
class Fine < ApplicationRecord
has_many :violations
has_many :articles, through: :violations
has_many :letters, through: :violations
has_many :commas, through: :violations
end
#models/article.rb
class Article < ApplicationRecord
has_many :letters
has_many :violations
has_many :fines, through: :violations
end
#models/letter.rb
class Letter < ApplicationRecord
belongs_to :article
has_many :commas
has_many :violations
has_many :fines, through: :violations
end
#models/comma.rb
class Comma < ApplicationRecord
belongs_to :letter
has_many :violations
has_many :fines, through: :violations
end
#models/violation.rb
class Violation < ApplicationRecord
belongs_to :fine
belongs_to :article
belongs_to :letter, optional: true
belongs_to :comma, optional: true
end
When I print the fine in PDF I need to show violations: articles, letters and commas. I have difficulty creating a form to compile the fine because it is too deep. I am using Active Admin, when I create a new fine I want to associate many violations.
Violation example:
Violation.new
=> #<Violation id: nil, article_id: nil, fine_id: nil, letter_id: nil, comma_id: nil, note: nil, created_at: nil, updated_at: nil>
How can I create a form (using Active Admin, which uses Formtastic) to associate many violations to a fine? Example form:
Example (with sample data):
Violation.new fine: fine, article: a, letter: a.letters.last, comma: a.letters.second.commas.last
=> #<Violation id: nil, article_id: 124, fine_id: 66, letter_id: 10, comma_id: 4, note: nil, created_at: nil, updated_at: nil>
In my humble opinion, your question is rather vague and difficult to answer based only on the provided information. Since I can't produce an answer that will definitely solve your issue, allow me to try and point you in the right direction.
Rendering the form
First let's understand the problem here: you're trying to create an association record in a nested resource form.
You need to customize the form for Fine to include a form for each violation. Look at how ActiveAdmin handles nested resources. It should be something like:
ActiveAdmin.register Fine do
form do |f|
inputs 'Violations' do
f.has_many :violations do |vf|
vf.input :article, as: :select, collection: Article.all
vf.input :letter, as: :select, collection: Letter.all
vf.input :comma, as: :select, collection: Comma.all
end
end
end
end
Put simply, this is the answer to your question "How can I create a form (using Active Admin, who use Formtastic) to associate many violations to a Fine?".
Caveats
As you probably already noticed, there are a couple of problems with this approach.
First, it is nothing like your example. You can easily change things for Formtastic to add the check-boxes by using as: :check_boxes, but you'll find the check-boxes are not organized as you want with that pretty indentation. As far as I know, there is no way for you to do this with Formtastic. Instead, I believe you would have to use a partial.
Using a partial you can easily go through the articles, and render a check-box for each of them and go through each one's letters, and so on. However, bear in mind this form will require you to customize the controller so it understands each of these check-boxes and creates the respective violations. Not as straight forward.
Second, there is nothing enforcing the data integrity here. One could select an article, the letter of another one, and the comma of a third one (by the way, I hope you have a validation to protect you from this). To have the form dynamically change, so only the letters of a given article are shown after its selection, and same thing for the commas, would require some client-side logic. Not worth the trouble if you ask me.
Conclusion
Your question is far from simple and obvious, both to answer and to solve. One option you always have is a custom set of routes for managing such resources outside ActiveAdmin. Remember, tools like this are only as valuable as the work they take from you. If you're having to fight it, better to just step out of each other's way.
Hope this helps, in any way.
Solved:
f.has_many :violations do |vf|
vf.input :article, as: :select, include_blank: false, collection: options_for_select(Article.all.map {|article| [article.number, article.id, { :'data-article-id' => article.id, :'data-letters' => article.letters.map(&:id).to_json }]})
vf.input :letter, as: :select, collection: options_for_select(Letter.all.map {|letter| [letter.letter, letter.id, { :'hidden' => true, :'data-letter-id' => letter.id, :'data-article-id' => letter.article.id, :'data-commas' => letter.commas.map(&:id).to_json }]})
vf.input :comma, as: :select, collection: options_for_select(Comma.all.map {|comma| [comma.number, comma.id, { :'hidden' => true, :'data-comma-id' => comma.id, :'data-letter-id' => comma.letter.id }]})
end
And with a bit of javascript:
$(document).on('has_many_add:after', '.has_many_container', function (e, fieldset, container) {
selects = fieldset.find('select');
article_select = selects[0];
letter_select = selects[1];
comma_select = selects[2];
$(article_select).on("change", function () {
$(letter_select).prop('selectedIndex', 0);
$(comma_select).prop('selectedIndex', 0);
$("#" + letter_select.id + " option").prop("hidden", true);
$("#" + comma_select.id + " option").prop("hidden", true);
letters = $(this).find(':selected').data('letters');
$.each(letters, function (index, id) {
$("#" + letter_select.id + " option[data-letter-id='" + id + "']").removeAttr("hidden");
});
});
$(letter_select).on("change", function () {
$(comma_select).prop('selectedIndex', 0);
$("#" + comma_select.id + " option").prop("hidden", true);
commas = $(this).find(':selected').data('commas');
$.each(commas, function (index, id) {
$("#" + comma_select.id + " option[data-comma-id='" + id + "']").removeAttr("hidden");
});
});
});
I show all Articles, Letters and Commas in the selectbox. Initially Commas and Letters are hidden, then when a User click an Article the Letter's selectbox show only the related Letters.
The Commas code works same as Letters.
After I can add some validations in the Violation model.
I have two models: Worker and Skill.
A worker can have multiple skills. Each skill has its own name (e.g. 'php', 'web-development' etc.)
In my search form I want to be able to filter workers (with the help of searchkick) according to their skills. If multiple skills are selected in the form, the search results should only include those workers, who have all the skills which have been selected.
I tried the following:
worker.rb
has_many :worker_skills
has_many :skills, through: worker_skills
searchkick
scope :search_import, -> { includes(:skills) }
def search_data
{
skills_name: skills.pluck(:name)
}
end
skill.rb
has_many :worker_skills
has_many :workers, through: :worker_skills
workers_controller.rb
def index
...
#workers = Worker.search(
params[:q],
where: {
skills_name: params[:skills]
}
end
...
end
This returns me all the workers who have at least one of the selected skills.
Example:
Filters selected: ['php', 'ruby']
Result: All workers who either have 'php' - OR - 'ruby' as a skill.
How can I achieve it to only return those workers who have both skills at the same time?i.e. 'php' - AND - 'ruby'
If you look here https://github.com/ankane/searchkick#querying you can use the all parameter so that it must match with all elements in the where clause. Do the following in your workers_controller.rb:
def index
...
#workers = Worker.search(
params[:q],
where: {
skills_name: {
all: params[:skills]
}
}
end
...
end
Also keep in mind to ensure that you have values in params[:skills]. If not you get an undefined method `each' for nil:NilClass Error. So maybe disable the search button until some values were selected.
I used gem 'searchkick' for searching in my application.
here is my association and searchkick settings .
product.rb
belongs_to :sub_category
belongs_to :brand
belongs_to :product_type
has_one :category, through: :sub_category
searchkick match: :word_start,word_start: [:name], suggest: [:name]
scope :search_import, -> { includes(:brand, :sub_category, :category, :product_type) }
def search_data
{
name: name,
category_name: category.name,
sub_category_name: sub_category.name,
brand: brand.name,
product_type: product_type.name
}
end
I don't how searchkick works exactly. but i want to show the category name, sub category name and product type name in search results.
For an example,
I have list of alcohols products which belongs to different categories like beer, wine, whiskey etc.
if i search for beer, it should show beer in search results because beer is category which associated to product. i don't want the products which are associated to beer category, i want to beer category in search results.
here is my query for search query
response = Product.search( params[:query], suggest: true, fields: ["name^10", "description"], limit: 5, operator: "or",misspellings: {below: 5} ).results
it is similar functionality of any e commerce application, like if i search iPhone 7 in flipkart application it will show iPhone 7 as category in search result, if i click on that all products related to iPhone 7 will show in one page.
i don't know how to achieve this , any help will appreciated.
I think you are looking for aggregation feature. Just look the searchkick doc, you will find your answer.
Note: I am giving answer from Stackoverflow app. So I will later update it with some code from laptop.
I have to do multi search , Here is the service which i made for getting associated model with multi search.
module Search
class RetriveData
def self.multi_search(params)
#params = params
create_searchable_variables_for_multi_model_search
Searchkick.multi_search(array_of_searchkick_objects)
array_of_searchkick_objects.map(&:results).flatten
end
private
def self.array_of_searchkick_objects
array_of_models.map do |klass|
instance_variable_get("##{klass.to_s.downcase}s")
end
end
def self.searchable_fields
["name^10","sub_category_name","keyword_name",
"category_name","product_type_name","brand_name"]
end
def self.create_searchable_variables_for_multi_model_search
array_of_models.each do |klass|
instance_variable_set("##{klass.to_s.downcase}s",
klass.search( #params[:query],
constraints(klass) ))
end
end
def self.constraints(klass)
{
fields: searchable_fields,
suggest: true,
operator: "or",
misspellings: {below: 5},
where: klass == Product ? { or: [[{available_cities_name:
#params[:city]},{available_cities_short_name:
#params[:short_name]}]] } : {},
execute: false
}
end
def self.array_of_models
[Category, SubCategory, Brand, ProductType, Product]
end
end
end
Source is here
In my application I have models Campaign & Map:
class Campaign < ActiveRecord::Base
has_many :maps, :dependent => :destroy
class Map < ActiveRecord::Base
is_impressionable :counter_cache => true, :unique => :request_hash
belongs_to :campaign, :counter_cache => true, touch: true
In my Map model, I'm using is_impressionable that comes with installing impressionist gem & I have added counter_cache as well that will update my impressions_count for every visit (in my maps table).
In my campaigns#show view, Im trying to add a chart for maps impressions by using morris.js (I have added all needed files in my application and can see the chart).
In my chart, I want to show impressions on campaign.maps, but I am getting wrong data into my chart.
So basically, go to my impressions table, and sum all visits that my campaign.maps has on the giving day.
This is what I have until now:
module CampaignsHelper
def impressions_chart_data_campaign(maps)
(7.days.ago.to_date..Date.today).map do |date|
{
created_at: date,
impressions: Impression.where("date(created_at) =?", date).where(id: maps).size
}
end
end
end
My campaigns#show view:
= content_tag :div, "", id: "impressions_charts_double", data: {impressions: impressions_chart_data_campaign(#campaign.maps)}
:javascript
Morris.Area({
element: 'impressions_charts_double',
data: $('#impressions_charts_double').data('impressions'),
xkey: 'created_at',
ykeys: ['impressions'],
labels: ['Impressions']
});
As mentioned I am getting completely wrong numbers. This is queries in my console:
I'm not sure What Im doing wrong.
This line looks suspect:
impressions: Impression.where("date(created_at) =?", date).where(id: maps).size
The where(id: maps) is selecting on the id column for impressions, whereas you presumably want to be selecting on impressionable_type and impressionable_id?
I have a service that does a request,
.factory('movieService', ['$http', function($http) {
return {
loadMovies: function() {
return $http.get('/movies_users.json');
}
};
}])
This is the JSON output and is the result of 2 tables being joined. A user table and a movie table. As you can see the users are associated with 1 or more movies.
[
{"id":1,
"email":"peter#peter.nl",
"movies":[
{
"id":4,
"title":"Creed",
movie_id":"312221"
},
{
"id":5,
"title":"Star Wars: Episode VII - The Force Awakens",
"movie_id":"140607"
}
]
},
{"id":2,
"email":"jan#jan.com",
"movies":[
{
"id":4,
"title":"Creed",
movie_id":"312221"
}
]
}
]
And this is the movies_users_controller.rb
def index
movie = Movie.all
render :json => movie.to_json(:include => :users)
end
Is it possible to only show the current user in the JSON output instead of both users?
Is it possible to only show the current user in the JSON output
instead of both users?
That implies that you have some authentication system (if you don't, you can have a look at devise).
Instead of fetching all the movies, just get the movies of the current user.
#movies = current_user.movies
In order to make this work, you'll have to have a relationship between the User model and the Movie model, something like this:
# user.rb
has_many :user_movies
has_many :movies, through: user_movies
# user_movie.rb
belongs_to :movie
belongs_to :user
# movie.rb
has_many :user_movies
has_many :users, through: :user_movies
Also, it seems that you are building an API, I would advice to use something like jbuilder to build your json object, it will be cleaner and you will be able to display pretty much everything you want.