diff --git a/Gemfile b/Gemfile index e4acd80a..d792d5f0 100644 --- a/Gemfile +++ b/Gemfile @@ -71,4 +71,4 @@ gem 'squash_ruby', require: 'squash/ruby' gem 'squash_rails', require: 'squash/rails' gem 'sitemap_generator', '~> 5.0.5' gem 'newrelic_rpm' - +gem 'twitter-typeahead-rails' diff --git a/Gemfile.lock b/Gemfile.lock index e2ea00e2..db6a51bc 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -294,6 +294,10 @@ GEM thread_safe (0.3.5) tilt (1.4.1) tins (1.3.4) + twitter-typeahead-rails (0.10.5) + actionpack (>= 3.1) + jquery-rails + railties (>= 3.1) tzinfo (1.2.2) thread_safe (~> 0.1) uglifier (2.7.0) @@ -343,4 +347,5 @@ DEPENDENCIES squash_rails squash_ruby therubyracer + twitter-typeahead-rails uglifier (>= 1.3.0) diff --git a/Rakefile b/Rakefile index 18f6ee3b..5c82e066 100644 --- a/Rakefile +++ b/Rakefile @@ -5,7 +5,7 @@ require File.expand_path('../config/application', __FILE__) Rails.application.load_tasks -BLACKLIGHT_JETTY_VERSION = '4.10.2' +BLACKLIGHT_JETTY_VERSION = '4.10.4' ZIP_URL = "https://github.com/projectblacklight/blacklight-jetty/archive/v#{BLACKLIGHT_JETTY_VERSION}.zip" require 'jettywrapper' diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index d815aa61..797b5aea 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -17,3 +17,5 @@ // Required by Blacklight //= require blacklight/blacklight //= require_tree . + +//= require twitter/typeahead.min diff --git a/app/assets/javascripts/modules/autocomplete.js b/app/assets/javascripts/modules/autocomplete.js new file mode 100644 index 00000000..6f9cec6e --- /dev/null +++ b/app/assets/javascripts/modules/autocomplete.js @@ -0,0 +1,22 @@ +$(document).on('ready page:load', function() { + var terms = new Bloodhound({ + datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'), + queryTokenizer: Bloodhound.tokenizers.whitespace, + remote: { + url: '/suggest?q=%QUERY' + } + }); + + terms.initialize(); + + $('input.search_q').typeahead({ + hint: true, + highlight: true, + minLength: 2 + }, + { + name: 'terms', + displayKey: 'term', + source: terms.ttAdapter() + }); +}); diff --git a/app/assets/stylesheets/earthworks.css.scss b/app/assets/stylesheets/earthworks.css.scss index fdfb500f..f41798e9 100644 --- a/app/assets/stylesheets/earthworks.css.scss +++ b/app/assets/stylesheets/earthworks.css.scss @@ -10,6 +10,7 @@ @import 'modules/show'; @import 'modules/sul_footer'; @import 'modules/top_navbar'; +@import 'modules/typeahead'; @import 'modules/zero_results'; diff --git a/app/assets/stylesheets/modules/typeahead.scss b/app/assets/stylesheets/modules/typeahead.scss new file mode 100644 index 00000000..8cc7694d --- /dev/null +++ b/app/assets/stylesheets/modules/typeahead.scss @@ -0,0 +1,31 @@ +.twitter-typeahead { + float: left; + width: 100%; + z-index: 10000; + + .tt-input.form-control { + width: 100%; + } + + .tt-hint.form-control { + width: 100%; + } + + .tt-dropdown-menu { + @extend .dropdown-menu; + font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; + + width: 100%; + + .tt-suggestion p{ + font-size: 14px; + padding-left: 10px; + } + + .tt-cursor { + background-color: $dropdown-link-hover-bg; + color: $dropdown-link-hover-color; + text-decoration: none; + } + } +} diff --git a/app/controllers/suggest_controller.rb b/app/controllers/suggest_controller.rb new file mode 100644 index 00000000..9bff7013 --- /dev/null +++ b/app/controllers/suggest_controller.rb @@ -0,0 +1,3 @@ +class SuggestController < ApplicationController + include Earthworks::Suggest +end diff --git a/config/application.rb b/config/application.rb index b14b075c..7e1a59f8 100644 --- a/config/application.rb +++ b/config/application.rb @@ -11,6 +11,9 @@ class Application < Rails::Application config.application_name = 'EarthWorks' require 'rights_metadata' + require 'suggest/response' + require 'suggest/search_helper' + require 'suggest' # Settings in config/environments/* take precedence over those specified here. # Application configuration should go into files in config/initializers # -- all .rb files in that directory are automatically loaded. diff --git a/config/blacklight.yml b/config/blacklight.yml index 3977b3bf..60636aa2 100644 --- a/config/blacklight.yml +++ b/config/blacklight.yml @@ -11,8 +11,8 @@ # how to start up solr, generally for automated testing. development: - url: <%= ENV['SOLR_URL'] || "http://127.0.0.1:8983/solr" %> + url: <%= ENV['SOLR_URL'] || "http://127.0.0.1:8983/solr/blacklight-core" %> test: &test - url: <%= ENV['TEST_SOLR_URL'] || "http://127.0.0.1:8888/solr" %> + url: <%= ENV['TEST_SOLR_URL'] || "http://127.0.0.1:8888/solr/blacklight-core" %> production: - url: <%= ENV['SOLR_URL'] || "http://127.0.0.1:8983/solr" %> + url: <%= ENV['SOLR_URL'] || "http://127.0.0.1:8983/solr/blacklight-core" %> diff --git a/config/routes.rb b/config/routes.rb index 4e004c5b..bb0c42a8 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -7,6 +7,8 @@ match 'users/auth/webauth/logout' => 'devise/sessions#destroy', :as => :destroy_user_session, :via => Devise.mappings[:user].sign_out_via end + resources :suggest, only: :index, defaults: { format: 'json' } + resource :feedback_form, path: 'feedback', only: [:new, :create] get 'feedback' => 'feedback_forms#new' # The priority is based upon order of creation: first created -> highest priority. diff --git a/config/solr_configs/schema.xml b/config/solr_configs/schema.xml index 0234e0cd..ced33f7b 100644 --- a/config/solr_configs/schema.xml +++ b/config/solr_configs/schema.xml @@ -35,6 +35,10 @@ + + + + @@ -154,5 +177,20 @@ - + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/config/solr_configs/solrconfig.xml b/config/solr_configs/solrconfig.xml index 3e93169c..6434f3f7 100644 --- a/config/solr_configs/solrconfig.xml +++ b/config/solr_configs/solrconfig.xml @@ -147,7 +147,13 @@ dc_subject_sm layer_geom_type_s solr_year_i + + true + + + spellcheck + @@ -177,4 +183,51 @@ *:* - \ No newline at end of file + + + + + default + spell + solr.DirectSolrSpellChecker + + internal + + 0.5 + + 2 + + 1 + + 5 + + 4 + + 0.01 + + + + + + + mySuggester + FuzzyLookupFactory + textSuggest + true + suggest + + + + + + true + 5 + mySuggester + + + suggest + + + diff --git a/lib/suggest.rb b/lib/suggest.rb new file mode 100644 index 00000000..6f816f58 --- /dev/null +++ b/lib/suggest.rb @@ -0,0 +1,17 @@ +module Earthworks + module Suggest + extend ActiveSupport::Concern + include Suggest::SearchHelper + + ## + # Get suggestion results from the Solr index + def index + @response = get_suggestions params + respond_to do |format| + format.json do + render json: @response.suggestions + end + end + end + end +end diff --git a/lib/suggest/response.rb b/lib/suggest/response.rb new file mode 100644 index 00000000..6eed93df --- /dev/null +++ b/lib/suggest/response.rb @@ -0,0 +1,24 @@ +module Earthworks + module Suggest + class Response + attr_reader :response, :request_params + + ## + # Creates a suggest response + # @param [RSolr::HashWithResponse] response + # @param [Hash] request_params + def initialize(response, request_params) + @response = response + @request_params = request_params + end + + ## + # Trys the suggestor response to return suggestions if they are + # present + # @return [Array] + def suggestions + response.try(:[], 'suggest').try(:[], 'mySuggester').try(:[], request_params[:q]).try(:[], 'suggestions') || [] + end + end + end +end diff --git a/lib/suggest/search_helper.rb b/lib/suggest/search_helper.rb new file mode 100644 index 00000000..e54c7dec --- /dev/null +++ b/lib/suggest/search_helper.rb @@ -0,0 +1,26 @@ +module Earthworks + module Suggest + module SearchHelper + extend ActiveSupport::Concern + include Blacklight::SearchHelper + + ## + # For now, only use the q parameter to create a + # Earthworks::Suggest::Response + # @param [Hash] params + # @return [Earthworks::Suggest::Response] + def get_suggestions(params) + request_params = { q: params[:q] } + Earthworks::Suggest::Response.new suggest_results(request_params), request_params + end + + ## + # Query the suggest handler using RSolr::Client::send_and_receive + # @param [Hash] request_params + # @return [RSolr::HashWithResponse] + def suggest_results(request_params) + repository.connection.send_and_receive('suggest', params: request_params) + end + end + end +end diff --git a/spec/controllers/suggest_controller_spec.rb b/spec/controllers/suggest_controller_spec.rb new file mode 100644 index 00000000..7dd40097 --- /dev/null +++ b/spec/controllers/suggest_controller_spec.rb @@ -0,0 +1,20 @@ +require 'rails_helper' + +describe SuggestController do + describe 'GET index' do + it 'assigns @response' do + get :index, format: 'json' + expect(assigns(:response)).to be_an Earthworks::Suggest::Response + end + it 'renders json' do + get :index, format: 'json' + expect(response.body).to eq [].to_json + end + it 'returns suggestions' do + get :index, format: 'json', q: 'st' + json = JSON.parse(response.body) + expect(json.count).to eq 2 + expect(json.first['term']).to eq 'stanford' + end + end +end diff --git a/spec/features/search_spec.rb b/spec/features/search_spec.rb new file mode 100644 index 00000000..410a1294 --- /dev/null +++ b/spec/features/search_spec.rb @@ -0,0 +1,12 @@ +require 'rails_helper' + +feature 'Search' do + feature 'spelling suggestions' do + scenario 'are turned on' do + visit root_path + fill_in 'q', with: 'standford' + click_button 'search' + expect(page).to have_content 'Did you mean to type:' + end + end +end diff --git a/spec/lib/suggest/response_spec.rb b/spec/lib/suggest/response_spec.rb new file mode 100644 index 00000000..7ce042c0 --- /dev/null +++ b/spec/lib/suggest/response_spec.rb @@ -0,0 +1,48 @@ +require 'rails_helper' + +describe Earthworks::Suggest::Response do + let(:empty_response) { Earthworks::Suggest::Response.new({}, q: 'hello') } + let(:response) do + Earthworks::Suggest::Response.new( + { + 'responseHeader' => { + 'status' => 0, + 'QTime' => 42 + }, + 'suggest' => { + 'mySuggester' => { + 'st' => { + 'numFound' => 2, + 'suggestions' => [ + { + 'term' => 'stanford', + 'weight' => 3, + 'payload' => '' + }, + { + 'term' => 'statistics', + 'weight' => 1, + 'payload' => '' + } + ] + } + } + } + }, + q: 'st' + ) + end + + describe '#initialize' do + it 'creates a Earthworks::Suggest::Response' do + expect(empty_response).to be_an Earthworks::Suggest::Response + end + end + describe '#suggestions' do + it 'returns an array of suggestions' do + expect(response.suggestions).to be_an Array + expect(response.suggestions.count).to eq 2 + expect(response.suggestions.first['term']).to eq 'stanford' + end + end +end diff --git a/spec/lib/suggest/search_helper_spec.rb b/spec/lib/suggest/search_helper_spec.rb new file mode 100644 index 00000000..855f0ff1 --- /dev/null +++ b/spec/lib/suggest/search_helper_spec.rb @@ -0,0 +1,44 @@ +require 'rails_helper' + +describe Earthworks::Suggest::SearchHelper do + + class SearchHelperTestClass + include Earthworks::Suggest::SearchHelper + + attr_accessor :blacklight_config + attr_accessor :repository + + def initialize blacklight_config, conn + self.blacklight_config = blacklight_config + self.repository = Blacklight::SolrRepository.new(blacklight_config) + self.repository.connection = conn + end + + def params + {} + end + end + + subject { SearchHelperTestClass.new blacklight_config, blacklight_solr } + + let(:blacklight_config) { Blacklight::Configuration.new } + let(:copy_of_catalog_config) { ::CatalogController.blacklight_config.deep_copy } + let(:blacklight_solr) { RSolr.connect(Blacklight.connection_config) } + + + + describe '#get_suggestions' do + it 'returns a Earthworks::Suggest::Response' do + expect(subject.get_suggestions q: 'test').to be_an Earthworks::Suggest::Response + end + end + describe '#suggest_results' do + it 'queries the suggest handler with params' do + allow(blacklight_solr).to receive(:get) do |path, params| + expect(path).to eq 'suggest' + expect(params).to eq params: { q: 'st' } + end + subject.query_solr q: 'st' + end + end +end