Skip to content

Commit

Permalink
process additional context and add to final json results
Browse files Browse the repository at this point in the history
  • Loading branch information
elrayle committed Jan 25, 2019
1 parent b132863 commit 0f4ca35
Show file tree
Hide file tree
Showing 14 changed files with 142 additions and 15 deletions.
7 changes: 6 additions & 1 deletion app/controllers/qa/linked_data_terms_controller.rb
Expand Up @@ -35,7 +35,7 @@ def reload
# get "/search/linked_data/:vocab(/:subauthority)"
# @see Qa::Authorities::LinkedData::SearchQuery#search
def search
terms = @authority.search(query, subauth: subauthority, language: language, replacements: replacement_params)
terms = @authority.search(query, subauth: subauthority, language: language, replacements: replacement_params, context: context?)
cors_allow_origin_header(response)
render json: terms
rescue Qa::ServiceUnavailable
Expand Down Expand Up @@ -189,6 +189,11 @@ def jsonld?
format == 'jsonld'
end

def context?
context = params.fetch(:context, false)
context == 'true' ? true : false
end

def validate_auth_reload_token
token = params.key?(:auth_token) ? params[:auth_token] : nil
valid = Qa.config.valid_authority_reload_token?(token)
Expand Down
12 changes: 7 additions & 5 deletions app/models/qa/linked_data/config/context_map.rb
Expand Up @@ -5,14 +5,15 @@ module Config
class ContextMap
attr_reader :properties # [Array<Qa::LinkedData::Config::ContextPropertyMap>] set of property map models

attr_reader :context_map, :groups
private :context_map, :groups
attr_reader :context_map, :groups, :prefixes
private :context_map, :groups, :prefixes

# @param [Hash] context_map that defines groups and properties for additional context to display during the selection process
# @option context_map [Hash] :groups (optional) predefine group ids and labels to be used in the properties section to group properties
# @option groups [Hash] key=group_id; value=[Hash] with group_label_i18n and/or group_label_default
# @option context_map [Array<Hash>] :properties (optional) property maps defining how to get and display the additional context (if none, context will not be added)
# @example
# @param [Hash] shortcut names for URI prefixes with key = part of predicate that is the same for all terms (e.g. { "madsrdf": "http://www.loc.gov/mads/rdf/v1#" })
# @example context_map example
# {
# "groups": {
# "dates": {
Expand All @@ -38,8 +39,9 @@ class ContextMap
# }
# ]
# }
def initialize(context_map = {})
def initialize(context_map = {}, prefixes = {})
@context_map = context_map
@prefixes = prefixes
extract_groups
extract_properties
end
Expand All @@ -53,7 +55,7 @@ def group_label(group_id)
def extract_properties
@properties = []
properties_map = Qa::LinkedData::Config::Helper.fetch(context_map, :properties, {})
properties_map.each { |property_map| @properties << ContextPropertyMap.new(property_map) }
properties_map.each { |property_map| @properties << ContextPropertyMap.new(property_map, prefixes) }
end

def extract_groups
Expand Down
17 changes: 14 additions & 3 deletions app/models/qa/linked_data/config/context_property_map.rb
@@ -1,13 +1,15 @@
# Defines the external authority predicates used to extract additional context from the graph.
require 'ldpath'

module Qa
module LinkedData
module Config
class ContextPropertyMap
attr_reader :group_id, # id that identifies which group the property should be in
:label # plain text label extracted from locales or using the default

attr_reader :property_map, :ldpath
private :property_map, :ldpath
attr_reader :property_map, :ldpath, :prefixes
private :property_map, :ldpath, :prefixes

# @param [Hash] property_map defining information to return to provide context
# @option property_map [String] :group_id (optional) default label to use for a property (default: no label)
Expand All @@ -25,13 +27,14 @@ class ContextPropertyMap
# "selectable": false,
# "drillable": false
# }
def initialize(property_map = {})
def initialize(property_map = {}, prefixes = {})
@property_map = property_map
@group_id = Qa::LinkedData::Config::Helper.fetch_symbol(property_map, :group_id, nil)
@label = extract_label
@ldpath = Qa::LinkedData::Config::Helper.fetch_required(property_map, :ldpath, false)
@selectable = Qa::LinkedData::Config::Helper.fetch_boolean(property_map, :selectable, false)
@drillable = Qa::LinkedData::Config::Helper.fetch_boolean(property_map, :drillable, false)
@prefixes = prefixes
end

# Can this property be the selected value?
Expand All @@ -46,6 +49,14 @@ def drillable?
@drillable
end

def ldpath_program
return @program if @program.present?
program_code = ""
prefixes.each { |key, url| program_code << "@prefix #{key} : <#{url}> \;\n" }
program_code << "property = #{ldpath} \;"
@program = Ldpath::Program.parse program_code
end

