I am trying to implement an autocomplete search bar that searches posts in my Rails 4.2 app. I have elasticsearch installed, searchkick gem, and typeahead.js for autocomplete on the frontend.
To that end, I followed a tutorial (https://shellycloud.com/blog/2013/10/adding-search-and-autocomplete-to-a-rails-app-with-elasticsearch) and have the following things set up:
In my routes.rb I have set up the collection route like so:
resources :posts do
collection do
get :autocomplete
end
resources :attachments
end
In my Post.rb model:
searchkick autocomplete: ['title', 'excerpt']
In my posts_controller.rb
def index
if params[:query].present?
#posts = Post.search(params[:query])
end
end
def autocomplete
render json: Post.search(params[:query], autocomplete: true, limit: 10).map do |post|
{title:post.title, excerpt: post.excerpt}
end
end
In my posts.js
$(document).ready(function() {
var posts = new Bloodhound({
datumTokenizer: Bloodhound.tokenizers.obj.whitespace('title'),
queryTokenizer: Bloodhound.tokenizers.whitespace,
prefetch: "/posts/autocomplete?query=%QUERY",
remote: {
url: "/posts/autocomplete?query=%QUERY"
}
});
$('#query').typeahead({
name: 'posts',
display: 'title',
source: posts
});
})
Finally, in my view index.html.haml I have:
=form_tag(posts_path, method: :get) do
=text_field_tag(:query, params[:query], autocomplete: 'off', class: 'typeahead')
%input{:type=>"submit", :value=> t('search')}
When I type something in the search box, I get this:
GET XHR http://localhost/posts/autocomplete [HTTP/1.1 400 Bad Request 177ms]
I can't tell what is going on. For what its worth, I have tried hitting http://localhost/posts/autocomplete.json?query=something and it works in that it will return the matching result. I have also tried replacing autocomplete with autocomplete.json in the JS I posted above.
Still, I get the same error in the console (with autocomplete.json in place of autocomplete)
Can you help?
Related
Novice here, having some difficulties implementing an autocomplete using Searchkick and Typeahead/bloodhound.
First of all I have a index/welcome home page where I have my input field for my search box. This then searches over multiple models for names of different Cells and different cellular markers (not important for this).
First of all in my routes file, im unsure wether to have my autocomplete 'attached' to my "welcome" route or my "Cells" route, I've tried both but doesn't seem to make a difference, currently it is set as this:
resources :cells do
collection do
match 'autocomplete' => 'welcome#autocomplete', via: [:get], as: :autocomplete
end
end
resources :welcome #index / home page
my form input is:
<%= form_tag(#Search, :action => 'index', :method => "get", id: "search-form") do %>
<%= text_field_tag(:q, nil, placeholder: "Search...", class: "form-control rounded-left typeahead", autocomplete: "off") %>
<% end %>
My model is as follows:
searchkick text_start: [:name], callbacks: :async, suggest: [:name], word_start: [:name]
and my controller action:
def autocomplete
render json: Cell.search(params[:q], {
fields: ["name^5"],
autocomplete: true,
limit: 5,
load: false,
misspellings: false
}).map(&:name)
end
finally the javascript:
<script src="typeahead.bundle.js"></script>
<script>
var Cells = new Bloodhound({
datumTokenizer: Bloodhound.tokenizers.whitespace,
queryTokenizer: Bloodhound.tokenizers.whitespace,
remote: {
url: '/welcome/autocomplete?query=%QUERY',
wildcard: '%QUERY'
}
});
$('#typeahead').typeahead(null, {
source: Cells
});
</script>
I guess I am just very confused on how to properly set this up, I'm finding the Searchkick docs to be difficult to extrapolate to my purposes as I'm pretty novice at all this :) If anyone is able to help or point me to a good guide (that isn't years old) that would be amazing, as I'm pretty sure I'm just going around in circles. Is this the best way to go about implementing an autocomplete or is there a better way to do this?
Thanks for reading, and thanks for any help :)
A quick update for any future readers:
Im certain in the routes, the code for your autocomplete must point towards your controller that has your autocomplete method, for me it is my welcome controller.
resources :welcome do
collection do
match 'autocomplete' => 'welcome#autocomplete', via: [:get], as: :autocomplete
end
end
Im happy with my autocomplete method in my controller and am satisfied that I need the current javascript (currently in tags in my html
<script>
var autocomplete = new Bloodhound({
datumTokenizer: Bloodhound.tokenizers.whitespace,
queryTokenizer: Bloodhound.tokenizers.whitespace,
remote: {
url: '/welcome/autocomplete?q=%QUERY',
wildcard: '%QUERY'
}
});
$('#typeahead').typeahead(null, {
source: autocomplete
});
</script>
var autocomplete variable needs to be used below for the source: (I didn't realise this before)
still having difficulties as the autocomplete is still not working but I think I'm making progress
The bootstrap-typeahead-rails gem's README kicks the question over to Twitter's typeahead.js README. This left much to be desired.
This Stack Overflow answer provides detailed instructions for the twitter-typeahead-rails gem. I wanted to see something like that for the bootstrap-typeahead-rails gem.
Here's my guide. It follows #ihaztehcodez's example. This example assumes a model Thing and adds a form to the index view for searching things by the model's name attribute.
A few notes:
I'm using Rails 4 (4.2.1).
For search queries, I'm using the Searchlight gem.
For templates, I'm using the slim-rails gem.
Styling is left as an exercise for the developer.
Add gem to gemfile
# Gemfile
# Typeahead gem
gem 'bootstrap-typeahead-rails'
# Optional gems
gem 'searchlight'
gem 'slim-rails'
Include typeahead files in asset manifests
Stylesheet (SASS)
# app/assets/stylesheets/application.scss
*= require bootstrap-typeahead-rails
Javascript
# app/assets/javascripts/application.js
//= require bootstrap-typeahead-rails
//= require_tree .
Add typeahead route to routes file
# config/routes.rb
get 'things/typeahead/:query' => 'things#typeahead'
Add typeahead javascript code
# app/assets/javascripts/things.js
var onReady = function() {
// initialize bloodhound engine
var searchSelector = 'input.typeahead';
var bloodhound = new Bloodhound({
datumTokenizer: function (d) {
return Bloodhound.tokenizers.whitespace(d.value);
},
queryTokenizer: Bloodhound.tokenizers.whitespace,
// sends ajax request to remote url where %QUERY is user input
remote: '/things/typeahead/%QUERY',
limit: 50
});
bloodhound.initialize();
// initialize typeahead widget and hook it up to bloodhound engine
// #typeahead is just a text input
$(searchSelector).typeahead(null, {
displayKey: 'name',
source: bloodhound.ttAdapter()
});
// this is the event that is fired when a user clicks on a suggestion
$(searchSelector).bind('typeahead:selected', function(event, datum, name) {
//console.debug('Suggestion clicked:', event, datum, name);
window.location.href = '/things/' + datum.id;
});
};
Add relevant methods/actions to controller
# app/controllers/things_controller.rb
# GET /things
# GET /things.json
def index
#search = ThingSearch.new(search_params)
#things = search_params.present? ? #search.results : Thing.all
end
# GET /things/typeahead/:query
def typeahead
#search = ThingSearch.new(typeahead: params[:query])
render json: #search.results
end
private
def search_params
params[:thing_search] || {}
end
Add search form to index view (using SLIM gem)
# app/views/things/index.html.slim
div.search.things
= form_for #search, url: things_path, method: :get do |f|
div.form-group.row
div.col-sm-3
div.col-sm-6
= f.text_field :name_like, {class: 'typeahead form-control',
placeholder: "Search by name"}
= f.submit 'Search', {class: 'btn btn-primary'}
div.col-sm-3.count
| Showing <strong>#{#things.length}</strong> Thing#{#things.length != 1 ? 's' : ''}
Create Searchlight search class
If you prefer not to use Searchlight, use the ActiveRecord query interface in the model.
# app/searches/thing_search.rb
class ThingSearch < Searchlight::Search
search_on Thing.all
searches :name_like, :typeahead
# Note: these two methods are identical but they could reasonably differ.
def search_name_like
search.where("name ILIKE ?", "%#{name_like}%")
end
def search_typeahead
search.where("name ILIKE ?", "%#{typeahead}%")
end
end
#klenwell's answer is out of date. Here's how I got it to work:
I'm using:
Bootstrap v3.3.6
bloodhound 0.11.1
bootstrap3-typeahead 3.1.0
jQuery 2.2.0
My model is called Destination.
app/models/destination_search.rb:
class DestinationSearch < Searchlight::Search
def base_query
Destination.all
end
def search_typeahead
query.where("name ILIKE", "%#{typeahead}%")
end
end
controller:
class DestinationsController < APIController
def typeahead
render json: DestinationSearch.new(typeahead: params[:query]).results
end
end
JS:
var bloodhound = new Bloodhound({
datumTokenizer: function (d) {
return Bloodhound.tokenizers.whitespace(d.value);
},
queryTokenizer: Bloodhound.tokenizers.whitespace,
remote: {
url: '/api/destinations/typeahead?query=%QUERY',
wildcard: "%QUERY",
},
limit: 10
});
bloodhound.initialize();
$(document).ready(function () {
$(".destination-typeahead").typeahead({
source: function (query, process) {
return bloodhound.search(query, process, process);
},
});
});
and in the view:
<%= text_field_tag :destination, class: "destination-typeahead" %>
It feels a little bit hacky how I'm passing the process method into bloodhound.search twice - this is because bloodhound#search takes two callbacks as arguments, one that deals with cached/prefetched data and one that deals with data pulled dynamically via AJAX. I might not be using #search 100% correctly, but this approach works, and it's a simple start.
Ok so what I have is a library database system where a user can type in books in a search bar and then the books that match their search are displayed when they click enter. What I want to do is get it so that when the user types "a" into the search bar all records will be displayed in a drop down below the menu with the letter "a", and then when they input the next letter "l" all records with "al" will be displayed in a drop down, and then "ali", and so on. I do not have much knowledge of gems, but want to learn. Is their a way I can do this? I am using rails 4.0.1 and at the minute have a fuzzy search method.
Or if you want to roll your own...
This is the gist of it, might require a bit of tweaking:
routes.rb
get '/search' => 'search#autocomplete', as: :search_autocomplete
search_controller.rb
def autocomplete
search_term = params[:search]
#results = YourModel.where( "your_field LIKE search_term" )
respond_to do |format|
format.json #results
end
end
your_view.html.erb
<%= form_tag( search_autocomplete_path, method: "get" ) do %>
<%= text_field_tag( :search, params[:search], placeholder: 'Enter a search term...', :id => 'autocomplete_search' ) %>
<%= submit_tag( "Go" ) %>
<div id="autocomplete_search_results">
<% end %>
some_coffeescript_file.js.coffee
$( document ).ready ->
$.ajax '/search',
type: 'GET'
parameters: $( "#autocomplete_search" ).val()
dataType: 'json'
success: ( data ) ->
# here you'll have to append the results to whichever div/container you have in place
$( '#autocomplete_search_results' ).append "#{ data }"
Checkout Twitter typeahead.js jquery plugin https://github.com/yourabi/twitter-typeahead-rails.
I have set up my application with the searchkick gem and am working on the autocomplete feature for my Presenters index. My problem is that I am searching on multiple fields (first_name and last_name), and while I can search for a presenter by both fields - for example, if I have Presenter 'Jennifer Hall', I can type either 'Jennifer' or 'Hall' and the name 'Jennifer Hall' will appear in the autocomplete drop down, but if I type 'Jennifer ' (once I add the space), the name disappears and there are no more suggestions. My question is how can I fix this?
My Code
I have followed this tutorial:
Adding search and autocomplete to a Rails app with Elasticsearch
presenter.rb:
class Presenter < ActiveRecord::Base
searchkick autocomplete: ['first_name', 'last_name'],
suggest: ['first_name', 'last_name']
...
end
presenters_controller.rb:
class PresentersController < ApplicationController
def index
if params[:query].present?
#presenters = Presenter.search(params[:query],
fields: [:first_name, :last_name],
page: params[:page])
else
#presenters = Presenter.all.page params[:page]
end
end
def autocomplete
render json: Presenter.search(params[:query],
autocomplete: true,
fields: [:first_name, :last_name],
limit: 10).map { |presenter|
presenter.first_name + " " + presenter.last_name
}
end
...
end
I got the .map{ |presenter| etc. } idea from here:
how do i map multiple attributes in rails with searchkick gem
However, when I followed the solution exactly, I only got 'undefined' in my drop-down of suggested names. Changing it to presenter.first_name + " " + presenter.last_name made it so the full name appears in the drop-down.
routes.rb:
resources :presenters, :only => [:index, :show] do
collection do
get :autocomplete
end
index.html.haml:
.container
= form_tag presenters_path, method: :get do
- if params[:query].present?
.btn-search.btn-clearsearch
= link_to "clear", presenters_path
- else
.search
Search
= text_field_tag :query, params[:query],
id: "presenter_search",
autocomplete: "off"
= submit_tag "go", class: "btn-search"
I am using typeahead.js, so that works with:
application.html.haml:
= javascript_include_tag 'application'
= javascript_include_tag
"//cdnjs.cloudflare.com/ajax/libs/typeahead.js/0.9.3/typeahead.min.js"
and
presenter.js.coffee:
$ ->
$('#presenter_search').typeahead
name: "presenter"
remote: "/presenters/autocomplete?query=%QUERY"
So, once again, my problem is that if I type the first name, all options matching that first name will appear in drop-down (both first and last name), but as soon as I type the space, the entire list of suggestions goes away. I have no way of searching, say, any first and last name 'Jennifer H'. I can guess that the problem is with the mapping in the controller/autocomplete action, but typing:
.map(&:first_name, &:last_name)
or
.map(&:first_name, :last_name)
generate errors, and
.map{ |presenter| presenter.slice(:first_name, :last_name) }
makes all suggestions in the drop-down read 'undefined.'
Any suggestions on how to solve this?
Hope this helps. Remember to reindex with rake searchkick:reindex CLASS=Model_name
// in model
def full_name
[first_name, last_name].join(' ')
end
searchkick text_middle: ['full_name'], autocomplete: ['full_name']
def search_data
{
full_name: full_name
}
end
// in controller
users = User.search params[:query], fields: [{"full_name" => :text_middle}]
This is how you use autocomplete with jQuery Tokeninput and ActsAsTaggableOn.
In my situation i am using a nested form but it shouldnt matter. Everything below is code that works.
Code
Product Model:
attr_accessible :tag_list # i am using the regular :tag_list
acts_as_taggable_on :tags # Tagging products
Products Controller:
#1. Define the tags path
#2. Searches ActsAsTaggable::Tag Model look for :name in the created table.
#3. it finds the tags.json path and whats on my form.
#4. it is detecting the attribute which is :name for your tags.
def tags
#tags = ActsAsTaggableOn::Tag.where("tags.name LIKE ?", "%#{params[:q]}%")
respond_to do |format|
format.json { render :json => #tags.map{|t| {:id => t.name, :name => t.name }}}
end
end
Routes:
# It has to find the tags.json or in my case /products/tags.json
get "products/tags" => "products#tags", :as => :tags
Application.js:
$(function() {
$("#product_tags").tokenInput("/products/tags.json", {
prePopulate: $("#product_tags").data("pre"),
preventDuplicates: true,
noResultsText: "No results, needs to be created.",
animateDropdown: false
});
});
Form:
<%= p.text_field :tag_list,
:id => "product_tags",
"data-pre" => #product.tags.map(&:attributes).to_json %>
Issue 1(SOLVED)
Must have the line:
format.json { render :json => #tags.collect{|t| {:id => t.name, :name => t.name }}}
Note - You can use #tags.map here as well and you dont have to change the form either.
Below are the 2 issues on why you needed to do this:
I have the following Tag: {"id":1,"name":"Food"}. When I save a Product, tagged "Food", it should save as ID: 1 when it searches and finds the name "Food". Currently, it saves a new Tag with a new ID that references the "Food" ID, i.e. {"id":19,"name":"1"}. Instead, it should be finding the ID, showing the name, and doing a find_or_create_by so it doesn't create a new Tag.
Issue 2(SOLVED)
When I go to products/show to see the tags by doing <%= #product.tag_list %>. The name appears as "Tags: 1", when it really should be "Tags: Food".
How can I fix these issues?
You should define a route in your routes.rb which should handle products/tags path. You can define it like:
get "products/tags" => "products#tags", :as => :tags
Thus should give you a tags_path helper which should evaluate to /products/tags. This should get rid of the errors you mentioned in the question. Be sure to add this route before defining resources :product in your routes.rb
Now onto acts-as-taggable-on, I haven't used this gem, but you should look at method all_tag_counts documentation. Your ProductsController#tags method will need some changes on the following lines. I am not sure if its exactly what would be required, as I use Mongoid and can't test it out.
def tags
#tags = Product.all_tag_counts.(:conditions => ["#{ActsAsTaggableOn::Tag.table_name}.name LIKE ?", "%#{params[:q]}%"])
respond_to do |format|
format.json { render :json => #tags.collect{|t| {:id => t.name, :name => t.name } }
end
end
little add-on:
If you want to create the tags on the fly, you could do this in your controller:
def tags
query = params[:q]
if query[-1,1] == " "
query = query.gsub(" ", "")
Tag.find_or_create_by_name(query)
end
#Do the search in memory for better performance
#tags = ActsAsTaggableOn::Tag.all
#tags = #tags.select { |v| v.name =~ /#{query}/i }
respond_to do |format|
format.json{ render :json => #tags.map(&:attributes) }
end
end
This will create the tag, whenever the space bar is hit.
You could then add this search setting in the jquery script:
noResultsText: 'No result, hit space to create a new tag',
It's a little dirty but it works for me.
There is a bug in Application.js code. There is an extra ) after "/products/tags.json". Remove the extra ). The code should be:
$("#product_tags").tokenInput("/products/tags.json", {
prePopulate: $("#product_tags").data("pre"),
preventDuplicates: true,
noResultsText: "No results, needs to be created.",
animateDropdown: false
});
I don't know if this is the entirety of your error, but you are not hitting the proper URL with the tokenInput plugin.
This
$("#product_tag_list").tokenInput("/products/tags.json"), {
should be
$("#product_tag_list").tokenInput("/products.json"), {
As I said, I don't know if this is the only problem you are having, but if you change this, does it work?
EDIT:
I have never used ActsAsTaggableOn. Does it create a Tag model for you to use?
From the looks of it on github, if you wanted to query all tags, you might have to use its namespace as opposed to just Tag, meaning ActsAsTaggableOn::Tag. For example, you can see how they access Tags directly in some of the specs.
I had problems with editing the tags if for example the model failed to validate,
I changed
<%= p.text_field :tag_list,
:id => "product_tags",
"data-pre" => #product.tags.map(&:attributes).to_json %>
to
<%= p.text_field :tag_list,
:id => "product_tags",
"data-pre" => #product.tag_list.map {|tag| {:id => tag, :name => tag } }.to_json %>
If the form failed to validate on first submission, it was creating tags as the ID's of the tags it had created on subsequent submissions.
Two notes: if you're getting the tags changed by numbers on the POST request, use:
tokenValue: "name"
And if you're trying to add non-existent tags, use (undocumented):
allowFreeTagging: true