Created
February 28, 2014 19:48
-
-
Save tomas-stefano/9278410 to your computer and use it in GitHub Desktop.
Solr Abstraction
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
module Morpheus | |
module Adapters | |
module Solr | |
class Relation | |
include ActiveModel::Model | |
attr_accessor :connection, :relation_class, :where | |
delegate :present?, :blank?, :empty?, to: :to_a | |
delegate :each, :map, :collect, :select, :find, :last, :first, to: :to_a | |
delegate :total, :facet_fields, to: :response | |
# @api public | |
# | |
# Define the rows size to pass to Solr. | |
# | |
# @param per_page_number the page number that will be calculated to pass in the start Solr option. | |
# | |
# @example | |
# | |
# Relation.new.per_page(50) | |
# Relation.new.limit(100) | |
# | |
def per_page(per_page_number) | |
@per_page = per_page_number | |
self | |
end | |
alias :limit :per_page | |
# @api public | |
# | |
# Define the page number to pass to Solr. | |
# | |
# @param page_number the string that will pass to solr. | |
# | |
# @example | |
# | |
# Relation.new.page('2') | |
# | |
def page(page_number) | |
@page = page_number | |
self | |
end | |
# @api public | |
# | |
# Define the order column to pass to Solr. | |
# | |
# @param order_by the string that will pass to solr. | |
# | |
# @example | |
# | |
# Relation.new.order('') | |
# | |
def order(order_by) | |
@order = order_by | |
self | |
end | |
# @api public | |
# | |
# Define the field query param to send to Solr. | |
# | |
# @param field_query the exact field query, that will be send in params. | |
# | |
# @example | |
# | |
# Relation.new.filter(name: 'Saraiva') | |
# | |
def filter(field_query) | |
@filter = field_query | |
self | |
end | |
# @api public | |
# | |
# Change the records parser, to only parse by the FacetResponseHandler. | |
# | |
def facets | |
@facets = true | |
self | |
end | |
# @api public | |
# | |
# Returns the start param to send to Solr. | |
# | |
# @example | |
# | |
# Relation.new.start | |
# | |
def start | |
if @per_page.present? and @page.present? | |
@per_page.to_i * (@page.to_i - 1) | |
end | |
end | |
# @api public | |
# | |
# Returns the total description and the actual records from this current page. | |
# | |
# <b>The per page default is 50 and the page default is 1.</b> | |
# | |
# @example | |
# | |
# Relation.new(connection: connection, relation_class: Solr::Category).page(2).total_description | |
# | |
# def total_description | |
# limit_number = @per_page || 25 | |
# page_number = @page || 1 | |
# initial = start.to_i + 1 | |
# final = limit_number.to_i * page_number.to_i | |
# final = total if final > total | |
# | |
# I18n.t('admin.navigation.total_description', initial: initial, final: final, total: total) | |
# end | |
# @api public | |
# | |
# Make the solr request and setup the document collection to relation object | |
# | |
# @return <Array> | |
# | |
# @example | |
# | |
# Relation.new(connection: connection, relation_class: Solr::Category).to_a | |
# | |
def to_a | |
return collection if collection.present? | |
response.records.each { |record| collection.push(record) } | |
collection | |
rescue RuntimeError, RSolr::Error::Http => exception | |
Rails.logger.error exception.message | |
collection | |
end | |
# @api public | |
# | |
# Returns the proxy object that wraps the response from Solr, passing the relation class. | |
# See Solr::ResponseHandler for more information. | |
# | |
def response | |
@response ||= if @facets.present? | |
Solr::FacetResponseHandler.new(relation_class, response: response_body) | |
else | |
Solr::ResponseHandler.new(relation_class, response: response_body) | |
end | |
end | |
# @api public | |
# | |
# Returns the Solr Raw response for wt ruby. | |
# | |
# <b>You need to pass a connection that responds to #get method.</b> | |
# | |
def response_body | |
Rails.logger.info("\e[36m #{relation_class} Load #{connection.base_uri.path} \e\[0m \e[01m#{params}\e[0m") | |
connection.get('select', params: params) | |
end | |
# @api public | |
# | |
# Returns all collection array from solr. | |
# | |
def collection | |
@collection ||= [] | |
end | |
def inspect | |
"#<Solr::Relation #{to_a}>" | |
end | |
# @api private | |
# | |
# Returns all params to send to Solr remove all blank values. | |
# | |
def params | |
{ | |
q: Solr::WhereNode.new(self.where), | |
rows: @per_page, | |
start: start, | |
sort: @order, | |
fq: Solr::WhereNode.new(@filter) | |
}.delete_if { |key, value| value.blank? } | |
end | |
# Create this alias just to be ActiveRecord like. | |
# | |
# @example | |
# | |
# Solr::Relation.new(where: 'name_pt:Saraiva').explain | |
# | |
alias :explain :params | |
end | |
end | |
class WhereNode < String | |
# @param node can be a String or a Hash | |
# | |
def initialize(node) | |
@node = node | |
super(build.to_s) | |
end | |
# Build 'q' param to send to Solr. | |
# | |
# @example | |
# | |
# # With String | |
# Solr::WhereNode.new('name_pt:"Saraiva"') | |
# # => 'name_pt:"Saraiva"' | |
# | |
# # With Hash | |
# Solr::WhereNode.new(name_pt: "Saraiva") | |
# # => 'name_pt:"Saraiva"' | |
# | |
# # With Hash with many fields | |
# Solr::WhereNode.new(name_pt: 'Saraiva', status: true) | |
# # => '(name_pt:"Saraiva") AND (status:true)' | |
# | |
# # With value as Array | |
# Solr::WhereNode.new(id: [1, 2, 3, 4]) | |
# # => 'id:(1 OR 2 OR 3 OR 4)' | |
# | |
def build | |
return @node if @node.is_a?(String) or @node.blank? | |
@nodes = @node.collect.each { |key, value| build_criteria(key, value) } | |
if @nodes.size > 1 | |
@nodes.map! { |node| "(#{node})" }.join(' AND ') | |
else | |
@nodes.join() | |
end | |
end | |
def build_criteria(key, value) | |
formatted_value = if value.is_a?(Array) | |
"(#{Array(value).join(' OR ')})" | |
else | |
build_value(value) | |
end | |
"#{key}:#{formatted_value}" | |
end | |
def build_value(value) | |
value == '*' ? value : %{"#{value}"} | |
end | |
end | |
class Base | |
include Virtus | |
extend ActiveModel::Naming | |
extend ActiveModel::Translation | |
def category_path(categories) | |
Array(categories.find { |category| category.id.to_i == category_id.to_i }.try(:parent_path)) | |
end | |
class << self | |
attr_accessor :resource_name | |
def connection | |
RSolr.connect( | |
url: "#{Settings.solr.url}/#{resource_name}", | |
read_timeout: Settings.solr.read_timeout, | |
open_timeout: Settings.solr.open_timeout, | |
retry_503: Settings.solr.retry_503, | |
retry_after_limit: Settings.solr.retry_after_limit | |
) | |
end | |
# @api public | |
# | |
# Set the resource name that will be search in Solr Request. | |
# | |
# @example | |
# | |
# class MyClass < Solr::Base | |
# # Will search by solr_url/my_class | |
# resource :my_class | |
# end | |
# | |
def resource(name) | |
self.resource_name = name.to_s | |
end | |
# @api public | |
# | |
# Make the where criteria passing what will have inside the 'q' Solr param. | |
# | |
# @returns <Solr::Relation> | |
# | |
# @example | |
# | |
# Base.where("name_pt:Saraiva").order("name_pt ASC").limit(100).page(1) | |
# | |
def where(criteria) | |
Solr::Relation.new(connection: connection, relation_class: self, where: criteria) | |
end | |
# @api public | |
# | |
def find(id) | |
where(id: id).first | |
end | |
# @api public | |
# Make the basic search passing an object that responds to: #valid_for_basic_search?, #basic_query, #per_page and #page. | |
# | |
# @returns [Solr::Relation] | |
# | |
def basic_search(offer_search) | |
# return [] unless offer_search.valid_for_basic_search? | |
# | |
# search(offer_search) | |
end | |
# @api public | |
# Make the basic search passing an object that responds to: #basic_query, #per_page and #page. | |
# | |
# @returns [Solr::Relation] | |
# | |
def search(search) | |
# order_query = "#{fields[offer_search.order_field_name.to_sym]} #{offer_search.order_type}" | |
# where(offer_search.basic_query).per_page(offer_search.per_page).page(offer_search.page).order(order_query).filter(offer_search.filter) | |
end | |
end | |
def ==(other) | |
id == other.id | |
end | |
end | |
class ResponseHandler | |
attr_reader :resource_class | |
def initialize(resource_class, options={}) | |
@resource_class = resource_class | |
@response = options.fetch(:response) | |
end | |
def docs | |
response['docs'].to_a | |
end | |
def total | |
response['numFound'].to_i | |
end | |
def records | |
docs.collect do |document| | |
resource_class.new.tap do |new_object| | |
attributes = new_object.attributes | |
attributes.each { |key, value| attributes[key] = document[key.to_s] } | |
new_object.attributes = attributes | |
end | |
end | |
end | |
def facet_fields | |
FacetFields.new(@response['facet_counts']['facet_fields']) | |
end | |
private | |
def response | |
@response['response'] || {} | |
end | |
end | |
class FacetFields < Hash | |
def initialize(fields={}) | |
@fields = fields | |
assign_facet_field | |
super | |
end | |
def assign_facet_field | |
@fields.each do |facet_field, facet_values| | |
self[facet_field] = facet_values.each_slice(2).collect do |name, count| | |
FacetField.new(name: name, count: count, field: facet_field) | |
end | |
end | |
end | |
end | |
class FacetField | |
include ActiveModel::Model | |
attr_accessor :name, :count, :field | |
def ==(other_facet_field) | |
name == other_facet_field.name && count == other_facet_field.count && field == other_facet_field.field | |
end | |
end | |
module Solr | |
class FacetResponseHandler | |
attr_reader :field_name | |
def initialize(collection_object, options={}) | |
@collection_object = collection_object | |
@response = options.fetch(:response) | |
@field_name = options[:field_name] || 'name_so' | |
end | |
def records | |
facet_fields[@field_name].each_slice(2).collect do |new_field_name, count| | |
@collection_object.new(@field_name => new_field_name) | |
end | |
end | |
def facet_fields | |
@response['facet_counts']['facet_fields'] | |
end | |
end | |
end | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment