Skip to content

Commit

Permalink
Merge pull request #1288 from projectblacklight/typeahead
Browse files Browse the repository at this point in the history
Adds autocomplete typeahead feature
  • Loading branch information
cbeer committed Nov 6, 2015
2 parents 66fc8b8 + cc74046 commit 0f31a3f
Show file tree
Hide file tree
Showing 21 changed files with 344 additions and 2 deletions.
32 changes: 32 additions & 0 deletions app/assets/javascripts/blacklight/autocomplete.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*global Bloodhound */

Blacklight.onLoad(function() {
'use strict';

$('[data-autocomplete-enabled="true"]').each(function() {
var $el = $(this);
var suggestUrl = $el.data().autocompletePath;

var terms = new Bloodhound({
datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'),
queryTokenizer: Bloodhound.tokenizers.whitespace,
remote: {
url: suggestUrl + '?q=%QUERY',
wildcard: '%QUERY'
}
});

terms.initialize();

$el.typeahead({
hint: true,
highlight: true,
minLength: 2
},
{
name: 'terms',
displayKey: 'term',
source: terms.ttAdapter()
});
});
});
4 changes: 4 additions & 0 deletions app/assets/javascripts/blacklight/blacklight.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
//= require blacklight/search_context
//= require blacklight/collapsable
//= require blacklight/facet_load
//= require blacklight/autocomplete
//
//Bootstrap JS for providing collapsable tablet/mobile menu/alert boxes
//= require bootstrap/transition
Expand All @@ -19,6 +20,9 @@
//= require bootstrap/alert
//= require bootstrap/modal

// Twitter Typeahead for autocomplete
//= require twitter/typeahead

/* Blacklight has a Javascript setup meant to support local disabling,
modification, and use of Blacklight behaviors.
Expand Down
1 change: 1 addition & 0 deletions app/assets/stylesheets/blacklight/_blacklight_base.scss
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@
@import "blacklight/facets";
@import "blacklight/search_history";
@import "blacklight/modal";
@import "blacklight/twitter_typeahead";
30 changes: 30 additions & 0 deletions app/assets/stylesheets/blacklight/_twitter_typeahead.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
.twitter-typeahead {
float: left;
width: 100%;
z-index: 10000;

.tt-input.form-control {
width: 100%;
}

.tt-hint.form-control {
width: 100%;
}

.tt-menu {
@extend .dropdown-menu;
font-family: $font-family-base;
width: 100%;
}

.tt-cursor {
background-color: $dropdown-link-hover-bg;
color: $dropdown-link-hover-color;
text-decoration: none;
}

.tt-suggestion {
font-size: 14px;
padding: 5px 5px 5px 10px;
}
}
24 changes: 24 additions & 0 deletions app/controllers/concerns/blacklight/suggest.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
module Blacklight
module Suggest
extend ActiveSupport::Concern

included do
include Blacklight::Configurable
include Blacklight::SearchHelper

copy_blacklight_config_from(CatalogController)
end

def index
respond_to do |format|
format.json do
render json: suggestions_service.suggestions
end
end
end

def suggestions_service
Blacklight::SuggestSearch.new(params, repository).suggestions
end
end
end
33 changes: 33 additions & 0 deletions app/controllers/concerns/blacklight/suggest_search.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
module Blacklight
class SuggestSearch
attr_reader :request_params, :repository

##
# @param [Hash] params
def initialize(params, repository)
@request_params = { q: params[:q] }
@repository = repository
end

##
# For now, only use the q parameter to create a
# Blacklight::Suggest::Response
# @return [Blacklight::Suggest::Response]
def suggestions
Blacklight::Suggest::Response.new suggest_results, request_params, suggest_handler_path
end

##
# Query the suggest handler using RSolr::Client::send_and_receive
# @return [RSolr::HashWithResponse]
def suggest_results
repository.connection.send_and_receive(suggest_handler_path, params: request_params)
end

##
# @return [String]
def suggest_handler_path
repository.blacklight_config.autocomplete_path
end
end
end
3 changes: 3 additions & 0 deletions app/controllers/suggest_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class SuggestController < ApplicationController
include Blacklight::Suggest
end
10 changes: 10 additions & 0 deletions app/helpers/blacklight/suggest_helper_behavior.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module Blacklight
module SuggestHelperBehavior
##
# @return [Boolean] should autocomplete be enabled in the UI
def autocomplete_enabled?
blacklight_config.autocomplete_enabled.present? &&
blacklight_config.autocomplete_path.present?
end
end
end
3 changes: 3 additions & 0 deletions app/helpers/suggest_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module SuggestHelper
include Blacklight::SuggestHelperBehavior
end
26 changes: 26 additions & 0 deletions app/models/concerns/blacklight/suggest/response.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
module Blacklight
module Suggest
class Response
attr_reader :response, :request_params, :suggest_path

##
# Creates a suggest response
# @param [RSolr::HashWithResponse] response
# @param [Hash] request_params
# @param [String] suggest_path
def initialize(response, request_params, suggest_path)
@response = response
@request_params = request_params
@suggest_path = suggest_path
end

##
# Trys the suggestor response to return suggestions if they are
# present
# @return [Array]
def suggestions
response.try(:[], suggest_path).try(:[], 'mySuggester').try(:[], request_params[:q]).try(:[], 'suggestions') || []
end
end
end
end
2 changes: 1 addition & 1 deletion app/views/catalog/_search_form.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<% end %>

<label for="q" class="sr-only"><%= t('blacklight.search.form.search.label') %></label>
<%= text_field_tag :q, params[:q], placeholder: t('blacklight.search.form.search.placeholder'), class: "search_q q form-control", id: "q", autofocus: should_autofocus_on_search_box? %>
<%= text_field_tag :q, params[:q], placeholder: t('blacklight.search.form.search.placeholder'), class: "search_q q form-control", id: "q", autofocus: should_autofocus_on_search_box?, data: { autocomplete_enabled: autocomplete_enabled?, autocomplete_path: blacklight.suggest_index_path } %>

<span class="input-group-btn">
<button type="submit" class="btn btn-primary search-btn" id="search">
Expand Down
1 change: 1 addition & 0 deletions blacklight.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Gem::Specification.new do |s|
s.add_dependency "rsolr", "~> 1.0.11" # Library for interacting with rSolr.
s.add_dependency "bootstrap-sass", "~> 3.2"
s.add_dependency "deprecation"
s.add_dependency "twitter-typeahead-rails", '~> 0.11'

s.add_development_dependency "solr_wrapper"
s.add_development_dependency "rspec-rails", "~> 3.0"
Expand Down
1 change: 1 addition & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@
put "saved_searches/save/:id", :to => "saved_searches#save", :as => "save_search"
delete "saved_searches/forget/:id", :to => "saved_searches#forget", :as => "forget_search"
post "saved_searches/forget/:id", :to => "saved_searches#forget"
resources :suggest, only: :index, defaults: { format: 'json' }
end
1 change: 1 addition & 0 deletions lib/blacklight/engine.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ class Engine < Rails::Engine
engine_name "blacklight"

require 'bootstrap-sass'
require 'twitter-typeahead-rails'

# BlacklightHelper is needed by all helpers, so we inject it
# into action view base here.
Expand Down
4 changes: 4 additions & 0 deletions lib/generators/blacklight/templates/catalog_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -180,5 +180,9 @@ class <%= controller_name.classify %>Controller < ApplicationController
# If there are more than this many search results, no spelling ("did you
# mean") suggestion is offered.
config.spell_max = 5

# Configuration for autocomplete suggestor
config.autocomplete_enabled = true
config.autocomplete_path = 'suggest'
end
end
15 changes: 14 additions & 1 deletion solr/conf/schema.xml
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,15 @@
</analyzer>
</fieldType>

<fieldType class="solr.TextField" name="textSuggest" positionIncrementGap="100">
<analyzer>
<tokenizer class="solr.KeywordTokenizerFactory"/>
<filter class="solr.StandardFilterFactory"/>
<filter class="solr.LowerCaseFilterFactory"/>
<filter class="solr.RemoveDuplicatesTokenFilterFactory"/>
</analyzer>
</fieldType>

<!-- charFilter + WhitespaceTokenizer -->
<!--
<fieldType name="text_char_norm" class="solr.TextField" positionIncrementGap="100" >
Expand Down Expand Up @@ -519,6 +528,7 @@
<dynamicField name="*_sort" type="alphaOnlySort" indexed="true" stored="false" multiValued="false" />
<dynamicField name="*_unstem_search" type="text_general" indexed="true" stored="false" multiValued="true" />
<dynamicField name="*spell" type="textSpell" indexed="true" stored="false" multiValued="true" />
<dynamicField name="*suggest" type="textSuggest" indexed="true" stored="false" multiValued="true" />

<!-- uncomment the following to ignore any fields that don't already match an existing
field name or dynamic field, rather than reporting them as an error.
Expand Down Expand Up @@ -591,7 +601,10 @@
<copyField source="subject_t" dest="opensearch_display"/>
<copyField source="subject_addl_t" dest="opensearch_display"/>


<!-- for suggestions -->
<copyField source="*_t" dest="suggest"/>
<copyField source="*_facet" dest="suggest"/>

<!-- Above, multiple source fields are copied to the [text] field.
Another way to map multiple source fields to the same
destination field is to use the dynamic field syntax.
Expand Down
21 changes: 21 additions & 0 deletions solr/conf/solrconfig.xml
Original file line number Diff line number Diff line change
Expand Up @@ -387,5 +387,26 @@
-->
</searchComponent>

<searchComponent name="suggest" class="solr.SuggestComponent">
<lst name="suggester">
<str name="name">mySuggester</str>
<str name="lookupImpl">FuzzyLookupFactory</str>
<str name="suggestAnalyzerFieldType">textSuggest</str>
<str name="buildOnCommit">true</str>
<str name="field">suggest</str>
</lst>
</searchComponent>

<requestHandler name="/suggest" class="solr.SearchHandler" startup="lazy">
<lst name="defaults">
<str name="suggest">true</str>
<str name="suggest.count">5</str>
<str name="suggest.dictionary">mySuggester</str>
</lst>
<arr name="components">
<str>suggest</str>
</arr>
</requestHandler>

</config>

22 changes: 22 additions & 0 deletions spec/controllers/blacklight/suggest_search_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
require 'spec_helper'

describe Blacklight::SuggestSearch do
let(:params) { {q: 'test'} }
let(:suggest_path) { 'suggest' }
let(:connection) { double('connection', send_and_receive: 'sent')}
let(:repository) { double('repository', connection: connection) }
let(:suggest_search) { described_class.new(params, repository)}
describe '#suggestions' do
it 'returns a Blacklight::Suggest::Response' do
expect(suggest_search).to receive(:suggest_results).and_return([])
expect(suggest_search).to receive(:suggest_handler_path).and_return(suggest_path)
expect(suggest_search.suggestions).to be_an Blacklight::Suggest::Response
end
end
describe '#suggest_results' do
it 'should call send_and_recieve from a repository connection' do
expect(suggest_search).to receive(:suggest_handler_path).and_return(suggest_path)
expect(suggest_search.suggest_results).to eq 'sent'
end
end
end
17 changes: 17 additions & 0 deletions spec/controllers/suggest_controller_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
require 'spec_helper'

describe SuggestController do
routes { Blacklight::Engine.routes }
describe 'GET index' do
it 'returns JSON' do
get :index, format: 'json'
expect(response.body).to eq [].to_json
end
it 'returns suggestions' do
get :index, format: 'json', q: 'new'
json = JSON.parse(response.body)
expect(json.count).to eq 3
expect(json.first['term']).to eq 'new jersey'
end
end
end
41 changes: 41 additions & 0 deletions spec/helpers/suggest_helper_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
require 'spec_helper'

describe SuggestHelper do
before do
allow(helper).to receive(:blacklight_config).and_return(blacklight_config)
end
describe '#autocomplete_enabled?' do
describe 'with autocomplete config' do
let(:blacklight_config) do
Blacklight::Configuration.new.configure do |config|
config.autocomplete_enabled = true
config.autocomplete_path = 'suggest'
end
end
it 'is enabled' do
expect(helper.autocomplete_enabled?).to be true
end
end
describe 'without disabled config' do
let(:blacklight_config) do
Blacklight::Configuration.new.configure do |config|
config.autocomplete_enabled = false
config.autocomplete_path = 'suggest'
end
end
it 'is disabled' do
expect(helper.autocomplete_enabled?).to be false
end
end
describe 'without path config' do
let(:blacklight_config) do
Blacklight::Configuration.new.configure do |config|
config.autocomplete_enabled = true
end
end
it 'is disabled' do
expect(helper.autocomplete_enabled?).to be false
end
end
end
end
Loading

0 comments on commit 0f31a3f

Please sign in to comment.