def group?
group_id.present?
end
Expand Down
53 changes: 53 additions & 0 deletions app/services/qa/linked_data/mapper/context_mapper_service.rb
@@ -0,0 +1,53 @@
# Provide service for mapping predicates to object values.
module Qa
module LinkedData
module Mapper
class ContextMapperService
class_attribute :graph_service
self.graph_service = Qa::LinkedData::GraphService

class << self
# Extract predicates specified in the predicate_map from the graph and return as a value map for a single subject URI.
# @param graph [RDF::Graph] the graph from which to extract result values
# @param context_map [Qa::LinkedData::Config::ContextMap] defines properties to extract from the graph to provide additional context
# @param subject_uri [RDF::URI] the subject within the graph for which the values are being extracted
# @return [<Hash<Symbol><Array<Object>>] mapped context values and information with hash of map key = array of object values for predicates identified in predicate_map.
# @example value map for a single result
# {:uri=>[#<RDF::URI:0x3fcff54a829c URI:http://id.loc.gov/authorities/names/n2010043281>],
# :id=>[#<RDF::Literal:0x3fcff4a367b4("n2010043281")>],
# :label=>[#<RDF::Literal:0x3fcff54a9a98("Valli, Sabrina"@en)>],
# :altlabel=>[],
# :sort=>[#<RDF::Literal:0x3fcff54b4c18("2")>]}
def map_context(graph:, context_map:, subject_uri:)
context = []
context_map.properties.each do |property_map|
values = fetch_values(property_map, graph, subject_uri)
next if values.blank?
context << construct_context(property_map, values)
end
context
end

private

def fetch_values(property_map, graph, subject_uri)
output = property_map.ldpath_program.evaluate subject_uri, graph
output.present? ? output['property'].uniq : nil
rescue
'PARSE ERROR'
end

def construct_context(property_map, values)
property_info = {}
property_info["group"] = property_map.group_id if property_map.group?
property_info["property"] = property_map.label
property_info["values"] = values
property_info["selectable"] = property_map.selectable?
property_info["drillable"] = property_map.drillable?
property_info
end
end
end
end
end
end
2 changes: 1 addition & 1 deletion app/services/qa/linked_data/mapper/graph_mapper_service.rb
Expand Up @@ -29,9 +29,9 @@ def self.map_values(graph:, predicate_map:, subject_uri:)
value_map = {}
predicate_map.each do |key, predicate|
values = predicate == :subject_uri ? [subject_uri] : graph_service.object_values(graph: graph, subject: subject_uri, predicate: predicate)
values = yield values if block_given?
value_map[key] = values
end
value_map = yield value_map if block_given?
value_map
end
end
Expand Down
Expand Up @@ -3,9 +3,10 @@ module Qa
module LinkedData
module Mapper
class SearchResultsMapperService
class_attribute :graph_mapper_service, :deep_sort_service
class_attribute :graph_mapper_service, :deep_sort_service, :context_mapper_service
self.graph_mapper_service = Qa::LinkedData::Mapper::GraphMapperService
self.deep_sort_service = Qa::LinkedData::DeepSortService
self.context_mapper_service = Qa::LinkedData::Mapper::ContextMapperService

class << self
# Extract predicates specified in the predicate_map from the graph and return as an array of value maps for each search result subject URI.
Expand All @@ -31,11 +32,17 @@ class << self
# :altlabel=>[],
# :sort=>[#<RDF::Literal:0x3fcff54b4c18("2")>]}
# ]
def map_values(graph:, predicate_map:, sort_key:, preferred_language: nil)
def map_values(graph:, predicate_map:, sort_key:, preferred_language: nil, context_map: nil)
search_matches = []
graph.subjects.each do |subject|
next if subject.anonymous? # skip blank nodes
values = graph_mapper_service.map_values(graph: graph, predicate_map: predicate_map, subject_uri: subject)
values = graph_mapper_service.map_values(graph: graph, predicate_map: predicate_map, subject_uri: subject) do |value_map|
next value_map if context_map.blank?
context = {}
context = context_mapper_service.map_context(graph: graph, context_map: context_map, subject_uri: subject) if context_map.present?
value_map[:context] = context
value_map
end
search_matches << values unless sort_key.present? && values[sort_key].blank?
end
search_matches = deep_sort_service.new(search_matches, sort_key, preferred_language).sort
Expand Down
15 changes: 13 additions & 2 deletions lib/qa/authorities/linked_data/search_query.rb
Expand Up @@ -25,13 +25,15 @@ def initialize(search_config)
# @param language [Symbol] (optional) language used to select literals when multi-language is supported (e.g. :en, :fr, etc.)
# @param replacements [Hash] (optional) replacement values with { pattern_name (defined in YAML config) => value }
# @param subauth [String] (optional) the subauthority to query
# @param context [Boolean] (optional) true if context should be returned with the results; otherwise, false (default: false)
# @return [String] json results
# @example Json Results for Linked Data Search
# [ {"uri":"http://id.worldcat.org/fast/5140","id":"5140","label":"Cornell, Joseph"},
# {"uri":"http://id.worldcat.org/fast/72456","id":"72456","label":"Cornell, Sarah Maria, 1802-1832"},
# {"uri":"http://id.worldcat.org/fast/409667","id":"409667","label":"Cornell, Ezra, 1807-1874"} ]
def search(query, language: nil, replacements: {}, subauth: nil)
def search(query, language: nil, replacements: {}, subauth: nil, context: false)
raise Qa::InvalidLinkedDataAuthority, "Unable to initialize linked data search sub-authority #{subauth}" unless subauth.nil? || subauthority?(subauth)
@context = context
@language = language_service.preferred_language(user_language: language, authority_language: search_config.language)
url = authority_service.build_url(action_config: search_config, action: :search, action_request: query, substitutions: replacements, subauthority: subauth)
Rails.logger.info "QA Linked Data search url: #{url}"
Expand All @@ -48,10 +50,18 @@ def load_graph(url:)

