europeana-blacklight 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|