├── ansr_blacklight
├── spec
│ ├── fixtures
│ │ └── config.yml
│ ├── spec_helper.rb
│ └── lib
│ │ ├── request_builders_spec.rb
│ │ ├── queryable_relation_spec.rb
│ │ ├── relation
│ │ ├── grouping_spec.rb
│ │ └── faceting_spec.rb
│ │ └── loaded_relation_spec.rb
├── Gemfile
├── ansr_blacklight-0.0.2.gem
├── ansr_blacklight-0.0.4.gem
├── lib
│ ├── ansr_blacklight
│ │ ├── solr.rb
│ │ ├── arel.rb
│ │ ├── arel
│ │ │ ├── visitors.rb
│ │ │ ├── visitors
│ │ │ │ ├── to_no_sql.rb
│ │ │ │ └── query_builder.rb
│ │ │ └── big_table.rb
│ │ ├── solr
│ │ │ ├── response
│ │ │ │ ├── more_like_this.rb
│ │ │ │ ├── group.rb
│ │ │ │ ├── pagination_methods.rb
│ │ │ │ ├── group_response.rb
│ │ │ │ └── spelling.rb
│ │ │ ├── request.rb
│ │ │ └── response.rb
│ │ ├── base.rb
│ │ ├── model
│ │ │ └── querying.rb
│ │ ├── relation
│ │ │ └── solr_projection_methods.rb
│ │ ├── connection_adapters
│ │ │ └── no_sql_adapter.rb
│ │ ├── relation.rb
│ │ └── request_builders.rb
│ └── ansr_blacklight.rb
├── README.md
└── ansr_blacklight.gemspec
├── Gemfile
├── ansr_dpla
├── app
│ └── models
│ │ ├── item.rb
│ │ └── collection.rb
├── fixtures
│ ├── dpla.yml
│ ├── empty.jsonld
│ ├── collection.json
│ ├── collection.jsonld
│ ├── collections.json
│ ├── collections.jsonld
│ ├── item.json
│ └── item.jsonld
├── Gemfile
├── lib
│ ├── ansr_dpla
│ │ ├── request.rb
│ │ ├── arel.rb
│ │ ├── arel
│ │ │ ├── visitors.rb
│ │ │ ├── visitors
│ │ │ │ ├── to_no_sql.rb
│ │ │ │ └── query_builder.rb
│ │ │ └── big_table.rb
│ │ ├── model.rb
│ │ ├── model
│ │ │ ├── pseudo_associate.rb
│ │ │ ├── base.rb
│ │ │ └── querying.rb
│ │ ├── relation.rb
│ │ ├── connection_adapters
│ │ │ └── no_sql_adapter.rb
│ │ └── api.rb
│ └── ansr_dpla.rb
├── spec
│ ├── adpla_test_api.rb
│ ├── spec_helper.rb
│ └── lib
│ │ ├── item_spec.rb
│ │ ├── relation
│ │ ├── select_spec.rb
│ │ ├── where_spec.rb
│ │ └── facet_spec.rb
│ │ ├── api_spec.rb
│ │ └── relation_spec.rb
├── test
│ ├── debug.rb
│ └── system.rb
├── ansr_dpla.gemspec
└── README.md
├── .gitignore
├── lib
├── ansr
│ ├── .DS_Store
│ ├── version.rb
│ ├── connection_adapters.rb
│ ├── arel
│ │ ├── visitors.rb
│ │ ├── visitors
│ │ │ ├── context.rb
│ │ │ ├── to_no_sql.rb
│ │ │ └── query_builder.rb
│ │ ├── configured_field.rb
│ │ ├── nodes.rb
│ │ └── big_table.rb
│ ├── arel.rb
│ ├── configurable.rb
│ ├── relation
│ │ ├── arel_methods.rb
│ │ ├── group.rb
│ │ ├── predicate_builder.rb
│ │ └── query_methods.rb
│ ├── model
│ │ └── connection_handler.rb
│ ├── sanitization.rb
│ ├── model.rb
│ ├── base.rb
│ ├── utils.rb
│ ├── connection_adapters
│ │ └── no_sql_adapter.rb
│ ├── facets.rb
│ ├── relation.rb
│ └── dummy_associations.rb
└── ansr.rb
├── ansr.gemspec
└── README.md
/ansr_blacklight/spec/fixtures/config.yml:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "http://rubygems.org"
2 |
3 | gemspec
--------------------------------------------------------------------------------
/ansr_dpla/app/models/item.rb:
--------------------------------------------------------------------------------
1 | class Item < Ansr::Dpla::Model::Base
2 | end
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | config/dpla.yml
2 | ansr_dpla/config/dpla.yml
3 | Gemfile.lock
4 | *.gem
--------------------------------------------------------------------------------
/ansr_dpla/app/models/collection.rb:
--------------------------------------------------------------------------------
1 | class Collection < Ansr::Dpla::Model::Base
2 | end
--------------------------------------------------------------------------------
/ansr_dpla/fixtures/dpla.yml:
--------------------------------------------------------------------------------
1 | :api_key: "fake_api_key"
2 | :url: "http://fake.dp.la/v0/"
--------------------------------------------------------------------------------
/ansr_dpla/fixtures/empty.jsonld:
--------------------------------------------------------------------------------
1 | {"count":0,"start":0,"limit":10,"docs":[],"facets":[]}
--------------------------------------------------------------------------------
/lib/ansr/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mistydemeo/ansr/master/lib/ansr/.DS_Store
--------------------------------------------------------------------------------
/lib/ansr/version.rb:
--------------------------------------------------------------------------------
1 | module Ansr
2 | VERSION = '0.0.5'
3 | def self.version
4 | VERSION
5 | end
6 | end
--------------------------------------------------------------------------------
/ansr_dpla/Gemfile:
--------------------------------------------------------------------------------
1 | source "http://rubygems.org"
2 |
3 | gemspec
4 | gem "ansr", :path => '..', :group => :development
--------------------------------------------------------------------------------
/ansr_blacklight/Gemfile:
--------------------------------------------------------------------------------
1 | source "http://rubygems.org"
2 |
3 | gemspec
4 | gem "ansr", :path => '..', :group => :development
--------------------------------------------------------------------------------
/ansr_blacklight/ansr_blacklight-0.0.2.gem:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mistydemeo/ansr/master/ansr_blacklight/ansr_blacklight-0.0.2.gem
--------------------------------------------------------------------------------
/ansr_blacklight/ansr_blacklight-0.0.4.gem:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mistydemeo/ansr/master/ansr_blacklight/ansr_blacklight-0.0.4.gem
--------------------------------------------------------------------------------
/ansr_dpla/lib/ansr_dpla/request.rb:
--------------------------------------------------------------------------------
1 | module Ansr::Dpla
2 | class Request < ::Ansr::OpenStructWithHashAccess
3 | attr_accessor :path
4 | end
5 | end
--------------------------------------------------------------------------------
/lib/ansr/connection_adapters.rb:
--------------------------------------------------------------------------------
1 | module Ansr
2 | module ConnectionAdapters
3 | require 'ansr/connection_adapters/no_sql_adapter'
4 | end
5 | end
--------------------------------------------------------------------------------
/ansr_blacklight/lib/ansr_blacklight/solr.rb:
--------------------------------------------------------------------------------
1 | module Ansr::Blacklight::Solr
2 | require 'ansr_blacklight/solr/request'
3 | require 'ansr_blacklight/solr/response'
4 | end
--------------------------------------------------------------------------------
/ansr_dpla/lib/ansr_dpla/arel.rb:
--------------------------------------------------------------------------------
1 | require 'active_record'
2 | module Ansr::Dpla
3 | module Arel
4 | require 'ansr_dpla/arel/big_table'
5 | require 'ansr_dpla/arel/visitors'
6 | end
7 | end
--------------------------------------------------------------------------------
/ansr_blacklight/lib/ansr_blacklight/arel.rb:
--------------------------------------------------------------------------------
1 | module Ansr::Blacklight
2 | module Arel
3 | require 'ansr_blacklight/arel/visitors'
4 | require 'ansr_blacklight/arel/big_table'
5 | end
6 | end
--------------------------------------------------------------------------------
/ansr_dpla/spec/adpla_test_api.rb:
--------------------------------------------------------------------------------
1 | module Ansr::Dpla
2 | class TestApi
3 | include Ansr::Configurable
4 | def items(opts={})
5 | end
6 | def collections(opts={})
7 | end
8 | end
9 | end
--------------------------------------------------------------------------------
/ansr_dpla/lib/ansr_dpla/arel/visitors.rb:
--------------------------------------------------------------------------------
1 | module Ansr::Dpla::Arel
2 | module Visitors
3 | require 'ansr_dpla/arel/visitors/query_builder'
4 | require 'ansr_dpla/arel/visitors/to_no_sql'
5 | end
6 | end
--------------------------------------------------------------------------------
/lib/ansr/arel/visitors.rb:
--------------------------------------------------------------------------------
1 | module Ansr::Arel
2 | module Visitors
3 | require 'ansr/arel/visitors/context'
4 | require 'ansr/arel/visitors/query_builder'
5 | require 'ansr/arel/visitors/to_no_sql'
6 | end
7 | end
--------------------------------------------------------------------------------
/ansr_dpla/lib/ansr_dpla/model.rb:
--------------------------------------------------------------------------------
1 | module Ansr::Dpla
2 | module Model
3 | autoload :PseudoAssociate, 'ansr_dpla/model/pseudo_associate'
4 | require 'ansr_dpla/model/querying'
5 | require 'ansr_dpla/model/base'
6 | end
7 | end
--------------------------------------------------------------------------------
/lib/ansr/arel.rb:
--------------------------------------------------------------------------------
1 | module Ansr
2 | module Arel
3 | autoload :ConfiguredField, 'ansr/arel/configured_field'
4 | require 'ansr/arel/big_table'
5 | require 'ansr/arel/nodes'
6 | require 'ansr/arel/visitors'
7 | end
8 | end
--------------------------------------------------------------------------------
/ansr_blacklight/lib/ansr_blacklight/arel/visitors.rb:
--------------------------------------------------------------------------------
1 | module Ansr::Blacklight::Arel
2 | module Visitors
3 | require 'ansr_blacklight/arel/visitors/query_builder'
4 | require 'ansr_blacklight/arel/visitors/to_no_sql'
5 | end
6 | end
--------------------------------------------------------------------------------
/ansr_dpla/lib/ansr_dpla/arel/visitors/to_no_sql.rb:
--------------------------------------------------------------------------------
1 | module Ansr::Dpla::Arel::Visitors
2 | class ToNoSql < Ansr::Arel::Visitors::ToNoSql
3 |
4 | def query_builder(opts = nil)
5 | Ansr::Dpla::Arel::Visitors::QueryBuilder.new(table, opts)
6 | end
7 |
8 | end
9 | end
--------------------------------------------------------------------------------
/lib/ansr/configurable.rb:
--------------------------------------------------------------------------------
1 | module Ansr
2 | module Configurable
3 | def config
4 | @config ||= {}
5 | if block_given?
6 | yield @config
7 | end
8 | @config
9 | end
10 |
11 | alias_method :configure, :config
12 |
13 | end
14 | end
--------------------------------------------------------------------------------
/ansr_dpla/lib/ansr_dpla.rb:
--------------------------------------------------------------------------------
1 | require 'ansr'
2 | module Ansr::Dpla
3 | require 'ansr_dpla/api'
4 | require 'ansr_dpla/request'
5 | require 'ansr_dpla/connection_adapters/no_sql_adapter'
6 | require 'ansr_dpla/arel'
7 | require 'ansr_dpla/relation'
8 | require 'ansr_dpla/model'
9 | end
--------------------------------------------------------------------------------
/ansr_dpla/fixtures/collection.json:
--------------------------------------------------------------------------------
1 | {"docs":[{"_id":"bpl--commonwealth:2j62s484w","title":"Leslie Jones Collection","ingestDate":"2014-02-15T17:00:26.122359","ingestionSequence":5,"ingestType":"collection","@id":"http://dp.la/api/collections/460c76299e1b0a46afea352b1ab8f556","id":"460c76299e1b0a46afea352b1ab8f556"}],"count":1}
--------------------------------------------------------------------------------
/ansr_dpla/test/debug.rb:
--------------------------------------------------------------------------------
1 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'app/models'))
2 | require 'rails'
3 | require 'adpla'
4 | require 'item'
5 | class Logger
6 | def info(msg)
7 | puts msg
8 | end
9 | alias :warn :info
10 | alias :error :info
11 | alias :debug :info
12 | end
13 |
14 | puts Item.table.inspect
--------------------------------------------------------------------------------
/lib/ansr/relation/arel_methods.rb:
--------------------------------------------------------------------------------
1 | module Ansr
2 | module ArelMethods
3 | # Returns the Arel object associated with the relation.
4 | # duplicated to respect access control
5 | def arel # :nodoc:
6 | @arel ||= build_arel
7 | end
8 |
9 | def arel_table
10 | model().table
11 | end
12 |
13 | end
14 | end
--------------------------------------------------------------------------------
/ansr_blacklight/lib/ansr_blacklight/arel/visitors/to_no_sql.rb:
--------------------------------------------------------------------------------
1 | module Ansr::Blacklight::Arel::Visitors
2 | class ToNoSql < Ansr::Arel::Visitors::ToNoSql
3 |
4 | def initialize(table, http_method=:get)
5 | super(table)
6 | end
7 |
8 | def query_builder()
9 | Ansr::Blacklight::Arel::Visitors::QueryBuilder.new(table)
10 | end
11 |
12 | end
13 |
14 | end
--------------------------------------------------------------------------------
/lib/ansr/arel/visitors/context.rb:
--------------------------------------------------------------------------------
1 | module Ansr::Arel::Visitors
2 | class Context
3 | attr_reader :attribute
4 | def initialize(attribute)
5 | @attribute = attribute
6 | end
7 | end
8 |
9 | # create some thin subclasses in this module
10 | %W(Facet Filter From Order ProjectionTraits).each do |name|
11 | const_set(name, Class.new(Context))
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/ansr_blacklight/lib/ansr_blacklight/solr/response/more_like_this.rb:
--------------------------------------------------------------------------------
1 | module Ansr::Blacklight::Solr::Response::MoreLikeThis
2 | def more_like document
3 | mlt = more_like_this[document.id]
4 | return [] unless mlt and mlt['docs']
5 |
6 | mlt['docs']
7 | end
8 |
9 | def more_like_this
10 | return {} unless self[:moreLikeThis]
11 |
12 | self[:moreLikeThis]
13 | end
14 | end
--------------------------------------------------------------------------------
/ansr_dpla/lib/ansr_dpla/model/pseudo_associate.rb:
--------------------------------------------------------------------------------
1 | # a class to pretend the unfindable "associations" are real models
2 | module Ansr::Dpla
3 | module Model
4 | class PseudoAssociate
5 | def initialize(doc = {})
6 | @doc = doc.with_indifferent_access
7 | end
8 |
9 | def method_missing(name, *args)
10 | @doc[name] or super
11 | end
12 | end
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/ansr_dpla/fixtures/collection.jsonld:
--------------------------------------------------------------------------------
1 | {
2 | "docs": [
3 | {
4 | "_id": "bpl--commonwealth:2j62s484w",
5 | "title": "Leslie Jones Collection",
6 | "ingestDate": "2014-02-15T17:00:26.122359",
7 | "ingestionSequence": 5,
8 | "ingestType": "collection",
9 | "@id": "http://dp.la/api/collections/460c76299e1b0a46afea352b1ab8f556",
10 | "id": "460c76299e1b0a46afea352b1ab8f556"
11 | }
12 | ],
13 | "count": 1
14 | }
--------------------------------------------------------------------------------
/lib/ansr/arel/configured_field.rb:
--------------------------------------------------------------------------------
1 | module Ansr::Arel
2 | class ConfiguredField < ::Arel::Attributes::Attribute
3 | attr_reader :config
4 | def initialize(relation, name, config={})
5 | super(relation, name)
6 | @config = {}.merge(config)
7 | end
8 | def query
9 | @config[:query]
10 | end
11 | def local
12 | @config[:local]
13 | end
14 | def method_missing(method, *args)
15 | @config[method] = args if args.first
16 | @config[method]
17 | end
18 | end
19 | end
--------------------------------------------------------------------------------
/ansr_blacklight/lib/ansr_blacklight/base.rb:
--------------------------------------------------------------------------------
1 | module Ansr::Blacklight
2 | class Base < Ansr::Base
3 | include Ansr::Blacklight::Model::Querying
4 |
5 | self.abstract_class = true
6 |
7 | def self.solr_search_params_logic
8 | @solr_search_params_logic || []
9 | end
10 |
11 | def self.solr_search_params_logic=(vals)
12 | @solr_search_params_logic=vals
13 | end
14 |
15 | def self.build_default_scope
16 | rel = super
17 | solr_search_params_logic.each {|method| rel = self.send(method, rel)}
18 | rel
19 | end
20 | end
21 | end
--------------------------------------------------------------------------------
/lib/ansr/model/connection_handler.rb:
--------------------------------------------------------------------------------
1 | module Ansr
2 | module Model
3 | class ConnectionHandler
4 | def initialize(connection_class)
5 | @connection_class = connection_class
6 | end
7 |
8 | def adapter
9 | @connection_class
10 | end
11 |
12 | # retrieve a datasource adapter
13 | def retrieve_connection(klass)
14 | @connection_class.new(klass)
15 | end
16 |
17 | def retrieve_connection_pool(klass)
18 | retrieve_connection(klass)
19 | end
20 |
21 | def connected?(klass)
22 | true
23 | end
24 | end
25 | end
26 | end
--------------------------------------------------------------------------------
/lib/ansr.rb:
--------------------------------------------------------------------------------
1 | require 'active_support'
2 | require 'active_record'
3 | module Ansr
4 | extend ActiveSupport::Autoload
5 | eager_autoload do
6 | autoload :Configurable
7 | autoload :ConnectionAdapters
8 | autoload :DummyAssociations
9 | autoload :Arel
10 | autoload :Facets
11 | autoload :Base
12 | autoload :Model
13 | autoload :Sanitization
14 | autoload :Relation
15 | autoload :OpenStructWithHashAccess, 'ansr/utils'
16 | autoload_under 'relation' do
17 | autoload :Group
18 | autoload :PredicateBuilder
19 | autoload :ArelMethods
20 | autoload :QueryMethods
21 | end
22 | end
23 | end
--------------------------------------------------------------------------------
/ansr_dpla/lib/ansr_dpla/model/base.rb:
--------------------------------------------------------------------------------
1 | require 'ansr'
2 | module Ansr::Dpla
3 | module Model
4 | class Base < Ansr::Base
5 | self.abstract_class = true
6 |
7 | include Querying
8 |
9 | def self.inherited(subclass)
10 | super(subclass)
11 | subclass.configure do |config|
12 | config[:table_class] = Ansr::Dpla::Arel::BigTable
13 | end
14 | end
15 |
16 | def assign_nested_parameter_attributes(pairs)
17 | pairs.each do |k, v|
18 | v = PseudoAssociate.new(v) if Hash === v
19 | _assign_attribute(k, v)
20 | end
21 | end
22 | end
23 | end
24 | end
--------------------------------------------------------------------------------
/lib/ansr/sanitization.rb:
--------------------------------------------------------------------------------
1 | module Ansr
2 | module Sanitization
3 | extend ActiveSupport::Concern
4 | module ClassMethods
5 | def expand_hash_conditions_for_sql_aggregates(conditions)
6 | conditions.reduce({}) {|memo, k, v| }
7 | conditions
8 | end
9 |
10 | def sanitize_sql_for_conditions(condition, table_name = table_name())
11 | condition
12 | end
13 |
14 | def sanitize_sql_hash_for_conditions(attrs, default_table_name = self.table_name)
15 | attrs
16 | end
17 |
18 | def sanitize_sql(condition, table_name = table_name())
19 | sanitize_sql_for_conditions(condition, table_name)
20 | end
21 | end
22 | end
23 | end
--------------------------------------------------------------------------------
/ansr_blacklight/lib/ansr_blacklight/solr/response/group.rb:
--------------------------------------------------------------------------------
1 | class Ansr::Blacklight::Solr::Response::Group < Ansr::Group
2 |
3 | include Ansr::Blacklight::Solr::Response::PaginationMethods
4 |
5 | attr_reader :response
6 |
7 | def initialize group_key, model, group, response
8 | super(group_key, model, group)
9 | @response = response
10 | end
11 |
12 | def doclist
13 | group[:doclist]
14 | end
15 |
16 | # short cut to response['numFound']
17 | def total
18 | doclist[:numFound].to_s.to_i
19 | end
20 |
21 | def start
22 | doclist[:start].to_s.to_i
23 | end
24 |
25 | def docs
26 | doclist[:docs].map {|doc| model.new(doc)} #TODO do we need to have the solrResponse in the item?
27 | end
28 |
29 | def field
30 | response.group_field
31 | end
32 | end
--------------------------------------------------------------------------------
/lib/ansr/relation/group.rb:
--------------------------------------------------------------------------------
1 | # encapsulate a set of a response documents grouped on a field
2 | module Ansr
3 | class Group
4 | attr_reader :field, :key, :group, :model
5 | def initialize(group_key, model, group)
6 | @field, @key = group_key.first
7 | @model = model
8 | @group = group
9 | end
10 |
11 | # size of the group
12 | def total
13 | raise "Group#total must be implemented by subclass"
14 | end
15 |
16 | # offset in the response
17 | def start
18 | raise "Group#start must be implemented by subclass"
19 | end
20 |
21 | # model instances belonging to this group
22 | def records
23 | raise "Group#records must be implemented by subclass"
24 | end
25 |
26 | # the field from which the key value was selected
27 | def field
28 | raise "Group#field must be implemented by subclass"
29 | end
30 | end
31 | end
--------------------------------------------------------------------------------
/ansr_blacklight/lib/ansr_blacklight/model/querying.rb:
--------------------------------------------------------------------------------
1 | module Ansr::Blacklight::Model
2 | module Querying
3 | extend ActiveSupport::Concern
4 |
5 | module ClassMethods
6 |
7 | def solr
8 | Ansr::Blacklight.solr
9 | end
10 |
11 | def build_default_scope
12 | rel = Ansr::Blacklight::Relation.new(model(), table())
13 | rel
14 | end
15 |
16 | def unique_key
17 | table().unique_key
18 | end
19 |
20 | def default_connection_handler
21 | @default_connection_handler ||= Ansr::Model::ConnectionHandler.new(Ansr::Blacklight::ConnectionAdapters::NoSqlAdapter)
22 | end
23 |
24 | def references
25 | []
26 | end
27 | def ansr_query(*args)
28 | ansr_query = super(*args)
29 | ansr_query.http_method = args[2] if args[2]
30 | ansr_query
31 | end
32 | end
33 | end
34 | end
--------------------------------------------------------------------------------
/ansr_blacklight/lib/ansr_blacklight/solr/response/pagination_methods.rb:
--------------------------------------------------------------------------------
1 | require 'kaminari'
2 | module Ansr::Blacklight::Solr::Response::PaginationMethods
3 |
4 | include Kaminari::PageScopeMethods
5 | include Kaminari::ConfigurationMethods::ClassMethods
6 |
7 | def limit_value #:nodoc:
8 | rows
9 | end
10 |
11 | def offset_value #:nodoc:
12 | start
13 | end
14 |
15 | def total_count #:nodoc:
16 | total
17 | end
18 |
19 | def model_name
20 | if !docs.empty? and docs.first.respond_to? :model_name
21 | docs.first.model_name
22 | end
23 | end
24 |
25 | ## Methods in kaminari master that we'd like to use today.
26 | # Next page number in the collection
27 | def next_page
28 | current_page + 1 unless last_page?
29 | end
30 |
31 | # Previous page number in the collection
32 | def prev_page
33 | current_page - 1 unless first_page?
34 | end
35 | end
--------------------------------------------------------------------------------
/ansr_dpla/lib/ansr_dpla/model/querying.rb:
--------------------------------------------------------------------------------
1 | require 'active_record'
2 | module Ansr::Dpla
3 | module Model
4 | module Querying
5 | extend ActiveSupport::Concern
6 |
7 | module ClassMethods
8 | def build_default_scope
9 | Ansr::Dpla::Relation.new(model(), table())
10 | end
11 |
12 | def api
13 | @api ||= begin
14 | a = (config[:api] || Ansr::Dpla::Api).new
15 | a.config{|x| x.merge!(self.config)}
16 | a
17 | end
18 | end
19 |
20 | def api=(api)
21 | @api = api
22 | end
23 |
24 | def connection_handler
25 | @connection_handler ||= Ansr::Model::ConnectionHandler.new(Ansr::Dpla::ConnectionAdapters::NoSqlAdapter)
26 | end
27 |
28 | def references
29 | ['provider', 'object']
30 | end
31 | end
32 | end
33 | end
34 | end
--------------------------------------------------------------------------------
/ansr_dpla/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', '..', 'lib'))
2 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
3 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'app/models'))
4 | $LOAD_PATH.unshift(File.dirname(__FILE__))
5 |
6 | require 'rspec/autorun'
7 | require 'loggable'
8 | require 'ansr'
9 | require 'ansr_dpla'
10 | require 'adpla_test_api'
11 | require 'blacklight'
12 | require 'item'
13 | require 'collection'
14 |
15 | RSpec.configure do |config|
16 |
17 | end
18 |
19 | def fixture_path(path)
20 | File.join(File.dirname(__FILE__), '..', 'fixtures', path)
21 | end
22 |
23 | def fixture path, &block
24 | if block_given?
25 | open(fixture_path(path)) &block
26 | else
27 | open(fixture_path(path))
28 | end
29 | end
30 |
31 | def read_fixture(path)
32 | _f = fixture(path)
33 | _f.read
34 | ensure
35 | _f and _f.close
36 | end
37 |
--------------------------------------------------------------------------------
/lib/ansr/arel/nodes.rb:
--------------------------------------------------------------------------------
1 | module Ansr::Arel::Nodes
2 | class ConfiguredUnary < ::Arel::Nodes::Node
3 | attr_reader :expr, :opts
4 |
5 | def initialize(expr, opts={})
6 | @expr = expr
7 | @opts = opts
8 | end
9 |
10 | def method_missing(method, *args)
11 | @opts[method] = args if args.first
12 | @opts[method]
13 | end
14 |
15 | end
16 |
17 | class Facet < ConfiguredUnary
18 |
19 | def order(*val)
20 | if val.first
21 | val = val.first.downcase.to_sym if String === val
22 | @opts[:order] = val if val == :asc or val == :desc
23 | end
24 | @opts[:order]
25 | end
26 |
27 | def prefix(*val)
28 | @opts[:prefix] = val.first.to_s if val.first
29 | @opts[:prefix]
30 | end
31 |
32 | def limit(*val)
33 | @opts[:limit] = val.first.to_s if val.first
34 | @opts[:limit]
35 | end
36 | end
37 | class Filter < ConfiguredUnary; end
38 | class Highlight < ConfiguredUnary; end
39 | class ProjectionTraits < ConfiguredUnary; end
40 | class Spellcheck < ConfiguredUnary; end
41 | end
--------------------------------------------------------------------------------
/ansr_blacklight/lib/ansr_blacklight/relation/solr_projection_methods.rb:
--------------------------------------------------------------------------------
1 | module Ansr::Blacklight
2 | module SolrProjectionMethods
3 | def defType_value
4 | @values[:defType]
5 | end
6 |
7 | def defType_value=(value)
8 | raise ImmutableRelation if @loaded
9 | @values[:defType] = value
10 | end
11 |
12 | def defType(value)
13 | spawn.defType!(value)
14 | end
15 |
16 | def defType!(value)
17 | self.defType_value= value
18 | self
19 | end
20 |
21 | def defType_unscoping
22 | end
23 |
24 | def wt_value
25 | @values[:wt]
26 | end
27 |
28 | def wt_value=(value)
29 | raise ImmutableRelation if @loaded
30 | @values[:wt] = value
31 | end
32 |
33 | def wt(value)
34 | spawn.wt!(value)
35 | end
36 |
37 | def wt!(value)
38 | self.wt_value= (value)
39 | self
40 | end
41 |
42 | def wt_unscoping
43 | end
44 |
45 | # omitHeader
46 |
47 | # timeAllowed
48 |
49 | # debug (true, :timing, :query, :results)
50 |
51 | # explainOther
52 |
53 | # debug.explain.structured
54 | end
55 | end
--------------------------------------------------------------------------------
/lib/ansr/arel/visitors/to_no_sql.rb:
--------------------------------------------------------------------------------
1 | module Ansr::Arel::Visitors
2 | class ToNoSql < Arel::Visitors::Visitor
3 | attr_reader :table
4 |
5 | def initialize(big_table)
6 | super()
7 | @table = big_table
8 | end
9 |
10 | def query_builder(opts = nil)
11 | Ansr::Arel::Visitors::QueryBuilder.new(table, opts)
12 | end
13 |
14 | # the object generated by this method will be passed to the NoSqlAdapter#execute
15 | def visit_Arel_Nodes_SelectStatement(object, attribute)
16 | builder = query_builder
17 |
18 | if object.with
19 | builder.visit(object.with, attribute)
20 | end
21 |
22 | object.cores.each { |x| builder.visit_Arel_Nodes_SelectCore(x, attribute) }
23 |
24 | unless object.orders.empty?
25 |
26 | object.orders.each do |x|
27 | oa = Ansr::Arel::Visitors::Order.new(attribute)
28 | builder.visit x, oa
29 | end
30 | end
31 |
32 | builder.visit(object.limit, attribute) if object.limit
33 | builder.visit(object.offset, attribute) if object.offset
34 | # not relevant
35 |
36 | builder.query_opts
37 | end
38 |
39 | end
40 |
41 | end
42 |
--------------------------------------------------------------------------------
/ansr.gemspec:
--------------------------------------------------------------------------------
1 | require File.join(File.dirname(__FILE__), 'lib/ansr/version')
2 | Gem::Specification.new do |spec|
3 | spec.name = 'ansr'
4 | spec.version = Ansr.version
5 | spec.platform = Gem::Platform::RUBY
6 | spec.authors = ["Benjamin Armintor"]
7 | spec.email = ["armintor@gmail.com"]
8 | spec.summary = 'ActiveRecord-style relations for no-sql data sources'
9 | spec.description = 'Wrapping the no-sql data sources in Rails-like models and relations'
10 | spec.homepage = 'http://github.com/barmintor/ansr'
11 | spec.license = "APACHE2"
12 | spec.required_ruby_version = '>= 1.9.3'
13 | spec.files = `git ls-files`.split("\n")
14 | spec.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
15 | spec.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
16 | spec.require_paths = ["lib"]
17 |
18 | spec.add_dependency 'loggable'
19 | spec.add_dependency 'arel', '~> 4', '>= 4.0.2'
20 | spec.add_dependency 'activerecord', '~> 4', '>= 4.0.3'
21 |
22 | spec.add_development_dependency("rake")
23 | spec.add_development_dependency("bundler", ">= 1.0.14")
24 | spec.add_development_dependency("rspec", '~>2')
25 | spec.add_development_dependency("yard")
26 | end
--------------------------------------------------------------------------------
/ansr_dpla/ansr_dpla.gemspec:
--------------------------------------------------------------------------------
1 | require File.join(File.dirname(__FILE__), '../lib/ansr/version')
2 | version = Ansr.version
3 | Gem::Specification.new do |spec|
4 | spec.name = 'ansr_dpla'
5 | spec.version = version
6 | spec.platform = Gem::Platform::RUBY
7 | spec.authors = ["Benjamin Armintor"]
8 | spec.email = ["armintor@gmail.com"]
9 | spec.summary = 'ActiveRecord-style models and relations for DPLA APIs'
10 | spec.description = 'Wrapping the DPLA APIs in Rails-like models and relations'
11 | spec.homepage = 'https://github.com/barmintor/ansr/tree/master/ansr_dpla'
12 | spec.files = `git ls-files`.split("\n")
13 | spec.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
14 | spec.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
15 | spec.require_paths = ["lib"]
16 |
17 | spec.add_dependency 'ansr', version
18 | spec.add_dependency 'json-ld'
19 | spec.add_dependency 'rest-client'
20 | spec.add_dependency 'loggable'
21 | spec.add_dependency 'blacklight', '>=5.1.0'
22 | spec.add_dependency 'sass-rails'
23 | spec.add_development_dependency("rake")
24 | spec.add_development_dependency("bundler", ">= 1.0.14")
25 | spec.add_development_dependency("rspec")
26 | spec.add_development_dependency("yard")
27 | end
--------------------------------------------------------------------------------
/ansr_blacklight/lib/ansr_blacklight/solr/request.rb:
--------------------------------------------------------------------------------
1 | module Ansr::Blacklight::Solr
2 | class Request < ::HashWithIndifferentAccess
3 | attr_accessor :path
4 |
5 | SINGULAR_KEYS = %W{ facet fl q qt rows start spellcheck spellcheck.q sort
6 | per_page wt hl group defType}
7 | ARRAY_KEYS = %W{facet.field facet.query facet.pivot fq hl.fl }
8 |
9 | def initialize(constructor = {})
10 | if constructor.is_a?(Hash)
11 | super()
12 | update(constructor)
13 | else
14 | super(constructor)
15 | end
16 | ARRAY_KEYS.each do |key|
17 | self[key] ||= []
18 | end
19 | end
20 |
21 | def append_filter_query(query)
22 | self['fq'] << query
23 | end
24 |
25 | def append_facet_fields(values)
26 | (self['facet.field'] += Array(values)).uniq!
27 | self['facet'] = true unless values.blank?
28 | end
29 |
30 | def append_facet_query(values)
31 | self['facet.query'] += Array(values)
32 | end
33 |
34 | def append_facet_pivot(query)
35 | self['facet.pivot'] << query
36 | end
37 |
38 | def append_highlight_field(query)
39 | self['hl.fl'] << query
40 | end
41 |
42 | def to_hash
43 | reject {|key, value| ARRAY_KEYS.include?(key) && value.blank?}
44 | end
45 | end
46 | end
--------------------------------------------------------------------------------
/ansr_blacklight/lib/ansr_blacklight/solr/response/group_response.rb:
--------------------------------------------------------------------------------
1 | class Ansr::Blacklight::Solr::Response::GroupResponse
2 |
3 | include Ansr::Blacklight::Solr::Response::PaginationMethods
4 |
5 | attr_reader :key, :model, :group, :response
6 |
7 | def initialize key, model, group, response
8 | @key = key
9 | @model = model
10 | @group = group
11 | @response = response
12 | end
13 |
14 | alias_method :group_field, :key
15 |
16 | def groups
17 | @groups ||= group["groups"].map do |g|
18 | Ansr::Blacklight::Solr::Response::Group.new({key => g[:groupValue]}, model, g, self)
19 | end
20 | end
21 |
22 | def group_limit
23 | params.fetch(:'group.limit', 1).to_s.to_i
24 | end
25 |
26 | def total
27 | # ngroups is only available in Solr 4.1+
28 | # fall back on the number of facet items for that field?
29 | (group["ngroups"] || (response.facet_by_field_name(key) || []).length).to_s.to_i
30 | end
31 |
32 | def start
33 | params[:start].to_s.to_i
34 | end
35 |
36 | def method_missing meth, *args, &block
37 |
38 | if response.respond_to? meth
39 | response.send(meth, *args, &block)
40 | else
41 | super
42 | end
43 |
44 | end
45 |
46 | def respond_to? meth
47 | response.respond_to?(meth) || super
48 | end
49 |
50 | end
--------------------------------------------------------------------------------
/ansr_blacklight/lib/ansr_blacklight/connection_adapters/no_sql_adapter.rb:
--------------------------------------------------------------------------------
1 | module Ansr::Blacklight::ConnectionAdapters
2 | class NoSqlAdapter < Ansr::ConnectionAdapters::NoSqlAdapter
3 |
4 | def self.connection_for(klass)
5 | Ansr::Blacklight.solr
6 | end
7 |
8 | def initialize(klass, logger = nil, pool = nil) #:nodoc:
9 | super(klass, klass.solr, logger, pool)
10 | # the RSolr class has one query method, with the name of the selector the first parm?
11 | @method = :send_and_receive
12 | @http_method = klass.method
13 | @visitor = Ansr::Blacklight::Arel::Visitors::ToNoSql.new(@table)
14 | end
15 |
16 | # RSolr
17 | def raw_connection
18 | @connection
19 | end
20 |
21 | def adapter_name
22 | 'Solr'
23 | end
24 |
25 | def to_sql(*args)
26 | to_nosql(*args)
27 | end
28 |
29 | def execute(query, name='ANSR-SOLR')
30 | query = query.dup
31 | # TODO: execution context to assign :post to params[:method]
32 | params = {params: query, method: @http_method}
33 | params[:data] = params.delete(:params) if @http_method == :post
34 | raw_response = @connection.send(@method, query.path || 'select', params)
35 | Ansr::Blacklight::Solr::Response.new(raw_response, raw_response['params'])
36 | end
37 |
38 | end
39 | end
--------------------------------------------------------------------------------
/ansr_blacklight/README.md:
--------------------------------------------------------------------------------
1 | Ansr::Blacklight
2 | =================
3 |
4 | A re-implementation of Blacklight's Solr model with find/search functionality moved behind ActiveRecord::Relation subclasses.
5 |
6 | QUESTIONS
7 |
8 | Is a closer conformation to the expectations from ActiveRecord valuable enough to forego use of Sunspot (https://github.com/sunspot/sunspot)?
9 |
10 | REQUEST REQUIREMENTS
11 |
12 | Considering the following block from the BL Solr request code:
13 | SINGULAR_KEYS = %W{ facet fl q qt rows start spellcheck spellcheck.q sort
14 | per_page wt hl group defType}
15 | ARRAY_KEYS = %W{facet.field facet.query facet.pivot fq hl.fl }
16 |
17 | facet : a boolean field indicating the requested presence of facet info in response
18 | fl : the selected fields
19 | q : the query (fielding?)
20 | qt : query type; indicates queryHandler in Solr
21 | rows : corresponds to limit
22 | start : corresponds to offset
23 | spellcheck : boolean?
24 | spellcheck.q : ?
25 | sort : ?
26 | facet.field : the fields for which facet info is requested
27 | facet.query : ?
28 | facet.pivot : ?
29 | fq : ?
30 | hl.fl : field to highlight
31 | How is facet query different from filter query (fq)?
32 |
33 | Relations must be configurable with default parameters; this is fairly easy to do with a template Relation to spawn the default scope from.
34 |
35 | RESPONSE REQUIREMENTS
36 |
37 | tbd
38 |
--------------------------------------------------------------------------------
/ansr_blacklight/lib/ansr_blacklight/arel/big_table.rb:
--------------------------------------------------------------------------------
1 | module Ansr::Blacklight::Arel
2 | class BigTable < Ansr::Arel::BigTable
3 | attr_accessor :name
4 |
5 | def initialize(klass, engine=nil, config=nil)
6 | super(klass, engine)
7 | @name = 'select'
8 | self.config(config)
9 | end
10 |
11 | delegate :index_fields, to: :config
12 | delegate :show_fields, to: :config
13 | delegate :sort_fields, to: :config
14 |
15 | def filterable
16 | config.facet_fields.keys
17 | end
18 |
19 | alias_method :facets, :filterable
20 |
21 | def filterable?(field)
22 | filterable.include? field
23 | end
24 |
25 | def constrainable
26 | index_fields.keys
27 | end
28 |
29 | def constrainable?(field)
30 | index_fields.include?(field)
31 | end
32 |
33 | def selectable
34 | show_fields.keys + index_fields.keys
35 | end
36 |
37 | def selectable?(field)
38 | show_fields.include? field
39 | end
40 |
41 | def fields
42 | (constrainable + selectable + filterable).uniq
43 | end
44 |
45 | def sortable
46 | sort_fields.keys
47 | end
48 |
49 | def sortable?(field)
50 | sort_fields.include? field
51 | end
52 |
53 | def primary_key
54 | @primary_key ||= ::Arel::Attribute.new(self, config.document_unique_id_param.to_s)
55 | end
56 | end
57 | end
--------------------------------------------------------------------------------
/lib/ansr/arel/visitors/query_builder.rb:
--------------------------------------------------------------------------------
1 | module Ansr::Arel::Visitors
2 | class QueryBuilder < Arel::Visitors::Visitor
3 | attr_reader :table
4 | def initialize(table)
5 | @table = table
6 | end
7 |
8 | def visit(object, attribute=nil)
9 | super(object, attribute)
10 | end
11 |
12 | def visit_Ansr_Arel_BigTable(object, attribute)
13 | visit object.name, attribute if Ansr::Arel::Visitors::From === attribute
14 | @table = object if Ansr::Arel::BigTable === object and Ansr::Arel::Visitors::From === attribute
15 | end
16 |
17 | def visit_Arel_Nodes_SelectCore(object, attribute)
18 | visit(object.froms, From.new(attribute)) if object.froms
19 | object.projections.each { |x| visit(x, attribute) if x}
20 | object.wheres.each { |x| visit(x, attribute) if x}
21 | object.groups.each {|x| visit(x, attribute) if x}
22 | self
23 | end
24 |
25 | def visit_Symbol o, a
26 | visit o.to_s, a
27 | end
28 |
29 | def visit_Array o, a
30 | o.map { |x| visit x, a }
31 | end
32 |
33 | def visit_Arel_Nodes_And(object, attribute)
34 | visit(object.children, attribute)
35 | end
36 |
37 | def field_key_from_node(node)
38 | table.model.field_name(node)
39 | end
40 |
41 | # determines whether multiple values should accumulate or overwrite in merges
42 | def multiple?(field_key)
43 | false
44 | end
45 |
46 | end
47 | end
--------------------------------------------------------------------------------
/ansr_blacklight/ansr_blacklight.gemspec:
--------------------------------------------------------------------------------
1 | require File.join(File.dirname(__FILE__), '../lib/ansr/version')
2 | version = Ansr.version
3 | Gem::Specification.new do |spec|
4 | spec.name = 'ansr_blacklight'
5 | spec.version = version
6 | spec.platform = Gem::Platform::RUBY
7 | spec.authors = ["Benjamin Armintor"]
8 | spec.email = ["armintor@gmail.com"]
9 | spec.summary = 'ActiveRecord-style models and relations for Blacklight'
10 | spec.description = 'Wrapping the Blacklight/RSolr in Rails-like models and relations'
11 | spec.homepage = 'https://github.com/barmintor/ansr/tree/master/ansr_blacklight'
12 | spec.files = `git ls-files`.split("\n")
13 | spec.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
14 | spec.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
15 | spec.require_paths = ["lib"]
16 |
17 | spec.add_dependency 'ansr', version
18 | spec.add_dependency 'json-ld'
19 | spec.add_dependency 'rest-client'
20 | spec.add_dependency 'loggable'
21 | spec.add_dependency "rails", ">= 3.2.6", "< 5"
22 | spec.add_dependency "rsolr", "~> 1.0.6" # Library for interacting with rSolr.
23 | spec.add_dependency "kaminari", "~> 0.13" # the pagination (page 1,2,3, etc..) of our search results
24 | spec.add_dependency 'sass-rails'
25 | spec.add_development_dependency("rake")
26 | spec.add_development_dependency("bundler", ">= 1.0.14")
27 | spec.add_development_dependency "rspec-rails"
28 | spec.add_development_dependency("yard")
29 | end
--------------------------------------------------------------------------------
/ansr_dpla/lib/ansr_dpla/relation.rb:
--------------------------------------------------------------------------------
1 | require 'yaml'
2 | module Ansr::Dpla
3 | class Relation < ::Ansr::Relation
4 |
5 | def initialize(klass, table, values = {})
6 | raise "Cannot search nil model" if klass.nil?
7 | super(klass, table, values)
8 | end
9 |
10 | def facet_values=(values)
11 | values.each {|value| raise "#{value.expr.name.to_sym} is not facetable" unless table.facets.include? value.expr.name.to_sym}
12 | super
13 | end
14 |
15 | def empty?
16 | count == 0
17 | end
18 |
19 | def many?
20 | count > 1
21 | end
22 |
23 | def offset!(value)
24 | page_size = self.limit_value || default_limit_value
25 | if (value.to_i % page_size.to_i) != 0
26 | raise "Bad offset #{value} for page size #{page_size}"
27 | end
28 | self.offset_value=value
29 | self
30 | end
31 |
32 | def count
33 | self.load
34 | @response['count']
35 | end
36 |
37 | def facets_from(response)
38 | f = {}
39 | (response['facets'] || {}).inject(f) do |h,(k,v)|
40 |
41 | if v['total'] != 0
42 | items = v['terms'].collect do |term|
43 | Ansr::Facets::FacetItem.new(:value => term['term'], :hits => term['count'])
44 | end
45 | options = {:sort => 'asc', :offset => 0}
46 | h[k] = Ansr::Facets::FacetField.new k, items, options
47 | end
48 | h
49 | end
50 | f
51 | end
52 |
53 | def docs_from(response)
54 | response['docs']
55 | end
56 |
57 | end
58 |
59 |
60 | end
--------------------------------------------------------------------------------
/ansr_dpla/fixtures/collections.json:
--------------------------------------------------------------------------------
1 | {"count":5,"start":0,"limit":10,"docs":[{"id":"460c76299e1b0a46afea352b1ab8f556","ingestDate":"2014-02-15T17:00:26.122359","_rev":"372-9d33a508d82b97621b456623321886b3","title":"Leslie Jones Collection","_id":"bpl--commonwealth:2j62s484w","ingestType":"collection","@id":"http://dp.la/api/collections/460c76299e1b0a46afea352b1ab8f556","ingestionSequence":5,"score":6.857883},{"id":"7c7eaa8eee08d3c9b32d55662c0de58b","ingestDate":"2014-02-05T03:09:43.108843","_rev":"137-6262bdd35a8cf3397aefed97d5f2731e","title":"Thomas Jones Davies Bible Records","_id":"scdl-usc--davies","ingestType":"collection","@id":"http://dp.la/api/collections/7c7eaa8eee08d3c9b32d55662c0de58b","ingestionSequence":7,"score":6.177192},{"id":"f807cdcefe2c93f7f71bf99f47899e1f","ingestDate":"2014-01-19T09:04:22.366278","_rev":"3-83d42699bd0836d639d948d921a73d66","title":"Kirby Jones Papers, 1963 - 1974","_id":"nara--1159","ingestType":"collection","@id":"http://dp.la/api/collections/f807cdcefe2c93f7f71bf99f47899e1f","ingestionSequence":5,"score":6.0006475},{"id":"c1ae618e5958fd9c9e869ebbfd47bc34","ingestDate":"2014-01-19T09:04:21.373908","_rev":"3-5f88173336cf37fb8e33ddbfb1c62705","title":"Joseph M. Jones Papers, 1928 - 1987","_id":"nara--604476","ingestType":"collection","@id":"http://dp.la/api/collections/c1ae618e5958fd9c9e869ebbfd47bc34","ingestionSequence":5,"score":5.391162},{"id":"e1e5fee5c47484e307ccdbb717a75e7f","ingestDate":"2014-01-30T12:12:19.274456","_rev":"4-6d0514009fcf0b2cc68a178fdf3209ac","title":"John Paul Jones papers, 1924-1999","_id":"smithsonian--john_paul_jones_papers_1924_1999","ingestType":"collection","@id":"http://dp.la/api/collections/e1e5fee5c47484e307ccdbb717a75e7f","ingestionSequence":8,"score":5.317501}],"facets":[]}
--------------------------------------------------------------------------------
/lib/ansr/arel/big_table.rb:
--------------------------------------------------------------------------------
1 | require 'arel'
2 | module Ansr
3 | module Arel
4 | class BigTable < ::Arel::Table
5 | attr_writer :primary_key
6 | attr_reader :fields, :facets, :sorts
7 |
8 |
9 | attr_reader :klass
10 | alias :model :klass
11 |
12 | def self.primary_key
13 | @primary_key ||= 'id'
14 | end
15 |
16 | def self.primary_key=(key)
17 | @primary_key = key
18 | end
19 |
20 | def initialize(klass, engine=nil)
21 | super(klass.name, engine.nil? ? klass.engine : engine)
22 | @klass = klass.model
23 | @fields = []
24 | @facets = []
25 | @sorts = []
26 | @field_configs = {}
27 | end
28 |
29 | def primary_key
30 | @primary_key ||= ::Arel::Attribute.new( self, self.class.primary_key )
31 | end
32 |
33 | def primary_key=(key)
34 | @primary_key = ::Arel::Attribute.new( self, key.to_s )
35 | end
36 |
37 | def [] name
38 | name = (name.respond_to? :name) ? name.name.to_sym : name.to_sym
39 | (@field_configs.include? name) ? Ansr::Arel::ConfiguredField.new(self, name, @field_configs[name]) : ::Arel::Attribute.new( self, name)
40 | end
41 |
42 | def configure_fields
43 | if block_given?
44 | yield @field_configs
45 | end
46 | @field_configs
47 | end
48 | def fields
49 | if block_given?
50 | yield @fields
51 | end
52 | @fields
53 | end
54 | def facets
55 | if block_given?
56 | yield @facets
57 | end
58 | @facets
59 | end
60 | def sorts
61 | if block_given?
62 | yield @sorts
63 | end
64 | @sorts
65 | end
66 | end
67 | end
68 | end
69 |
--------------------------------------------------------------------------------
/ansr_dpla/lib/ansr_dpla/connection_adapters/no_sql_adapter.rb:
--------------------------------------------------------------------------------
1 | module Ansr::Dpla
2 | module ConnectionAdapters
3 | class NoSqlAdapter < Ansr::ConnectionAdapters::NoSqlAdapter
4 |
5 | def self.connection_for(klass)
6 | klass.api
7 | end
8 |
9 | def initialize(klass, logger = nil, pool = nil) #:nodoc:
10 | super(klass, klass.api, logger, pool)
11 | @visitor = Ansr::Dpla::Arel::Visitors::ToNoSql.new(@table)
12 | end
13 |
14 | def to_sql(*args)
15 | to_nosql(*args)
16 | end
17 |
18 | def execute(query, name='ANSR-DPLA')
19 | method = query.path
20 | query = query.to_h if Ansr::Dpla::Request === query
21 | query = query.dup
22 | aliases = query.delete(:aliases)
23 | json = @connection.send(method, query)
24 | json = json.length > 0 ? JSON.load(json) : {'docs' => [], 'facets' => []}
25 | if json['docs'] and aliases
26 | json['docs'].each do |doc|
27 | aliases.each do |k,v|
28 | if doc[k]
29 | old = doc.delete(k)
30 | if old and doc[v]
31 | doc[v] = Array(doc[v]) if doc[v]
32 | Array(old).each {|ov| doc[v] << ov}
33 | else
34 | doc[v] = old
35 | end
36 | end
37 | end
38 | end
39 | end
40 | json
41 | end
42 |
43 | def table_exists?(table_name)
44 | ['Collection', 'Item'].include? table_name
45 | end
46 |
47 | def sanitize_limit(limit_value)
48 | if (0..500) === limit_value.to_s.to_i
49 | limit_value
50 | else
51 | Ansr::Relation::DEFAULT_PAGE_SIZE
52 | end
53 | end
54 |
55 | end
56 | end
57 |
58 | end
--------------------------------------------------------------------------------
/ansr_blacklight/lib/ansr_blacklight/relation.rb:
--------------------------------------------------------------------------------
1 | module Ansr::Blacklight
2 | class Relation < Ansr::Relation
3 | include Ansr::Blacklight::SolrProjectionMethods
4 | ::ActiveRecord::Relation::VALID_UNSCOPING_VALUES << :defType << :wt
5 | ::ActiveRecord::Relation::SINGLE_VALUE_METHODS << :defType << :wt
6 |
7 | delegate :blacklight_config, to: :model
8 |
9 | delegate :docs, to: :response
10 | delegate :params, to: :response
11 | delegate :facet_pivot, to: :response
12 | delegate :facet_queries, to: :response
13 | # overrides for query response handling
14 | def docs_from(response)
15 | grouped? ? [] : response.docs
16 | end
17 |
18 | def facets_from(response)
19 | response.facets
20 | end
21 |
22 | def total
23 | response.total
24 | end
25 |
26 | # overrides for weird Blacklight expectations
27 | def max_pages
28 | if Kaminari.config.respond_to? :max_pages
29 | nil
30 | else
31 | super
32 | end
33 | end
34 |
35 | def limit_value
36 | (super || default_limit_value)
37 | end
38 |
39 | def build_arel
40 | arel = super
41 | solr_props = {}
42 | solr_props[:defType] = defType_value if defType_value
43 | solr_props[:wt] = wt_value if wt_value
44 | unless solr_props.empty?
45 | prop_node = Ansr::Arel::Nodes::ProjectionTraits.new arel.grouping(arel.projections), solr_props
46 | arel.projections = [prop_node]
47 | end
48 | arel
49 | end
50 |
51 | def spelling
52 | loaded
53 | response.spelling
54 | end
55 |
56 | def grouped?
57 | loaded? ? response.grouped? : !group_values.blank?
58 | end
59 |
60 | def group_by(key=self.group_values.first)
61 | loaded
62 | response.group(key, model)
63 | end
64 | end
65 | end
--------------------------------------------------------------------------------
/ansr_blacklight/lib/ansr_blacklight.rb:
--------------------------------------------------------------------------------
1 | require 'ansr'
2 | require 'rsolr'
3 | module Ansr::Blacklight
4 | extend ActiveSupport::Autoload
5 | autoload :SolrProjectionMethods, 'ansr_blacklight/relation/solr_projection_methods'
6 | require 'ansr_blacklight/solr'
7 | require 'ansr_blacklight/request_builders'
8 | require 'ansr_blacklight/arel'
9 | require 'ansr_blacklight/connection_adapters/no_sql_adapter'
10 | require 'ansr_blacklight/relation'
11 | require 'ansr_blacklight/model/querying'
12 | require 'ansr_blacklight/base'
13 |
14 | def self.solr_file
15 | "#{::Rails.root.to_s}/config/solr.yml"
16 | end
17 |
18 | def self.solr
19 | @solr ||= RSolr.connect(Ansr::Blacklight.solr_config)
20 | end
21 |
22 | def self.solr_config
23 | @solr_config ||= begin
24 | raise "The #{::Rails.env} environment settings were not found in the solr.yml config" unless solr_yml[::Rails.env]
25 | solr_yml[::Rails.env].symbolize_keys
26 | end
27 | end
28 |
29 | def self.solr_yml
30 | require 'erb'
31 | require 'yaml'
32 |
33 | return @solr_yml if @solr_yml
34 | unless File.exists?(solr_file)
35 | raise "You are missing a solr configuration file: #{solr_file}. Have you run \"rails generate blacklight:install\"?"
36 | end
37 |
38 | begin
39 | @solr_erb = ERB.new(IO.read(solr_file)).result(binding)
40 | rescue Exception => e
41 | raise("solr.yml was found, but could not be parsed with ERB. \n#{$!.inspect}")
42 | end
43 |
44 | begin
45 | @solr_yml = YAML::load(@solr_erb)
46 | rescue StandardError => e
47 | raise("solr.yml was found, but could not be parsed.\n")
48 | end
49 |
50 | if @solr_yml.nil? || !@solr_yml.is_a?(Hash)
51 | raise("solr.yml was found, but was blank or malformed.\n")
52 | end
53 |
54 | return @solr_yml
55 | end
56 |
57 | end
--------------------------------------------------------------------------------
/lib/ansr/model.rb:
--------------------------------------------------------------------------------
1 | module Ansr
2 | module Model
3 | extend ActiveSupport::Concern
4 | module ClassMethods
5 | def spawn
6 | s = build_default_scope
7 | s.references!(references())
8 | end
9 |
10 | def inherited(subclass)
11 | super
12 | # a hack for sanitize sql overrides to work, and some others where @klass used in place of klass()
13 | subclass.instance_variable_set("@klass", subclass)
14 | # a hack for the intermediate abstract model classes to work with table_name
15 | subclass.instance_variable_set("@table_name", subclass.name)
16 | end
17 |
18 | def model
19 | m = begin
20 | instance_variable_get "@klass"
21 | end
22 | raise "#{name()}.model() -> nil" unless m
23 | m
24 | end
25 |
26 | def references
27 | []
28 | end
29 |
30 | def table
31 | type = (config[:table_class] || Ansr::Arel::BigTable)
32 | if @table
33 | # allow the table class to be reconfigured
34 | @table = nil unless @table.class == type
35 | end
36 | @table ||= type.new(self)
37 | end
38 |
39 | def engine
40 | model()
41 | end
42 |
43 | def model
44 | @klass
45 | end
46 |
47 | def build_default_scope
48 | Ansr::Relation.new(model(), table())
49 | end
50 |
51 | def column_types
52 | TypeProxy.new(table())
53 | end
54 |
55 | class TypeProxy
56 | def initialize(table)
57 | @table = table
58 | end
59 |
60 | def [](name)
61 | # this should delegate to the NoSqlAdapter
62 | ::ActiveRecord::ConnectionAdapters::Column.new(name.to_s, nil, String)
63 | end
64 | end
65 | end
66 |
67 | require 'ansr/model/connection_handler'
68 | end
69 | end
--------------------------------------------------------------------------------
/ansr_dpla/spec/lib/item_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Item do
4 | describe '.configure' do
5 | it "should store the configured Api class" do
6 | Item.configure {|x| x.merge!(:api=>Ansr::Dpla::TestApi, :api_key => :dummy)}
7 | expect(Item.api).to be_a Ansr::Dpla::TestApi
8 | end
9 | end
10 |
11 | describe '.find' do
12 | it "should find an item given an id" do
13 | mock_api = double('api')
14 | Item.api = mock_api
15 | mock_api.should_receive(:items).with(:id=>"123", :page_size=>1).and_return(read_fixture('item.jsonld'))
16 | Item.find('123')
17 | end
18 | it "should raise an exception for a bad id" do
19 | mock_api = double('api')
20 | Item.api = mock_api
21 | mock_api.should_receive(:items).with(:id=>"123", :page_size=>1).and_return(read_fixture('empty.jsonld'))
22 | expect {Item.find('123')}.to raise_error
23 | end
24 | end
25 |
26 | describe '.where' do
27 | before do
28 | Item.configure{|x| x.merge!({:api=>Ansr::Dpla::TestApi, :api_key => :dummy})}
29 | end
30 | it 'should return a Relation when there is query information' do
31 | expect(Item.where({:q=>'kittens'})).to be_a Ansr::Relation
32 | end
33 | it 'should return itself when there is no query information' do
34 | expect(Item.where({})).to be Item
35 | end
36 | end
37 |
38 | describe 'accessor methods' do
39 | before do
40 | mock_api = double('api')
41 | Item.api = mock_api
42 | @hash = JSON.parse(read_fixture('item.jsonld'))['docs'][0]
43 | @test = Item.new(@hash)
44 | end
45 |
46 | it 'should dispatch method names to the hash' do
47 | @test.dataProvider.should == "Boston Public Library"
48 | @test.sourceResource.identifier.should == [ "Local accession: 08_06_000884" ]
49 | end
50 |
51 | it 'should miss methods for undefined fields' do
52 | expect {@test.foo}.to raise_error
53 | end
54 | end
55 |
56 |
57 | end
--------------------------------------------------------------------------------
/lib/ansr/base.rb:
--------------------------------------------------------------------------------
1 | require 'active_record'
2 | module Ansr
3 | class Base < ActiveRecord::Base
4 | include Ansr::Model
5 | extend Ansr::Configurable
6 | extend Ansr::QueryMethods
7 | extend Ansr::ArelMethods
8 | include Ansr::Sanitization
9 | #TODO remove the dummy associations
10 | include Ansr::DummyAssociations
11 |
12 | self.abstract_class = true
13 |
14 | def self.method
15 | @method ||= :get
16 | end
17 |
18 | def self.method=(method)
19 | @method = method
20 | end
21 |
22 | def initialize doc={}, options={}
23 | super(filter_source_hash(doc), options)
24 | @source_doc = doc
25 | end
26 |
27 | def core_initialize(attributes = nil, options = {})
28 | defaults = self.class.column_defaults.dup
29 | defaults.each { |k, v| defaults[k] = v.dup if v.duplicable? }
30 |
31 | @attributes = self.class.initialize_attributes(defaults)
32 | @column_types_override = nil
33 | @column_types = self.class.column_types
34 |
35 | init_internals
36 | init_changed_attributes
37 | ensure_proper_type
38 | populate_with_current_scope_attributes
39 |
40 | # +options+ argument is only needed to make protected_attributes gem easier to hook.
41 | # Remove it when we drop support to this gem.
42 | init_attributes(attributes, options) if attributes
43 |
44 | yield self if block_given?
45 | run_callbacks :initialize unless _initialize_callbacks.empty?
46 | end
47 |
48 | def filter_source_hash(doc)
49 | fields = self.class.model().table().fields()
50 | filtered = doc.select do |k,v|
51 | fields.include? k.to_sym
52 | end
53 | filtered.with_indifferent_access
54 | end
55 |
56 | def columns(name=self.name)
57 | super(name)
58 | end
59 |
60 | def [](key)
61 | @source_doc[key]
62 | end
63 |
64 | def has_key? key
65 | @source_doc.has_key? key
66 | end
67 | end
68 | end
--------------------------------------------------------------------------------
/ansr_blacklight/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', '..', 'lib'))
2 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
3 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'app/models'))
4 | $LOAD_PATH.unshift(File.dirname(__FILE__))
5 |
6 | ENV["RAILS_ENV"] ||= 'test'
7 | require 'ansr'
8 | require 'rails/all'
9 | require 'rspec/rails'
10 | require 'loggable'
11 | require 'ansr_blacklight'
12 | #require 'blacklight'
13 |
14 | RSpec.configure do |config|
15 | # == Mock Framework
16 | #
17 | # If you prefer to use mocha, flexmock or RR, uncomment the appropriate line:
18 | #
19 | # config.mock_with :mocha
20 | # config.mock_with :flexmock
21 | # config.mock_with :rr
22 | config.mock_with :rspec
23 |
24 | # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures
25 | # config.fixture_path = "#{::Rails.root}/spec/fixtures"
26 |
27 | # If you're not using ActiveRecord, or you'd prefer not to run each of your
28 | # examples within a transaction, remove the following line or assign false
29 | # instead of true.
30 | #config.use_transactional_fixtures = true
31 | end
32 |
33 | def fixture_path(path)
34 | File.join(File.dirname(__FILE__), '..', 'fixtures', path)
35 | end
36 |
37 | def fixture path, &block
38 | if block_given?
39 | open(fixture_path(path)) &block
40 | else
41 | open(fixture_path(path))
42 | end
43 | end
44 |
45 | def read_fixture(path)
46 | _f = fixture(path)
47 | _f.read
48 | ensure
49 | _f and _f.close
50 | end
51 |
52 | def stub_solr(response='')
53 | solr = double('Solr')
54 | solr.stub(:send_and_receive).and_return(eval(response))
55 | solr
56 | end
57 |
58 | def create_response(response, params = {})
59 | Ansr::Blacklight::Solr::Response.new(response, params)
60 | end
61 |
62 | class TestModel < Ansr::Blacklight::Base
63 | configure do |config|
64 | config[:unique_key] = 'id'
65 | end
66 | def self.solr=(solr)
67 | @solr = solr
68 | end
69 | def self.solr
70 | @solr
71 | end
72 | end
73 |
--------------------------------------------------------------------------------
/ansr_dpla/test/system.rb:
--------------------------------------------------------------------------------
1 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'app/models'))
2 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', '..', 'lib'))
3 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
4 | require 'ansr_dpla'
5 | require 'item'
6 |
7 | # you config the model with a hash including an API key for dp.la/v2, or the path to a YAML file
8 | open('config/dpla.yml') do |blob|
9 | Item.config {|x| x.merge! YAML.load(blob)}
10 | end
11 |
12 | # then you can find single items with known IDs
13 | puts Item.find("7eb617e559007e1ad6d95bd30a30b16b")
14 |
15 |
16 | # or you can search with ActiveRecord::Relations
17 | opts = {q: 'kittens', facets: 'sourceResource.contributor'}
18 | rel = Item.where(opts)
19 | # this means the results are lazy-loaded, #load or #to_a will load them
20 | rel.to_a
21 | # you can also assemble the queries piecemeal with the Relation's decorator pattern
22 | # the where decorator adds query fields
23 | # the select decorator adds response fields
24 | # the limit decorator adds a page size limit
25 | # the offset decorator adds a non-zero starting point in the response set
26 | # the filter decorator adds filter/facet fields and optionally values to query them on
27 | rel = Item.where(q: 'kittens').limit(2).facet('sourceResource.contributor').select('sourceResource.title')
28 | rel.to_a.each do |item|
29 | puts "#{item["id"]} \"#{item['sourceResource.title']}\""
30 | end
31 | # the filter values for the query are available on the relation after it is loaded
32 | rel.facets.each do |k,f|
33 | puts "#{k} values"
34 | f.items.each do |item|
35 | puts "facet: \"#{item.value}\" : #{item.hits}"
36 | end
37 | end
38 | # the loaded Relation has attributes describing the response set
39 | rel.count # the size of the response
40 | # the where decorator can be negated
41 | rel = rel.where.not(q: 'cats')
42 |
43 | rel.to_a.each do |item|
44 | puts "#{item["id"]} \"#{item['sourceResource.title']}\" \"#{item['originalRecord']}\""
45 | end
46 |
47 | rel.facets.each do |k,f|
48 | puts "#{k} values"
49 | f.items.each do |item|
50 | puts " \"#{item.value}\" : #{item.hits}"
51 | end
52 | end
--------------------------------------------------------------------------------
/lib/ansr/utils.rb:
--------------------------------------------------------------------------------
1 | require 'ostruct'
2 | module Ansr
3 | class OpenStructWithHashAccess < OpenStruct
4 | delegate :keys, :each, :map, :has_key?, :empty?, :delete, :length, :reject!, :select!, :include, :fetch, :to_json, :as_json, :to => :to_h
5 |
6 | def []=(key, value)
7 | send "#{key}=", value
8 | end
9 |
10 | def [](key)
11 | send key
12 | end
13 |
14 | def to_h
15 | @table
16 | end
17 |
18 | def merge other_hash
19 | self.class.new to_h.merge((other_hash if other_hash.is_a? Hash) || other_hash.to_h)
20 | end
21 |
22 | def merge! other_hash
23 | @table.merge!((other_hash if other_hash.is_a? Hash) || other_hash.to_h)
24 | end
25 | end
26 |
27 | class NestedOpenStructWithHashAccess < OpenStructWithHashAccess
28 | attr_reader :nested_class
29 | delegate :default_proc=, :to => :to_h
30 |
31 | def initialize klass, *args
32 | @nested_class = klass
33 | hash = {}
34 |
35 | hashes_and_keys = args.flatten
36 | lazy_configs = hashes_and_keys.extract_options!
37 |
38 | args.each do |v|
39 | if v.is_a? Hash
40 | key = v.first
41 | value = v[key]
42 |
43 | hash[key] = nested_class.new value
44 | else
45 | hash[v] = nested_class.new
46 | end
47 | end
48 |
49 | lazy_configs.each do |k,v|
50 | hash[k] = nested_class.new v
51 | end
52 |
53 | super hash
54 | set_default_proc!
55 | end
56 |
57 | def << key
58 | @table[key]
59 | end
60 |
61 | def []=(key, value)
62 | if value.is_a? Hash
63 | send "#{key}=", nested_class.new(value)
64 | else
65 | send "#{key}=", value
66 | end
67 | end
68 |
69 | def marshal_dump
70 | h = to_h.dup
71 | h.default = nil
72 |
73 | [nested_class, h]
74 | end
75 |
76 | def marshal_load x
77 | @nested_class = x.first
78 | super x.last
79 | set_default_proc!
80 | end
81 |
82 | private
83 | def set_default_proc!
84 | self.default_proc = lambda do |hash, key|
85 | hash[key] = self.nested_class.new
86 | end
87 | end
88 | end
89 | end
90 |
--------------------------------------------------------------------------------
/ansr_dpla/spec/lib/relation/select_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Ansr::Relation do
4 | before do
5 | @kittens = read_fixture('kittens.jsonld')
6 | @faceted = read_fixture('kittens_faceted.jsonld')
7 | @empty = read_fixture('empty.jsonld')
8 | @mock_api = double('api')
9 | Item.config{ |x| x[:api_key] = :foo}
10 | Item.engine.api= @mock_api
11 | end
12 |
13 | subject { Ansr::Dpla::Relation.new(Item, Item.table) }
14 |
15 | describe '#select' do
16 | describe 'with a block given' do
17 | it "should build an array" do
18 | test = subject.where(q:'kittens')
19 | @mock_api.should_receive(:items).with(:q => 'kittens').and_return(@kittens)
20 | actual = test.select {|d| true}
21 | expect(actual).to be_a(Array)
22 | expect(actual.length).to eql(test.limit_value)
23 | actual = test.select {|d| false}
24 | expect(actual).to be_a(Array)
25 | expect(actual.length).to eql(0)
26 | end
27 | end
28 | describe 'with a String or Symbol key given' do
29 | it 'should change the requested document fields' do
30 | test = subject.where(q:'kittens')
31 | @mock_api.should_receive(:items).with(:q => 'kittens', :fields=>:name).and_return('')
32 | test = test.select('name')
33 | test.load
34 | end
35 | end
36 | describe 'with a list of keys' do
37 | it "should add all the requested document fields" do
38 | test = subject.where(q:'kittens')
39 | @mock_api.should_receive(:items).with(:q => 'kittens', :fields=>[:name,:foo]).and_return('')
40 | test = test.select(['name','foo'])
41 | test.load
42 | end
43 | it "should add all the requested document fields and proxy them" do
44 | test = subject.where(q:'kittens')
45 | @mock_api.should_receive(:items).with(:q => 'kittens', :fields=>:object).and_return(@kittens)
46 | test = test.select('object AS my_object')
47 | test.load
48 | expect(test.to_a.first['object']).to be_nil
49 | expect(test.to_a.first['my_object']).to eql('http://ark.digitalcommonwealth.org/ark:/50959/6682xj30d/thumbnail')
50 | end
51 | end
52 | end
53 |
54 | end
--------------------------------------------------------------------------------
/ansr_dpla/lib/ansr_dpla/api.rb:
--------------------------------------------------------------------------------
1 | require 'rest_client'
2 | module Ansr::Dpla
3 | class Api
4 | include Ansr::Configurable
5 |
6 | def config &block
7 | super &block
8 | raise "DPLA clients must be configured with an API key" unless @config[:api_key]
9 | @config
10 | end
11 |
12 |
13 | API_PARAM_KEYS = [:api_key, :callback, :facets, :fields, :page, :page_size, :sort_by, :sort_by_pin, :sort_order]
14 |
15 | def initialize(config=nil)
16 | self.config{|x| x.merge!(config)} if config
17 | end
18 |
19 | def api_key
20 | config[:api_key]
21 | end
22 |
23 | def url
24 | config[:url] || 'http://api.dp.la/v2/'
25 | end
26 |
27 | def path_for base, options = nil
28 | return "#{base}?api_key=#{self.api_key}" unless options.is_a? Hash
29 | options = {:api_key=>api_key}.merge(options)
30 | API_PARAM_KEYS.each do |query_key|
31 | options[query_key] = options[query_key].join(',') if options[query_key].is_a? Array
32 | end
33 | (options.keys - API_PARAM_KEYS).each do |query_key|
34 | options[query_key] = options[query_key].join(' AND ') if options[query_key].is_a? Array
35 | options[query_key].sub!(/^OR /,'')
36 | options[query_key].gsub!(/\s+AND\sOR\s+/, ' OR ')
37 | end
38 | "#{base}" + (("?#{options.map { |key, value| "#{CGI::escape(key.to_s)}=#{CGI::escape(value.to_s)}"}.join("&") }" if options and not options.empty?) || '')
39 | end
40 |
41 | def client
42 | @client ||= RestClient::Resource.new(self.url)
43 | end
44 |
45 | def items_path(options={})
46 | path_for('items', options)
47 | end
48 |
49 | def items(options = {})
50 | client[items_path(options)].get
51 | end
52 |
53 | def item_path(id)
54 | path_for("items/#{id}")
55 | end
56 |
57 | def item(id)
58 | client[item_path(id)].get
59 | end
60 |
61 | def collections_path(options={})
62 | path_for('collections', options)
63 | end
64 |
65 | def collections(options = {})
66 | client[collections_path(options)].get
67 | end
68 |
69 | def collection_path(id)
70 | path_for("collections/#{id}")
71 | end
72 |
73 | def collection(id)
74 | client[collection_path(id)].get
75 | end
76 |
77 | end
78 | end
--------------------------------------------------------------------------------
/ansr_dpla/fixtures/collections.jsonld:
--------------------------------------------------------------------------------
1 | {
2 | "count": 5,
3 | "start": 0,
4 | "limit": 10,
5 | "docs": [
6 | {
7 | "id": "460c76299e1b0a46afea352b1ab8f556",
8 | "ingestDate": "2014-02-15T17:00:26.122359",
9 | "_rev": "372-9d33a508d82b97621b456623321886b3",
10 | "title": "Leslie Jones Collection",
11 | "_id": "bpl--commonwealth:2j62s484w",
12 | "ingestType": "collection",
13 | "@id": "http://dp.la/api/collections/460c76299e1b0a46afea352b1ab8f556",
14 | "ingestionSequence": 5,
15 | "score": 6.857883
16 | },
17 | {
18 | "id": "7c7eaa8eee08d3c9b32d55662c0de58b",
19 | "ingestDate": "2014-02-05T03:09:43.108843",
20 | "_rev": "137-6262bdd35a8cf3397aefed97d5f2731e",
21 | "title": "Thomas Jones Davies Bible Records",
22 | "_id": "scdl-usc--davies",
23 | "ingestType": "collection",
24 | "@id": "http://dp.la/api/collections/7c7eaa8eee08d3c9b32d55662c0de58b",
25 | "ingestionSequence": 7,
26 | "score": 6.177192
27 | },
28 | {
29 | "id": "f807cdcefe2c93f7f71bf99f47899e1f",
30 | "ingestDate": "2014-01-19T09:04:22.366278",
31 | "_rev": "3-83d42699bd0836d639d948d921a73d66",
32 | "title": "Kirby Jones Papers, 1963 - 1974",
33 | "_id": "nara--1159",
34 | "ingestType": "collection",
35 | "@id": "http://dp.la/api/collections/f807cdcefe2c93f7f71bf99f47899e1f",
36 | "ingestionSequence": 5,
37 | "score": 6.0006475
38 | },
39 | {
40 | "id": "c1ae618e5958fd9c9e869ebbfd47bc34",
41 | "ingestDate": "2014-01-19T09:04:21.373908",
42 | "_rev": "3-5f88173336cf37fb8e33ddbfb1c62705",
43 | "title": "Joseph M. Jones Papers, 1928 - 1987",
44 | "_id": "nara--604476",
45 | "ingestType": "collection",
46 | "@id": "http://dp.la/api/collections/c1ae618e5958fd9c9e869ebbfd47bc34",
47 | "ingestionSequence": 5,
48 | "score": 5.391162
49 | },
50 | {
51 | "id": "e1e5fee5c47484e307ccdbb717a75e7f",
52 | "ingestDate": "2014-01-30T12:12:19.274456",
53 | "_rev": "4-6d0514009fcf0b2cc68a178fdf3209ac",
54 | "title": "John Paul Jones papers, 1924-1999",
55 | "_id": "smithsonian--john_paul_jones_papers_1924_1999",
56 | "ingestType": "collection",
57 | "@id": "http://dp.la/api/collections/e1e5fee5c47484e307ccdbb717a75e7f",
58 | "ingestionSequence": 8,
59 | "score": 5.317501
60 | }
61 | ],
62 | "facets": [
63 |
64 | ]
65 | }
--------------------------------------------------------------------------------
/ansr_blacklight/lib/ansr_blacklight/solr/response.rb:
--------------------------------------------------------------------------------
1 | ## copied directly from Blacklight::SolrResponse
2 | class Ansr::Blacklight::Solr::Response < HashWithIndifferentAccess
3 |
4 | require 'ansr_blacklight/solr/response/pagination_methods'
5 |
6 | autoload :Spelling, 'ansr_blacklight/solr/response/spelling'
7 | autoload :MoreLikeThis, 'ansr_blacklight/solr/response/more_like_this'
8 | autoload :GroupResponse, 'ansr_blacklight/solr/response/group_response'
9 | autoload :Group, 'ansr_blacklight/solr/response/group'
10 |
11 | include PaginationMethods
12 |
13 | attr_reader :request_params
14 | def initialize(data, request_params)
15 | super(data)
16 | @request_params = request_params
17 | extend Spelling
18 | extend Ansr::Facets
19 | extend InternalResponse
20 | extend MoreLikeThis
21 | end
22 |
23 | def header
24 | self['responseHeader']
25 | end
26 |
27 | def update(other_hash)
28 | other_hash.each_pair { |key, value| self[key] = value }
29 | self
30 | end
31 |
32 | def params
33 | (header and header['params']) ? header['params'] : request_params
34 | end
35 |
36 | def rows
37 | params[:rows].to_i
38 | end
39 |
40 | def docs
41 | @docs ||= begin
42 | response['docs'] || []
43 | end
44 | end
45 |
46 | def spelling
47 | self['spelling']
48 | end
49 |
50 | def grouped(model)
51 | @groups ||= self["grouped"].map do |field, group|
52 | # grouped responses can either be grouped by:
53 | # - field, where this key is the field name, and there will be a list
54 | # of documents grouped by field value, or:
55 | # - function, where the key is the function, and the documents will be
56 | # further grouped by function value, or:
57 | # - query, where the key is the query, and the matching documents will be
58 | # in the doclist on THIS object
59 | if group["groups"] # field or function
60 | GroupResponse.new field, model, group, self
61 | else # query
62 | Group.new({field => field}, model, group, self)
63 | end
64 | end
65 | end
66 |
67 | def group key, model
68 | grouped(model).select { |x| x.key == key }.first
69 | end
70 |
71 | def grouped?
72 | self.has_key? "grouped"
73 | end
74 |
75 | module InternalResponse
76 | def response
77 | self[:response] || {}
78 | end
79 |
80 | # short cut to response['numFound']
81 | def total
82 | response[:numFound].to_s.to_i
83 | end
84 |
85 | def start
86 | response[:start].to_s.to_i
87 | end
88 |
89 | def empty?
90 | total == 0
91 | end
92 |
93 | end
94 | end
95 |
--------------------------------------------------------------------------------
/lib/ansr/connection_adapters/no_sql_adapter.rb:
--------------------------------------------------------------------------------
1 | require 'active_record'
2 | require 'arel/visitors/bind_visitor'
3 | module Ansr
4 | module ConnectionAdapters
5 | class NoSqlAdapter < ActiveRecord::ConnectionAdapters::AbstractAdapter
6 | attr_reader :table
7 | def initialize(klass, connection, logger = nil, pool = nil)
8 | super(connection, logger, pool)
9 | @table = klass.table
10 | @visitor = nil
11 | end
12 |
13 | # Converts an arel AST to NOSQL Query
14 | def to_nosql(arel, binds = [])
15 | arel = arel.ast if arel.respond_to?(:ast)
16 | if arel.is_a? ::Arel::Nodes::Node
17 | binds = binds.dup
18 | visitor.accept(arel) do
19 | quote(*binds.shift.reverse)
20 | end
21 | else # assume it is already serialized
22 | arel
23 | end
24 | end
25 |
26 | # attr_accessor :visitor is a self.class::BindSubstitution in unprepared contexts
27 | class BindSubstitution < ::Arel::Visitors::MySQL # :nodoc:
28 | include ::Arel::Visitors::BindVisitor
29 | end
30 |
31 | # Executes +query+ statement in the context of this connection using
32 | # +binds+ as the bind substitutes. +name+ is logged along with
33 | # the executed +query+ statement.
34 | def execute(query, name = 'ANSR-NOSQL')
35 | end
36 |
37 | # called back from ::Arel::Table
38 | def primary_key(table_name)
39 | 'id' # table.primary_key || 'id'
40 | end
41 |
42 | def table_exists?(name)
43 | true
44 | end
45 |
46 | def schema_cache
47 | ActiveRecord::ConnectionAdapters::SchemaCache.new(self)
48 | end
49 |
50 | # this is called by the BigTable impl
51 | # should it be retired in favor of the more domain-appropriate 'fields'? Not usually seen by clients anyway.
52 | def columns(table_name, *rest)
53 | @table.fields.map {|s| ::ActiveRecord::ConnectionAdapters::Column.new(s.to_s, nil, String)}
54 | end
55 |
56 | def sanitize_limit(limit_value)
57 | if limit_value.to_s.to_i >= 0
58 | limit_value
59 | else
60 | Ansr::Relation::DEFAULT_PAGE_SIZE
61 | end
62 | end
63 |
64 | def sanitize_filter_name(filter_value)
65 | if filter_value.is_a? Array
66 | return filter_value.collect {|x| sanitize_filter_name(x)}.compact
67 | else
68 | if @table.facets.include? filter_value.to_sym
69 | return filter_value
70 | else
71 | raise "#{filter_value} is not a filterable field"
72 | #Rails.logger.warn "Ignoring #{filter_value} (not a filterable field)" if Rails.logger
73 | #return nil
74 | end
75 | end
76 | end
77 |
78 | end
79 | end
80 | end
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ansr
2 | ====
3 |
4 | ActiveRecord(No-SQL)::Relation + Blacklight
5 |
6 | Ansr is a library for building ActiveRecord-style models and Relation implementations that query no-SQL data sources.
7 | Ansr is motivated by a proposed refactoring of Blacklight at Code4Lib 2014.
8 |
9 | [Blacklight](https://github.com/projectblacklight/blacklight) (BL) defines itself as “an open source Solr user interface discovery platform.” The coupling to Solr is evident in the structure: Solr querying facilities are sprinkled throughout several mixins that are included in BL controllers. This results in a codebase that cannot, as is regularly asked on the mailing lists, be used in front of another document store (eg ElasticSeach). But this is not necessarily the case.
10 |
11 | BL might be refactored to locate the actual Solr querying machinery behind the core model of BL apps (currently called SolrDocument). Refactoring the codebase this way would realize several benefits:
12 |
13 | 1. Adherence to the Principle of Least Surprise: The BL document model would behave more like a Rails model backed by RDBMS. When bringing new developers into a BL project, familiarity with the standard patterns of Rails would translate more immediately to the BL context.
14 |
15 | 2. Flexible abstraction of the document store: Moving the specifics of querying the document store would make the introduction of models interacting with other stores possible. For example, the DPLA REST API exposes some Solr-like concepts, and a proof-of-concept model for an ActiveRecord-like approach to searching them can be seen at (https://github.com/barmintor/ansr/tree/master/ansr_dpla)
16 |
17 | 3. Clearer testing strategies and ease of console debugging
18 |
19 | 4. Clarification of BL’s relationship to RSolr as the provider to an analog for ActiveRecord::Relation
20 |
21 | What would such a refactor require?
22 |
23 | 1. A definition of the backend requirements of BL beyond a reference to Solr per se: indexed documents with fields, a concept of facets corresponding to the Solr/Lucene definitions, the ability to expose Hash-like representations of results.
24 |
25 | 2. A relocation of the searching methods from Blacklight::Catalog and Blacklight::SolrHelper into a model generated to include Solr code
26 |
27 | 3. An accommodation of controller-specific Solr configuration, possibly resolved by having the BL config register Solr parms with the model a la SolrDocument extensions in BL 4
28 |
29 | 4. An abstraction of the fielded/faceted search parameters to mimic ActiveRecord limits
30 |
31 | 5. A partner institution capable of producing integration and system testing support for, at minimum, another Lucene-backed document store (ElasticSearch)
32 |
33 | Since these changes are incompatible with current BL, they are proposed as a principal feature of BL 6.0.
34 |
35 | An example of the kinds of models to be implemented can be seen in a [DPLA proof of concept](https://github.com/barmintor/ansr/tree/master/ansr_dpla).
36 |
--------------------------------------------------------------------------------
/ansr_dpla/README.md:
--------------------------------------------------------------------------------
1 | Ansr::Dpla
2 | =====
3 |
4 | DPLA + ActiveRecord::Relation + Blacklight
5 |
6 | This project creates a Rails model that can be used to search the DPLA's public REST API (http://dp.la/info/developers/codex/). The goal of this project is to provide an avenue by which data from the DPLA REST API might be explored via a [Blacklight](https://github.com/projectblacklight/blacklight) application. It is proof-of-concept for a broader proposal described at (https://github.com/barmintor/ansr)
7 |
8 | To use the Item and Collection models, they must first be configured with a DPLA API key:
9 |
10 | Item.config({:api_key => 'your api key'})
11 | Instructions for creating an API key are here: http://dp.la/info/developers/codex/policies/#get-a-key
12 |
13 | Once the model is configured, it can be queried like any other Rails model.
14 |
15 | item_id = "7eb617e559007e1ad6d95bd30a30b16b"
16 | Item.find(item_id)
17 | Item.dataProvider # returns a single string value
18 |
19 | The Item and Collection models are searched like other Rails models, with relations. To search a single field, call 'where':
20 |
21 | rel = Item.where(q: 'kittens') # full text search for 'kittens'
22 |
23 | Where clauses can be negated:
24 |
25 | rel = rel.where.not(q: => 'cats') # maximize cuteness density
26 | Where clauses support simple unions:
27 |
28 | rel = rel.where.or(q: => 'puppies') # so egalitarian
29 | These relations are lazy-loaded; they make no queries until data is required:
30 |
31 | rel.load # loads the data if not loaded
32 | rel.to_a # loads the data if not loaded, returns an array of model instances
33 | rel.filters # loads the data if not loaded, returns a list of the filters/facets for the query
34 | rel.count # returns the number of records loaded
35 |
36 | This is to support a decorator pattern on the relations:
37 |
38 | rel = Item.where(q; 'kittens').where.not(q: 'cats')
39 | rel = rel.limit(25).offset(50) # start on the third page of 25
40 |
41 | In addition to where, there are a number of other decorator clauses:
42 |
43 | # select limits the fields returned
44 | rel = rel.select(:id, :"sourceResource.title")
45 | # limit sets the maximum number of records to return, default is 10
46 | rel = rel.limit(25)
47 | # offset sets a starting point; it must be a multiple of the limit
48 | rel = rel.offset(50) # start on page 3
49 | # order adds sort clauses
50 | rel = rel.order(:"sourceResource.title")
51 |
52 | When using the filter decorator, you can add field names without a query. This will add the field to the relations filter information after load:
53 |
54 | # filter adds filter/facet clauses
55 | rel = rel.filter(:"collection.id" => '460c76299e1b0a46afea352b1ab8f556')
56 | rel = rel.filter(:isShownAt)
57 | rel.filters # -> a list of two filters, and the counts for the filter constraint values in the response set
58 |
59 | And more, as per ActiveRecord and illustrated in the specs.
60 |
--------------------------------------------------------------------------------
/lib/ansr/facets.rb:
--------------------------------------------------------------------------------
1 | require 'ostruct'
2 |
3 | module Ansr::Facets
4 |
5 | # represents a facet value; which is a field value and its hit count
6 | class FacetItem < OpenStruct
7 | def initialize *args
8 | options = args.extract_options!
9 |
10 | # Backwards-compat method signature
11 | value = args.shift
12 | hits = args.shift
13 |
14 | options[:value] = value if value
15 | options[:hits] = hits if hits
16 |
17 | super(options)
18 | end
19 |
20 | def label
21 | super || value
22 | end
23 |
24 | def as_json(props = nil)
25 | table.as_json(props)
26 | end
27 | end
28 |
29 | # represents a facet; which is a field and its values
30 | class FacetField
31 | attr_reader :name, :items
32 | def initialize name, items, options = {}
33 | @name, @items = name, items
34 | @options = options
35 | end
36 |
37 | def limit
38 | @options[:limit]
39 | end
40 |
41 | def sort
42 | @options[:sort] || 'index'
43 | end
44 |
45 | def offset
46 | @options[:offset] || 0
47 | end
48 | end
49 |
50 | # @response.facets.each do |facet|
51 | # facet.name
52 | # facet.items
53 | # end
54 | # "caches" the result in the @facets instance var
55 | def facets
56 | @facets ||= begin
57 | facet_fields.map do |(facet_field_name,values_and_hits)|
58 | items = []
59 | options = {}
60 | values_and_hits.each_slice(2) do |k,v|
61 | items << FacetItem.new(:value => k, :hits => v)
62 | end
63 | options[:sort] = (params[:"f.#{facet_field_name}.facet.sort"] || params[:'facet.sort'])
64 | if params[:"f.#{facet_field_name}.facet.limit"] || params[:"facet.limit"]
65 | options[:limit] = (params[:"f.#{facet_field_name}.facet.limit"] || params[:"facet.limit"]).to_i
66 | end
67 |
68 | if params[:"f.#{facet_field_name}.facet.offset"] || params[:'facet.offset']
69 | options[:offset] = (params[:"f.#{facet_field_name}.facet.offset"] || params[:'facet.offset']).to_i
70 | end
71 | FacetField.new(facet_field_name, items, options)
72 | end
73 | end
74 | end
75 |
76 | # pass in a facet field name and get back a Facet instance
77 | def facet_by_field_name(name)
78 | @facets_by_field_name ||= {}
79 | @facets_by_field_name[name] ||= (
80 | facets.detect{|facet|facet.name.to_s == name.to_s}
81 | )
82 | end
83 |
84 | def facet_counts
85 | @facet_counts ||= self['facet_counts'] || {}
86 | end
87 |
88 | # Returns the hash of all the facet_fields (ie: {'instock_b' => ['true', 123, 'false', 20]}
89 | def facet_fields
90 | @facet_fields ||= facet_counts['facet_fields'] || {}
91 | end
92 |
93 | # Returns all of the facet queries
94 | def facet_queries
95 | @facet_queries ||= facet_counts['facet_queries'] || {}
96 | end
97 |
98 | # Returns all of the facet queries
99 | def facet_pivot
100 | @facet_pivot ||= facet_counts['facet_pivot'] || {}
101 | end
102 |
103 | end # end Facets
104 |
--------------------------------------------------------------------------------
/ansr_dpla/spec/lib/relation/where_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Ansr::Relation do
4 | before do
5 | @kittens = read_fixture('kittens.jsonld')
6 | @faceted = read_fixture('kittens_faceted.jsonld')
7 | @empty = read_fixture('empty.jsonld')
8 | @mock_api = double('api')
9 | Item.config{ |x| x[:api_key] = :foo}
10 | Item.engine.api= @mock_api
11 | end
12 | describe '#where' do
13 | it "do a single field, single value where" do
14 | test = Ansr::Relation.new(Item, Item.table).where(:q=>'kittens')
15 | @mock_api.should_receive(:items).with(:q => 'kittens').and_return('')
16 | test.load
17 | end
18 | it "do a single field, multiple value where" do
19 | test = Ansr::Relation.new(Item, Item.table).where(:q=>['kittens', 'cats'])
20 | @mock_api.should_receive(:items).with(:q => ['kittens','cats']).and_return('')
21 | test.load
22 | end
23 | it "do merge single field, multiple value wheres" do
24 | test = Ansr::Relation.new(Item, Item.table).where(:q=>'kittens').where(:q=>'cats')
25 | @mock_api.should_receive(:items).with(:q => ['kittens','cats']).and_return('')
26 | test.load
27 | end
28 | it "do a multiple field, single value where" do
29 | test = Ansr::Relation.new(Item, Item.table).where(:q=>'kittens',:foo=>'bears')
30 | @mock_api.should_receive(:items).with(:q => 'kittens', :foo=>'bears').and_return('')
31 | test.load
32 | end
33 | it "should keep scope distinct from spawned Relations" do
34 | test = Ansr::Relation.new(Item, Item.table).where(:q=>'kittens')
35 | test.where(:q=>'cats')
36 | @mock_api.should_receive(:items).with(:q => 'kittens').and_return('')
37 | test.load
38 | end
39 | describe '#not' do
40 | it 'should exclude a specified map of field values' do
41 | test = Ansr::Relation.new(Item, Item.table)
42 | test = test.where(:foo =>'kittens')
43 | test = test.where.not(:foo => 'cats')
44 | @mock_api.should_receive(:items).with(:foo => ['kittens', 'NOT cats']).and_return('')
45 | test.load
46 | end
47 |
48 | pending 'should exclude a value from the default query' do
49 | test = Ansr::Relation.new(Item, Item.table)
50 | test = test.where('kittens')
51 | test = test.where.not('cats')
52 | @mock_api.should_receive(:items).with(:q => ['kittens', 'NOT cats']).and_return('')
53 | test.load
54 | end
55 |
56 | pending 'should exclude a specified field' do
57 | test = Ansr::Relation.new(Item, Item.table)
58 | test = test.where(:foo => 'kittens')
59 | test = test.where.not('cats')
60 | @mock_api.should_receive(:items).with(:foo => 'kittens', :q => 'NOT cats').and_return('')
61 | test.load
62 | end
63 | end
64 | describe '#or' do
65 | it 'should union a specified map of field values' do
66 | test = Ansr::Relation.new(Item, Item.table)
67 | test = test.where(:foo =>'kittens')
68 | test = test.where.or(:foo => 'cats')
69 | @mock_api.should_receive(:items).with(:foo => ['kittens', 'OR cats']).and_return('')
70 | test.load
71 | end
72 | end
73 | end
74 | end
--------------------------------------------------------------------------------
/ansr_blacklight/lib/ansr_blacklight/solr/response/spelling.rb:
--------------------------------------------------------------------------------
1 | # A mixin for making access to the spellcheck component data easy.
2 | #
3 | # response.spelling.words
4 | #
5 | module Ansr::Blacklight::Solr::Response::Spelling
6 |
7 | def spelling
8 | @spelling ||= Base.new(self)
9 | end
10 |
11 | class Base
12 |
13 | attr :response
14 |
15 | def initialize(response)
16 | @response = response
17 | end
18 |
19 | # returns an array of spelling suggestion for specific query words,
20 | # as provided in the solr response. Only includes words with higher
21 | # frequency of occurrence than word in original query.
22 | # can't do a full query suggestion because we only get info for each word;
23 | # combination of words may not have results.
24 | # Thanks to Naomi Dushay!
25 | def words
26 | @words ||= (
27 | word_suggestions = []
28 | spellcheck = self.response[:spellcheck]
29 | if spellcheck && spellcheck[:suggestions]
30 | suggestions = spellcheck[:suggestions]
31 | unless suggestions.nil?
32 | # suggestions is an array:
33 | # (query term)
34 | # (hash of term info and term suggestion)
35 | # ...
36 | # (query term)
37 | # (hash of term info and term suggestion)
38 | # 'correctlySpelled'
39 | # true/false
40 | # collation
41 | # (suggestion for collation)
42 | if suggestions.index("correctlySpelled") #if extended results
43 | i_stop = suggestions.index("correctlySpelled")
44 | elsif suggestions.index("collation")
45 | i_stop = suggestions.index("collation")
46 | else
47 | i_stop = suggestions.length
48 | end
49 | # step through array in 2s to get info for each term
50 | 0.step(i_stop-1, 2) do |i|
51 | term = suggestions[i]
52 | term_info = suggestions[i+1]
53 | # term_info is a hash:
54 | # numFound =>
55 | # startOffset =>
56 | # endOffset =>
57 | # origFreq =>
58 | # suggestion => [{ frequency =>, word => }] # for extended results
59 | # suggestion => ['word'] # for non-extended results
60 | origFreq = term_info['origFreq']
61 | if suggestions.index("correctlySpelled")
62 | word_suggestions << term_info['suggestion'].map do |suggestion|
63 | suggestion['word'] if suggestion['freq'] > origFreq
64 | end
65 | else
66 | # only extended suggestions have frequency so we just return all suggestions
67 | word_suggestions << term_info['suggestion']
68 | end
69 | end
70 | end
71 | end
72 | word_suggestions.flatten.compact.uniq
73 | )
74 | end
75 |
76 | def collation
77 | # FIXME: DRY up with words
78 | spellcheck = self.response[:spellcheck]
79 | if spellcheck && spellcheck[:suggestions]
80 | suggestions = spellcheck[:suggestions]
81 | unless suggestions.nil?
82 | if suggestions.index("collation")
83 | suggestions[suggestions.index("collation") + 1]
84 | end
85 | end
86 | end
87 | end
88 |
89 | end
90 |
91 | end
92 |
93 |
--------------------------------------------------------------------------------
/ansr_dpla/spec/lib/relation/facet_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Ansr::Dpla::Relation do
4 | before do
5 | @kittens = read_fixture('kittens.jsonld')
6 | @faceted = read_fixture('kittens_faceted.jsonld')
7 | @empty = read_fixture('empty.jsonld')
8 | @mock_api = double('api')
9 | Item.config{ |x| x[:api_key] = :foo}
10 | Item.engine.api= @mock_api
11 | end
12 |
13 | subject { Ansr::Dpla::Relation.new(Item, Item.table) }
14 | describe '#filter' do
15 | describe "do a single field, single value filter" do
16 | it "with facet" do
17 | test = subject.filter(:object=>'kittens').facet(:object)
18 | @mock_api.should_receive(:items).with(:object => 'kittens', :facets => :object).and_return('')
19 | test.load
20 | end
21 | it "without facet" do
22 | test = subject.filter(:object=>'kittens')
23 | @mock_api.should_receive(:items).with(:object => 'kittens').and_return('')
24 | test.load
25 | end
26 | end
27 | it "do a single field, multiple value filter" do
28 | test = subject.filter(:object=>['kittens', 'cats']).facet(:object)
29 | @mock_api.should_receive(:items).with(:object => ['kittens','cats'], :facets => :object).and_return('')
30 | test.load
31 | end
32 | it "do merge single field, multiple value filters" do
33 | test = subject.filter(:"provider.name"=>'kittens').filter(:"provider.name"=>'cats').facet(:"provider.name")
34 | @mock_api.should_receive(:items).with(hash_including(:"provider.name" => ['kittens','cats'], :facets => :"provider.name")).and_return('')
35 | test.load
36 | end
37 | it "do a multiple field, single value filter" do
38 | test = subject.filter(:object=>'kittens',:isShownAt=>'bears').facet([:object, :isShownAt])
39 | @mock_api.should_receive(:items).with(hash_including(:object => 'kittens', :isShownAt=>'bears', :facets => [:object, :isShownAt])).and_return('')
40 | test.load
41 | end
42 | it "should keep scope distinct from spawned Relations" do
43 | test = subject.filter(:"provider.name"=>'kittens').facet(:"provider.name")
44 | test.where(:q=>'cats')
45 | @mock_api.should_receive(:items).with(:"provider.name" => 'kittens', :facets => :"provider.name").and_return('')
46 | test.load
47 | end
48 | it "should raise an error if the requested field is not a facetable field" do
49 | expect {subject.facet(:foo)}.to raise_error
50 | end
51 | it "should facet without a filter" do
52 | test = subject.facet(:object)
53 | @mock_api.should_receive(:items).with(:facets => :object).and_return('')
54 | test.load
55 | end
56 | end
57 |
58 | describe '#filters' do
59 | it 'should return Blacklight types' do
60 | # Blacklight::SolrResponse::Facets::FacetItem.new(:value => s, :hits => v)
61 | # options = {:sort => 'asc', :offset => 0}
62 | # Blacklight::SolrResponse::Facets::FacetField.new name, items, options
63 | test = subject.where(:q=>'kittens')
64 | @mock_api.should_receive(:items).with(:q => 'kittens').and_return(@faceted)
65 | test.load
66 | fkey = test.facets.keys.first
67 | facet = test.facets[fkey]
68 | expect(facet).to be_a(Ansr::Facets::FacetField)
69 | facet.items
70 | end
71 | it 'should dispatch a query with no docs requested if not loaded' do
72 | test = subject.where(:q=>'kittens')
73 | @mock_api.should_receive(:items).with(:q => 'kittens', :page_size=>0).once.and_return(@faceted)
74 | fkey = test.facets.keys.first
75 | facet = test.facets[fkey]
76 | expect(facet).to be_a(Ansr::Facets::FacetField)
77 | expect(test.loaded?).to be_false
78 | test.facets # make sure we memoized the facet values
79 | end
80 | end
81 |
82 | end
--------------------------------------------------------------------------------
/lib/ansr/relation/predicate_builder.rb:
--------------------------------------------------------------------------------
1 | module Ansr
2 | class PredicateBuilder # :nodoc:
3 | def self.build_from_hash(klass, attributes, default_table)
4 | queries = []
5 |
6 | attributes.each do |column, value|
7 | table = default_table
8 |
9 | if value.is_a?(Hash)
10 | if value.empty?
11 | queries << '1=0'
12 | else
13 | table = default_table.class.new(column, default_table.engine)
14 | association = klass.reflect_on_association(column.to_sym)
15 |
16 | value.each do |k, v|
17 | queries.concat expand(association && association.klass, table, k, v)
18 | end
19 | end
20 | else
21 | column = column.to_s
22 | # we remove the below since big table fields don't really have associations
23 | # if column.include?('.')
24 | # table_name, column = column.split('.', 2)
25 | # table = Ansr::Arel::BigTable.new(table_name, default_table.engine)
26 | # end
27 |
28 | queries.concat expand(klass, table, column, value)
29 | end
30 | end
31 |
32 | queries
33 | end
34 |
35 | def self.expand(klass, table, column, value)
36 | queries = []
37 |
38 | # Find the foreign key when using queries such as:
39 | # Post.where(author: author)
40 | #
41 | # For polymorphic relationships, find the foreign key and type:
42 | # PriceEstimate.where(estimate_of: treasure)
43 | if klass && value.class < Base && reflection = klass.reflect_on_association(column.to_sym)
44 | if reflection.polymorphic?
45 | queries << build(table[reflection.foreign_type], value.class.base_class)
46 | end
47 |
48 | column = reflection.foreign_key
49 | end
50 |
51 | queries << build(table[column], value)
52 | queries
53 | end
54 |
55 | def self.references(attributes)
56 | attributes.map do |key, value|
57 | if value.is_a?(Hash)
58 | key
59 | else
60 | key = key.to_s
61 | key.split('.').first if key.include?('.')
62 | end
63 | end.compact
64 | end
65 |
66 | private
67 | def self.build(attribute, value)
68 | case value
69 | when Array
70 | values = value.to_a.map {|x| x.is_a?(Base) ? x.id : x}
71 | ranges, values = values.partition {|v| v.is_a?(Range)}
72 |
73 | values_predicate = if values.include?(nil)
74 | values = values.compact
75 |
76 | case values.length
77 | when 0
78 | attribute.eq(nil)
79 | when 1
80 | attribute.eq(values.first).or(attribute.eq(nil))
81 | else
82 | attribute.in(values).or(attribute.eq(nil))
83 | end
84 | else
85 | attribute.in(values)
86 | end
87 |
88 | array_predicates = ranges.map { |range| attribute.in(range) }
89 | array_predicates << values_predicate
90 | array_predicates.inject { |composite, predicate| composite.or(predicate) }
91 | when ActiveRecord::Relation
92 | value = value.select(value.klass.arel_table[value.klass.primary_key]) if value.select_values.empty?
93 | attribute.in(value.arel.ast)
94 | when Range
95 | attribute.in(value)
96 | when ActiveRecord::Base
97 | attribute.eq(value.id)
98 | when Class
99 | # FIXME: I think we need to deprecate this behavior
100 | attribute.eq(value.name)
101 | else
102 | attribute.eq(value)
103 | end
104 | end
105 | end
106 | end
107 |
--------------------------------------------------------------------------------
/ansr_dpla/lib/ansr_dpla/arel/big_table.rb:
--------------------------------------------------------------------------------
1 | module Ansr::Dpla
2 | module Arel
3 | class BigTable < Ansr::Arel::BigTable
4 |
5 | FIELDS = [
6 | # can we list the fields from the DPLA v2 api?
7 | # the sourceResource, originalRecord, and provider fields need to be associations, right?
8 | :"_id",
9 | :"dataProvider",
10 | :"sourceResource",
11 | :"object",
12 | :"ingestDate",
13 | :"originalRecord",
14 | :"ingestionSequence",
15 | :"isShownAt",
16 | :"hasView",
17 | :"provider",
18 | :"@context",
19 | :"ingestType",
20 | :"@id",
21 | :"id"
22 | ]
23 |
24 | FACETS = [
25 | :"sourceResource.contributor",
26 | :"sourceResource.date.begin",
27 | :"sourceResource.date.end",
28 | :"sourceResource.language.name",
29 | :"sourceResource.language.iso639",
30 | :"sourceResource.format",
31 | :"sourceResource.stateLocatedIn.name",
32 | :"sourceResource.stateLocatedIn.iso3166-2",
33 | :"sourceResource.spatial.name",
34 | :"sourceResource.spatial.country",
35 | :"sourceResource.spatial.region",
36 | :"sourceResource.spatial.county",
37 | :"sourceResource.spatial.state",
38 | :"sourceResource.spatial.city",
39 | :"sourceResource.spatial.iso3166-2",
40 | :"sourceResource.spatial.coordinates",
41 | :"sourceResource.subject.@id",
42 | :"sourceResource.subject.name",
43 | :"sourceResource.temporal.begin",
44 | :"sourceResource.temporal.end",
45 | :"sourceResource.type",
46 | :"hasView.@id",
47 | :"hasView.format",
48 | :"isPartOf.@id",
49 | :"isPartOf.name",
50 | :"isShownAt",
51 | :"object",
52 | :"provider.@id",
53 | :"provider.name",
54 | ]
55 |
56 | SORTS = [
57 | :"id",
58 | :"@id",
59 | :"sourceResource.id",
60 | :"sourceResource.contributor",
61 | :"sourceResource.date.begin",
62 | :"sourceResource.date.end",
63 | :"sourceResource.extent",
64 | :"sourceResource.language.name",
65 | :"sourceResource.language.iso639",
66 | :"sourceResource.format",
67 | :"sourceResource.stateLocatedIn.name",
68 | :"sourceResource.stateLocatedIn.iso3166-2",
69 | :"sourceResource.spatial.name",
70 | :"sourceResource.spatial.country",
71 | :"sourceResource.spatial.region",
72 | :"sourceResource.spatial.county",
73 | :"sourceResource.spatial.state",
74 | :"sourceResource.spatial.city",
75 | :"sourceResource.spatial.iso3166-2",
76 | :"sourceResource.spatial.coordinates",
77 | :"sourceResource.subject.@id",
78 | :"sourceResource.subject.type",
79 | :"sourceResource.subject.name",
80 | :"sourceResource.temporal.begin",
81 | :"sourceResource.temporal.end",
82 | :"sourceResource.title",
83 | :"sourceResource.type",
84 | :"hasView.@id",
85 | :"hasView.format",
86 | :"isPartOf.@id",
87 | :"isPartOf.name",
88 | :"isShownAt",
89 | :"object",
90 | :"provider.@id",
91 | :"provider.name",
92 | ]
93 |
94 | def initialize(klass, opts={})
95 | super(klass.model())
96 | self.name = klass.name.downcase.pluralize
97 | @fields += (opts[:fields] || FIELDS)
98 | @facets += (opts[:facets] || FACETS)
99 | @sorts += (opts[:sorts] || SORTS)
100 | self.config(opts[:config]) if opts[:config]
101 | end
102 |
103 | def name
104 | super.pluralize.downcase
105 | end
106 | end
107 | end
108 | end
--------------------------------------------------------------------------------
/lib/ansr/relation.rb:
--------------------------------------------------------------------------------
1 | require 'yaml'
2 | require 'kaminari'
3 | module Ansr
4 | class Relation < ::ActiveRecord::Relation
5 | attr_reader :response
6 | attr_accessor :filters, :count, :context, :resource
7 | ::ActiveRecord::Relation::VALID_UNSCOPING_VALUES << :facet << :spellcheck
8 | ::ActiveRecord::Relation::SINGLE_VALUE_METHODS << :spellcheck
9 | DEFAULT_PAGE_SIZE = 10
10 |
11 | include Sanitization::ClassMethods
12 | include QueryMethods
13 | include ::Kaminari::PageScopeMethods
14 |
15 | alias :start :offset_value
16 |
17 | def initialize(klass, table, values = {})
18 | raise "Cannot search nil model" if klass.nil?
19 | super(klass, table, values)
20 | end
21 |
22 | def resource
23 | rsrc = @klass.name.downcase
24 | rsrc << ((rsrc =~ /s$/) ? 'es' : 's')
25 | rsrc.to_sym
26 | end
27 |
28 | def default_limit_value
29 | DEFAULT_PAGE_SIZE
30 | end
31 |
32 | def load
33 | exec_queries unless loaded?
34 | self
35 | end
36 |
37 | # Converts relation objects to Array.
38 | def to_a
39 | load
40 | @records
41 | end
42 |
43 | # Forces reloading of relation.
44 | def reload
45 | reset
46 | load
47 | end
48 |
49 | def empty?
50 | count == 0
51 | end
52 |
53 | def many?
54 | count > 1
55 | end
56 |
57 | def offset!(value)
58 | self.offset_value=value
59 | self
60 | end
61 |
62 | def count()
63 | self.load
64 | @response.count
65 | end
66 |
67 | def total
68 | count
69 | end
70 | alias :total_count :total
71 |
72 | def max_pages
73 | (total.to_f / limit_value).ceil
74 | end
75 |
76 | # override to parse filters from response
77 | def facets_from(response)
78 | {} and raise "this is a dead method!"
79 | end
80 |
81 | # override to parse docs from response
82 | def docs_from(response)
83 | []
84 | end
85 |
86 | def facets
87 | if loaded?
88 | @facet_cache = facets_from(response)
89 | else
90 | @facet_cache ||= begin
91 | query = self.limit(0)
92 | query.load
93 | query.facets
94 | end
95 | end
96 | end
97 |
98 | def spawn
99 | s = self.class.new(@klass, @table, @values.dup)
100 | s.references!(references_values())
101 | s
102 | end
103 |
104 | def grouped?
105 | false
106 | end
107 |
108 | def group_by(key=self.group_values.first)
109 | []
110 | end
111 |
112 | def to_nosql
113 | spawn.to_nosql!
114 | end
115 |
116 | def to_nosql!
117 | ansr_query(arel, bind_values)
118 | end
119 |
120 | private
121 |
122 | # override to prevent default selection of all fields
123 | def build_select(arel, selects)
124 | unless selects.empty?
125 | @implicit_readonly = false
126 | arel.project(*selects)
127 | #else
128 | # arel.project(@klass.arel_table[Arel.star])
129 | end
130 | end
131 |
132 |
133 | def exec_queries
134 | default_scoped = with_default_scope
135 |
136 | if default_scoped.equal?(self)
137 | @response = model.find_by_nosql(arel, bind_values)
138 | @records = docs_from(@response).collect do |d|
139 | model.new(d)
140 | end
141 |
142 | # this is ceremonial, it's always true
143 | readonly = readonly_value.nil? || readonly_value
144 | @records.each { |record| record.readonly! } if readonly
145 | else
146 | @records = default_scoped.to_a
147 | end
148 |
149 | self.limit_value = default_limit_value unless self.limit_value
150 | self.offset_value = 0 unless self.offset_value
151 | @filter_cache = nil # unload any cached filters
152 | @loaded = true
153 | @records
154 | end
155 | end
156 |
157 | end
--------------------------------------------------------------------------------
/ansr_blacklight/spec/lib/request_builders_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Ansr::Blacklight::RequestBuilders do
4 | class RequestBuildersTestClass
5 | attr_accessor :table
6 | include Ansr::Blacklight::RequestBuilders
7 | end
8 | subject { RequestBuildersTestClass.new }
9 | let(:relation) { double('Relation')}
10 | before(:each) do
11 | subject.table = {'facet_name' => Ansr::Arel::ConfiguredField.new(relation, 'facet_name',:date => nil, :query => nil, :tag => nil)}
12 | end
13 | describe "filter_value_to_fq_string" do
14 |
15 | it "should use the raw handler for strings" do
16 | expect(subject.send(:filter_value_to_fq_string, "facet_name", "my value")).to eq "{!raw f=facet_name}my value"
17 | end
18 |
19 | it "should pass booleans through" do
20 | expect(subject.send(:filter_value_to_fq_string, "facet_name", true)).to eq "facet_name:true"
21 | end
22 |
23 | it "should pass boolean-like strings through" do
24 | expect(subject.send(:filter_value_to_fq_string, "facet_name", "true")).to eq "facet_name:true"
25 | end
26 |
27 | it "should pass integers through" do
28 | expect(subject.send(:filter_value_to_fq_string, "facet_name", 1)).to eq "facet_name:1"
29 | end
30 |
31 | it "should pass integer-like strings through" do
32 | expect(subject.send(:filter_value_to_fq_string, "facet_name", "1")).to eq "facet_name:1"
33 | end
34 |
35 | it "should pass floats through" do
36 | expect(subject.send(:filter_value_to_fq_string, "facet_name", 1.11)).to eq "facet_name:1\\.11"
37 | end
38 |
39 | it "should pass floats through" do
40 | expect(subject.send(:filter_value_to_fq_string, "facet_name", "1.11")).to eq "facet_name:1\\.11"
41 | end
42 |
43 | it "should escape negative integers" do
44 | expect(subject.send(:filter_value_to_fq_string, "facet_name", -1)).to eq "facet_name:\\-1"
45 | end
46 |
47 | it "should pass date-type fields through" do
48 | subject.table = {'facet_name' => Ansr::Arel::ConfiguredField.new(relation, 'facet_name',:date => true, :query => nil, :tag => nil)}
49 |
50 | expect(subject.send(:filter_value_to_fq_string, "facet_name", "2012-01-01")).to eq "facet_name:2012\\-01\\-01"
51 | end
52 |
53 | it "should escape datetime-type fields" do
54 | subject.table['facet_name'] = Ansr::Arel::ConfiguredField.new(relation, 'facet_name',:date => true, :query => nil, :tag => nil)
55 |
56 | expect(subject.send(:filter_value_to_fq_string, "facet_name", "2003-04-09T00:00:00Z")).to eq "facet_name:2003\\-04\\-09T00\\:00\\:00Z"
57 | end
58 |
59 | it "should format Date objects correctly" do
60 | subject.table['facet_name'] = Ansr::Arel::ConfiguredField.new(relation, 'facet_name',:date => nil, :query => nil, :tag => nil)
61 | d = DateTime.parse("2003-04-09T00:00:00")
62 | expect(subject.send(:filter_value_to_fq_string, "facet_name", d)).to eq "facet_name:2003\\-04\\-09T00\\:00\\:00Z"
63 | end
64 |
65 | it "should handle range requests" do
66 | expect(subject.send(:filter_value_to_fq_string, "facet_name", 1..5)).to eq "facet_name:[1 TO 5]"
67 | end
68 |
69 | it "should add tag local parameters" do
70 | subject.table['facet_name'] = Ansr::Arel::ConfiguredField.new(relation, 'facet_name',:date => nil, :query => nil, :tag => 'asdf')
71 |
72 | expect(subject.send(:filter_value_to_fq_string, "facet_name", true)).to eq "{!tag=asdf}facet_name:true"
73 | expect(subject.send(:filter_value_to_fq_string, "facet_name", "my value")).to eq "{!raw f=facet_name tag=asdf}my value"
74 | end
75 |
76 | describe "#with_tag_ex" do
77 | it "should add an !ex local parameter if the facet configuration requests it" do
78 | expect(subject.with_ex_local_param("xyz", "some-value")).to eq "{!ex=xyz}some-value"
79 | end
80 |
81 | it "should not add an !ex local parameter if it isn't configured" do
82 | mock_field = double()
83 | expect(subject.with_ex_local_param(nil, "some-value")).to eq "some-value"
84 | end
85 | end
86 |
87 | end
88 | end
--------------------------------------------------------------------------------
/lib/ansr/dummy_associations.rb:
--------------------------------------------------------------------------------
1 | module Ansr
2 | module DummyAssociations
3 | extend ActiveSupport::Concern
4 | module ClassMethods
5 | def create_reflection(macro, name, scope, options, active_record)
6 | case macro
7 | when :has_many, :belongs_to, :has_one, :has_and_belongs_to_many
8 | klass = options[:through] ? DummyThroughReflection : DummyAssociationReflection
9 | reflection = klass.new(macro, name, scope, options, active_record)
10 | when :composed_of
11 | reflection = DummyAggregateReflection.new(macro, name, scope, options, active_record)
12 | end
13 |
14 | self.reflections = self.reflections.merge(name => reflection)
15 | reflection
16 | end
17 |
18 | # Returns an array of AggregateReflection objects for all the aggregations in the class.
19 | def reflect_on_all_aggregations
20 | reflections.values.grep(DummyAggregateReflection)
21 | end
22 |
23 | # Returns the AggregateReflection object for the named +aggregation+ (use the symbol).
24 | #
25 | # Account.reflect_on_aggregation(:balance) # => the balance AggregateReflection
26 | #
27 | def reflect_on_aggregation(aggregation)
28 | reflection = reflections[aggregation]
29 | reflection if reflection.is_a?(DummyAggregateReflection)
30 | end
31 |
32 | # Returns an array of DummyAssociationReflection objects for all the
33 | # associations in the class. If you only want to reflect on a certain
34 | # association type, pass in the symbol (:has_many, :has_one,
35 | # :belongs_to) as the first parameter.
36 | #
37 | # Example:
38 | #
39 | # Account.reflect_on_all_associations # returns an array of all associations
40 | # Account.reflect_on_all_associations(:has_many) # returns an array of all has_many associations
41 | #
42 | def reflect_on_all_associations(macro = nil)
43 | association_reflections = reflections.values.grep(DummyAssociationReflection)
44 | macro ? association_reflections.select { |reflection| reflection.macro == macro } : association_reflections
45 | end
46 |
47 | # Returns the DummyAssociationReflection object for the +association+ (use the symbol).
48 | #
49 | # Account.reflect_on_association(:owner) # returns the owner AssociationReflection
50 | # Invoice.reflect_on_association(:line_items).macro # returns :has_many
51 | #
52 | def reflect_on_association(association)
53 | reflection = reflections[association]
54 | reflection if reflection.is_a?(DummyAssociationReflection)
55 | end
56 |
57 | # Returns an array of AssociationReflection objects for all associations which have :autosave enabled.
58 | def reflect_on_all_autosave_associations
59 | reflections.values.select { |reflection| reflection.options[:autosave] }
60 | end
61 | end
62 | class DummyReflection < ActiveRecord::Reflection::AggregateReflection
63 | def initialize(macro, name, scope, options, active_record)
64 | super(macro, name, scope, options, active_record)
65 | @symbol = name
66 | end
67 |
68 | def polymorphic?
69 | false
70 | end
71 |
72 | def foreign_key
73 | @symbol # ??
74 | end
75 | def collection?
76 | [:has_one, :has_many, :has_and_belongs_to_many].include? macro
77 | end
78 | def validate?
79 | false
80 | end
81 | def association_class
82 | DummyAssociation
83 | end
84 |
85 | def check_validity!
86 | true
87 | end
88 | end
89 | class DummyAssociationReflection < DummyReflection; end
90 | class DummyAggregateReflection < DummyReflection; end
91 | class DummyThroughReflection < DummyReflection; end
92 | class DummyAssociation < ActiveRecord::Associations::Association
93 | def writer(*args); end
94 | def reader(*args)
95 | self
96 | end
97 | def loaded?
98 | true
99 | end
100 | def identifier
101 | nil
102 | end
103 | end
104 | end
105 | end
--------------------------------------------------------------------------------
/ansr_blacklight/spec/lib/queryable_relation_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Ansr::Blacklight::Relation do
4 |
5 | class ConfiguredTable < Ansr::Arel::BigTable
6 |
7 | end
8 |
9 | class OtherTable < ConfiguredTable
10 | def name
11 | 'outside'
12 | end
13 |
14 | end
15 |
16 | context "a bunch of query stuff" do
17 |
18 | before(:each) do
19 | Object.const_set('QueryTestModel', Class.new(TestModel))
20 | QueryTestModel.configure do |config|
21 | config[:table_class] = ConfiguredTable
22 | end
23 | QueryTestModel.solr = stub_solr
24 | other_table.configure_fields do |config|
25 | hash = (config[:configured] ||= {})
26 | hash[:local] = {:property => 'test', :escape => 'tes"t'}
27 | end
28 | @visitor = Ansr::Blacklight::Arel::Visitors::ToNoSql.new(QueryTestModel.table)
29 |
30 | ## COMMON AREL CONCEPTS ##
31 | # from() indicates the big table name for the relation; in BL/Solr this maps to the request path
32 | @relation = QueryTestModel.from(ConfiguredTable.new(QueryTestModel))
33 | # as() indicates an alias for the big table; in BL/Solr this maps to the :qt param
34 | @relation.as!('hey')
35 | # constraints map directly
36 | @relation.where!(:configured=> "what's")
37 |
38 | # as do offsets and limits
39 | @relation.offset!(21)
40 | @relation.limit!(12)
41 | @relation.group!("I")
42 | ## COMMON NO-SQL CONCEPTS ##
43 | # facets are a kind of projection with attributes (attribute support is optional)
44 | @relation.facet!("title_facet", limit: "vest")
45 | # filters are a type of constraint
46 | @relation.filter!({"name_facet" => "Fedo"})
47 | @relation.facet!("name_facet", limit: 10)
48 | @relation.facet!(limit: 20)
49 | @relation.highlight!("I", 'fl' => "wish")
50 | @relation.spellcheck!("a", q: "fleece")
51 | ## SOLR ECCENTRICITIES ##
52 | # these are present for compatibility, but not expected to be used generically
53 | @relation.wt!("going")
54 | @relation.defType!("had")
55 | end
56 |
57 | after(:each) do
58 | @relation = nil
59 | Object.send(:remove_const, :QueryTestModel)
60 | end
61 |
62 | let(:table) { QueryTestModel.table }
63 | let(:other_table) { OtherTable.new(QueryTestModel) }
64 | describe "#from" do
65 |
66 | let(:visitor) { @visitor }
67 | subject {@relation.from(other_table)}
68 |
69 | it "should set the path to the table name" do
70 | query = visitor.accept subject.build_arel.ast
71 | expect(query.path).to eql('outside')
72 | end
73 |
74 | it "should change the table" do
75 | expect(subject.from_value.first).to be_a ConfiguredTable
76 | end
77 | end
78 |
79 | describe "#as" do
80 |
81 | subject {@relation.as('hey')}
82 | let(:visitor) { @visitor }
83 |
84 | it "should set the :qt parameter" do
85 | query = visitor.accept subject.build_arel.ast
86 | expect(query.to_hash[:qt]).to eql 'hey'
87 | end
88 | end
89 |
90 | describe "#facet" do
91 |
92 | subject { @relation.facet(limit: 20)}
93 | let(:visitor) { @visitor }
94 |
95 | it "should set default facet parms when no field expr is given" do
96 | rel = subject.facet(limit: 20)
97 | query = visitor.accept rel.build_arel.ast
98 | end
99 |
100 | it "should set pivot facet field params" do
101 | end
102 | end
103 |
104 | context "a mix of queryable relations" do
105 | subject { @relation.from(other_table) }
106 | let(:visitor) { @visitor }
107 |
108 | it "should accept valid parameters" do
109 | query = visitor.accept subject.build_arel.ast
110 | expect(query.path).to eq('outside')
111 | expect(query.to_hash).to eq({"defType" => "had",
112 | "f.name_facet.facet.limit" => "10",
113 | "f.title_facet.facet.limit" => "vest",
114 | "facet" => true,
115 | "facet.field" => [:title_facet,:name_facet],
116 | "facet.limit" => "20",
117 | "fq" => ["{!raw f=name_facet}Fedo"],
118 | "group" => "I",
119 | "hl" => "I",
120 | "hl.fl" => "wish",
121 | "q" => "{!property=test escape='tes\\\"t'}what's",
122 | "qt" => "hey",
123 | "rows" => "12",
124 | "spellcheck" => "a",
125 | "spellcheck.q" => "fleece",
126 | "start" => "21",
127 | "wt" => "going"
128 | })
129 | end
130 | end
131 | end
132 | end
133 |
--------------------------------------------------------------------------------
/ansr_dpla/spec/lib/api_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Ansr::Dpla::Api do
4 |
5 | describe '#config' do
6 | before do
7 | @test = Ansr::Dpla::Api.new
8 | end
9 |
10 | it "should be configurable with a Hash" do
11 | config_fixture = {:api_key => :foo, :url => :bar}
12 | @test.config{|x| x.merge!(config_fixture)}
13 | expect(@test.url).to eql(:bar)
14 | end
15 |
16 | it "should be configurable with a path to a yaml" do
17 | open(fixture_path('dpla.yml')) do |blob|
18 | config = YAML.load(blob)
19 | @test.config{|x| x.merge!(config)}
20 | end
21 | expect(@test.url).to eql('http://fake.dp.la/v0/')
22 | end
23 |
24 | it "should raise an error of the config doesn't have required fields" do
25 | expect { Ansr::Dpla::Api.new.config{|x| x.merge!(:url => :foo)}}.to raise_error
26 | Ansr::Dpla::Api.new.config {|x| x.merge!({:url => :foo, :api_key => :foo})}
27 | end
28 |
29 | end
30 |
31 | describe '#path_for' do
32 | before do
33 | @test = Ansr::Dpla::Api.new
34 | @test.config{|x| x.merge!(:api_key => :testing)}
35 | end
36 |
37 |
38 | describe 'queries' do
39 | it "should build paths for basic queries" do
40 | fixture = {:foo => ['kittens', 'cats'], :bar => ['puppies', 'dogs']}
41 | expected = 'tests?api_key=testing&foo=kittens+AND+cats&bar=puppies+AND+dogs'
42 | expect(@test.path_for('tests', fixture)).to eql(expected)
43 | expect(@test.items_path(fixture)).to eql(expected.sub(/^tests/,'items'))
44 | expect(@test.collections_path(fixture)).to eql(expected.sub(/^tests/,'collections'))
45 | expect(@test.item_path('foo')).to eql('items/foo?api_key=testing')
46 | expect(@test.collection_path('foo')).to eql('collections/foo?api_key=testing')
47 | end
48 |
49 | it "should build paths for OR and NOT queries" do
50 | fixture = {:foo => ['kittens', 'NOT cats']}
51 | expected = 'tests?api_key=testing&foo=kittens+AND+NOT+cats'
52 | expect(@test.path_for('tests', fixture)).to eql(expected)
53 |
54 | fixture = {:foo => ['NOT kittens', 'cats']}
55 | expected = 'tests?api_key=testing&foo=NOT+kittens+AND+cats'
56 | expect(@test.path_for('tests', fixture)).to eql(expected)
57 |
58 | fixture = {:foo => ['kittens', 'OR cats']}
59 | expected = 'tests?api_key=testing&foo=kittens+OR+cats'
60 | expect(@test.path_for('tests', fixture)).to eql(expected)
61 |
62 | fixture = {:foo => ['OR kittens', 'cats']}
63 | expected = 'tests?api_key=testing&foo=kittens+AND+cats'
64 | expect(@test.path_for('tests', fixture)).to eql(expected)
65 | end
66 | end
67 |
68 | it "should build paths for facets right" do
69 | fixture = {:foo => 'kittens', :facets => :bar}
70 | expected = 'tests?api_key=testing&foo=kittens&facets=bar'
71 | expect(@test.path_for('tests', fixture)).to eql(expected)
72 |
73 | fixture = {:foo => 'kittens', :facets => [:bar, :baz]}
74 | expected = 'tests?api_key=testing&foo=kittens&facets=bar%2Cbaz'
75 | expect(@test.path_for('tests', fixture)).to eql(expected)
76 |
77 | fixture = {:foo => 'kittens', :facets => 'bar,baz'}
78 | expected = 'tests?api_key=testing&foo=kittens&facets=bar%2Cbaz'
79 | expect(@test.path_for('tests', fixture)).to eql(expected)
80 | end
81 |
82 | it "should build paths for sorts right" do
83 | fixture = {:foo => 'kittens', :sort_by => [:bar, :baz]}
84 | expected = 'tests?api_key=testing&foo=kittens&sort_by=bar%2Cbaz'
85 | expect(@test.path_for('tests', fixture)).to eql(expected)
86 |
87 | fixture = {:foo => 'kittens', :sort_by => 'bar,baz'}
88 | expected = 'tests?api_key=testing&foo=kittens&sort_by=bar%2Cbaz'
89 | expect(@test.path_for('tests', fixture)).to eql(expected)
90 |
91 | fixture = {:foo => 'kittens', :sort_by => 'bar,baz', :sort_order => :desc}
92 | expected = 'tests?api_key=testing&foo=kittens&sort_by=bar%2Cbaz&sort_order=desc'
93 | expect(@test.path_for('tests', fixture)).to eql(expected)
94 | end
95 |
96 | it "should build paths for field selections right" do
97 | fixture = {:foo => 'kittens', :fields => [:bar, :baz]}
98 | expected = 'tests?api_key=testing&foo=kittens&fields=bar%2Cbaz'
99 | expect(@test.path_for('tests', fixture)).to eql(expected)
100 |
101 | fixture = {:foo => 'kittens', :fields => 'bar,baz'}
102 | expected = 'tests?api_key=testing&foo=kittens&fields=bar%2Cbaz'
103 | expect(@test.path_for('tests', fixture)).to eql(expected)
104 | end
105 |
106 | it "should build paths for limits and page sizes right" do
107 | fixture = {:foo => 'kittens', :page_size=>25, :page=>4}
108 | expected = 'tests?api_key=testing&foo=kittens&page_size=25&page=4'
109 | expect(@test.path_for('tests', fixture)).to eql(expected)
110 | end
111 |
112 | end
113 | end
114 |
--------------------------------------------------------------------------------
/ansr_blacklight/spec/lib/relation/grouping_spec.rb:
--------------------------------------------------------------------------------
1 | # -*- encoding : utf-8 -*-
2 | require 'spec_helper'
3 |
4 | # check the methods that do solr requests. Note that we are not testing if
5 | # solr gives "correct" responses, as that's out of scope (it's a part of
6 | # testing the solr code itself). We *are* testing if blacklight code sends
7 | # queries to solr such that it gets appropriate results. When a user does a search,
8 | # do we get data back from solr (i.e. did we properly configure blacklight code
9 | # to talk with solr and get results)? when we do a document request, does
10 | # blacklight code get a single document returned?)
11 | #
12 | describe Ansr::Blacklight do
13 |
14 | class TestTable < Ansr::Arel::BigTable
15 |
16 | def [](val)
17 | key = (Arel::Attributes::Attribute === val) ? val.name.to_sym : val.to_sym
18 | key == :configured ? Ansr::Arel::ConfiguredField.new(key, {:property => 'test', :escape => 'tes"t'}) : super(val)
19 | end
20 |
21 | def fields
22 | [:id]
23 | end
24 |
25 | end
26 |
27 | def stub_solr(mock_query_response)
28 | @solr ||= double('Solr')
29 | @solr.stub(:send_and_receive).and_return(eval(mock_query_response))
30 | @solr
31 | end
32 |
33 | before do
34 | Object.const_set('GroupModel', Class.new(TestModel))
35 | GroupModel.solr = stub_solr(sample_response.to_s)
36 | GroupModel.configure do |config|
37 | config[:table_class] = TestTable
38 | end
39 | end
40 |
41 | after do
42 | Object.send(:remove_const, :GroupModel)
43 | end
44 |
45 | let(:response) do
46 | rel = GroupModel.group("result_group_ssi")
47 | rel.load
48 | rel
49 | end
50 |
51 | let(:group_response) do
52 | response.group_by
53 | end
54 |
55 | let(:groups) do
56 | group_response.groups #.first
57 | end
58 |
59 | subject do
60 | groups.first
61 | end
62 |
63 | describe Ansr::Blacklight::Solr::Response::Group do
64 | describe "#doclist" do
65 | it "should be the raw list of documents from solr" do
66 | expect(subject.doclist).to be_a Hash
67 | expect(subject.doclist['docs'].first[:id]).to eq 1
68 | end
69 | end
70 |
71 | describe "#total" do
72 | it "should be the number of results found in a group" do
73 | expect(subject.total).to eq 2
74 | end
75 | end
76 |
77 | describe "#start" do
78 | it "should be the offset for the results in the group" do
79 | expect(subject.start).to eq 0
80 | end
81 | end
82 |
83 | describe "#docs" do
84 | it "should be a list of GroupModels" do
85 | subject.docs.each do |doc|
86 | expect(doc).to be_a_kind_of GroupModel
87 | end
88 |
89 | expect(subject.docs.first.id).to eq 1
90 | end
91 | end
92 |
93 | describe "#field" do
94 | it "should be the field the group belongs to" do
95 | expect(subject.field).to eq "result_group_ssi"
96 | end
97 | end
98 | end
99 |
100 | describe Ansr::Blacklight::Solr::Response do
101 | let(:response) do
102 | create_response(sample_response)
103 | end
104 |
105 | let(:group) do
106 | response.grouped(GroupModel).select { |x| x.key == "result_group_ssi" }.first
107 | end
108 |
109 | describe "groups" do
110 | it "should return an array of Groups" do
111 | response.grouped(GroupModel).should be_a Array
112 |
113 | expect(group.groups).to have(2).items
114 | group.groups.each do |group|
115 | expect(group).to be_a Ansr::Blacklight::Solr::Response::Group
116 | end
117 | end
118 | it "should include a list of SolrDocuments" do
119 |
120 | group.groups.each do |group|
121 | group.docs.each do |doc|
122 | expect(doc).to be_a GroupModel
123 | end
124 | end
125 | end
126 | end
127 |
128 | describe "total" do
129 | it "should return the ngroups value" do
130 | expect(group.total).to eq 3
131 | end
132 | end
133 |
134 | describe "facets" do
135 | it "should exist in the response object (not testing, we just extend the module)" do
136 | expect(group).to respond_to :facets
137 | end
138 | end
139 |
140 | describe "rows" do
141 | it "should get the rows from the response" do
142 | expect(group.rows).to eq 3
143 | end
144 | end
145 |
146 | describe "group_field" do
147 | it "should be the field name for the current group" do
148 | expect(group.group_field).to eq "result_group_ssi"
149 | end
150 | end
151 |
152 | describe "group_limit" do
153 | it "should be the number of documents to return for a group" do
154 | expect(group.group_limit).to eq 5
155 | end
156 | end
157 | end
158 |
159 | def sample_response
160 | {"responseHeader" => {"params" =>{"rows" => 3, "group.limit" => 5}},
161 | "grouped" =>
162 | {'result_group_ssi' =>
163 | {'groups' => [{'groupValue'=>"Group 1", 'doclist'=>{'numFound'=>2, 'start' => 0, 'docs'=>[{:id=>1}, {:id => 'x'}]}},
164 | {'groupValue'=>"Group 2", 'doclist'=>{'numFound'=>3, 'docs'=>[{:id=>2}, :id=>3]}}
165 | ],
166 | 'ngroups' => "3"
167 | }
168 | }
169 | }
170 | end
171 | end
--------------------------------------------------------------------------------
/ansr_dpla/fixtures/item.json:
--------------------------------------------------------------------------------
1 | {"docs":[{"_id":"bpl--oai:digitalcommonwealth.org:commonwealth:6682xj30d","dataProvider":"Boston Public Library","sourceResource":{"isPartOf":["Leslie Jones Collection","Leslie Jones Collection. Animals: Dogs & Cats"],"description":["Title from information provided by Leslie Jones or the Boston Public Library on the negative or negative sleeve.","Date supplied by cataloger."],"creator":"Jones, Leslie, 1886-1967","rights":"Copyright (c) Leslie Jones. This work is licensed for use under a Creative Commons Attribution Non-Commercial No Derivatives License (CC BY-NC-ND).","collection":[{"@id":"http://dp.la/api/collections/460c76299e1b0a46afea352b1ab8f556","id":"460c76299e1b0a46afea352b1ab8f556","title":"Leslie Jones Collection"}],"date":{"begin":"1917","end":"1934","displayDate":"1917-1934"},"extent":"1 negative : glass, black & white ; 4 x 5 in.","stateLocatedIn":[{"name":"Massachusetts"}],"title":"Kittens","identifier":["Local accession: 08_06_000884"],"type":"image","subject":[{"name":"Cats"},{"name":"Baby animals"}]},"object":"http://ark.digitalcommonwealth.org/ark:/50959/6682xj30d/thumbnail","ingestDate":"2014-02-15T15:10:38.797024","originalRecord":{"header":{"datestamp":"2013-09-20T14:14:36Z","identifier":"oai:digitalcommonwealth.org:commonwealth:6682xj30d","setSpec":"commonwealth:2j62s484w"},"metadata":{"mods:mods":{"mods:subject":[{"mods:topic":"Cats","authority":"lctgm"},{"mods:topic":"Baby animals","authority":"lctgm"}],"mods:genre":[{"displayLabel":"general","authorityURI":"http://id.loc.gov/vocabulary/graphicMaterials","#text":"Photographs","authority":"gmgpc","valueURI":"http://id.loc.gov/vocabulary/graphicMaterials/tgm007721"},{"displayLabel":"specific","authorityURI":"http://id.loc.gov/vocabulary/graphicMaterials","#text":"Glass negatives","authority":"gmgpc","valueURI":"http://id.loc.gov/vocabulary/graphicMaterials/tgm004561"}],"xmlns:mods":"http://www.loc.gov/mods/v3","mods:typeOfResource":"still image","mods:recordInfo":{"mods:languageOfCataloging":{"mods:languageTerm":{"authorityURI":"http://id.loc.gov/vocabulary/iso639-2","#text":"English","type":"text","authority":"iso639-2b","valueURI":"http://id.loc.gov/vocabulary/iso639-2/eng"}},"mods:descriptionStandard":{"#text":"gihc","authority":"marcdescription"},"mods:recordOrigin":"human prepared","mods:recordContentSource":"Boston Public Library"},"mods:titleInfo":{"usage":"primary","mods:title":"Kittens"},"mods:physicalDescription":{"mods:extent":"1 negative : glass, black & white ; 4 x 5 in.","mods:digitalOrigin":"reformatted digital","mods:internetMediaType":["image/tiff","image/jpeg"]},"mods:identifier":[{"#text":"08_06_000884","type":"local-accession"},{"#text":"http://ark.digitalcommonwealth.org/ark:/50959/6682xj30d","type":"uri"}],"version":"3.4","mods:note":["Title from information provided by Leslie Jones or the Boston Public Library on the negative or negative sleeve.","Date supplied by cataloger."],"mods:name":{"mods:namePart":["Jones, Leslie",{"#text":"1886-1967","type":"date"}],"mods:role":{"mods:roleTerm":{"authorityURI":"http://id.loc.gov/vocabulary/relators","#text":"Photographer","type":"text","authority":"marcrelator","valueURI":"http://id.loc.gov/vocabulary/relators/pht"}},"type":"personal","authority":"local"},"mods:location":[{"mods:physicalLocation":"Boston Public Library","mods:holdingSimple":{"mods:copyInformation":{"mods:subLocation":"Print Department"}}},{"mods:url":[{"access":"preview","#text":"http://ark.digitalcommonwealth.org/ark:/50959/6682xj30d/thumbnail"},{"usage":"primary","access":"object in context","#text":"http://ark.digitalcommonwealth.org/ark:/50959/6682xj30d"}]}],"mods:accessCondition":[{"#text":"Copyright (c) Leslie Jones.","type":"use and reproduction"},{"#text":"This work is licensed for use under a Creative Commons Attribution Non-Commercial No Derivatives License (CC BY-NC-ND).","type":"use and reproduction"}],"xmlns:xsi":"http://www.w3.org/2001/XMLSchema-instance","xmlns:xlink":"http://www.w3.org/1999/xlink","xsi:schemaLocation":"http://www.loc.gov/mods/v3 http://www.loc.gov/standards/mods/v3/mods-3-4.xsd","mods:originInfo":{"mods:dateCreated":[{"encoding":"w3cdtf","#text":"1917","qualifier":"approximate","keyDate":"yes","point":"start"},{"encoding":"w3cdtf","#text":"1934","qualifier":"approximate","point":"end"}]},"mods:relatedItem":[{"type":"host","mods:titleInfo":{"mods:title":"Leslie Jones Collection"}},{"type":"series","mods:titleInfo":{"mods:title":"Animals: Dogs & Cats"}}]}},"id":"oai:digitalcommonwealth.org:commonwealth:6682xj30d","collection":[{"@id":"http://dp.la/api/collections/460c76299e1b0a46afea352b1ab8f556","id":"460c76299e1b0a46afea352b1ab8f556","title":"Leslie Jones Collection"}],"provider":{"@id":"http://dp.la/api/contributor/digital-commonwealth","name":"Digital Commonwealth"}},"ingestionSequence":5,"isShownAt":"http://ark.digitalcommonwealth.org/ark:/50959/6682xj30d","provider":{"@id":"http://dp.la/api/contributor/digital-commonwealth","name":"Digital Commonwealth"},"@context":{"begin":{"@id":"dpla:dateRangeStart","@type":"xsd:date"},"@vocab":"http://purl.org/dc/terms/","hasView":"edm:hasView","name":"xsd:string","object":"edm:object","dpla":"http://dp.la/terms/","collection":"dpla:aggregation","edm":"http://www.europeana.eu/schemas/edm/","end":{"@id":"dpla:end","@type":"xsd:date"},"state":"dpla:state","aggregatedDigitalResource":"dpla:aggregatedDigitalResource","coordinates":"dpla:coordinates","isShownAt":"edm:isShownAt","stateLocatedIn":"dpla:stateLocatedIn","sourceResource":"edm:sourceResource","dataProvider":"edm:dataProvider","originalRecord":"dpla:originalRecord","provider":"edm:provider","LCSH":"http://id.loc.gov/authorities/subjects"},"ingestType":"item","@id":"http://dp.la/api/items/9f695d8c16a4978061a25076a43aa11f","id":"9f695d8c16a4978061a25076a43aa11f"}],"count":1}
--------------------------------------------------------------------------------
/ansr_blacklight/lib/ansr_blacklight/request_builders.rb:
--------------------------------------------------------------------------------
1 | module Ansr::Blacklight
2 | ##
3 | # This module contains methods that transform user parameters into parameters that are sent
4 | # as a request to Solr when RequestBuilders#solr_search_params is called.
5 | #
6 | module RequestBuilders
7 | extend ActiveSupport::Concern
8 |
9 | def local_field_params(facet_field)
10 | cf = table[facet_field]
11 | if (cf.is_a? Ansr::Arel::ConfiguredField)
12 | return cf.config.fetch(:local, {})
13 | else
14 | return {}
15 | end
16 | end
17 | # A helper method used for generating solr LocalParams, put quotes
18 | # around the term unless it's a bare-word. Escape internal quotes
19 | # if needed.
20 | def solr_param_quote(val, options = {})
21 | options[:quote] ||= '"'
22 | unless val =~ /^[a-zA-Z0-9$_\-\^]+$/
23 | val = options[:quote] +
24 | # Yes, we need crazy escaping here, to deal with regexp esc too!
25 | val.gsub("'", "\\\\\'").gsub('"', "\\\\\"") +
26 | options[:quote]
27 | end
28 | return val
29 | end
30 |
31 | ##
32 | # Take the user-entered query, and put it in the solr params,
33 | # including config's "search field" params for current search field.
34 | # also include setting spellcheck.q.
35 | def add_query_to_solr(field_key, value, opts={})
36 | ###
37 | # Merge in search field configured values, if present, over-writing general
38 | # defaults
39 | ###
40 |
41 | if (::Arel::Nodes::As === field_key)
42 | solr_request[:qt] = field_key.right.to_s
43 | field_key = field_key.left
44 | end
45 |
46 | search_field = table[field_key]
47 | ##
48 | # Create Solr 'q' including the user-entered q, prefixed by any
49 | # solr LocalParams in config, using solr LocalParams syntax.
50 | # http://wiki.apache.org/solr/LocalParams
51 | ##
52 | if (Ansr::Arel::ConfiguredField === search_field && !search_field.config.empty?)
53 | local_params = search_field.config.fetch(:local,{}).merge(opts).collect do |key, val|
54 | key.to_s + "=" + solr_param_quote(val, :quote => "'")
55 | end.join(" ")
56 | solr_request[:q] = local_params.empty? ? value : "{!#{local_params}}#{value}"
57 | search_field.config.fetch(:query,{}).each do |k,v|
58 | solr_request[k] = v
59 | end
60 | else
61 | solr_request[:q] = value if value
62 | end
63 |
64 | ##
65 | # Set Solr spellcheck.q to be original user-entered query, without
66 | # our local params, otherwise it'll try and spellcheck the local
67 | # params! Unless spellcheck.q has already been set by someone,
68 | # respect that.
69 | #
70 | # TODO: Change calling code to expect this as a symbol instead of
71 | # a string, for consistency? :'spellcheck.q' is a symbol. Right now
72 | # rspec tests for a string, and can't tell if other code may
73 | # insist on a string.
74 | solr_request["spellcheck.q"] = value unless solr_request["spellcheck.q"]
75 | end
76 |
77 | ##
78 | # Add any existing facet limits, stored in app-level HTTP query
79 | # as :f, to solr as appropriate :fq query.
80 | def add_filter_fq_to_solr(solr_request, user_params)
81 |
82 | # convert a String value into an Array
83 | if solr_request[:fq].is_a? String
84 | solr_request[:fq] = [solr_request[:fq]]
85 | end
86 |
87 | # :fq, map from :f.
88 | if ( user_params[:f])
89 | f_request_params = user_params[:f]
90 |
91 | f_request_params.each_pair do |facet_field, value_list|
92 | opts = local_field_params(facet_field).merge(user_params.fetch(:opts,{}))
93 | Array(value_list).each do |value|
94 | solr_request.append_filter_query filter_value_to_fq_string(facet_field, value, user_params[:opts])
95 | end
96 | end
97 | end
98 | end
99 |
100 | def with_ex_local_param(ex, value)
101 | if ex
102 | "{!ex=#{ex}}#{value}"
103 | else
104 | value
105 | end
106 | end
107 |
108 | private
109 |
110 | ##
111 | # Convert a filter/value pair into a solr fq parameter
112 | def filter_value_to_fq_string(facet_key, value, facet_opts=nil)
113 | facet_field = table[facet_key]
114 | facet_config = (Ansr::Arel::ConfiguredField === facet_field) ? facet_field : nil
115 | facet_default = (::Arel.star == facet_key)
116 | local_params = local_field_params(facet_key)
117 | local_params.merge!(facet_opts) if facet_opts
118 | local_params = local_params.collect {|k,v| "#{k.to_s}=#{v.to_s}"}
119 | local_params << "tag=#{facet_config.tag}" if facet_config and facet_config.tag
120 |
121 | prefix = ""
122 | prefix = "{!#{local_params.join(" ")}}" unless local_params.empty?
123 |
124 | fq = case
125 | when (facet_config and facet_config.query)
126 | facet_config.query[value][:fq] if facet_config.query[value]
127 | when (facet_config and facet_config.date)
128 | # in solr 3.2+, this could be replaced by a !term query
129 | "#{prefix}#{facet_field.name}:#{RSolr.escape(value)}"
130 | when (value.is_a?(DateTime) or value.is_a?(Date) or value.is_a?(Time))
131 | "#{prefix}#{facet_field.name}:#{RSolr.escape(value.to_time.utc.strftime("%Y-%m-%dT%H:%M:%SZ"))}"
132 | when (value.is_a?(TrueClass) or value.is_a?(FalseClass) or value == 'true' or value == 'false'),
133 | (value.is_a?(Integer) or (value.to_i.to_s == value if value.respond_to? :to_i)),
134 | (value.is_a?(Float) or (value.to_f.to_s == value if value.respond_to? :to_f))
135 | "#{prefix}#{facet_field.name}:#{RSolr.escape(value.to_s)}"
136 | when value.is_a?(Range)
137 | "#{prefix}#{facet_field.name}:[#{RSolr.escape(value.first.to_s)} TO #{RSolr.escape(value.last.to_s)}]"
138 | else
139 | "{!raw f=#{facet_field.name}#{(" " + local_params.join(" ")) unless local_params.empty?}}#{value}"
140 | end
141 |
142 |
143 | end
144 | end
145 | end
--------------------------------------------------------------------------------
/ansr_dpla/lib/ansr_dpla/arel/visitors/query_builder.rb:
--------------------------------------------------------------------------------
1 | module Ansr::Dpla::Arel::Visitors
2 | class QueryBuilder < Ansr::Arel::Visitors::QueryBuilder
3 | attr_reader :query_opts
4 |
5 | def initialize(table, query_opts=nil)
6 | super(table)
7 | @query_opts = query_opts ||= Ansr::Dpla::Request.new
8 | @query_opts.path = table.name
9 | end
10 |
11 | # determines whether multiple values should accumulate or overwrite in merges
12 | def multiple?(field_key)
13 | true
14 | end
15 |
16 | def visit_String o, a
17 | case a
18 | when Ansr::Arel::Visitors::From
19 | query_opts.path = o
20 | when Ansr::Arel::Visitors::Facet
21 | filter_field(o.to_sym)
22 | when Ansr::Arel::Visitors::Order
23 | order(o)
24 | else
25 | raise "visited String \"#{o}\" with #{a.to_s}"
26 | end
27 | end
28 |
29 | def visit_Arel_SqlLiteral(n, attribute)
30 | select_val = n.to_s.split(" AS ")
31 | case attribute
32 | when Ansr::Arel::Visitors::Order
33 | order(n.to_s)
34 | when Ansr::Arel::Visitors::Facet
35 | filter_field(select_val[0].to_sym)
36 | else
37 | field(select_val[0].to_sym)
38 | if select_val[1]
39 | query_opts.aliases ||= {}
40 | query_opts.aliases[select_val[0]] = select_val[1]
41 | end
42 | end
43 | end
44 |
45 | def visit_Ansr_Arel_Nodes_Filter(object, attribute)
46 | expr = object.expr
47 | case expr
48 | when ::Arel::SqlLiteral
49 | visit expr, Ansr::Arel::Visitors::Filter.new(attribute) if object.select
50 | when ::Arel::Attributes::Attribute
51 | name = object.expr.name
52 | name = "#{expr.relation.name}.#{name}" if expr.relation.name.to_s != table.name.to_s
53 | visit name, Ansr::Arel::Visitors::Filter.new(attribute) if object.select
54 | else
55 | raise "Unexpected filter expression type #{object.expr.class}"
56 | end
57 | end
58 |
59 | def visit_Ansr_Arel_Nodes_Facet(object, attribute)
60 | expr = object.expr
61 | case expr
62 | when ::Arel::SqlLiteral
63 | visit expr, Ansr::Arel::Visitors::Facet.new(attribute)
64 | when ::Arel::Attributes::Attribute
65 | name = object.expr.name
66 | name = "#{expr.relation.name}.#{name}" if expr.relation.name.to_s != table.name.to_s
67 | visit name, Ansr::Arel::Visitors::Facet.new(attribute) if object.select
68 | else
69 | raise "Unexpected filter expression type #{object.expr.class}"
70 | end
71 | end
72 |
73 | def projections
74 | query_opts[:fields] || []
75 | end
76 |
77 | def filter_projections
78 | query_opts[:facets] || []
79 | end
80 |
81 |
82 | def field(field_name)
83 | return unless field_name
84 | old = query_opts[:fields] ? Array(query_opts[:fields]) : []
85 | field_names = (old + Array(field_name)).uniq
86 | if field_names[0]
87 | query_opts[:fields] = field_names[1] ? field_names : field_names[0]
88 | end
89 | end
90 |
91 | def filter_field(field_name)
92 | return unless field_name
93 | field_name = Array(field_name)
94 | field_name.each {|fn| raise "#{fn} is not a facetable field" unless table.facets.include? fn.to_sym}
95 | old = query_opts[:facets] ? Array(query_opts[:facets]) : []
96 | field_names = (old + Array(field_name)).uniq
97 | if field_names[0]
98 | query_opts[:facets] = field_names[1] ? field_names : field_names[0]
99 | end
100 | end
101 |
102 | def add_where_clause(attr_node, val)
103 | field_key = field_key_from_node(attr_node)
104 | if query_opts[field_key]
105 | query_opts[field_key] = Array(query_opts[field_key]) << val
106 | else
107 | query_opts[field_key] = val
108 | end
109 | end
110 |
111 | # the DPLA API makes no distinction between filter and normal queries
112 | def visit_Arel_Nodes_Equality(object, attribute)
113 | add_where_clause(object.left, object.right)
114 | end
115 |
116 | def visit_Arel_Nodes_NotEqual(object, attribute)
117 | add_where_clause(object.left, "NOT " + object.right)
118 | end
119 | def visit_Arel_Nodes_Or(object, attribute)
120 | add_where_clause(object.left, "OR " + object.right)
121 | end
122 |
123 | def visit_Arel_Nodes_Grouping(object, attribute)
124 | visit object.expr, attribute
125 | end
126 |
127 | def visit_Arel_Nodes_Ordering(object, attribute)
128 | if query_opts[:sort_by]
129 | query_opts[:sort_by] = Array[query_opts[:sort_by]] << object.expr.name
130 | else
131 | query_opts[:sort_by] = object.expr.name
132 | end
133 | direction = :asc if (::Arel::Nodes::Ascending === object and direction)
134 | direction = :desc if (::Arel::Nodes::Descending === object)
135 | query_opts[:sort_order] = direction if direction
136 | end
137 |
138 | def order(*arel_nodes)
139 | direction = nil
140 | nodes = []
141 | arel_nodes.inject(nodes) do |c, n|
142 | if ::Arel::Nodes::Ordering === n
143 | c << n
144 | elsif n.is_a? String
145 | _ns = n.split(',')
146 | _ns.each do |_n|
147 | _p = _n.split(/\s+/)
148 | if (_p[1])
149 | _p[1] = _p[1].downcase.to_sym
150 | else
151 | _p[1] = :asc
152 | end
153 | c << table[_p[0].to_sym].send(_p[1])
154 | end
155 | end
156 | c
157 | end
158 | nodes.each do |node|
159 | if ::Arel::Nodes::Ordering === node
160 | if query_opts[:sort_by]
161 | query_opts[:sort_by] = Array[query_opts[:sort_by]] << node.expr.name
162 | else
163 | query_opts[:sort_by] = node.expr.name
164 | end
165 | direction = :asc if (::Arel::Nodes::Ascending === node and direction)
166 | direction = :desc if (::Arel::Nodes::Descending === node)
167 | end
168 | end
169 | query_opts[:sort_order] = direction if direction
170 | end
171 |
172 | def visit_Arel_Nodes_Limit(object, attribute)
173 | value = object.expr
174 | if value and (value = value.to_i)
175 | raise "Page size cannot be > 500 (#{value}" if value > 500
176 | query_opts[:page_size] = value
177 | end
178 | end
179 |
180 | def visit_Arel_Nodes_Offset(object, attribute)
181 | value = object.expr
182 | if value
183 | query_opts[:page] = (value.to_i / (query_opts[:page_size] || Ansr::Relation::DEFAULT_PAGE_SIZE)) + 1
184 | end
185 | end
186 |
187 | end
188 | end
--------------------------------------------------------------------------------
/ansr_blacklight/lib/ansr_blacklight/arel/visitors/query_builder.rb:
--------------------------------------------------------------------------------
1 | module Ansr::Blacklight::Arel::Visitors
2 | class QueryBuilder < Ansr::Arel::Visitors::QueryBuilder
3 | include Ansr::Blacklight::RequestBuilders
4 | attr_reader :solr_request, :path
5 |
6 | def initialize(table)
7 | super(table)
8 | @solr_request = Ansr::Blacklight::Solr::Request.new
9 | table.configure_fields.each do |k,v|
10 | unless v[:select].blank?
11 | v[:select].each do |sk, sv|
12 | key = "f.#{k}.#{sk}".to_sym
13 | @solr_request[key] = sv
14 | end
15 | end
16 | end
17 | @path = table.name || 'select'
18 | end
19 |
20 | public
21 | def query_opts
22 | solr_request
23 | end
24 |
25 | # determines whether multiple values should accumulate or overwrite in merges
26 | def multiple?(field_key)
27 | true
28 | end
29 |
30 | def visit_String o, a
31 | case a
32 | when Ansr::Arel::Visitors::From
33 | query_opts.path = o
34 | when Ansr::Arel::Visitors::Filter
35 | filter_field(o.to_sym)
36 | when Ansr::Arel::Visitors::Order
37 | order(o)
38 | when Ansr::Arel::Visitors::ProjectionTraits
39 | select_val = o.to_s.split(" AS ")
40 | field(select_val[0].to_sym)
41 | if select_val[1]
42 | query_opts.aliases ||= {}
43 | query_opts.aliases[select_val[0]] = select_val[1]
44 | end
45 | else
46 | raise "visited String \"#{o}\" with #{a.to_s}"
47 | end
48 | end
49 |
50 |
51 | def visit_Arel_Nodes_TableAlias(object, attribute)
52 | solr_request[:qt] = object.name.to_s
53 | opts = {qt: object.name.to_s}
54 | if (cf = table[object.name]).is_a? Ansr::Arel::ConfiguredField
55 | opts.merge!(cf.config.fetch(:query,{}))
56 | end
57 | solr_request.merge!(opts)
58 | visit object.relation, attribute
59 | end
60 |
61 | def visit_Ansr_Arel_Nodes_ProjectionTraits(object, attribute)
62 | solr_request[:wt] = object.wt if object.wt
63 | solr_request[:defType] = object.defType if object.defType
64 | visit(object.expr, Ansr::Arel::Visitors::ProjectionTraits.new(attribute))
65 | end
66 |
67 | def visit_Arel_SqlLiteral(n, attribute)
68 | select_val = n.to_s.split(" AS ")
69 | if Ansr::Arel::Visitors::Filter === attribute
70 | solr_request.append_facet_fields(select_val[0].to_sym)
71 | else
72 | field(select_val[0].to_sym)
73 | if select_val[1]
74 | query_opts.aliases ||= {}
75 | query_opts.aliases[select_val[0]] = select_val[1]
76 | end
77 | end
78 | end
79 |
80 | def from(value)
81 | if value.respond_to? :name
82 | solr_request.path = value.name
83 | else
84 | solr_request.path = value.to_s
85 | end
86 | self.table=value if (value.is_a? Ansr::Arel::BigTable)
87 | end
88 |
89 | def field(field_name)
90 | return unless field_name
91 | old = query_opts[:fields] ? Array(query_opts[:fields]) : []
92 | field_names = (old + Array(field_name)).uniq
93 | if field_names[0]
94 | query_opts[:fields] = field_names[1] ? field_names : field_names[0]
95 | end
96 | end
97 |
98 | def filter_field(field_name)
99 | return unless field_name
100 | old = solr_request[:"facet.field"] ? Array(solr_request[:"facet.field"]) : []
101 | fields = Array(field_name).delete_if {|x| old.include? x}
102 | solr_request.append_facet_fields(fields)
103 | end
104 |
105 | def visit_Arel_Nodes_Equality(object, attribute)
106 | field_key = (object.left.respond_to? :expr) ? field_key_from_node(object.left.expr) : field_key_from_node(object.left)
107 | opts = {}
108 | opts.merge!(local_field_params(field_key))
109 | opts.merge!(object.left.config.fetch(:local,{})) if object.left.respond_to? :config
110 | if Ansr::Arel::Visitors::Filter === attribute or Ansr::Arel::Nodes::Filter === object.left
111 | add_filter_fq_to_solr(solr_request, f: {field_key => object.right}, opts: opts)
112 | else
113 | # check the table for configured fields
114 | add_query_to_solr(field_key, object.right, opts)
115 | end
116 | end
117 |
118 | def visit_Arel_Nodes_NotEqual(object, attribute)
119 | end
120 |
121 | def visit_Arel_Nodes_Or(object, attribute)
122 | end
123 |
124 | def visit_Arel_Nodes_Grouping(object, attribute)
125 | visit object.expr, attribute
126 | end
127 |
128 | def visit_Arel_Nodes_Group(object, attribute)
129 | solr_request[:group] = object.expr.to_s
130 | end
131 |
132 | def visit_Ansr_Arel_Nodes_Facet(object, attribute)
133 | name = object.expr
134 | name = name.name if name.respond_to? :name
135 | default = object.pivot || object.query
136 | if name == ::Arel.star
137 | prefix = "facet."
138 | default = true
139 | else
140 | filter_field(name.to_sym) unless default
141 | solr_request.append_facet_fields(name.to_sym) unless default
142 | prefix = "f.#{name}.facet."
143 | end
144 | # there's got to be a helper for this
145 | if object.pivot
146 | solr_request.append_facet_pivot with_ex_local_param(object.ex, Array(object.pivot).join(","))
147 | elsif object.query
148 | solr_request.append_facet_query object.query.map { |k, x| with_ex_local_param(object.ex, x[:fq]) }
149 | else
150 | object.opts.each do |att, value|
151 | solr_request["#{prefix}#{att.to_s}".to_sym] = value.to_s unless att == :ex
152 | end
153 | solr_request.append_facet_fields with_ex_local_param(object.ex, name.to_sym) unless default
154 | end
155 | end
156 |
157 | def visit_Ansr_Arel_Nodes_Spellcheck(object, attribute)
158 | unless object.expr == false
159 | solr_request[:spellcheck] = object.expr.to_s
160 | end
161 | object.opts.each do |att, val|
162 | solr_request["spellcheck.#{att.to_s}".to_sym] = val if att != :select
163 | end
164 | end
165 |
166 | def visit_Ansr_Arel_Nodes_Highlight(object, attribute)
167 | unless object.expr == false or object.expr == true
168 | solr_request[:hl] = object.expr.to_s
169 | end
170 | object.opts.each do |att, val|
171 | solr_request["hl.#{att.to_s}".to_sym] = val if att != :select
172 | end
173 | end
174 |
175 | def order(*arel_nodes)
176 | direction = nil
177 | nodes = []
178 | arel_nodes.inject(nodes) do |c, n|
179 | if ::Arel::Nodes::Ordering === n
180 | c << n
181 | elsif n.is_a? String
182 | _ns = n.split(/,\s*/)
183 | _ns.each do |_n|
184 | _p = _n.split(/\s+/)
185 | if (_p[1])
186 | _p[1] = _p[1].downcase.to_sym
187 | else
188 | _p[1] = :asc
189 | end
190 | c << table[_p[0].to_sym].send(_p[1])
191 | end
192 | end
193 | c
194 | end
195 | nodes.each do |node|
196 | if ::Arel::Nodes::Ordering === node
197 | if solr_request[:sort_by]
198 | solr_request[:sort_by] = Array[solr_request[:sort_by]] << node.expr.name
199 | else
200 | solr_request[:sort_by] = node.expr.name
201 | end
202 | direction = :asc if (::Arel::Nodes::Ascending === node and direction)
203 | direction = :desc if (::Arel::Nodes::Descending === node)
204 | end
205 | end
206 | solr_request[:sort_order] = direction if direction
207 | end
208 |
209 | def visit_Arel_Nodes_Limit(object, attribute)
210 | value = object.expr
211 | if value and (value = value.to_i)
212 | raise "Page size cannot be > 500 (#{value}" if value > 500
213 | solr_request[:rows] = value.to_s
214 | end
215 | end
216 |
217 | def visit_Arel_Nodes_Offset(object, attribute)
218 | value = object.expr
219 | if value
220 | solr_request[:start] = value.to_s
221 | end
222 | end
223 |
224 | end
225 | end
--------------------------------------------------------------------------------
/lib/ansr/relation/query_methods.rb:
--------------------------------------------------------------------------------
1 | require 'active_record'
2 | module Ansr
3 | module QueryMethods
4 | include Ansr::Configurable
5 | include ActiveRecord::QueryMethods
6 |
7 | ActiveRecord::QueryMethods::WhereChain.class_eval <<-RUBY
8 | def or(opts, *rest)
9 | where_value = @scope.send(:build_where, opts, rest).map do |rel|
10 | case rel
11 | when ::Arel::Nodes::In
12 | ::Arel::Nodes::Or.new(rel.left, rel.right)
13 | when ::Arel::Nodes::Equality
14 | ::Arel::Nodes::Or.new(rel.left, rel.right)
15 | when String
16 | ::Arel::Nodes::Or.new(::Arel::Nodes::SqlLiteral.new(rel))
17 | else
18 | ::Arel::Nodes::Or.new(rel)
19 | end
20 | end
21 | @scope.where_values += where_value.flatten
22 | @scope
23 | end
24 | RUBY
25 |
26 |
27 | def filter_values
28 | @values[:filter] ||= []
29 | end
30 |
31 | def filter_values=(values)
32 | raise ImmutableRelation if @loaded
33 | @values[:filter] = values
34 | end
35 |
36 | def filter(expr)
37 | check_if_method_has_arguments!("filter", expr)
38 | spawn.filter!(expr)
39 | end
40 |
41 | def filter!(expr)
42 | return self if expr.empty?
43 |
44 | filter_nodes = build_where(expr)
45 | return self unless filter_nodes
46 | filters = []
47 | filter_nodes.each do |filter_node|
48 | case filter_node
49 | when ::Arel::Nodes::In, ::Arel::Nodes::Equality
50 | filter_node.left = Ansr::Arel::Nodes::Filter.new(filter_node.left)
51 | when ::Arel::SqlLiteral
52 | filter_node = Ansr::Arel::Nodes::Filter.new(filter_node)
53 | when String, Symbol
54 | filter_node = Ansr::Arel::Nodes::Filter.new(::Arel::SqlLiteral.new(filter_node.to_s))
55 | else
56 | raise "unexpected filter node type #{filter_node.class}"
57 | end
58 | filters << filter_node
59 | end
60 | self.filter_values+= filters
61 |
62 | self
63 | end
64 |
65 | def filter_unscoping(target_value)
66 | target_value_sym = target_value.to_sym
67 |
68 | filter_values.reject! do |rel|
69 | case rel
70 | when ::Arel::Nodes::In, ::Arel::Nodes::Equality
71 | subrelation = (rel.left.kind_of?(::Arel::Attributes::Attribute) ? rel.left : rel.right)
72 | subrelation.name.to_sym == target_value_sym
73 | else
74 | raise "unscope(filter: #{target_value.inspect}) failed: unscoping #{rel.class} is unimplemented."
75 | end
76 | end
77 | end
78 |
79 | def facet(expr, opts = {})
80 | spawn.facet!(expr, opts)
81 | end
82 |
83 | def facet!(expr, opts={})
84 | self.facet_values+= build_facets(expr, opts)
85 | self
86 | end
87 |
88 | def facet_values
89 | @values[:facets] || []
90 | end
91 |
92 | def facet_values=(values)
93 | raise ActiveRecord::ImmutableRelation if @loaded
94 | @values[:facets]=values
95 | end
96 |
97 | def facet_unscoping(target_value)
98 | end
99 |
100 | def filter_name(expr)
101 | connection.sanitize_filter_name(field_name(expr))
102 | end
103 |
104 | def as(args)
105 | spawn.as!(args)
106 | end
107 |
108 | def as!(args)
109 | self.as_value= args
110 | self
111 | end
112 |
113 | def as_value
114 | @values[:as]
115 | end
116 |
117 | def as_value=(args)
118 | raise ActiveRecord::ImmutableRelation if @loaded
119 | @values[:as] = args
120 | end
121 |
122 | def as_unscoping(*args)
123 | @values.delete(:as)
124 | end
125 |
126 | def highlight(expr, opts={})
127 | spawn.highlight!(expr, opts)
128 | end
129 |
130 | def highlight!(expr, opts = {})
131 | self.highlight_value= Ansr::Arel::Nodes::Highlight.new(expr, opts)
132 | end
133 |
134 | def highlight_value
135 | @values[:highlight]
136 | end
137 |
138 | def highlight_value=(val)
139 | raise ActiveRecord::ImmutableRelation if @loaded
140 | @values[:highlight] = val
141 | end
142 |
143 | def highlight_unscoping(*args)
144 | @values.delete(:highlight)
145 | end
146 |
147 | def spellcheck(expr, opts={})
148 | spawn.spellcheck!(expr, opts)
149 | end
150 |
151 | def spellcheck!(expr, opts = {})
152 | self.spellcheck_value= Ansr::Arel::Nodes::Spellcheck.new(expr, opts)
153 | end
154 |
155 | def spellcheck_value
156 | @values[:spellcheck]
157 | end
158 |
159 | def spellcheck_value=(val)
160 | raise ActiveRecord::ImmutableRelation if @loaded
161 | @values[:spellcheck] = val
162 | end
163 |
164 | def spellcheck_unscoping(*args)
165 | @values.delete(:spellcheck)
166 | end
167 |
168 | def field_name(expr)
169 | if expr.is_a? Array
170 | return expr.collect{|x| field_name(x)}.compact
171 | else
172 | case expr
173 | when ::Arel::Nodes::Binary
174 | if expr.left.relation.name != model().table.name
175 | # oof, this is really hacky
176 | field_name = "#{expr.left.relation.name}.#{expr.left.name}".to_sym
177 | field_name = expr.left.name.to_sym
178 | else
179 | field_name = expr.left.name.to_sym
180 | end
181 | when ::Arel::Attributes::Attribute, Ansr::Arel::ConfiguredField
182 | if expr.relation.name != model().table.name
183 | # oof, this is really hacky
184 | field_name = "#{expr.relation.name}.#{expr.name}".to_sym
185 | field_name = expr.name.to_sym
186 | else
187 | field_name = expr.name.to_sym
188 | end
189 | when ::Arel::Nodes::Unary, Ansr::Arel::Nodes::Filter
190 | if expr.expr.relation.name != model().table.name
191 | # oof, this is really hacky
192 | field_name = "#{expr.expr.relation.name}.#{expr.expr.name}".to_sym
193 | field_name = expr.expr.name.to_sym
194 | else
195 | field_name = expr.expr.name.to_sym
196 | end
197 | else
198 | field_name = expr.to_sym
199 | end
200 | return field_name
201 | end
202 | end
203 |
204 | def all_facet_fields
205 | FACETS
206 | end
207 |
208 | def all_sort_fields
209 | SORTS
210 | end
211 |
212 | def find(id)
213 | klass = model()
214 | rel = where(klass.table.primary_key.name => id).limit(1)
215 | rel.to_a
216 | unless rel.to_a[0]
217 | raise 'Bad ID'
218 | end
219 | rel.to_a.first
220 | end
221 |
222 | def collapse_wheres(arel, wheres)
223 | predicates = wheres.map do |where|
224 | next where if ::Arel::Nodes::Equality === where
225 | where = Arel.sql(where) if String === where # SqlLiteral-ize
226 | ::Arel::Nodes::Grouping.new(where)
227 | end
228 |
229 | arel.where(::Arel::Nodes::And.new(predicates)) if predicates.present?
230 | end
231 |
232 | def collapse_filters(arel, filters)
233 | predicates = filters.map do |filter|
234 | next filter if ::Arel::Nodes::Equality === filter
235 | filter = Arel.sql(filter) if String === filter # SqlLiteral-ize
236 | ::Arel::Nodes::Grouping.new(filter)
237 | end
238 |
239 | arel.where(::Arel::Nodes::And.new(predicates)) if predicates.present?
240 | end
241 |
242 | # Could filtering be moved out of intersection into one arel tree?
243 | def build_arel
244 | arel = super
245 | collapse_filters(arel, (filter_values).uniq)
246 | arel.projections << @values[:spellcheck] if @values[:spellcheck]
247 | arel.projections << @values[:highlight] if @values[:highlight]
248 | arel.projections += facet_values
249 | arel.from arel.create_table_alias(arel.source.left, as_value) if as_value
250 | arel
251 | end
252 |
253 | # cloning from ActiveRecord::QueryMethods.build_where as a first pass
254 | def build_facets(expr, opts={})
255 | case expr
256 | when Hash
257 | build_facets(::Arel.star,expr)
258 | when Array
259 | r = expr.inject([]) {|m,e| m.concat build_facets(e,opts)}
260 | when String, Symbol, ::Arel::Nodes::SqlLiteral
261 | [Ansr::Arel::Nodes::Facet.new(::Arel::Attributes::Attribute.new(table, expr.to_s), opts)]
262 | when ::Arel::Attributes::Attribute
263 | [Ansr::Arel::Nodes::Facet.new(expr, opts)]
264 | else
265 | [expr]
266 | end
267 | end
268 |
269 | # cloning from ActiveRecord::QueryMethods.build_where to use our PredicateBuilder
270 | def build_where(opts, other = [])
271 | case opts
272 | when String, Array
273 | #TODO: Remove duplication with: /activerecord/lib/active_record/sanitization.rb:113
274 | values = Hash === other.first ? other.first.values : other
275 |
276 | values.grep(ActiveRecord::Relation) do |rel|
277 | self.bind_values += rel.bind_values
278 | end
279 |
280 | [@klass.send(:sanitize_sql, other.empty? ? opts : ([opts] + other))]
281 | when Hash
282 | attributes = @klass.send(:expand_hash_conditions_for_aggregates, opts)
283 |
284 | attributes.values.grep(ActiveRecord::Relation) do |rel|
285 | self.bind_values += rel.bind_values
286 | end
287 |
288 | PredicateBuilder.build_from_hash(klass, attributes, table)
289 | else
290 | [opts]
291 | end
292 | end
293 |
294 | def find_by_nosql(*args)
295 | model.connection.execute(ansr_query(*args))
296 | end
297 |
298 | def ansr_query(*args)
299 | if args.first
300 | arel, bind_values = [args[0], args[1]]
301 | @ansr_query = model.connection.to_nosql(arel, bind_values)
302 | end
303 | @ansr_query
304 | end
305 | end
306 | end
--------------------------------------------------------------------------------
/ansr_dpla/fixtures/item.jsonld:
--------------------------------------------------------------------------------
1 | {
2 | "docs": [
3 | {
4 | "_id": "bpl--oai:digitalcommonwealth.org:commonwealth:6682xj30d",
5 | "dataProvider": "Boston Public Library",
6 | "sourceResource": {
7 | "isPartOf": [
8 | "Leslie Jones Collection",
9 | "Leslie Jones Collection. Animals: Dogs & Cats"
10 | ],
11 | "description": [
12 | "Title from information provided by Leslie Jones or the Boston Public Library on the negative or negative sleeve.",
13 | "Date supplied by cataloger."
14 | ],
15 | "creator": "Jones, Leslie, 1886-1967",
16 | "rights": "Copyright (c) Leslie Jones. This work is licensed for use under a Creative Commons Attribution Non-Commercial No Derivatives License (CC BY-NC-ND).",
17 | "collection": [
18 | {
19 | "@id": "http://dp.la/api/collections/460c76299e1b0a46afea352b1ab8f556",
20 | "id": "460c76299e1b0a46afea352b1ab8f556",
21 | "title": "Leslie Jones Collection"
22 | }
23 | ],
24 | "date": {
25 | "begin": "1917",
26 | "end": "1934",
27 | "displayDate": "1917-1934"
28 | },
29 | "extent": "1 negative : glass, black & white ; 4 x 5 in.",
30 | "stateLocatedIn": [
31 | {
32 | "name": "Massachusetts"
33 | }
34 | ],
35 | "title": "Kittens",
36 | "identifier": [
37 | "Local accession: 08_06_000884"
38 | ],
39 | "type": "image",
40 | "subject": [
41 | {
42 | "name": "Cats"
43 | },
44 | {
45 | "name": "Baby animals"
46 | }
47 | ]
48 | },
49 | "object": "http://ark.digitalcommonwealth.org/ark:/50959/6682xj30d/thumbnail",
50 | "ingestDate": "2014-02-15T15:10:38.797024",
51 | "originalRecord": {
52 | "header": {
53 | "datestamp": "2013-09-20T14:14:36Z",
54 | "identifier": "oai:digitalcommonwealth.org:commonwealth:6682xj30d",
55 | "setSpec": "commonwealth:2j62s484w"
56 | },
57 | "metadata": {
58 | "mods:mods": {
59 | "mods:subject": [
60 | {
61 | "mods:topic": "Cats",
62 | "authority": "lctgm"
63 | },
64 | {
65 | "mods:topic": "Baby animals",
66 | "authority": "lctgm"
67 | }
68 | ],
69 | "mods:genre": [
70 | {
71 | "displayLabel": "general",
72 | "authorityURI": "http://id.loc.gov/vocabulary/graphicMaterials",
73 | "#text": "Photographs",
74 | "authority": "gmgpc",
75 | "valueURI": "http://id.loc.gov/vocabulary/graphicMaterials/tgm007721"
76 | },
77 | {
78 | "displayLabel": "specific",
79 | "authorityURI": "http://id.loc.gov/vocabulary/graphicMaterials",
80 | "#text": "Glass negatives",
81 | "authority": "gmgpc",
82 | "valueURI": "http://id.loc.gov/vocabulary/graphicMaterials/tgm004561"
83 | }
84 | ],
85 | "xmlns:mods": "http://www.loc.gov/mods/v3",
86 | "mods:typeOfResource": "still image",
87 | "mods:recordInfo": {
88 | "mods:languageOfCataloging": {
89 | "mods:languageTerm": {
90 | "authorityURI": "http://id.loc.gov/vocabulary/iso639-2",
91 | "#text": "English",
92 | "type": "text",
93 | "authority": "iso639-2b",
94 | "valueURI": "http://id.loc.gov/vocabulary/iso639-2/eng"
95 | }
96 | },
97 | "mods:descriptionStandard": {
98 | "#text": "gihc",
99 | "authority": "marcdescription"
100 | },
101 | "mods:recordOrigin": "human prepared",
102 | "mods:recordContentSource": "Boston Public Library"
103 | },
104 | "mods:titleInfo": {
105 | "usage": "primary",
106 | "mods:title": "Kittens"
107 | },
108 | "mods:physicalDescription": {
109 | "mods:extent": "1 negative : glass, black & white ; 4 x 5 in.",
110 | "mods:digitalOrigin": "reformatted digital",
111 | "mods:internetMediaType": [
112 | "image/tiff",
113 | "image/jpeg"
114 | ]
115 | },
116 | "mods:identifier": [
117 | {
118 | "#text": "08_06_000884",
119 | "type": "local-accession"
120 | },
121 | {
122 | "#text": "http://ark.digitalcommonwealth.org/ark:/50959/6682xj30d",
123 | "type": "uri"
124 | }
125 | ],
126 | "version": "3.4",
127 | "mods:note": [
128 | "Title from information provided by Leslie Jones or the Boston Public Library on the negative or negative sleeve.",
129 | "Date supplied by cataloger."
130 | ],
131 | "mods:name": {
132 | "mods:namePart": [
133 | "Jones, Leslie",
134 | {
135 | "#text": "1886-1967",
136 | "type": "date"
137 | }
138 | ],
139 | "mods:role": {
140 | "mods:roleTerm": {
141 | "authorityURI": "http://id.loc.gov/vocabulary/relators",
142 | "#text": "Photographer",
143 | "type": "text",
144 | "authority": "marcrelator",
145 | "valueURI": "http://id.loc.gov/vocabulary/relators/pht"
146 | }
147 | },
148 | "type": "personal",
149 | "authority": "local"
150 | },
151 | "mods:location": [
152 | {
153 | "mods:physicalLocation": "Boston Public Library",
154 | "mods:holdingSimple": {
155 | "mods:copyInformation": {
156 | "mods:subLocation": "Print Department"
157 | }
158 | }
159 | },
160 | {
161 | "mods:url": [
162 | {
163 | "access": "preview",
164 | "#text": "http://ark.digitalcommonwealth.org/ark:/50959/6682xj30d/thumbnail"
165 | },
166 | {
167 | "usage": "primary",
168 | "access": "object in context",
169 | "#text": "http://ark.digitalcommonwealth.org/ark:/50959/6682xj30d"
170 | }
171 | ]
172 | }
173 | ],
174 | "mods:accessCondition": [
175 | {
176 | "#text": "Copyright (c) Leslie Jones.",
177 | "type": "use and reproduction"
178 | },
179 | {
180 | "#text": "This work is licensed for use under a Creative Commons Attribution Non-Commercial No Derivatives License (CC BY-NC-ND).",
181 | "type": "use and reproduction"
182 | }
183 | ],
184 | "xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance",
185 | "xmlns:xlink": "http://www.w3.org/1999/xlink",
186 | "xsi:schemaLocation": "http://www.loc.gov/mods/v3 http://www.loc.gov/standards/mods/v3/mods-3-4.xsd",
187 | "mods:originInfo": {
188 | "mods:dateCreated": [
189 | {
190 | "encoding": "w3cdtf",
191 | "#text": "1917",
192 | "qualifier": "approximate",
193 | "keyDate": "yes",
194 | "point": "start"
195 | },
196 | {
197 | "encoding": "w3cdtf",
198 | "#text": "1934",
199 | "qualifier": "approximate",
200 | "point": "end"
201 | }
202 | ]
203 | },
204 | "mods:relatedItem": [
205 | {
206 | "type": "host",
207 | "mods:titleInfo": {
208 | "mods:title": "Leslie Jones Collection"
209 | }
210 | },
211 | {
212 | "type": "series",
213 | "mods:titleInfo": {
214 | "mods:title": "Animals: Dogs & Cats"
215 | }
216 | }
217 | ]
218 | }
219 | },
220 | "id": "oai:digitalcommonwealth.org:commonwealth:6682xj30d",
221 | "collection": [
222 | {
223 | "@id": "http://dp.la/api/collections/460c76299e1b0a46afea352b1ab8f556",
224 | "id": "460c76299e1b0a46afea352b1ab8f556",
225 | "title": "Leslie Jones Collection"
226 | }
227 | ],
228 | "provider": {
229 | "@id": "http://dp.la/api/contributor/digital-commonwealth",
230 | "name": "Digital Commonwealth"
231 | }
232 | },
233 | "ingestionSequence": 5,
234 | "isShownAt": "http://ark.digitalcommonwealth.org/ark:/50959/6682xj30d",
235 | "provider": {
236 | "@id": "http://dp.la/api/contributor/digital-commonwealth",
237 | "name": "Digital Commonwealth"
238 | },
239 | "@context": {
240 | "begin": {
241 | "@id": "dpla:dateRangeStart",
242 | "@type": "xsd:date"
243 | },
244 | "@vocab": "http://purl.org/dc/terms/",
245 | "hasView": "edm:hasView",
246 | "name": "xsd:string",
247 | "object": "edm:object",
248 | "dpla": "http://dp.la/terms/",
249 | "collection": "dpla:aggregation",
250 | "edm": "http://www.europeana.eu/schemas/edm/",
251 | "end": {
252 | "@id": "dpla:end",
253 | "@type": "xsd:date"
254 | },
255 | "state": "dpla:state",
256 | "aggregatedDigitalResource": "dpla:aggregatedDigitalResource",
257 | "coordinates": "dpla:coordinates",
258 | "isShownAt": "edm:isShownAt",
259 | "stateLocatedIn": "dpla:stateLocatedIn",
260 | "sourceResource": "edm:sourceResource",
261 | "dataProvider": "edm:dataProvider",
262 | "originalRecord": "dpla:originalRecord",
263 | "provider": "edm:provider",
264 | "LCSH": "http://id.loc.gov/authorities/subjects"
265 | },
266 | "ingestType": "item",
267 | "@id": "http://dp.la/api/items/9f695d8c16a4978061a25076a43aa11f",
268 | "id": "9f695d8c16a4978061a25076a43aa11f"
269 | }
270 | ],
271 | "count": 1
272 | }
--------------------------------------------------------------------------------
/ansr_dpla/spec/lib/relation_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Ansr::Dpla::Relation do
4 | before do
5 | @kittens = read_fixture('kittens.jsonld')
6 | @faceted = read_fixture('kittens_faceted.jsonld')
7 | @empty = read_fixture('empty.jsonld')
8 | @mock_api = double('api')
9 | Item.config {|x| x[:api_key] = :foo }
10 | Item.engine.api= @mock_api
11 | end
12 |
13 | subject { Ansr::Dpla::Relation.new(Item, Item.table) }
14 |
15 | describe "#initialize" do
16 | it "should identify the correct resource for the model" do
17 | class Foo
18 | def self.table
19 | nil
20 | end
21 | end
22 |
23 | test = Ansr::Dpla::Relation.new(Foo, Foo.table)
24 | expect(test.resource).to eql(:foos)
25 | end
26 | end
27 |
28 | describe ".load" do
29 | it "should fetch the appropriate REST resource" do
30 | test = subject.where(:q=>'kittens')
31 | @mock_api.should_receive(:items).with(:q => 'kittens').and_return('')
32 | test.load
33 | end
34 |
35 | describe "Relation attributes" do
36 | it "should set attributes correctly from a response" do
37 | test = subject.where(:q=>'kittens')
38 | @mock_api.stub(:items).and_return(@kittens)
39 | test.load
40 | expect(test.count).to eql(144)
41 | expect(test.offset_value).to eql(0)
42 | expect(test.limit_value).to eql(10)
43 | end
44 |
45 | it "should set attributes correctly for an empty response" do
46 | test = subject.where(:q=>'kittens')
47 | @mock_api.stub(:items).and_return(@empty)
48 | test.load
49 | expect(test.count).to eql(0)
50 | expect(test.offset_value).to eql(0)
51 | expect(test.limit_value).to eql(10)
52 | end
53 | end
54 | end
55 |
56 | describe '#all' do
57 | it 'should fail because we don\'t have a test yet'
58 | end
59 |
60 | describe '#none' do
61 | it 'should fail because we don\'t have a test yet'
62 | end
63 |
64 | describe '.order' do
65 | describe 'with symbol parms' do
66 | it "should add sort clause to query" do
67 | test = subject.where(:q=>'kittens').order(:foo)
68 | expect(test).to be_a(Ansr::Relation)
69 | @mock_api.should_receive(:items).with(:q => 'kittens',:sort_by=>:foo).and_return('')
70 | test.load
71 | end
72 |
73 | it "should add multiple sort clauses to query" do
74 | test = subject.where(:q=>'kittens').order(:foo).order(:bar)
75 | expect(test).to be_a(Ansr::Relation)
76 | @mock_api.should_receive(:items).with(:q => 'kittens',:sort_by=>[:foo,:bar]).and_return('')
77 | test.load
78 | end
79 |
80 | it "should sort in descending order if necessary" do
81 | test = subject.where(:q=>'kittens').order(:foo => :desc)
82 | expect(test).to be_a(Ansr::Relation)
83 | @mock_api.should_receive(:items).with(:q => 'kittens',:sort_by=>:foo, :sort_order=>:desc).and_return('')
84 | test.load
85 | end
86 | end
87 | describe 'with String parms' do
88 | it "should add sort clause to query" do
89 | test = subject.where(q:'kittens').order("foo")
90 | expect(test).to be_a(Ansr::Relation)
91 | @mock_api.should_receive(:items).with(:q => 'kittens',:sort_by=>:foo).and_return('')
92 | test.load
93 | end
94 |
95 | it "should sort in descending order if necessary" do
96 | test = subject.where(q:'kittens').order("foo DESC")
97 | expect(test).to be_a(Ansr::Relation)
98 | @mock_api.should_receive(:items).with(:q => 'kittens',:sort_by=>:foo, :sort_order=>:desc).and_return('')
99 | test.load
100 | end
101 | end
102 | end
103 |
104 | describe '#reorder' do
105 | it "should replace existing order" do
106 | test = subject.where(q:'kittens').order("foo DESC")
107 | test = test.reorder("foo ASC")
108 | expect(test).to be_a(Ansr::Relation)
109 | @mock_api.should_receive(:items).with(:q => 'kittens',:sort_by=>:foo).and_return('')
110 | test.load
111 | end
112 | end
113 |
114 | describe '#reverse_order' do
115 | it "should replace existing DESC order" do
116 | test = subject.where(q:'kittens').order("foo DESC")
117 | test = test.reverse_order
118 | expect(test).to be_a(Ansr::Relation)
119 | @mock_api.should_receive(:items).with(:q => 'kittens',:sort_by=>:foo).and_return('')
120 | test.load
121 | end
122 |
123 | it "should replace existing ASC order" do
124 | test = subject.where(q:'kittens').order("foo ASC")
125 | test = test.reverse_order
126 | expect(test).to be_a(Ansr::Relation)
127 | @mock_api.should_receive(:items).with(:q => 'kittens',:sort_by=>:foo, :sort_order=>:desc).and_return('')
128 | test.load
129 | end
130 | end
131 |
132 | describe '#unscope' do
133 | it 'should remove clauses only from spawned Relation' do
134 | test = subject.where(q:'kittens').order("foo DESC")
135 | test2 = test.unscope(:order)
136 | expect(test2).to be_a(Ansr::Relation)
137 | @mock_api.should_receive(:items).with(:q => 'kittens').and_return('')
138 | test2.load
139 | expect(test.order_values.empty?).to be_false
140 | end
141 | # ActiveRecord::QueryMethods.VALID_UNSCOPING_VALUES =>
142 | # Set.new([:where, :select, :group, :order, :lock, :limit, :offset, :joins, :includes, :from, :readonly, :having])
143 | it 'should reject bad scope keys' do
144 | test = subject.where(q:'kittens').order("foo DESC")
145 | expect { test.unscope(:foo) }.to raise_error
146 | end
147 | end
148 |
149 | describe '#select' do
150 | describe 'with a block given' do
151 | it "should build an array" do
152 | test = subject.where(q:'kittens')
153 | @mock_api.should_receive(:items).with(:q => 'kittens').and_return(@kittens)
154 | actual = test.select {|d| true}
155 | expect(actual).to be_a(Array)
156 | expect(actual.length).to eql(test.limit_value)
157 | actual = test.select {|d| false}
158 | expect(actual).to be_a(Array)
159 | expect(actual.length).to eql(0)
160 | end
161 | end
162 | describe 'with a String or Symbol key given' do
163 | it 'should change the requested document fields' do
164 | test = subject.where(q:'kittens')
165 | @mock_api.should_receive(:items).with(:q => 'kittens', :fields=>:name).and_return('')
166 | test = test.select('name')
167 | test.load
168 | end
169 | end
170 | describe 'with a list of keys' do
171 | it "should add all the requested document fields" do
172 | test = subject.where(q:'kittens')
173 | @mock_api.should_receive(:items).with(:q => 'kittens', :fields=>[:name,:foo]).and_return('')
174 | test = test.select(['name','foo'])
175 | test.load
176 | end
177 | it "should add all the requested document fields and proxy them" do
178 | test = subject.where(q:'kittens')
179 | @mock_api.should_receive(:items).with(:q => 'kittens', :fields=>:object).and_return(@kittens)
180 | test = test.select('object AS my_object')
181 | test.load
182 | expect(test.to_a.first['object']).to be_nil
183 | expect(test.to_a.first['my_object']).to eql('http://ark.digitalcommonwealth.org/ark:/50959/6682xj30d/thumbnail')
184 | end
185 | end
186 | end
187 |
188 | describe '#limit' do
189 | it "should add page_size to the query params" do
190 | test = subject.where(q:'kittens')
191 | @mock_api.should_receive(:items).with(:q => 'kittens', :page_size=>17).and_return('')
192 | test = test.limit(17)
193 | test.load
194 | end
195 | it "should raise an error if limit > 500" do
196 | test = subject.where(q:'kittens')
197 | test = test.limit(500)
198 | expect {test.load }.to raise_error
199 | end
200 | end
201 |
202 | describe '#offset' do
203 | it "should add page to the query params when page_size is defaulted" do
204 | test = subject.where(q:'kittens')
205 | @mock_api.should_receive(:items).with(:q => 'kittens', :page=>3).and_return('')
206 | test = test.offset(20)
207 | test.load
208 | end
209 | it 'should raise an error if an offset is requested that is not a multiple of the page size' do
210 | test = subject.where(q:'kittens').limit(12)
211 | expect{test.offset(17)}.to raise_error(/^Bad/)
212 | test.offset(24)
213 | end
214 | end
215 |
216 | describe '#to_a' do
217 | it 'should load the records and return them' do
218 | end
219 | end
220 |
221 | describe '#loaded?' do
222 | it 'should be false before and true after' do
223 | test = subject.where(q:'kittens')
224 | @mock_api.stub(:items).with(:q => 'kittens').and_return('')
225 | expect(test.loaded?).to be_false
226 | test.load
227 | expect(test.loaded?).to be_true
228 | end
229 | end
230 |
231 |
232 | describe '#any?'
233 |
234 |
235 | describe '#blank?' do
236 | it 'should load the records via to_a'
237 |
238 | end
239 |
240 | describe '#count' do
241 | end
242 |
243 | describe '#empty?' do
244 | it 'should not make another call if records are loaded' do
245 | test = subject.where(q:'kittens')
246 | @mock_api.should_receive(:items).with(:q => 'kittens').once.and_return(@empty)
247 | test.load
248 | expect(test.loaded?).to be_true
249 | expect(test.empty?).to be_true
250 | end
251 |
252 | end
253 |
254 | describe "#spawn" do
255 | it "should create new, independent Relations from Query methods"
256 |
257 | end
258 |
259 | describe 'RDBMS-specific methods' do
260 | describe '#references' do
261 | it 'should not do anything at all'
262 | end
263 | describe '#joins' do
264 | it 'should not do anything at all'
265 | end
266 | describe '#distinct and #uniq' do
267 | it 'should not do anything at all'
268 | end
269 | describe '#preload' do
270 | it 'should not do anything at all'
271 | end
272 | describe '#includes' do
273 | it 'should not do anything at all'
274 | end
275 | describe '#having' do
276 | it 'should not do anything at all'
277 | end
278 | end
279 |
280 | describe 'read-write methods' do
281 | describe '#readonly' do
282 | it "should blissfully ignore no-args or true"
283 |
284 | it "should refuse values of false"
285 |
286 | end
287 | describe '#create_with' do
288 | it 'should not do anything at all'
289 | end
290 | describe '#insert' do
291 | it 'should not do anything at all'
292 | end
293 | describe '#delete' do
294 | it 'should not do anything at all'
295 | end
296 | describe '#delete_all' do
297 | it 'should not do anything at all'
298 | end
299 | describe '#destroy' do
300 | it 'should not do anything at all'
301 | end
302 | describe '#destroy_all' do
303 | it 'should not do anything at all'
304 | end
305 | end
306 |
307 | end
--------------------------------------------------------------------------------
/ansr_blacklight/spec/lib/loaded_relation_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Ansr::Blacklight::Relation do
4 |
5 | def create_response
6 | raw_response = eval(mock_query_response)
7 | Ansr::Blacklight::Solr::Response.new(raw_response, raw_response['params'])
8 | end
9 |
10 | def stub_solr
11 | @solr ||= double('Solr')
12 | @solr.stub(:send_and_receive).and_return(eval(mock_query_response))
13 | @solr
14 | end
15 |
16 | before do
17 | Object.const_set('LoadTestModel', Class.new(TestModel))
18 | LoadTestModel.solr = stub_solr
19 | @relation = Ansr::Blacklight::Relation.new(LoadTestModel, Ansr::Arel::BigTable.new(LoadTestModel))
20 | @relation.load
21 | end
22 | after do
23 | Object.send(:remove_const, :LoadTestModel)
24 | end
25 | subject { @relation }
26 |
27 | let(:r) { subject }
28 |
29 | describe 'pagination' do
30 | it 'should have accurate pagination numbers' do
31 | expect(r.limit_value).to eq 10
32 | expect(r.count).to eq 26
33 | expect(r.offset_value).to eq 0
34 | end
35 |
36 | it "should provide kaminari pagination helpers" do
37 | expect(r.total_count).to eq(r.count)
38 | expect(r.next_page).to eq(r.current_page + 1)
39 | expect(r.prev_page).to eq(nil)
40 | if Kaminari.config.respond_to? :max_pages
41 | expect(r.max_pages).to be_nil
42 | end
43 | expect(r).to be_a_kind_of Kaminari::PageScopeMethods
44 | end
45 | end
46 |
47 | it 'should create a valid response class' do
48 | expect(r).to respond_to(:response)
49 | expect(r.docs).to have(11).docs
50 | expect(r.params[:echoParams]).to eq 'EXPLICIT'
51 |
52 | expect(r).to respond_to(:facets)
53 | end
54 |
55 | it 'should provide facet helpers' do
56 | r.facets.size.should == 2
57 |
58 | field_names = r.facets.collect{|facet|facet.name}
59 | field_names.include?('cat').should == true
60 | field_names.include?('manu').should == true
61 |
62 | first_facet = r.facets.select { |x| x.name == 'cat'}.first
63 | first_facet.name.should == 'cat'
64 |
65 | first_facet.items.size.should == 10
66 |
67 | expected = "electronics - 14, memory - 3, card - 2, connector - 2, drive - 2, graphics - 2, hard - 2, monitor - 2, search - 2, software - 2"
68 | received = first_facet.items.collect do |item|
69 | item.value + ' - ' + item.hits.to_s
70 | end.join(', ')
71 |
72 | expect(received).to eq expected
73 |
74 | r.facets.each do |facet|
75 | expect(facet).to respond_to :name
76 | expect(facet).to respond_to :sort
77 | expect(facet).to respond_to :offset
78 | expect(facet).to respond_to :limit
79 | facet.items.each do |item|
80 | expect(item).to respond_to :value
81 | expect(item).to respond_to :hits
82 | end
83 | end
84 | end
85 |
86 | it "should provide a model name helper" do
87 | expect(r.model_name).to eq LoadTestModel.name
88 | end
89 |
90 | describe "FacetItem" do
91 | it "should work with a field,value tuple" do
92 | item = Ansr::Facets::FacetItem.new('value', 15)
93 | puts item.class.name
94 | item.value.should == 'value'
95 | item.hits.should == 15
96 | end
97 |
98 | it "should work with a field,value + hash triple" do
99 | item = Ansr::Facets::FacetItem.new('value', 15, :a => 1, :value => 'ignored')
100 | item.value.should == 'value'
101 | item.hits.should == 15
102 | item.a.should == 1
103 | end
104 |
105 | it "should work like an openstruct" do
106 | item = Ansr::Facets::FacetItem.new(:value => 'value', :hits => 15)
107 |
108 | item.hits.should == 15
109 | item.value.should == 'value'
110 | item.should be_a_kind_of(OpenStruct)
111 | end
112 |
113 | it "should provide a label accessor" do
114 | item = Ansr::Facets::FacetItem.new('value', :hits => 15)
115 | item.label.should == 'value'
116 | end
117 |
118 | it "should use a provided label" do
119 | item = Ansr::Facets::FacetItem.new('value', 15, :label => 'custom label')
120 | item.label.should == 'custom label'
121 |
122 | end
123 |
124 | end
125 |
126 | it 'should return the correct value when calling facet_by_field_name' do
127 | r = create_response
128 | facet = r.facet_by_field_name('cat')
129 | facet.name.should == 'cat'
130 | end
131 |
132 | it 'should provide the responseHeader params' do
133 | raw_response = eval(mock_query_response)
134 | raw_response['responseHeader']['params']['test'] = :test
135 | r = Ansr::Blacklight::Solr::Response.new(raw_response, raw_response['params'])
136 | r.params['test'].should == :test
137 | end
138 |
139 | it 'should provide the solr-returned params and "rows" should be 11' do
140 | raw_response = eval(mock_query_response)
141 | r = Ansr::Blacklight::Solr::Response.new(raw_response, {})
142 | r.params[:rows].to_s.should == '11'
143 | end
144 |
145 | it 'should provide the ruby request params if responseHeader["params"] does not exist' do
146 | raw_response = eval(mock_query_response)
147 | raw_response.delete 'responseHeader'
148 | r = Ansr::Blacklight::Solr::Response.new(raw_response, :rows => 999)
149 | r.params[:rows].to_s.should == '999'
150 | end
151 |
152 | it 'should provide spelling suggestions for regular spellcheck results' do
153 | raw_response = eval(mock_response_with_spellcheck)
154 | r = Ansr::Blacklight::Solr::Response.new(raw_response, {})
155 | r.spelling.words.should include("dell")
156 | r.spelling.words.should include("ultrasharp")
157 | end
158 |
159 | it 'should provide spelling suggestions for extended spellcheck results' do
160 | raw_response = eval(mock_response_with_spellcheck_extended)
161 | r = Ansr::Blacklight::Solr::Response.new(raw_response, {})
162 | r.spelling.words.should include("dell")
163 | r.spelling.words.should include("ultrasharp")
164 | end
165 |
166 | it 'should provide no spelling suggestions when extended results and suggestion frequency is the same as original query frequency' do
167 | raw_response = eval(mock_response_with_spellcheck_same_frequency)
168 | r = Ansr::Blacklight::Solr::Response.new(raw_response, {})
169 | r.spelling.words.should == []
170 | end
171 |
172 | it 'should provide spelling suggestions for a regular spellcheck results with a collation' do
173 | raw_response = eval(mock_response_with_spellcheck_collation)
174 | r = Ansr::Blacklight::Solr::Response.new(raw_response, {})
175 | r.spelling.words.should include("dell")
176 | r.spelling.words.should include("ultrasharp")
177 | end
178 |
179 | it 'should provide spelling suggestion collation' do
180 | raw_response = eval(mock_response_with_spellcheck_collation)
181 | r = Ansr::Blacklight::Solr::Response.new(raw_response, {})
182 | r.spelling.collation.should == 'dell ultrasharp'
183 | end
184 |
185 | it "should provide MoreLikeThis suggestions" do
186 | raw_response = eval(mock_response_with_more_like_this)
187 | r = Ansr::Blacklight::Solr::Response.new(raw_response, {})
188 | r.more_like(double(:id => '79930185')).should have(2).items
189 | end
190 |
191 | it "should be empty when the response has no results" do
192 | r = Ansr::Blacklight::Solr::Response.new({}, {})
193 | r.stub(:total => 0)
194 | expect(r).to be_empty
195 | end
196 |
197 | def mock_query_response
198 | %({'responseHeader'=>{'status'=>0,'QTime'=>5,'params'=>{'facet.limit'=>'10','wt'=>'ruby','rows'=>'11','facet'=>'true','facet.field'=>['cat','manu'],'echoParams'=>'EXPLICIT','q'=>'*:*','facet.sort'=>'true'}},'response'=>{'numFound'=>26,'start'=>0,'docs'=>[{'id'=>'SP2514N','inStock'=>true,'manu'=>'Samsung Electronics Co. Ltd.','name'=>'Samsung SpinPoint P120 SP2514N - hard drive - 250 GB - ATA-133','popularity'=>6,'price'=>92.0,'sku'=>'SP2514N','timestamp'=>'2009-03-20T14:42:49.795Z','cat'=>['electronics','hard drive'],'spell'=>['Samsung SpinPoint P120 SP2514N - hard drive - 250 GB - ATA-133'],'features'=>['7200RPM, 8MB cache, IDE Ultra ATA-133','NoiseGuard, SilentSeek technology, Fluid Dynamic Bearing (FDB) motor']},{'id'=>'6H500F0','inStock'=>true,'manu'=>'Maxtor Corp.','name'=>'Maxtor DiamondMax 11 - hard drive - 500 GB - SATA-300','popularity'=>6,'price'=>350.0,'sku'=>'6H500F0','timestamp'=>'2009-03-20T14:42:49.877Z','cat'=>['electronics','hard drive'],'spell'=>['Maxtor DiamondMax 11 - hard drive - 500 GB - SATA-300'],'features'=>['SATA 3.0Gb/s, NCQ','8.5ms seek','16MB cache']},{'id'=>'F8V7067-APL-KIT','inStock'=>false,'manu'=>'Belkin','name'=>'Belkin Mobile Power Cord for iPod w/ Dock','popularity'=>1,'price'=>19.95,'sku'=>'F8V7067-APL-KIT','timestamp'=>'2009-03-20T14:42:49.937Z','weight'=>4.0,'cat'=>['electronics','connector'],'spell'=>['Belkin Mobile Power Cord for iPod w/ Dock'],'features'=>['car power adapter, white']},{'id'=>'IW-02','inStock'=>false,'manu'=>'Belkin','name'=>'iPod & iPod Mini USB 2.0 Cable','popularity'=>1,'price'=>11.5,'sku'=>'IW-02','timestamp'=>'2009-03-20T14:42:49.944Z','weight'=>2.0,'cat'=>['electronics','connector'],'spell'=>['iPod & iPod Mini USB 2.0 Cable'],'features'=>['car power adapter for iPod, white']},{'id'=>'MA147LL/A','inStock'=>true,'includes'=>'earbud headphones, USB cable','manu'=>'Apple Computer Inc.','name'=>'Apple 60 GB iPod with Video Playback Black','popularity'=>10,'price'=>399.0,'sku'=>'MA147LL/A','timestamp'=>'2009-03-20T14:42:49.962Z','weight'=>5.5,'cat'=>['electronics','music'],'spell'=>['Apple 60 GB iPod with Video Playback Black'],'features'=>['iTunes, Podcasts, Audiobooks','Stores up to 15,000 songs, 25,000 photos, or 150 hours of video','2.5-inch, 320x240 color TFT LCD display with LED backlight','Up to 20 hours of battery life','Plays AAC, MP3, WAV, AIFF, Audible, Apple Lossless, H.264 video','Notes, Calendar, Phone book, Hold button, Date display, Photo wallet, Built-in games, JPEG photo playback, Upgradeable firmware, USB 2.0 compatibility, Playback speed control, Rechargeable capability, Battery level indication']},{'id'=>'TWINX2048-3200PRO','inStock'=>true,'manu'=>'Corsair Microsystems Inc.','name'=>'CORSAIR XMS 2GB (2 x 1GB) 184-Pin DDR SDRAM Unbuffered DDR 400 (PC 3200) Dual Channel Kit System Memory - Retail','popularity'=>5,'price'=>185.0,'sku'=>'TWINX2048-3200PRO','timestamp'=>'2009-03-20T14:42:49.99Z','cat'=>['electronics','memory'],'spell'=>['CORSAIR XMS 2GB (2 x 1GB) 184-Pin DDR SDRAM Unbuffered DDR 400 (PC 3200) Dual Channel Kit System Memory - Retail'],'features'=>['CAS latency 2, 2-3-3-6 timing, 2.75v, unbuffered, heat-spreader']},{'id'=>'VS1GB400C3','inStock'=>true,'manu'=>'Corsair Microsystems Inc.','name'=>'CORSAIR ValueSelect 1GB 184-Pin DDR SDRAM Unbuffered DDR 400 (PC 3200) System Memory - Retail','popularity'=>7,'price'=>74.99,'sku'=>'VS1GB400C3','timestamp'=>'2009-03-20T14:42:50Z','cat'=>['electronics','memory'],'spell'=>['CORSAIR ValueSelect 1GB 184-Pin DDR SDRAM Unbuffered DDR 400 (PC 3200) System Memory - Retail']},{'id'=>'VDBDB1A16','inStock'=>true,'manu'=>'A-DATA Technology Inc.','name'=>'A-DATA V-Series 1GB 184-Pin DDR SDRAM Unbuffered DDR 400 (PC 3200) System Memory - OEM','popularity'=>5,'sku'=>'VDBDB1A16','timestamp'=>'2009-03-20T14:42:50.004Z','cat'=>['electronics','memory'],'spell'=>['A-DATA V-Series 1GB 184-Pin DDR SDRAM Unbuffered DDR 400 (PC 3200) System Memory - OEM'],'features'=>['CAS latency 3, 2.7v']},{'id'=>'3007WFP','inStock'=>true,'includes'=>'USB cable','manu'=>'Dell, Inc.','name'=>'Dell Widescreen UltraSharp 3007WFP','popularity'=>6,'price'=>2199.0,'sku'=>'3007WFP','timestamp'=>'2009-03-20T14:42:50.017Z','weight'=>401.6,'cat'=>['electronics','monitor'],'spell'=>['Dell Widescreen UltraSharp 3007WFP'],'features'=>['30" TFT active matrix LCD, 2560 x 1600, .25mm dot pitch, 700:1 contrast']},{'id'=>'VA902B','inStock'=>true,'manu'=>'ViewSonic Corp.','name'=>'ViewSonic VA902B - flat panel display - TFT - 19"','popularity'=>6,'price'=>279.95,'sku'=>'VA902B','timestamp'=>'2009-03-20T14:42:50.034Z','weight'=>190.4,'cat'=>['electronics','monitor'],'spell'=>['ViewSonic VA902B - flat panel display - TFT - 19"'],'features'=>['19" TFT active matrix LCD, 8ms response time, 1280 x 1024 native resolution']},{'id'=>'0579B002','inStock'=>true,'manu'=>'Canon Inc.','name'=>'Canon PIXMA MP500 All-In-One Photo Printer','popularity'=>6,'price'=>179.99,'sku'=>'0579B002','timestamp'=>'2009-03-20T14:42:50.062Z','weight'=>352.0,'cat'=>['electronics','multifunction printer','printer','scanner','copier'],'spell'=>['Canon PIXMA MP500 All-In-One Photo Printer'],'features'=>['Multifunction ink-jet color photo printer','Flatbed scanner, optical scan resolution of 1,200 x 2,400 dpi','2.5" color LCD preview screen','Duplex Copying','Printing speed up to 29ppm black, 19ppm color','Hi-Speed USB','memory card: CompactFlash, Micro Drive, SmartMedia, Memory Stick, Memory Stick Pro, SD Card, and MultiMediaCard']}]},'facet_counts'=>{'facet_queries'=>{},'facet_fields'=>{'cat'=>['electronics',14,'memory',3,'card',2,'connector',2,'drive',2,'graphics',2,'hard',2,'monitor',2,'search',2,'software',2],'manu'=>['inc',8,'apach',2,'belkin',2,'canon',2,'comput',2,'corp',2,'corsair',2,'foundat',2,'microsystem',2,'softwar',2]},'facet_dates'=>{}}})
199 | end
200 |
201 | # These spellcheck responses are all Solr 1.4 responses
202 | def mock_response_with_spellcheck
203 | %|{'responseHeader'=>{'status'=>0,'QTime'=>9,'params'=>{'spellcheck'=>'true','spellcheck.collate'=>'true','wt'=>'ruby','q'=>'hell ultrashar'}},'response'=>{'numFound'=>0,'start'=>0,'docs'=>[]},'spellcheck'=>{'suggestions'=>['hell',{'numFound'=>1,'startOffset'=>0,'endOffset'=>4,'suggestion'=>['dell']},'ultrashar',{'numFound'=>1,'startOffset'=>5,'endOffset'=>14,'suggestion'=>['ultrasharp']},'collation','dell ultrasharp']}}|
204 | end
205 |
206 | def mock_response_with_spellcheck_extended
207 | %|{'responseHeader'=>{'status'=>0,'QTime'=>8,'params'=>{'spellcheck'=>'true','spellcheck.collate'=>'true','wt'=>'ruby','spellcheck.extendedResults'=>'true','q'=>'hell ultrashar'}},'response'=>{'numFound'=>0,'start'=>0,'docs'=>[]},'spellcheck'=>{'suggestions'=>['hell',{'numFound'=>1,'startOffset'=>0,'endOffset'=>4,'origFreq'=>0,'suggestion'=>[{'word'=>'dell','freq'=>1}]},'ultrashar',{'numFound'=>1,'startOffset'=>5,'endOffset'=>14,'origFreq'=>0,'suggestion'=>[{'word'=>'ultrasharp','freq'=>1}]},'correctlySpelled',false,'collation','dell ultrasharp']}}|
208 | end
209 |
210 | def mock_response_with_spellcheck_same_frequency
211 | %|{'responseHeader'=>{'status'=>0,'QTime'=>8,'params'=>{'spellcheck'=>'true','spellcheck.collate'=>'true','wt'=>'ruby','spellcheck.extendedResults'=>'true','q'=>'hell ultrashar'}},'response'=>{'numFound'=>0,'start'=>0,'docs'=>[]},'spellcheck'=>{'suggestions'=>['hell',{'numFound'=>1,'startOffset'=>0,'endOffset'=>4,'origFreq'=>1,'suggestion'=>[{'word'=>'dell','freq'=>1}]},'ultrashard',{'numFound'=>1,'startOffset'=>5,'endOffset'=>14,'origFreq'=>1,'suggestion'=>[{'word'=>'ultrasharp','freq'=>1}]},'correctlySpelled',false,'collation','dell ultrasharp']}}|
212 | end
213 |
214 | # it can be the case that extended results are off and collation is on
215 | def mock_response_with_spellcheck_collation
216 | %|{'responseHeader'=>{'status'=>0,'QTime'=>3,'params'=>{'spellspellcheck.build'=>'true','spellcheck'=>'true','q'=>'hell','spellcheck.q'=>'hell ultrashar','wt'=>'ruby','spellcheck.collate'=>'true'}},'response'=>{'numFound'=>0,'start'=>0,'docs'=>[]},'spellcheck'=>{'suggestions'=>['hell',{'numFound'=>1,'startOffset'=>0,'endOffset'=>4,'suggestion'=>['dell']},'ultrashar',{'numFound'=>1,'startOffset'=>5,'endOffset'=>14,'suggestion'=>['ultrasharp']},'collation','dell ultrasharp']}}|
217 | end
218 |
219 | def mock_response_with_more_like_this
220 | %({'responseHeader'=>{'status'=>0,'QTime'=>8,'params'=>{'facet'=>'false','mlt.mindf'=>'1','mlt.fl'=>'subject_t','fl'=>'id','mlt.count'=>'3','mlt.mintf'=>'0','mlt'=>'true','q.alt'=>'*:*','qt'=>'search','wt'=>'ruby'}},'response'=>{'numFound'=>30,'start'=>0,'docs'=>[{'id'=>'00282214'},{'id'=>'00282371'},{'id'=>'00313831'},{'id'=>'00314247'},{'id'=>'43037890'},{'id'=>'53029833'},{'id'=>'77826928'},{'id'=>'78908283'},{'id'=>'79930185'},{'id'=>'85910001'}]},'moreLikeThis'=>{'00282214'=>{'numFound'=>0,'start'=>0,'docs'=>[]},'00282371'=>{'numFound'=>0,'start'=>0,'docs'=>[]},'00313831'=>{'numFound'=>1,'start'=>0,'docs'=>[{'id'=>'96933325'}]},'00314247'=>{'numFound'=>3,'start'=>0,'docs'=>[{'id'=>'2008543486'},{'id'=>'96933325'},{'id'=>'2009373513'}]},'43037890'=>{'numFound'=>0,'start'=>0,'docs'=>[]},'53029833'=>{'numFound'=>0,'start'=>0,'docs'=>[]},'77826928'=>{'numFound'=>1,'start'=>0,'docs'=>[{'id'=>'94120425'}]},'78908283'=>{'numFound'=>0,'start'=>0,'docs'=>[]},'79930185'=>{'numFound'=>2,'start'=>0,'docs'=>[{'id'=>'94120425'},{'id'=>'2007020969'}]},'85910001'=>{'numFound'=>0,'start'=>0,'docs'=>[]}}})
221 | end
222 | end
223 |
--------------------------------------------------------------------------------
/ansr_blacklight/spec/lib/relation/faceting_spec.rb:
--------------------------------------------------------------------------------
1 | # -*- encoding : utf-8 -*-
2 | require 'spec_helper'
3 |
4 | # check the methods that do solr requests. Note that we are not testing if
5 | # solr gives "correct" responses, as that's out of scope (it's a part of
6 | # testing the solr code itself). We *are* testing if blacklight code sends
7 | # queries to solr such that it gets appropriate results. When a user does a search,
8 | # do we get data back from solr (i.e. did we properly configure blacklight code
9 | # to talk with solr and get results)? when we do a document request, does
10 | # blacklight code get a single document returned?)
11 | #
12 | describe Ansr::Blacklight do
13 |
14 | class TestTable < Ansr::Arel::BigTable
15 | def name
16 | 'outside'
17 | end
18 |
19 | def [](val)
20 | key = (Arel::Attributes::Attribute === val) ? val.name.to_sym : val.to_sym
21 | key == :configured ? Ansr::Arel::ConfiguredField.new(nil, key, {:property => 'test', :escape => 'tes"t'}) : super(val)
22 | end
23 |
24 | def fields
25 | [:id]
26 | end
27 |
28 | end
29 |
30 | before do
31 | Object.const_set('FacetModel', Class.new(TestModel))
32 | FacetModel.solr = stub_solr
33 | FacetModel.configure do |config|
34 | config[:table_class] = TestTable
35 | end
36 | end
37 |
38 | after do
39 | Object.send(:remove_const, :FacetModel)
40 | end
41 |
42 | subject {
43 | Ansr::Blacklight::Arel::Visitors::ToNoSql.new(TestTable.new(FacetModel)).query_builder
44 | }
45 |
46 | let(:blacklight_solr) { subject.solr }
47 | let(:copy_of_catalog_config) { ::CatalogController.blacklight_config.deep_copy }
48 | let(:blacklight_config) { copy_of_catalog_config }
49 |
50 | before(:each) do
51 | @all_docs_query = ''
52 | @no_docs_query = 'zzzzzzzzzzzz'
53 | @single_word_query = 'include'
54 | @mult_word_query = 'tibetan history'
55 | # f[format][]=Book&f[language_facet][]=English
56 | @single_facet = {:format=>'Book'}
57 | @multi_facets = {:format=>'Book', :language_facet=>'Tibetan'}
58 | @bad_facet = {:format=>'666'}
59 | @subject_search_params = {:commit=>"search", :search_field=>"subject", :action=>"index", :"controller"=>"catalog", :"rows"=>"10", :"q"=>"wome"}
60 | @abc_resp = {'response' =>{ 'docs' => [id: 'abc']}}.to_json
61 | @no_docs_resp = {'response' =>{'docs' => []}}.to_json
62 | end
63 |
64 | def subject_search_params(rel)
65 | rel.limit(10).where!(q: 'wome')
66 | end
67 |
68 | describe "http_method configuration", :integration => true do
69 | subject { FacetModel }
70 |
71 | it "should send a post request to solr and get a response back" do
72 | subject.method = :post
73 | rel = subject.where(:q => @all_docs_query)
74 | subject.solr= stub_solr(JSON.parse(@abc_resp).inspect)
75 | expect(rel.length).to be >= 1
76 | end
77 | end
78 |
79 | # SPECS for actual search parameter generation
80 | describe "solr_search_params" do
81 |
82 | subject { FacetModel }
83 |
84 | it "allows customization of solr_search_params_logic" do
85 | # Normally you'd include a new module into (eg) your CatalogController
86 | # but a sub-class defininig it directly is simpler for test.
87 | subject.stub(:add_foo_to_solr_params) do |rel|
88 | rel.wt!("TESTING")
89 | end
90 |
91 | subject.solr_search_params_logic += [:add_foo_to_solr_params]
92 |
93 | expect(subject.build_default_scope.wt_value).to eql("TESTING")
94 | end
95 |
96 | describe "for an empty string search" do
97 | subject { Ansr::Blacklight::Arel::Visitors::ToNoSql.new(FacetModel.table) }
98 | it "should return empty string q in solr parameters" do
99 | rel = TestModel.where(q: "")
100 | solr_params = subject.accept(rel.build_arel.ast)
101 | expect(solr_params[:q]).to eq ""
102 | expect(solr_params["spellcheck.q"]).to eq ""
103 | end
104 | end
105 |
106 | describe "for request params also passed in as argument" do
107 | subject { Ansr::Blacklight::Arel::Visitors::ToNoSql.new(FacetModel.table) }
108 | it "should only have one value for the key 'q' regardless if a symbol or string" do
109 | rel = FacetModel.where(q: "some query").where('q' => 'another value')
110 | solr_params = subject.accept(rel.build_arel.ast)
111 | expect(solr_params[:q]).to eq 'another value'
112 | expect(solr_params['q']).to eq 'another value'
113 | end
114 | end
115 |
116 |
117 | describe "for one facet, no query" do
118 | subject { Ansr::Blacklight::Arel::Visitors::ToNoSql.new(FacetModel.table) }
119 | it "should have proper solr parameters" do
120 | rel = FacetModel.filter(@single_facet)
121 | solr_params = subject.accept(rel.build_arel.ast)
122 |
123 | expect(solr_params[:q]).to be_blank
124 | expect(solr_params["spellcheck.q"]).to be_blank
125 |
126 | @single_facet.each_value do |value|
127 | expect(solr_params[:fq]).to include("{!raw f=#{@single_facet.keys[0]}}#{value}")
128 | end
129 | end
130 | end
131 |
132 | describe "with Multi Facets, No Query" do
133 | subject { Ansr::Blacklight::Arel::Visitors::ToNoSql.new(FacetModel.table) }
134 | it 'should have fq set properly' do
135 | rel = FacetModel.filter(@multi_facets)
136 | solr_params = subject.accept(rel.build_arel.ast)
137 |
138 | @multi_facets.each_pair do |facet_field, value_list|
139 | value_list ||= []
140 | value_list = [value_list] unless value_list.respond_to? :each
141 | value_list.each do |value|
142 | expect(solr_params[:fq]).to include("{!raw f=#{facet_field}}#{value}" )
143 | end
144 | end
145 |
146 | end
147 | end
148 |
149 | describe "with Multi Facets, Multi Word Query" do
150 | subject { Ansr::Blacklight::Arel::Visitors::ToNoSql.new(FacetModel.table) }
151 | it 'should have fq and q set properly' do
152 | rel = FacetModel.filter(@multi_facets).where(q: @mult_word_query)
153 | solr_params = subject.accept(rel.build_arel.ast)
154 |
155 | @multi_facets.each_pair do |facet_field, value_list|
156 | value_list ||= []
157 | value_list = [value_list] unless value_list.respond_to? :each
158 | value_list.each do |value|
159 | expect(solr_params[:fq]).to include("{!raw f=#{facet_field}}#{value}" )
160 | end
161 | end
162 | expect(solr_params[:q]).to eq @mult_word_query
163 | end
164 | end
165 |
166 | describe "filter_value_to_fq_string" do
167 | let(:table) { FacetModel.table }
168 | subject { Ansr::Blacklight::Arel::Visitors::QueryBuilder.new(table) }
169 | it "should use the raw handler for strings" do
170 | expect(subject.send(:filter_value_to_fq_string, "facet_name", "my value")).to eq "{!raw f=facet_name}my value"
171 | end
172 |
173 | it "should pass booleans through" do
174 | expect(subject.send(:filter_value_to_fq_string, "facet_name", true)).to eq "facet_name:true"
175 | end
176 |
177 | it "should pass boolean-like strings through" do
178 | expect(subject.send(:filter_value_to_fq_string, "facet_name", "true")).to eq "facet_name:true"
179 | end
180 |
181 | it "should pass integers through" do
182 | expect(subject.send(:filter_value_to_fq_string, "facet_name", 1)).to eq "facet_name:1"
183 | end
184 |
185 | it "should pass integer-like strings through" do
186 | expect(subject.send(:filter_value_to_fq_string, "facet_name", "1")).to eq "facet_name:1"
187 | end
188 |
189 | it "should pass floats through" do
190 | expect(subject.send(:filter_value_to_fq_string, "facet_name", 1.11)).to eq "facet_name:1\\.11"
191 | end
192 |
193 | it "should pass floats through" do
194 | expect(subject.send(:filter_value_to_fq_string, "facet_name", "1.11")).to eq "facet_name:1\\.11"
195 | end
196 |
197 | it "should pass date-type fields through" do
198 | table.configure_fields do |config|
199 | config[:facet_name] = {date: true}
200 | end
201 |
202 | expect(subject.send(:filter_value_to_fq_string, "facet_name", "2012-01-01")).to eq "facet_name:2012\\-01\\-01"
203 | end
204 |
205 | it "should handle range requests" do
206 | expect(subject.send(:filter_value_to_fq_string, "facet_name", 1..5)).to eq "facet_name:[1 TO 5]"
207 | end
208 |
209 | it "should add tag local parameters" do
210 | table.configure_fields do |config|
211 | config[:facet_name] = {local: {tag: 'asdf'}}
212 | end
213 |
214 | expect(subject.send(:filter_value_to_fq_string, "facet_name", true)).to eq "{!tag=asdf}facet_name:true"
215 | expect(subject.send(:filter_value_to_fq_string, "facet_name", "my value")).to eq "{!raw f=facet_name tag=asdf}my value"
216 | end
217 | end
218 |
219 | describe "solr parameters for a field search from config (subject)" do
220 | let(:table) { FacetModel.table }
221 | subject { Ansr::Blacklight::Arel::Visitors::ToNoSql.new(table) }
222 | let(:rel) { FacetModel.build_default_scope.spawn }
223 | let(:blacklight_config) { copy_of_catalog_config }
224 | before do
225 | #@subject_search_params = {:commit=>"search", :search_field=>"subject", :action=>"index", :"controller"=>"catalog", :"rows"=>"10", :"q"=>"wome"}
226 | table.configure_fields do |config|
227 | hash = (config[:subject] ||= {local: {}, query: {}})
228 | hash[:query][:'spellcheck.dictionary'] = 'subject'
229 | hash[:query][:qt] = 'search'
230 | hash[:local][:pf] = '$subject_pf'
231 | hash[:local][:qf] = '$subject_qf'
232 | end
233 | rel.where!(subject: 'wome')
234 | end
235 | after do
236 | table.configure_fields { |config| config.clear }
237 | end
238 | it "should look up qt from field definition" do
239 | solr_params = subject.accept(rel.build_arel.ast)
240 | expect(solr_params[:qt]).to eq "search"
241 | end
242 | it "should not include weird keys not in field definition" do
243 | solr_params = subject.accept(rel.build_arel.ast)
244 | solr_params.to_hash.tap do |h|
245 | expect(h[:phrase_filters]).to be_nil
246 | expect(h[:fq]).to be_nil
247 | expect(h[:commit]).to be_nil
248 | expect(h[:action]).to be_nil
249 | expect(h[:controller]).to be_nil
250 | end
251 | end
252 | it "should include proper 'q', possibly with LocalParams" do
253 | solr_params = subject.accept(rel.build_arel.ast)
254 | expect(solr_params[:q]).to match(/(\{[^}]+\})?wome/)
255 | end
256 | it "should include proper 'q' when LocalParams are used" do
257 | solr_params = subject.accept(rel.build_arel.ast)
258 | if solr_params[:q] =~ /\{[^}]+\}/
259 | expect(solr_params[:q]).to match(/\{[^}]+\}wome/)
260 | end
261 | end
262 | it "should include spellcheck.q, without LocalParams" do
263 | solr_params = subject.accept(rel.build_arel.ast)
264 | expect(solr_params["spellcheck.q"]).to eq "wome"
265 | end
266 |
267 | it "should include spellcheck.dictionary from field def solr_parameters" do
268 | solr_params = subject.accept(rel.build_arel.ast)
269 | expect(solr_params[:"spellcheck.dictionary"]).to eq "subject"
270 | end
271 | it "should add on :solr_local_parameters using Solr LocalParams style" do
272 | solr_params = subject.accept(rel.build_arel.ast)
273 |
274 | #q == "{!pf=$subject_pf $qf=subject_qf} wome", make sure
275 | #the LocalParams are really there
276 | solr_params[:q] =~ /^\{!([^}]+)\}/
277 | key_value_pairs = $1.split(" ")
278 | expect(key_value_pairs).to include("pf=$subject_pf")
279 | expect(key_value_pairs).to include("qf=$subject_qf")
280 | end
281 | end
282 |
283 | describe "overriding of qt parameter" do
284 | let(:table) { FacetModel.table }
285 | subject { Ansr::Blacklight::Arel::Visitors::ToNoSql.new(table) }
286 | let(:rel) { FacetModel.build_default_scope.as('overriden') }
287 | let(:blacklight_config) { copy_of_catalog_config }
288 | it "should return the correct overriden parameter" do
289 | solr_params = subject.accept(rel.build_arel.ast)
290 | expect(solr_params[:qt]).to eq "overriden"
291 | end
292 | end
293 |
294 | describe "converts a String fq into an Array" do
295 | let(:table) { FacetModel.table }
296 | subject { Ansr::Blacklight::Arel::Visitors::ToNoSql.new(table) }
297 | let(:rel) { FacetModel.build_default_scope }
298 | it "should return the correct overriden parameter" do
299 | solr_params = subject.accept(rel.facet('a string').build_arel.ast)
300 | expect(solr_params[:fq]).to be_a_kind_of Array
301 | end
302 | end
303 |
304 | describe "#add_solr_fields_to_query" do
305 | let(:table) { FacetModel.table }
306 | subject { Ansr::Blacklight::Arel::Visitors::ToNoSql.new(table) }
307 | let(:rel) { FacetModel.build_default_scope}
308 | before do
309 | FacetModel.table.configure_fields do |config|
310 | config[:an_index_field] = {select: { 'hl.alternativeField' => 'field_x'}}
311 | config[:a_show_field] = {select: { 'hl.alternativeField' => 'field_y'}}
312 | end
313 | end
314 | after do
315 | FacetModel.table.configure_fields.clear
316 | end
317 |
318 | it "should add any extra solr parameters from index and show fields" do
319 | solr_params = subject.accept(rel.select(:an_index_field, :a_show_field).build_arel.ast)
320 | expect(solr_params[:'f.an_index_field.hl.alternativeField']).to eq "field_x"
321 | expect(solr_params[:'f.a_show_field.hl.alternativeField']).to eq "field_y"
322 | end
323 | end
324 |
325 | describe "#add_facetting_to_solr" do
326 |
327 | let(:table) { FacetModel.table }
328 | subject { Ansr::Blacklight::Arel::Visitors::ToNoSql.new(table) }
329 | let(:rel) {
330 | FacetModel.build_default_scope.spawn
331 | .facet('test_field', :sort => 'count')
332 | .facet('some-query', :query => {'x' => {:fq => 'some:query'}} , :ex => 'xyz')
333 | .facet('some-pivot', :pivot => ['a','b'], :ex => 'xyz')
334 | .facet('some-field', mincount: 15 )
335 | }
336 |
337 | it "should add sort parameters" do
338 | solr_params = subject.accept(rel.build_arel.ast)
339 | expect(solr_params[:facet]).to be_true
340 |
341 | expect(solr_params[:'facet.field']).to include(:'test_field')
342 | expect(solr_params[:'f.test_field.facet.sort']).to eq 'count'
343 | end
344 |
345 | it "should add facet exclusions" do
346 | solr_params = subject.accept(rel.build_arel.ast)
347 | expect(solr_params[:'facet.query']).to include('{!ex=xyz}some:query')
348 | expect(solr_params[:'facet.pivot']).to include('{!ex=xyz}a,b')
349 | end
350 |
351 | it "should add any additional solr_params" do
352 | solr_params = subject.accept(rel.build_arel.ast)
353 | expect(solr_params[:'f.some-field.facet.mincount']).to eq '15'
354 | end
355 | end
356 |
357 | describe "for :solr_local_parameters config" do
358 | let(:table) { FacetModel.table }
359 | subject { Ansr::Blacklight::Arel::Visitors::ToNoSql.new(table) }
360 | let(:rel) { FacetModel.build_default_scope.spawn }
361 | let(:result) { subject.accept rel.where(:custom_author_key => "query").build_arel.ast }
362 | before do
363 | #@subject_search_params = {:commit=>"search", :search_field=>"subject", :action=>"index", :"controller"=>"catalog", :"rows"=>"10", :"q"=>"wome"}
364 | table.configure_fields do |config|
365 | hash = (config[:custom_author_key] ||= {local: {}, query: {}, select: {}})
366 | hash[:query][:'spellcheck.dictionary'] = 'subject'
367 | hash[:query][:qt] = 'author_qt'
368 | hash[:query][:qf] = 'someField^1000'
369 | hash[:query][:ps] = '2'
370 | hash[:local][:pf] = "you'll have \" to escape this"
371 | hash[:local][:pf2] = '$pf2_do_not_escape_or_quote'
372 | hash[:local][:qf] = '$author_qf'
373 | end
374 | rel.where!(subject: 'wome')
375 | end
376 | after do
377 | table.configure_fields { |config| config.clear }
378 | end
379 |
380 | it "should pass through ordinary params" do
381 | expect(result[:qt]).to eq "author_qt"
382 | expect(result[:ps]).to eq "2"
383 | expect(result[:qf]).to eq "someField^1000"
384 | end
385 |
386 | it "should include include local params with escaping" do
387 | expect(result[:q]).to include('qf=$author_qf')
388 | expect(result[:q]).to include('pf=\'you\\\'ll have \\" to escape this\'')
389 | expect(result[:q]).to include('pf2=$pf2_do_not_escape_or_quote')
390 | end
391 | end
392 | end
393 |
394 | describe "solr_facet_params" do
395 | before do
396 | @facet_field = 'format'
397 | @generated_solr_facet_params = subject.solr_facet_params(@facet_field)
398 |
399 | @sort_key = Blacklight::Solr::FacetPaginator.request_keys[:sort]
400 | @page_key = Blacklight::Solr::FacetPaginator.request_keys[:page]
401 | end
402 |
403 | let(:blacklight_config) do
404 | Blacklight::Configuration.new do |config|
405 | config.add_facet_fields_to_solr_request!
406 | config.add_facet_field 'format'
407 | config.add_facet_field 'format_ordered', :sort => :count
408 | config.add_facet_field 'format_limited', :limit => 5
409 |
410 | end
411 | end
412 |
413 | pending 'sets rows to 0' do
414 | expect(@generated_solr_facet_params[:rows]).to eq 0
415 | end
416 | pending 'sets facets requested to facet_field argument' do
417 | expect(@generated_solr_facet_params["facet.field".to_sym]).to eq @facet_field
418 | end
419 | pending 'defaults offset to 0' do
420 | expect(@generated_solr_facet_params[:"f.#{@facet_field}.facet.offset"]).to eq 0
421 | end
422 | pending 'uses offset manually set, and converts it to an integer' do
423 | solr_params = subject.solr_facet_params(@facet_field, @page_key => 2)
424 | expect(solr_params[:"f.#{@facet_field}.facet.offset"]).to eq 20
425 | end
426 | pending 'defaults limit to 20' do
427 | solr_params = subject.solr_facet_params(@facet_field)
428 | expect(solr_params[:"f.#{@facet_field}.facet.limit"]).to eq 21
429 | end
430 |
431 | describe 'if facet_list_limit is defined in controller' do
432 | before do
433 | subject.stub facet_list_limit: 1000
434 | end
435 | pending 'uses controller method for limit' do
436 | solr_params = subject.solr_facet_params(@facet_field)
437 | expect(solr_params[:"f.#{@facet_field}.facet.limit"]).to eq 1001
438 | end
439 |
440 | pending 'uses controller method for limit when a ordinary limit is set' do
441 | solr_params = subject.solr_facet_params(@facet_field)
442 | expect(solr_params[:"f.#{@facet_field}.facet.limit"]).to eq 1001
443 | end
444 | end
445 |
446 | pending 'uses the default sort' do
447 | solr_params = subject.solr_facet_params(@facet_field)
448 | expect(solr_params[:"f.#{@facet_field}.facet.sort"]).to be_blank
449 | end
450 |
451 | pending "uses the field-specific sort" do
452 | solr_params = subject.solr_facet_params('format_ordered')
453 | expect(solr_params[:"f.format_ordered.facet.sort"]).to eq :count
454 | end
455 |
456 | pending 'uses sort provided in the parameters' do
457 | solr_params = subject.solr_facet_params(@facet_field, @sort_key => "index")
458 | expect(solr_params[:"f.#{@facet_field}.facet.sort"]).to eq 'index'
459 | end
460 | pending "comes up with the same params as #solr_search_params to constrain context for facet list" do
461 | search_params = {:q => 'tibetan history', :f=> {:format=>'Book', :language_facet=>'Tibetan'}}
462 | solr_search_params = subject.solr_search_params( search_params )
463 | solr_facet_params = subject.solr_facet_params('format', search_params)
464 |
465 | solr_search_params.each_pair do |key, value|
466 | # The specific params used for fetching the facet list we
467 | # don't care about.
468 | next if ['facets', "facet.field", 'rows', 'facet.limit', 'facet.offset', 'facet.sort'].include?(key)
469 | # Everything else should match
470 | expect(solr_facet_params[key]).to eq value
471 | end
472 |
473 | end
474 | end
475 |
476 | end
--------------------------------------------------------------------------------