def parse_search_authority_response
results = results_mapper_service.map_values(graph: @graph, predicate_map: preds_for_search, sort_key: :sort,
preferred_language: @language)
preferred_language: @language, context_map: context_map)
convert_results_to_json(results)
end

def context_map
context? ? search_config.context_map : nil
end

def context?
@context == true
end

def preds_for_search
label_pred_uri = search_config.results_label_predicate
raise Qa::InvalidConfiguration, "required label_predicate is missing in search configuration for LOD authority #{auth_name}" if label_pred_uri.nil?
Expand All @@ -78,6 +88,7 @@ def convert_result_to_json(result)
json_result[:uri] = result[:uri].first.to_s
json_result[:id] = result[:id].first.to_s
json_result[:label] = full_label(result[:label], result[:altlabel])
json_result[:context] = result[:context] if context?
json_result
end

Expand Down
1 change: 1 addition & 0 deletions qa.gemspec
Expand Up @@ -19,6 +19,7 @@ Gem::Specification.new do |s|
s.add_dependency 'activerecord-import'
s.add_dependency 'deprecation'
s.add_dependency 'faraday'
s.add_dependency 'ldpath'
s.add_dependency 'nokogiri', '~> 1.6'
s.add_dependency 'rails', '~> 5.0'
s.add_dependency 'rdf'
Expand Down
4 changes: 4 additions & 0 deletions spec/controllers/linked_data_terms_controller_spec.rb
Expand Up @@ -231,6 +231,10 @@
end
end
end

it 'returns basic data + context when context=true' do
skip 'Pending: Need to write #search test with and without the context parameter'
end
end

describe '#show' do
Expand Down
6 changes: 6 additions & 0 deletions spec/models/linked_data/config/context_map_spec.rb
Expand Up @@ -48,6 +48,12 @@
}
end

describe '#new' do
it 'tests required context_map parameter and optional prefixes parameter' do
skip 'Pending: Need to write test for #initialize method'
end
end

describe '#properties' do
it 'returns the configured url template' do
expect(subject.properties.size).to eq 3
Expand Down
10 changes: 10 additions & 0 deletions spec/models/linked_data/config/context_property_map_spec.rb
Expand Up @@ -38,6 +38,10 @@
expect { subject }.to raise_error(Qa::InvalidConfiguration, 'drillable must be true or false')
end
end

it 'processes prefixes parameter' do
skip 'Pending: Need to test passing prefixes parameter to #initialize'
end
end

describe '#selectable?' do
Expand Down Expand Up @@ -178,4 +182,10 @@
end
end
end

describe '#ldpath_program' do
it 'returns the ldpath program for this property map' do
skip 'Pending: Need to write tests for #ldpath_program'
end
end
end
@@ -0,0 +1,9 @@
require 'spec_helper'

RSpec.describe Qa::LinkedData::Mapper::ContextMapperService do
describe '.map_context' do
it 'needs tests' do
skip 'Pending: Need to write tests for the ContextMapperService class'
end
end
end
4 changes: 4 additions & 0 deletions spec/services/linked_data/mapper/graph_mapper_service_spec.rb
Expand Up @@ -71,6 +71,10 @@
validate_entry(subject, :sort, ['3'], RDF::Literal)
end
end

it 'yields to passed in block' do
skip 'Pending: Need to write test to confirm passed in block can modify value_map'
end
end

def validate_entry(results, key, values, entry_kind)
Expand Down
Expand Up @@ -59,5 +59,9 @@
expect(subjects).not_to include "http://id.worldcat.org/fast/510103"
expect(subjects).not_to include "_:b0"
end

it 'adds context if requested' do
skip 'Pending: Need to write tests checking that the context_mapper_service is called only when context is requested and exists'
end
end
end

0 comments on commit 0f4ca35

Please sign in to comment.