europeana-blacklight 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.hound.yml +10 -0
- data/.rspec +3 -0
- data/.rubocop.yml +1 -0
- data/.ruby-style.yml +1056 -0
- data/.travis.yml +6 -0
- data/Gemfile +9 -0
- data/LICENSE.md +119 -0
- data/README.md +59 -0
- data/Rakefile +4 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/europeana-blacklight.gemspec +28 -0
- data/lib/europeana/blacklight.rb +14 -0
- data/lib/europeana/blacklight/api_repository.rb +123 -0
- data/lib/europeana/blacklight/document.rb +117 -0
- data/lib/europeana/blacklight/document/more_like_this.rb +51 -0
- data/lib/europeana/blacklight/document/relations.rb +88 -0
- data/lib/europeana/blacklight/document_presenter.rb +36 -0
- data/lib/europeana/blacklight/facet_paginator.rb +8 -0
- data/lib/europeana/blacklight/response.rb +91 -0
- data/lib/europeana/blacklight/response/facets.rb +145 -0
- data/lib/europeana/blacklight/response/more_like_this.rb +19 -0
- data/lib/europeana/blacklight/response/pagination.rb +41 -0
- data/lib/europeana/blacklight/search_builder.rb +195 -0
- data/lib/europeana/blacklight/search_builder/channels.rb +18 -0
- data/lib/europeana/blacklight/search_builder/facet_pagination.rb +38 -0
- data/lib/europeana/blacklight/search_builder/more_like_this.rb +23 -0
- data/lib/europeana/blacklight/search_builder/ranges.rb +23 -0
- data/lib/europeana/blacklight/version.rb +6 -0
- metadata +164 -0
@@ -0,0 +1,51 @@
|
|
1
|
+
module Europeana
|
2
|
+
module Blacklight
|
3
|
+
class Document
|
4
|
+
##
|
5
|
+
# Methods for obtaining "more like this" documents
|
6
|
+
module MoreLikeThis
|
7
|
+
# @param [String] param Name of API parameter to restrict query to
|
8
|
+
# @return [String]
|
9
|
+
def more_like_this_query(param = nil)
|
10
|
+
queries = more_like_this_field_queries(param)
|
11
|
+
return nil unless queries.size > 0
|
12
|
+
field_queries = queries.join(' OR ')
|
13
|
+
"(#{field_queries}) NOT europeana_id:\"#{self.id}\""
|
14
|
+
end
|
15
|
+
|
16
|
+
protected
|
17
|
+
|
18
|
+
def more_like_this_logic
|
19
|
+
[
|
20
|
+
{ param: 'title', fields: 'title', boost: 0.3 },
|
21
|
+
{ param: 'who', fields: 'proxies.dcCreator', boost: 0.5 },
|
22
|
+
{ param: 'DATA_PROVIDER', fields: 'aggregations.edmDataProvider', boost: 0.2 },
|
23
|
+
{ param: 'what', fields: ['proxies.dcType', 'proxies.dcSubject', 'concepts.about'], boost: 0.8 },
|
24
|
+
]
|
25
|
+
end
|
26
|
+
|
27
|
+
def more_like_this_field_terms(*fields)
|
28
|
+
fields.flatten.map do |field|
|
29
|
+
fetch(field, []).compact
|
30
|
+
end.flatten
|
31
|
+
end
|
32
|
+
|
33
|
+
def more_like_this_field_queries(param = nil)
|
34
|
+
logic = more_like_this_logic.select do |component|
|
35
|
+
param.nil? || component[:param] == param
|
36
|
+
end
|
37
|
+
logic.map do |component|
|
38
|
+
field_terms = more_like_this_field_terms(component[:fields])
|
39
|
+
more_like_this_param_query(component[:param], field_terms, component[:boost])
|
40
|
+
end.compact
|
41
|
+
end
|
42
|
+
|
43
|
+
def more_like_this_param_query(param, terms, boost)
|
44
|
+
return nil unless terms.present?
|
45
|
+
or_terms = terms.map { |v| '"' + v + '"' }.join(' OR ')
|
46
|
+
"#{param}: (#{or_terms})^#{boost}"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
require 'active_support/core_ext/hash'
|
2
|
+
|
3
|
+
module Europeana
|
4
|
+
module Blacklight
|
5
|
+
class Document
|
6
|
+
##
|
7
|
+
# Methods for working with relations between objects in Europeana documents
|
8
|
+
module Relations
|
9
|
+
attr_reader :relations
|
10
|
+
|
11
|
+
def fetch_through_relation(key, *default)
|
12
|
+
field = nested_field_key(key)
|
13
|
+
container = nested_field_container(key)
|
14
|
+
value = [container].flatten.compact.collect do |target|
|
15
|
+
target.fetch(field, *default)
|
16
|
+
end.compact.flatten
|
17
|
+
end
|
18
|
+
|
19
|
+
def extract_relations(source_doc)
|
20
|
+
fields = source_doc.except(relation_keys)
|
21
|
+
|
22
|
+
relations = HashWithIndifferentAccess.new
|
23
|
+
|
24
|
+
relation_keys.each do |k|
|
25
|
+
if source_doc.key?(k)
|
26
|
+
if source_doc[k].is_a?(Hash)
|
27
|
+
relations[k] = self.class.new(source_doc[k], nil)
|
28
|
+
elsif source_doc[k].is_a?(Array)
|
29
|
+
relations[k] = source_doc[k].map { |v| self.class.new(v, nil) }
|
30
|
+
else
|
31
|
+
fail StandardError,
|
32
|
+
'Relations should be a collection of objects.'
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
[fields, relations]
|
38
|
+
end
|
39
|
+
|
40
|
+
def field_in_relation?(field)
|
41
|
+
keys = split_edm_key(field)
|
42
|
+
(keys.size > 1) && relations.key?(keys.first)
|
43
|
+
end
|
44
|
+
|
45
|
+
def nested_field_key(field)
|
46
|
+
split_edm_key(field).last
|
47
|
+
end
|
48
|
+
|
49
|
+
def nested_field_container(field)
|
50
|
+
container = self
|
51
|
+
if field_in_relation?(field)
|
52
|
+
keys = split_edm_key(field)
|
53
|
+
field = keys.last
|
54
|
+
keys[0..-2].each do |relation_key|
|
55
|
+
container = [container].flatten.collect { |d| d.send(relation_key.to_sym) }
|
56
|
+
end
|
57
|
+
end
|
58
|
+
container
|
59
|
+
end
|
60
|
+
|
61
|
+
def method_missing(m, *args, &b)
|
62
|
+
if has_relation?(m)
|
63
|
+
relations[m.to_s]
|
64
|
+
elsif relation_keys.include?(m)
|
65
|
+
[]
|
66
|
+
else
|
67
|
+
super
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def has_relation?(name)
|
72
|
+
relations.key?(name.to_s)
|
73
|
+
end
|
74
|
+
|
75
|
+
protected
|
76
|
+
|
77
|
+
def split_edm_key(key)
|
78
|
+
key.to_s.split('.')
|
79
|
+
end
|
80
|
+
|
81
|
+
def relation_keys
|
82
|
+
[:agents, :aggregations, :concepts, :europeanaAggregation, :places,
|
83
|
+
:providedCHOs, :proxies, :timespans, :webResources]
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Europeana
|
2
|
+
module Blacklight
|
3
|
+
##
|
4
|
+
# Blacklight document presenter for Europeana documents
|
5
|
+
class DocumentPresenter < ::Blacklight::DocumentPresenter
|
6
|
+
include ActionView::Helpers::AssetTagHelper
|
7
|
+
|
8
|
+
def render_document_show_field_value(field, options = {})
|
9
|
+
render_nested_field_value(field, :show, options)
|
10
|
+
end
|
11
|
+
|
12
|
+
def render_index_field_value(field, options = {})
|
13
|
+
render_nested_field_value(field, :index, options)
|
14
|
+
end
|
15
|
+
|
16
|
+
def render_nested_field_value(field, context, options = {})
|
17
|
+
key = @document.nested_field_key(field)
|
18
|
+
container = @document.nested_field_container(field)
|
19
|
+
|
20
|
+
field_config = @configuration.send(:"#{context}_fields")[key]
|
21
|
+
value = options[:value] || begin
|
22
|
+
[container].flatten.compact.collect do |target|
|
23
|
+
presenter = self.class.new(target, @controller, @configuration)
|
24
|
+
presenter.get_field_values(key, field_config, options)
|
25
|
+
end.compact.flatten
|
26
|
+
end
|
27
|
+
|
28
|
+
render_field_value(value, field_config)
|
29
|
+
end
|
30
|
+
|
31
|
+
def get_field_values(field, field_config, options = {})
|
32
|
+
Document.localize_lang_map(super)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
module Europeana
|
2
|
+
module Blacklight
|
3
|
+
##
|
4
|
+
# Europeana API response for BL
|
5
|
+
class Response < HashWithIndifferentAccess
|
6
|
+
require 'europeana/blacklight/response/pagination'
|
7
|
+
require 'europeana/blacklight/response/facets'
|
8
|
+
require 'europeana/blacklight/response/more_like_this'
|
9
|
+
|
10
|
+
include Pagination
|
11
|
+
include Facets
|
12
|
+
include MoreLikeThis
|
13
|
+
|
14
|
+
attr_reader :request_params
|
15
|
+
attr_accessor :document_model, :blacklight_config
|
16
|
+
|
17
|
+
def initialize(data, request_params, options = {})
|
18
|
+
super(force_to_utf8(data))
|
19
|
+
@request_params = request_params
|
20
|
+
self.document_model = options[:document_model] || Document
|
21
|
+
self.blacklight_config = options[:blacklight_config]
|
22
|
+
end
|
23
|
+
|
24
|
+
def update(other_hash)
|
25
|
+
other_hash.each_pair { |key, value| self[key] = value }
|
26
|
+
self
|
27
|
+
end
|
28
|
+
|
29
|
+
def params
|
30
|
+
self['params'] ? self['params'] : request_params
|
31
|
+
end
|
32
|
+
|
33
|
+
def rows
|
34
|
+
params[:rows].to_i
|
35
|
+
end
|
36
|
+
|
37
|
+
def sort
|
38
|
+
params[:sort]
|
39
|
+
end
|
40
|
+
|
41
|
+
def documents
|
42
|
+
@documents ||= (self.key?('object') ? [self['object']] : (self['items'] || [])).collect do |doc|
|
43
|
+
document_model.new(doc, self)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
alias_method :docs, :documents
|
47
|
+
|
48
|
+
def grouped
|
49
|
+
[]
|
50
|
+
end
|
51
|
+
|
52
|
+
def group(_key)
|
53
|
+
nil
|
54
|
+
end
|
55
|
+
|
56
|
+
def grouped?
|
57
|
+
false
|
58
|
+
end
|
59
|
+
|
60
|
+
def export_formats
|
61
|
+
documents.map { |x| x.export_formats.keys }.flatten.uniq
|
62
|
+
end
|
63
|
+
|
64
|
+
def total
|
65
|
+
self[:totalResults].to_s.to_i
|
66
|
+
end
|
67
|
+
|
68
|
+
def start
|
69
|
+
params[:start].to_s.to_i - 1
|
70
|
+
end
|
71
|
+
|
72
|
+
def empty?
|
73
|
+
total == 0
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def force_to_utf8(value)
|
79
|
+
case value
|
80
|
+
when Hash
|
81
|
+
value.each { |k, v| value[k] = force_to_utf8(v) }
|
82
|
+
when Array
|
83
|
+
value.each { |v| force_to_utf8(v) }
|
84
|
+
when String
|
85
|
+
value.force_encoding('utf-8') if value.respond_to?(:force_encoding)
|
86
|
+
end
|
87
|
+
value
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,145 @@
|
|
1
|
+
module Europeana
|
2
|
+
module Blacklight
|
3
|
+
class Response
|
4
|
+
##
|
5
|
+
# Facets for {Europeana::Blacklight::Response}
|
6
|
+
#
|
7
|
+
# Based on {Blacklight::SolrResponse::Facets} v5.10.2
|
8
|
+
module Facets
|
9
|
+
require 'ostruct'
|
10
|
+
|
11
|
+
# represents a facet value; which is a field value and its hit count
|
12
|
+
class FacetItem < OpenStruct
|
13
|
+
def initialize(*args)
|
14
|
+
options = args.extract_options!
|
15
|
+
|
16
|
+
# Backwards-compat method signature
|
17
|
+
value = args.shift
|
18
|
+
hits = args.shift
|
19
|
+
|
20
|
+
options[:value] = value if value
|
21
|
+
options[:hits] = hits if hits
|
22
|
+
|
23
|
+
super(options)
|
24
|
+
end
|
25
|
+
|
26
|
+
def label
|
27
|
+
super || value
|
28
|
+
end
|
29
|
+
|
30
|
+
def as_json(props = nil)
|
31
|
+
table.as_json(props)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# represents a facet; which is a field and its values
|
36
|
+
class FacetField
|
37
|
+
attr_reader :name, :items
|
38
|
+
|
39
|
+
def initialize(name, items, options = {})
|
40
|
+
@name, @items = name, items
|
41
|
+
@options = options
|
42
|
+
end
|
43
|
+
|
44
|
+
def limit
|
45
|
+
@options[:limit] || default_limit
|
46
|
+
end
|
47
|
+
|
48
|
+
def offset
|
49
|
+
@options[:offset] || default_offset
|
50
|
+
end
|
51
|
+
|
52
|
+
def sort
|
53
|
+
# Europeana API does not support facet sorting
|
54
|
+
nil
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
# @see http://labs.europeana.eu/api/search/#offset-and-limit-of-facets
|
60
|
+
def default_limit
|
61
|
+
100
|
62
|
+
end
|
63
|
+
|
64
|
+
# @see http://labs.europeana.eu/api/search/#offset-and-limit-of-facets
|
65
|
+
def default_offset
|
66
|
+
0
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def aggregations
|
71
|
+
@aggregations ||= {}.merge(facet_field_aggregations).merge(facet_query_aggregations)
|
72
|
+
end
|
73
|
+
|
74
|
+
def facet_fields
|
75
|
+
@facet_fields ||= self['facets'] || []
|
76
|
+
end
|
77
|
+
|
78
|
+
def facet_queries
|
79
|
+
@facet_queries ||= self['facet_queries'] || {}
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
##
|
85
|
+
# Convert API's facets response into a hash of
|
86
|
+
# {Europeana::Blacklight::Response::Facet::FacetField} objects
|
87
|
+
def facet_field_aggregations
|
88
|
+
facet_fields.each_with_object({}) do |facet, hash|
|
89
|
+
facet_field_name = facet['name']
|
90
|
+
|
91
|
+
items = facet['fields'].collect do |value|
|
92
|
+
FacetItem.new(value: value['label'], hits: value['count'])
|
93
|
+
end
|
94
|
+
|
95
|
+
hash[facet_field_name] = FacetField.new(facet_field_name, items, facet_field_aggregation_options(facet_field_name))
|
96
|
+
|
97
|
+
if blacklight_config && !blacklight_config.facet_fields[facet_field_name]
|
98
|
+
# alias all the possible blacklight config names..
|
99
|
+
blacklight_config.facet_fields.select { |_k, v| v.field == facet_field_name }.each do |key, _|
|
100
|
+
hash[key] = hash[facet_field_name]
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def facet_field_aggregation_options(name)
|
107
|
+
options = {}
|
108
|
+
|
109
|
+
if params[:"f.#{name}.facet.limit"]
|
110
|
+
options[:limit] = params[:"f.#{name}.facet.limit"].to_i
|
111
|
+
elsif params[:'facet.limit']
|
112
|
+
options[:limit] = params[:'facet.limit'].to_i
|
113
|
+
end
|
114
|
+
|
115
|
+
if params[:"f.#{name}.facet.offset"]
|
116
|
+
options[:offset] = params[:"f.#{name}.facet.offset"].to_i
|
117
|
+
elsif params[:'facet.offset']
|
118
|
+
options[:offset] = params[:'facet.offset'].to_i
|
119
|
+
end
|
120
|
+
|
121
|
+
options
|
122
|
+
end
|
123
|
+
|
124
|
+
##
|
125
|
+
# Aggregate API's facet_query response into the virtual facet fields
|
126
|
+
# defined in the blacklight configuration
|
127
|
+
def facet_query_aggregations
|
128
|
+
return {} unless blacklight_config
|
129
|
+
|
130
|
+
blacklight_config.facet_fields.select { |_k, v| v.query }.each_with_object({}) do |(field_name, facet_field), hash|
|
131
|
+
salient_facet_queries = facet_field.query.map { |_k, x| x[:fq] }
|
132
|
+
items = []
|
133
|
+
facet_queries.select { |k, _v| salient_facet_queries.include?(k) }.reject { |_value, hits| hits == 0 }.map do |value, hits|
|
134
|
+
salient_fields = facet_field.query.select { |_k, v| v[:fq] == value }
|
135
|
+
key = ((salient_fields.keys if salient_fields.respond_to? :keys) || salient_fields.first).first
|
136
|
+
items << FacetItem.new(value: key, hits: hits, label: facet_field.query[key][:label])
|
137
|
+
end
|
138
|
+
|
139
|
+
hash[field_name] = FacetField.new(field_name, items)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Europeana
|
2
|
+
module Blacklight
|
3
|
+
class Response
|
4
|
+
##
|
5
|
+
# MLT for{Europeana::Blacklight::Response}
|
6
|
+
#
|
7
|
+
# This will just return blank objects from the MLT methods BL expects.
|
8
|
+
module MoreLikeThis
|
9
|
+
def more_like(_document)
|
10
|
+
[]
|
11
|
+
end
|
12
|
+
|
13
|
+
def more_like_this
|
14
|
+
{}
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|