Skip to content

Instantly share code, notes, and snippets.

@jasper-lyons
Created March 25, 2020 12:36
Show Gist options
  • Save jasper-lyons/d06e1d378d5ad66c27ef57a9531235cb to your computer and use it in GitHub Desktop.
Save jasper-lyons/d06e1d378d5ad66c27ef57a9531235cb to your computer and use it in GitHub Desktop.
A rough implementation of a ruby interface to generate solr queries using ruby code.
module Search
@@searchable_attributes = []
def searchable(&block)
block.call(@@searchable_attributes)
end
def searchable_attributes
@@searchable_attributes
end
def search(type: LuceneQuery, &block)
type.new(self).tap do |instance|
instance.instance_eval(&block)
end
end
module Parser
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
@@operators = {}
def operator(symbol, &block)
@@operators[symbol] = block
end
def operators
@@operators
end
end
def operators
self.class.operators
end
def initialize(model)
@model = model
end
def parse(&block)
instance_eval(&block)
end
def method_missing(name, *args, **hargs)
if @model.searchable_attributes.include?(name)
Attribute.new(self, name)
else
ExpressionNode.new(self, name, *args, **hargs)
end
end
class ExpressionNode
def initialize(parser, operator, *params, **hparams)
@parser = parser
@operator = operator
@params = params
@hparams = hparams
end
def call(*args, **hargs)
ExpressionNode.new(@parser, :call, self, *args, **hargs)
end
def ==(other)
ExpressionNode.new(@parser, :equals, self, other)
end
def >(other)
ExpressionNode.new(@parser, :less_than, self, other)
end
def <(other)
ExpressionNode.new(@parser, :more_than, self, other)
end
def method_missing(operator, *args, **hargs)
ExpressionNode.new(@parser, operator, self, *args, **hargs)
end
def to_s
# and params that are also expression nodes might need wrapping!
if @parser.operators[@operator]
@parser.operators[@operator].call(*@params, **@hparams).to_s
elsif !@params.empty? || !@hparams.empty?
ExpressionNode.new(@parser, :call, @operator, *@params, **@hparams).to_s
else
@operator.to_s
end
end
alias to_str to_s
def to_hash
nil
end
def to_ary
nil
end
end
class Attribute < ExpressionNode
def initialize(parser, name)
@parser = parser
@name = name
end
def to_s
@name.to_s
end
alias to_str to_s
end
end
class StandardParser
include Parser
operator(:all) { "*:*" }
operator(:wrap) { |term| "(#{ term })" }
operator(:equals) { |a, b| "#{ a }:#{ b }" }
operator(:and) { |a, b| "#{ a } AND #{ b }" }
operator(:not) { |term| "!#{ term }" }
operator(:or) { |a, b| "#{ a } OR #{ b }" }
operator(:has_field) { |field| "#{field}:[* TO *]" }
operator(:between) { |a, b| "#{ a }:[#{ b.first } TO #{ b.last }]" }
operator(:more_than) { |a, b| "#{ a }:[#{ b } TO *]" }
operator(:less_than) { |a, b| "#{ a }:[* TO #{ b }]" }
operator(:boost) { |term, factor| "#{ term }^#{ factor }" }
operator(:constant_score) { |clause, score| "#{ clause }^=#{ score }" }
operator(:require) { |term| "+#{ term }" }
operator(:prohibit) { |term| "-#{ term }" }
operator(:filter) { |term| "filter(#{ term })" }
end
class FunctionParser
include Parser
operator(:call) { |function, *args, **hargs| "#{ function }(#{ args.join(',') })" }
FieldList = Struct.new(:fields) do
def to_s
fields.join(',')
end
end
end
class SortParser < FunctionParser
operator(:asc) { |term| "#{ term } asc" }
operator(:desc) { |term| "#{ term } desc" }
def parse(&block)
FieldList.new(super)
end
end
class FilterParser < StandardParser
end
class FieldListParser < FunctionParser
operator(:transform) do |factory, **hargs|
"[#{ [factory, *hargs.to_a.map { |pair| pair.join('=') }].join(' ') }]"
end
operator(:rename) { |a, b| "#{ b }:#{ a }" }
operator(:as) { |a, b| "#{ b }:#{ a }" }
def parse(&block)
FieldList.new(super)
end
end
class ExplainOtherParser < StandardParser
end
class FacetQueryParser < StandardParser
end
module QueryInterface
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
def param(name, symbol, parser: nil, multiple: false, &body)
if multiple
define_method(:"has_#{name}?") do
@params.select(&:first).include?(symbol.to_sym)
end
end
return define_method(name, &body) if body
define_method(name) do |value=nil, &block|
return if multiple && send(:"has_#{name}?")
if value
@params.push([symbol, value])
value
else
expression = parser.new(@model).parse(&block)
@params.push([symbol, expression])
expression
end
end
end
end
def initialize(model)
@model = model
@params = []
end
def params
@params
end
end
class TopLevelQuery
include QueryInterface
param('parser', :defType)
param('sort', :sort, parser: Search::SortParser)
param('start', :start)
param('rows', :rows)
param('with', :fq, parser: Search::FilterParser, multiple: true)
param('fields', :fl, parser: Search::FieldListParser)
param('debug', :debug, multiple: true)
param('explain', :explainOther, parser: Search::ExplainOtherParser)
param('finish_within', :timeAllowed)
param('terminate_early', :segmentTerminateEarly)
param('omit_header', :omit_header)
param('response_format', :wt)
param('cache', :cache)
param('log_params', :logParamsList)
param('echo_params', :echoParams)
def query_string
params.map { |param| param.join('=') }.join('&')
end
end
class LuceneQuery < TopLevelQuery
param('query', :q, parser: Search::StandardParser)
param('query_op', :'q.op')
param('default_searchable_field', :df)
param('split_on_whitespace', :sow)
param('facet', :facet, multiple: true) do |&block|
@params.push([:facet, true]) unless has_facet?
@params.push([:'facet.query', Search::FacetQueryParser.new(@model).parse(&block)])
end
end
end
class Model
extend Search
searchable do |attributes|
attributes << :type
attributes << :status
attributes << :created_at
attributes << :completed_action_plan
attributes << :case_plan_approved
end
end
from = 'a'
to = 'b'
search = Model.search do
query { all }
with { type == Model }
with { status == 'open' }
with { created_at.between(from..to) }
facet {
completed_action_plan == true).
and(case_plan_approved == true)
}
sort { [score.asc, price.desc, div(popularity, price).desc] }
fields { [score, price, created_at.as(start_date), product(price, popularity), transform(explain, style: nl)] }
end
puts search.query_string
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment