Searchkick aggregations for has_and_belongs_to_many - ruby-on-rails

I have a Rails 5 app and I'm trying to do an aggregations search for a has_and_belongs_to_many association.
Here is the code that I have so far:
event.rb:
class Event < ApplicationRecord
searchkick text_start: [:title]
has_and_belongs_to_many :services
has_and_belongs_to_many :sectors
def search_data
atributes.merge(
title: title,
description: description,
sector_name: sectors.map(&:name),
service_name: services.map(&:name)
)
end
end
events_controller.rb:
def index
query = params[:j].presence || "*"
conditions = {}
conditions[:sector] = params[:sector] if params[:sector].present?
conditions[:service] = params[:service] if params[:service].present?
conditions[:date] = params[:date] if params[:date].present?
#events = Event.search query, where: conditions, aggs: [:sector, :service, :date], order: {created_at: {order: "desc"}}, page: params[:page], per_page: 10
end
When I call Event.reindex in the console I was expecting to to show that the sectors and services had been indexed but it doesn't work.
To be honest I'm getting quite lost and going round in circles so any help would be much appreciated.

This is the code that ended up working for me:
event.rb:
def index
query = params[:j].presence || "*"
conditions = {start_date: {"gte": "now/d"}}
conditions[:sector_name] = params[:sector_name] if params[:sector_name].present?
conditions[:service_name] = params[:service_name] if params[:service_name].present?
conditions[:start_date] = params[:start_date] if params[:start_date].present?
#events = Event.search query, where: conditions, aggs: [:sector_name, :service_name], order: {start_date: {order: "asc", unmapped_type: "long"}}, page: params[:page], per_page: 10
end
events_controller.rb:
def search_data
{
title: title,
location: location,
description: description,
start_date: start_date,
sector_name: sectors.map(&:name),
service_name: services.map(&:name)
}
end

Related

Search form associated attributes with joins

I would like to find a record from the records models associated models, associated model attribute.
I have a VendorOrder model and would like to find a VendorOrder (or vendor_orders, if matching criteria) by searching the vendor_order.order.shipments.each {|shipment| shipment.shipping_label.tracking_number}, if possible. So searching for a tracking number.
Models:
VendorOrder
belongs_to :order
has_many :shipments
Order
has_many :vendor_orders
has_many :shipments
has_many :shipping_labels
Shipment
belongs_to :order
belongs_to :vendor_order
belongs_to :shipping_label
ShippingLabel
belongs_to :order
has_one :shipment
I have in controller,
#vendor_orders = VendorOrder.joins(:order).merge(Order.where(order_status: "paid")).order(created_at: :desc).paginate(page: params[:vendor_page], per_page: 25)
if params[:search]
#vendor_orders = VendorOrder.search(params[:search])
else
#vendor_orders = VendorOrder.joins(:order).merge(Order.where(order_status: "paid")).order(created_at: :desc).paginate(page: params[:vendor_page], per_page: 25)
end
In VendorOrder model,
def self.search(search)
if search
find(:all, :conditions => ['shipments.map {|shipment| shipment.shipping_label.tracking_number} LIKE ?', "%#{search}%"])
else
find(:all)
end
end
View:
<%= form_tag vendor_orders_path, :method => 'get' do %>
<p>
<%= text_field_tag :search, params[:search] %>
<%= submit_tag "Search", :name => nil %>
</p>
<% end %>
Error:
Couldn't find all VendorOrders with 'id': (all, {:conditions=>["shipments.map {|shipment| shipment.shipping_label.tracking_number} LIKE ?", "%99999999999999999999%"]}) (found 0 results, but was looking for 2).):
Obviously I am not fully understanding how this works. I have up until now only made search forms where the search within one model for a string.
How can I search through multiple layers of models but making sure a VendorOrder is the front model being searched for?
Update:
Using jvillian post:
Doing:
in model:
class << self
def find_by_tracking_number(tracking_number)
joins(:shipments).
where(shipments: {
id: Shipment.find_by(
shipping_label: ShippingLabel.where(tracking_number: tracking_number)
)
})
end
end
controller:
#vendor_orders = VendorOrder.joins(:order).merge(Order.where(order_status: "paid")).order(created_at: :desc).paginate(page: params[:vendor_page], per_page: 25)
if params[:search]
#vendor_orders = VendorOrder.find_by_tracking_number(params[:search])
else
#vendor_orders = VendorOrder.
joins(:order).
merge(Order.where(order_status: "paid")).
order(created_at: :desc).
paginate(page: params[:vendor_page], per_page: 25)
end
works to get things up and running but i run into the issue of no results.
Entering the numbers "99999999999999999999" should garner about 10 results but 0 appear.
on submit:
Parameters: {"utf8"=>"✓", "search"=>"99999999999999999999"}
does its magic:
SELECT "shipments".* FROM "shipments" WHERE "shipments"."shipping_label_id" IN (SELECT "shipping_labels"."id" FROM "shipping_labels" WHERE "shipping_labels"."tracking_number" = $1) LIMIT $2[0m [["tracking_number", "99999999999999999999"], ["LIMIT", 1]]
But then nothing after that, it just renders the page.
At this moment, there are about 10 VendorOrders that have shipments.map { |shpiment| shipment.shipping_label.tracking_number } that == "99999999999999999999"
Any reason why nothing is appearing? Missing something in my code to append the all results?
Give this a try:
VendorOrder.
joins(:shipments).
where(shipments: {
id: Shipment.find_by(shipping_label: ShippingLabel.find_by(tracking_number: #tracking_number))
})
To break it down, assume you have your tracking number in a variable called #tracking_number. This:
ShippingLabel.find_by(tracking_number: #tracking_number)
...will find your ShippingLabel. And this:
Shipment.find_by(shipping_label: ShippingLabel.find_by(tracking_number: #tracking_number))
...will find your Shipment that belongs_to the ShippingLabel.
Then, following the Specifying Conditions on the Joined Table section of the guide gives you the final query from way up above.
You might use this in the controller something like:
if params[:search]
#vendor_orders = VendorOrder.
joins(:shipments).
where(shipments: {
id: Shipment.find_by(
shipping_label: ShippingLabel.find_by(
tracking_number: params[:search]
)
)
})
else
#vendor_orders = VendorOrder.
joins(:order).
merge(Order.where(order_status: "paid")).
order(created_at: :desc).
paginate(page: params[:vendor_page], per_page: 25)
end
Or, you could put (some of) this in the VendorOrder model, something like:
class VendorOrder < ApplicationRecord
class << self
def find_by_tracking_number(tracking_number)
joins(:shipments).
where(shipments: {
id: Shipment.find_by(
shipping_label: ShippingLabel.find_by(tracking_number: tracking_number)
)
})
end
end
end
And then you might use it in a controller something like:
if params[:search]
#vendor_orders = VendorOrder.find_by_tracking_number(params[:search])
else
#vendor_orders = VendorOrder.
joins(:order).
merge(Order.where(order_status: "paid")).
order(created_at: :desc).
paginate(page: params[:vendor_page], per_page: 25)
end
That seems a little nicer, IMO, and hides some of the complexity from the controller.

Rails- Building dynamic query for filtering search results

I am trying to build a dynamic querying method to filter search results.
My models:
class Order < ActiveRecord::Base
scope :by_state, -> (state) { joins(:states).where("states.id = ?", state) }
scope :by_counsel, -> (counsel) { where("counsel_id = ?", counsel) }
scope :by_sales_rep, -> (sales) { where("sales_id = ?", sales) }
scope :by_year, -> (year) { where("title_number LIKE ?", "%NYN#{year}%") }
has_many :properties, :dependent => :destroy
has_many :documents, :dependent => :destroy
has_many :participants, :dependent => :destroy
has_many :states, through: :properties
belongs_to :action
belongs_to :role
belongs_to :type
belongs_to :sales, :class_name => 'Member'
belongs_to :counsel, :class_name => 'Member'
belongs_to :deal_name
end
class Property < ActiveRecord::Base
belongs_to :order
belongs_to :state
end
class State < ActiveRecord::Base
has_many :properties
has_many :orders, through: :properties
end
I have a page where I display ALL orders by default. I want to have check boxes to allow for filtering of the results. The filters are: Year, State, Sales, and Counsel. an example of a query is: All orders in 2016, 2015("order.title_number LIKE ?", "%NYN#{year}%") in states (has_many through) NJ, PA, CA, etc with sales_id unlimited ids and counsel_id unlimited counsel_ids.
In a nut shell I am trying to figure out how to create ONE query that takes into account ALL options the user checks. Here is my current query code:
def Order.query(opt = {})
results = []
orders = []
if !opt["state"].empty?
opt["state"].each do |value|
if orders.empty?
orders = Order.send("by_state", value)
else
orders << Order.send("by_state", value)
end
end
orders = orders.flatten
end
if !opt["year"].empty?
new_orders = []
opt["year"].each do |y|
new_orders = orders.by_year(y)
results << new_orders
end
end
if !opt["sales_id"].empty?
end
if !opt["counsel_id"].empty?
end
if !results.empty?
results.flatten
else
orders.flatten
end
end
Here is the solution I have come up with to allow for unlimited amount of filtering.
def self.query(opts = {})
orders = Order.all
opts.delete_if { |key, value| value.blank? }
const_query = ""
state_query = nil
counsel_query = nil
sales_query = nil
year_query = nil
queries = []
if opts["by_year"]
year_query = opts["by_year"].map do |val|
" title_number LIKE '%NYN#{val}%' "
end.join(" or ")
queries << year_query
end
if opts["by_sales_rep"]
sales_query = opts["by_sales_rep"].map do |val|
" sales_id = '#{val}' "
end.join(" or ")
queries << sales_query
end
if opts["by_counsel"]
counsel_query = opts["by_counsel"].map do |val|
" counsel_id = '#{val}' "
end.join(" or ")
queries << counsel_query
end
if opts["by_state"]
state_query = opts["by_state"].map do |val|
"states.id = '#{val}'"
end.join(" or ")
end
query_string = queries.join(" AND ")
if state_query
#orders = Order.joins(:states).where("#{state_query}")
#orders = #orders.where(query_string)
else
#orders = orders.where("#{query_string}")
end
#orders.order("title_number DESC")
end
What you're looking for a query/filter object, which is a common pattern. I wrote an answer similar to this, but I'll try to extract the important parts.
First you should move those logic to it's own object. When the search/filter object is initialized it should start with a relation query (Order.all or some base query) and then filter that as you go.
Here is a super basic example that isn't fleshed out but should get you on the right track. You would call it like so, orders = OrderQuery.call(params).
# /app/services/order_query.rb
class OrderQuery
def call(opts)
new(opts).results
end
private
attr_reader :opts, :orders
def new(opts={})
#opts = opts
#orders = Order.all # If using Rails 3 you'll need to use something like
# Order.where(1=1) to get a Relation instead of an Array.
end
def results
if !opt['state'].empty?
opt['state'].each do |state|
#orders = orders.by_state(state)
end
end
if !opt['year'].empty?
opt['year'].each do |year|
#orders = orders.by_year(year)
end
end
# ... all filtering logic
# you could also put this in private functions for each
# type of filter you support.
orders
end
end
EDIT: Using OR logic instead of AND logic
# /app/services/order_query.rb
class OrderQuery
def call(opts)
new(opts).results
end
private
attr_reader :opts, :orders
def new(opts={})
#opts = opts
#orders = Order.all # If using Rails 3 you'll need to use something like
# Order.where(1=1) to get a Relation instead of an Array.
end
def results
if !opt['state'].empty?
#orders = orders.where(state: opt['state'])
end
if !opt['year'].empty?
#orders = orders.where(year: opt['year'])
end
# ... all filtering logic
# you could also put this in private functions for each
# type of filter you support.
orders
end
end
The above syntax basically filters sayings if state is in this array of states and year is within this array of years.
In my case, the filter options came from the Controller's params, so I've done something like this:
The ActionController::Parameters structure:
{
all: <Can be true or false>,
has_planned_tasks: <Can be true or false>
... future filters params
}
The filter method:
def self.filter(filter_params)
filter_params.reduce(all) do |queries, filter_pair|
filter_key = filter_pair[0]
filter_value = filter_pair[1]
return {
all: proc { queries.where(deleted_at: nil) if filter_value == false },
has_planned_tasks: proc { queries.joins(:planned_tasks).distinct if filter_value == true },
}.fetch(filter_key).call || queries
end
end
Then I call the ModelName.filter(filter_params.to_h) in the Controller. I was able to add more conditional filters easily doing like this.
There's space for improving here, like extract the filters logic or the whole filter object, but I let you decide what is better in your context.
Here is one I built for an ecommerce order dashboard in Rails with the parameters coming from the controller.
This query will execute twice, once to count the orders and once to return the requested orders according to the parameters in the request.
This query supports:
Sort by column
Sort direction
Incremental Search - It'll search the beginning of a given field and returns those records that match enabling real-time suggestions while searching
Pagination (limited by 100 records per page)
I also have predefined values to sanitize some of the data.
This style is extremely clean and easy for others to read and modify.
Here's a sample query:
api/shipping/orders?pageNumber=1&orderStatus=unprocessedOrders&filters=standard,second_day&stores=82891&sort_column=Date&sort_direction=dsc&search_query=916
And here's the controller code:
user_id = session_user.id
order_status = params[:orderStatus]
status = {
"unprocessedOrders" => ["0", "1", "4", "5"],
"processedOrders" => ["2", "3", "6"],
"printedOrders" => ["3"],
"ratedOrders" => ["1"],
}
services = [
"standard",
"expedited",
"next_day",
"second_day"
]
countries = [
"domestic",
"international"
]
country_defs = {
domestic: ['US'],
international: ['CA', 'AE', 'EU', 'GB', 'MX', 'FR']
}
columns = {
Number: "order_number",
QTY: "order_qty",
Weight: "weight",
Status: "order_status",
Date: "order_date",
Carrier: "ship_with_carrier",
Service: "ship_with_carrier_code",
Shipping: "requestedShippingService",
Rate: "cheapest_rate",
Domestic: "country",
Batch: "print_batch_id",
Skus: "skus"
}
# sort_column=${sortColumn}&sort_direction=${sortDirection}&search_query=${searchQuery}
filters = params[:filters].split(',')
stores = params[:stores].split(',')
sort_column = params[:sort_column]
sort_direction = params[:sort_direction]
search_query = params[:search_query]
sort_by_column = columns[params[:sort_column].to_sym]
sort_direction = params[:sort_direction] == "asc" ? "asc" : "desc"
service_params = filters.select{ |p| services.include?(p) }
country_params = filters.select{ |p| countries.include?(p) }
order_status_params = filters.select{ |p| status[p] != nil }
query_countries = []
query_countries << country_defs[:"#{country_params[0]}"] if country_params[0]
query_countries << country_defs[:"#{country_params[1]}"] if country_params[1]
active_filters = [service_params, country_params].flatten
query = Order.where(user_id: user_id)
query = query.where(order_status: status[order_status]) if order_status_params.empty?
query = query.where("order_number ILIKE ? OR order_id::TEXT ILIKE ? OR order_info->'advancedOptions'->>'customField2' ILIKE ?", "%#{search_query}%", "%#{search_query}%", "%#{search_query}%") unless search_query.gsub(/\s+/, "").length == 0
query = query.where(requestedShippingService: service_params) unless service_params.empty?
query = query.where(country: "US") if country_params.include?("domestic") && !country_params.include?("international")
query = query.where.not(country: "US") if country_params.include?("international") && !country_params.include?("domestic")
query = query.where(order_status: status[order_status_params[0]]) unless order_status_params.empty?
query = query.where(store_id: stores) unless stores.empty?\
order_count = query.count
num_of_pages = (order_count.to_f / 100).ceil()
requested_page = params[:pageNumber].to_i
formatted_number = (requested_page.to_s + "00").to_i
query = query.offset(formatted_number - 100) unless requested_page == 1
query = query.limit(100)
query = query.order("#{sort_by_column}": :"#{sort_direction}") unless sort_by_column == "skus"
query = query.order("skus[1] #{sort_direction}") if sort_by_column == "skus"
query = query.order(order_number: :"#{sort_direction}")
orders = query.all
puts "After querying orders mem:" + mem.mb.to_s
requested_page = requested_page <= num_of_pages ? requested_page : 1
options = {}
options[:meta] = {
page_number: requested_page,
pages: num_of_pages,
type: order_status,
count: order_count,
active_filters: active_filters
}
render json: OrderSerializer.new(orders, options).serialized_json

Searchkick get all results when using pagination

I have the following search query for products using searchkick:
def index
#filter = params[:filter].blank? ? nil : Search.find(params[:filter])
if #filter.present?
#products = #filter.products(set_order_column(#sort), #direction, params[:page], #per_page)
else
query = #query.presence || "*"
#products = Product.search(
query,
where: {
active: true
},
page: params[:page],
per_page: #per_page,
order: [{
"#{set_order_column(#sort)}" => "#{#direction}"
}],
misspellings: { distance: 2 },
fields: fields)
end
#filter_product_ids = #products.map(&:id)
end
There is the variable filter_product_ids where I need to store all results not filtered results by #per_page. Is it possible to do that? Right now, there are results just for results #per_page.
The goal to get all the results without pagination is to get uniq values for all products used to various product categorization for filtering on the website.
Thank you for any hints, Miroslav
My solution is to put this in it's own class (service class), then
class ProductFilter
attr_reader :search_params, :search_query, :pagination_params
def initialize(search_params, search_query, pagination_params)
#search_params = search_params
#search_query = search_query
#pagination_params = pagination_params
end
# Will return all records from searchkick, unpaginated
def filtered_products
Product.search(search_query, search_param_stuff)
end
# Will return all filtered_products & then paginate on that object
def paginated_products
filtered_products.records.paginate(pagination_params)
end
private
def search_param_stuff
search_params
end
end
In any case, searchkick's method for pagination actually works on the searchkick records object, so you can just pull the ES query (without passing in pagination params) in one method, then paginate on the filtered_products.records.
Just remove
params[:page], #per_page
and
page: params[:page],
per_page: #per_page,
from your code.

Advance search with elasticsearch and rails

I want to use ElasticSearch to search with multiple parameters (name, sex, age at a time).
what I've done so far is included elastic search in my model and added a as_indexed_json method for indexing and included relationship.
require 'elasticsearch/model'
class User < ActiveRecord::Base
include Elasticsearch::Model
include Elasticsearch::Model::Callbacks
belongs_to :product
belongs_to :item
validates :product_id, :item_id, :weight, presence: true
validates :product_id, uniqueness: {scope: [:item_id] }
def as_indexed_json(options = {})
self.as_json({
only: [:id],
include: {
product: { only: [:name, :price] },
item: { only: :name },
}
})
end
def self.search(query)
# i'm sure this method is wrong I just don't know how to call them from their respective id's
__elasticsearch__.search(
query: {
filtered: {
filter: {
bool: {
must: [
{
match: {
"product.name" => query
}
}
],
must: [
{
match: {
"item.name" => query
}
}
]
}
}
}
}
)
end
end
User.import force: true
And In controller
def index
#category = Category.find(params[:category_id])
if params[:search].present? and params[:product_name].present?
#users = User.search(params[:product_name]).records
end
if params[:search].present? and params[:product_price].present?
#users = User.search(params[:product_price]).records
end
if params[:search].present? and params[:item].present?
if #users.present?
#users.search(item: params[:item], product: params[:product_name]).records
else
#users = User.search(params[:item]).records
end
end
end
There are basically 3 inputs for searching with product name , product price and item name, This is what i'm trying to do like if in search field only product name is present then
#users = User.search(params[:product_name]).records
this will give me records but If user inputs another filter say product price or item name in another search bar then it's not working. any ideas or where I'm doing wrong :/ stucked from last 3 days

Jbuilder not wrapping single object in collection

I have a index view in my rails application that allows filtering via search params. When a group op articles are returned its is wropped in an articles colllection like {"articles":[{"article":{"id":341,"updated":"2015-08-18T13:05:08.427Z","title":". But if only a single object is found the articles level is missing, {"article":{"id":398,"updated":"2015-08-07T11:37:26.200Z","title":. How can I fix it so that a single object behaves like multiple?
_articles.list.json.jbuilder
require 'uri'
require 'publish_on'
json.cache! ['v1', articles] do
json.articles articles do |article|
json.cache! ['v1', article] do
json.article do
json.id article.id
json.updated as_ns_date(article.updated_at)
json.title article.label
json.numberOfViews article.view_mappings.count
json.numberOfFavorites article.favorite_mappings.count
json.imageURLs article.images if article.images.any?
json.youtubeURL article.youtube unless article.youtube.blank?
json.tags article.categories.map(&:label)
json.isFeatured article.featured
json.isPublished article.is_published
json.published as_ns_date(article.publish_on)
end
end
end
end
index.json.jbuilder
json.partial! 'articles/articles_list', articles: #articles
articles_controller.rb
def index
#articles = SearchArticlesCommand.new(params).execute
render :index
end
search_articles_command.rb
class SearchArticlesCommand
def initialize(params = {})
#since = params[:since_date]
#keys = params[:search_query]
#category = params[:category]
end
def execute
Article.unscoped do
query = if #since.present?
Article.article.since_date(#since)
else
Article.published_article
end
query = query.search_by_keywords(#keys) if #keys.present?
query = query.search_by_category(#category) if #category.present?
query.select(:id, :updated_at, :label, :is_published, :featured, :slug, :created_at).order(created_at: :desc)
end
end
end
article.rb
class Article < Comfy::Cms::Page
include PgSearch
include ActionView::Helpers::SanitizeHelper
HOSTNAME = ENV['HOSTNAME'] || Socket.gethostname
has_many :view_mappings, dependent: :destroy
has_many :favorite_mappings, dependent: :destroy
pg_search_scope :search_by_keywords, against: [:content_cache, :label], using: { tsearch: { any_word: true, prefix: true } }
pg_search_scope :search_by_category, associated_against: {
categories: [:label]
}
scope :since_date, -> (date) { where('created_at > ? OR updated_at > ? ', date, date) if date.present? }
scope :folder, -> { where.not(layout_id: ENV['ARTICLE_LAYOUT_ID']) }
scope :published_article, -> { published.article }
scope :article, -> { where(layout_id: ENV['ARTICLE_LAYOUT_ID']) }
It is what i suspected. If you want the same behavior your query should return the same type of object when it finds one or many articles. The problem is that either you are returning an ActiveRecordRelation or a Article object depending on your params.
#articles = Article.all # => ActiveRecordRelation, an array per se
#articles = Article.find(1) # => Article object
When it comes to jbuilder to construct the JSON it checks if it is an array of objects and then wrap the json with a { keyword => array }. WHen it is a single object, it defaults to a single object {article: {}}.
The solution is simple, you can tweak your SearchArticlesCommand to always return an ActiveRecordRelation, even if it finds only one object.

Resources