After reading several sites (including elasticsearch's documentation) and experimenting around a lot, I'm having trouble getting highlights. I can do the basic keyword search, but it's clear I'm not grasping something. Here's my code.
Gems:
gem 'elasticsearch-model'
gem 'elasticsearch-rails'
Controller:
class TermsController < ApplicationController
def search
#terms = Term.search(params[:query]).results
end
end
Model:
require 'elasticsearch/model'
class Term < ActiveRecord::Base
include Elasticsearch::Model
include Elasticsearch::Model::Callbacks
settings index: { number_of_shards: 1, number_of_replicas: 0 } do
mappings dynamic: 'false' do
indexes :id, index: :not_analyzed
indexes :name, analyzer: 'spanish'
indexes :gender, index: :not_analyzed
indexes :part_of_speech, index: :not_analyzed
indexes :definition
indexes :etymology1
indexes :etymology2
indexes :uses
indexes :romance_cognates
indexes :notes1
indexes :notes2
indexes :quote1, analyzer: 'spanish'
indexes :quote2, analyzer: 'spanish'
end
end
def as_indexed_json(options = {})
as_json(
only: [:name, :gender, :part_of_speech, :definition, :etymology1, :etymology2, :uses, :romance_cognates, :notes1, :notes2, :quote1, :quote2]
)
end
def self.search(query)
__elasticsearch__.search(
{
query: {
multi_match: {
query: query,
fields: ['name', 'definition', 'etymology1', 'etymology2', 'uses', 'romance_cognates', 'notes1', 'notes2', 'quote1', 'quote2']
}
},
highlight: {
tags_schema: 'styled',
fields: {
:'*' => {}
}
}
}
)
end
end
# Delete the previous terms index in Elasticsearch
Term.__elasticsearch__.client.indices.delete index: Term.index_name rescue nil
# Create the new index with the new mapping
Term.__elasticsearch__.client.indices.create \
index: Term.index_name,
body: { settings: Term.settings.to_hash, mappings: Term.mappings.to_hash }
# Index all term records from the db to Elasticsearch
Term.import(force: true)
I also tried:
{
query: {
multi_match: {
query: query,
fields: ['name', 'definition', 'etymology1', 'etymology2', 'uses', 'romance_cognates', 'notes1', 'notes2', 'quote1', 'quote2']
}
},
highlight: {
fields: {
content: {'force_source': true}
}
}
}
and
{
query: {
multi_match: {
query: query,
fields: ['name', 'definition', 'etymology1', 'etymology2', 'uses', 'romance_cognates', 'notes1^5', 'notes2', 'quote1', 'quote2']
}
},
highlight: {
fields: {
content: {type: 'plain'}
}
}
}
and
{
query: {
multi_match: {
query: query,
fields: ['name', 'definition', 'etymology1', 'etymology2', 'uses', 'romance_cognates', 'notes1^5', 'notes2', 'quote1', 'quote2']
}
},
highlight: {
pre_tags: ['<tag1>']
post_tags: ['</tag1>']
fields: {
_all: {}
}
}
}
...Along with many other attempts I can't remember
It appears the key that I was missing as illustrated here is that I needed the try() method in my view template. I'm sure there's a more concise way of writing this, but a sample of my view syntax looks like this:
<%= term.try(:highlight).try(:definition) ? term.highlight.definition[0].html_safe : term.definition.html_safe %>
<%= term.try(:highlight).try(:etymology1) ? term.highlight.etymology1[0].html_safe : term.etymology1.html_safe %>
Related
I'm using elasticsearch on my rails app and every time I try to add a block to the def self.search(query) I get an error:
[400] {"error":{"root_cause":[{"type":"parse_exception","reason":"illegal latitude value [269.99999983236194] for [GeoDistanceSort] for field [distance_type]."}],"type":"search_phase_execution_exception","reason":"all shards failed","phase":"query","grouped":true,"failed_shards":[{"shard":0,"index":"prices","node":"AfyW4Pa4S-qKhua-3lY4rg","reason":{"type":"parse_exception","reason":"illegal latitude value [269.99999983236194] for [GeoDistanceSort] for field [distance_type]."}}]},"status":400}
Up to this point, it doesn't return an error:
def self.search(query)
__elasticsearch__.search(
{
query: {
multi_match: {
query: query,
fields: ['title^5', 'description']
}
},
highlight: {
pre_tags: ['<em>'],
post_tags: ['</em>'],
fields: {
title: {},
description: {},
}
}
}
)
end
if a try to add the sort block query inside the model, it returns the above error:
require 'elasticsearch/model'
require 'elasticsearch/model'
class Item < ApplicationRecord
mount_uploader :image, ImageUploader
include Elasticsearch::Model
include Elasticsearch::Model::Callbacks
index_name Rails.application.class.parent_name.underscore
document_type self.name.downcase
settings index: { number_of_shards: 1 } do
mappings dynamic: 'false' do
indexes :title, analyzer: 'english', index_options: 'offsets'
indexes :description, analyzer: 'english'
indexes :location, type: 'geo_point'
end
end
def location
[longitude.to_f, latitude.to_f]
end
def current_location
location = request.location
end
def self.search(query)
__elasticsearch__.search(
{
query: {
multi_match: {
query: query,
fields: ['title^5', 'description']
}
},
sort: [
{
_geo_distance: {
"pin.location": ["current_location"],
distance: ["radius"],
unit: ["km"],
mode: ["min"],
order: ["asc"],
distance_type: ["arc"],
}
}
],
highlight: {
pre_tags: ['<em>'],
post_tags: ['</em>'],
fields: {
title: {},
description: {},
}
}
}
)
end
end
Item.__elasticsearch__.client.indices.delete index: Item.index_name rescue nil
Item.__elasticsearch__.client.indices.create \
index: Item.index_name,
body: { settings: Item.settings.to_hash, mappings: Item.mappings.to_hash }
Item.import(force: true)
Update 1
I have installed the geocoder gem to get the location of each address added. So when I create the account with address I get the longitude and latitude coordinates with the following code. After that I added the longitude and langitude columns to the items table, and then when the user uploads an item the coordinates are updated on the items.rb as well.
Here is the account.rb setup:
geocoded_by :full_address
after_validation :geocode, if: ->(obj){ obj.full_address.present? }
def full_address
[street, city, state, zip_code, country].join(",")
end
I'm going to do elastic search on DoctorProfile and Subspeciality table. The error that I'm dealing with is that it gives the not found result. It takes a list of ids from doctor table but it doesn't gives desire result which is the doctor and with subspeciality.
this is what everything that i did:
I used these gems:
gem 'elasticsearch-model', git: 'git://github.com/elasticsearch/elasticsearch-rails.git'
gem 'elasticsearch-rails', git: 'git://github.com/elasticsearch/elasticsearch-rails.git'
gem 'elasticsearch-extensions', git: 'git://github.com/elasticsearch/elasticsearch-ruby.git'
my search method:
def search
query = params[:query]
#query.encode("UTF-8")
if query.nil?
render json: { message: "no query is provided" }, status: :unprocessable_entity
return
end
profiles = DoctorProfile.search(query).results.map { |r| r._source.id }
subspecialties = Subspecialty.search(query).results.map { |r| r._source.title }
subspecialties.uniq!
profiles = profiles + DoctorProfile.where("subspeciality in (?)", subspecialties).ids
profiles.uniq!
logger.info "################ The profile is #{profiles} ########################"
#doctors = DoctorProfile.find(profiles)
#cleaned_doctors = #doctors.select { |u| !u.user.nil? }
render json: #cleaned_doctors
end
in the doctor model:
after_commit on: [:create] do
__elasticsearch__.index_document if self.enabled?
end
after_commit on: [:update] do
__elasticsearch__.update_document if self.enabled?
end
after_commit on: [:destroy] do
__elasticsearch__.delete_document
end
settings index: {
number_of_shards: 1,
number_of_replicas: 0,
analysis: {
filter: {
autocomplete_filter: {
type: "edge_ngram",
min_gram: 1,
max_gram: 20
}
},
analyzer: {
autocomplete: {
type: "custom",
tokenizer: "standard",
filter: [
"lowercase",
"autocomplete_filter"
]
}
}
}
}
mappings dynamic: 'false' do
indexes :first_name, type: "string"
indexes :last_name, type: "string"
indexes :medical_code, type: "string"
indexes :expertise, type: "string", analyzer: "autocomplete", search_analyzer: "standard"
indexes :subspeciality, type: "string", analyzer: "autocomplete", search_analyzer: "standard"
end
def self.search(query)
__elasticsearch__.search(
{
query: {
multi_match: {
query: query,
fields: ['medical_code^10', 'subspeciality^5', 'expertise^2', 'first_name', 'last_name']
}
}
}
)
and in subspeciality model:
after_commit on: [:create] do
self.services = self.services.each {|str| str.force_encoding("UTF-8")}
__elasticsearch__.index_document if self.enabled?
end
after_commit on: [:update] do
self.services = self.services.each {|str| str.force_encoding("UTF-8")}
__elasticsearch__.update_document if self.enabled?
end
after_commit on: [:destroy] do
__elasticsearch__.delete_document
end
settings index: {
number_of_shards: 1,
number_of_replicas: 0,
analysis: {
filter: {
autocomplete_filter: {
type: "edge_ngram",
min_gram: 1,
max_gram: 20
}
},
analyzer: {
autocomplete: {
type: "custom",
tokenizer: "standard",
filter: [
"lowercase",
"autocomplete_filter"
]
}
}
}
}
mappings dynamic: 'false' do
indexes :description, type: "string", analyzer: "autocomplete", search_analyzer: "standard"
indexes :services, type: "string"
end
def self.search(query)
__elasticsearch__.search(
{
query: {
multi_match: {
query: query,
fields: ['description', 'services']
}
}
}
)
end
and this is my error in log :
Couldn't find all DoctorProfiles with 'id': (031addd8-9df8-4a53-974d-da0067302ad0, ff890720-4bfb-47d8-bdb8-3dc712b27f29, 869b28e1-cdd7-4bb6-b1d0-c7296e4b0637, 6dd6a784-c54b-4bb7-a0e1-337474ec4114, 234ccc87-f0c7-42f7-b96f-cf8d85487929, 543b621d-87aa-4a34-b6d6-62144c6a387e, 77e35144-9b93-48a0-a5bb-7b3addb99dff, d368f1df-3d1a-49ce-b6f5-f791df3294b1, d3dca8de-3143-4b03-90ec-e73a27c88960, 24abb0b3-2d11-457b-b95d-972462c4a37f) (found 2 results, but was looking for 10
i changed this line of code
#doctors = DoctorProfile.find(profiles)
to
#doctors = DoctorProfile.where("id in (?)",profiles)
and remove this line:
#cleaned_doctors = #doctors.select { |u| !u.user.nil? }
now i want to know what does this method do eactly.
#cleaned_doctors = #doctors.select { |u| !u.user.nil? }
to be mention, i have a table named user that doctorProfile has reference to it
Your code doesn't say much, but it seems that you have a boolean enabled on your model, which tells whether the record is to be indexed or not.
The issue is with the update callback, because if you change your model from enabled to not enabled, instead of removing it from the index, it just don't update existing information.
The correct callback would be
after_commit on: [:update] do
if enabled?
if previous_changes['enabled'] &&
!previous_changes['enabled'].first
# previously not enabled, we need to index it
__elasticsearch__.index_document
else
# previously enabled, we need to update it
__elasticsearch__.update_document
end
else
# not enabled
if previous_changes['enabled'] &&
previous_changes['enabled'].first
# previously enabled, delete
__elasticsearch__.delete_document
end
# if it wasn't enabled before, it's not in the index anyway.
# do nothing
end
end
The previous_changes hash stores the attributes that did change when saving the model, so you can check the previous value of the enabled attribute. See http://api.rubyonrails.org/classes/ActiveModel/Dirty.html#method-i-previous_changes
Once you have the new callback in place, rebuild the indexes to remove the bogus data in production if needed:
DoctorProfile.where(enabled: true).find_each { |dp| dp.__elasticsearch__.index_document }
Subspecialty.where(enabled: true).find_each { |dp| dp.__elasticsearch__.index_document }
I've got an Article model:
class Article < ActiveRecord::Base
include Tire::Model::Search
include Tire::Model::Callbacks
settings default_options do
mapping do
indexes :id, index: :not_analyzed
indexes :roles do
indexes :machine_name, analyzer: 'keyword'
end
indexes :published_at, type: 'date', include_in_all: false
end
end
end
where the default_options is:
index: { store: { type: Rails.env.test? ? :memory : :niofs },
analysis: {
analyzer: {
default: {
tokenizer: "standard",
filter: ["asciifolding", "lowercase", "snowball"],
char_filter: ["html_strip"]
}
}
}
I'm simply trying to search articles while filtering roles, but I don't have any idea how to do so. I've been trying something like that without success:
Tire.search("article") do
query { string 'foo bar baz' }
filter :nested, { path:'roles',
query: {
filtered: {
query: {
match_all: {}
},
filter: {
term:{'roles.machine_name' => ['da']}
}
}
}
}
end
This give me that error:
QueryParsingException[[development-oaciq::application-article] [nested] nested object under path [roles] is not of nested type];
After finding this question, it seems the nested filter wasn't required, it could be done like this:
Tire.search("article") do
query do
string 'foo bar baz'
term 'roles.machine_name', 'test'
end
end
I have a model named Movie that looks like this:
class Movie < ActiveRecord::Base
include Elasticsearch::Model
include Elasticsearch::Model::Callbacks
has_many :actors, after_add: [ lambda {|a,c| a.__elasticsearch__.index_document}],
after_remove: [ lambda {|a,c| a.__elasticsearch__.index_document}]
settings index: {number_of_shards: 1} do
mappings dynamic: 'false' do
indexes :title, analyzer: 'snowball', boost: 100
indexes :actors
end
end
def as_indexed_json(options={})
self.as_json(
include: {
actors: { only: :name}
}
)
end
end
When i do Movie.first.as_indexed_json , I get:
{"id"=>6, "title"=>"Back to the Future ",
"created_at"=>Wed, 03 Dec 2014 22:21:24 UTC +00:00,
"updated_at"=>Fri, 12 Dec 2014 23:40:03 UTC +00:00,
"actors"=>[{"name"=>"Michael J Fox"}, {"name"=>"Christopher Lloyd"},
{"name"=>"Lea Thompson"}]}
but when i do Movie.search("Christopher Lloyd").records.first i get: => nil .
What changes can i make to the index to search movies associated with the searched actor?
I used filtering query to solve this, first I created an ActiveSupport::Concern called searchable.rb, the concern looks like this:
module Searchable
extend ActiveSupport::Concern
included do
include Elasticsearch::Model
include Elasticsearch::Model::Callbacks
index_name [Rails.application.engine_name, Rails.env].join('_')
settings index: { number_of_shards: 3, number_of_replicas: 0} do
mapping do
indexes :title, type: 'multi_field' do
indexes :title, analyzer: 'snowball'
indexes :tokenized, analyzer: 'simple'
end
indexes :actors, analyzer: 'keyword'
end
def as_indexed_json(options={})
hash = self.as_json()
hash['actors'] = self.actors.map(&:name)
hash
end
def self.search(query, options={})
__set_filters = lambda do |key, f|
#search_definition[:post_filter][:and] ||= []
#search_definition[:post_filter][:and] |= [f]
end
#search_definition = {
query: {},
highlight: {
pre_tags: ['<em class="label label-highlight">'],
post_tags: ['</em>'],
fields: {
title: {number_of_fragments: 0}
}
},
post_filter: {},
aggregations: {
actors: {
filter: {bool: {must: [match_all: {}]}},
aggregations: {actors: {terms: {field: 'actors'}}}
}
}
}
unless query.blank?
#search_definition[:query] = {
bool: {
should: [
{
multi_match: {
query: query,
fields: ['title^10'],
operator: 'and'
}
}
]
}
}
else
#search_definition[:query] = { match_all: {} }
#search_definition[:sort] = {created_at: 'desc'}
end
if options[:actor]
f = {term: { actors: options[:taxon]}}
end
if options[:sort]
#search_definition[:sort] = { options[:sort] => 'desc'}
#search_definition[:track_scores] = true
end
__elasticsearch__.search(#search_definition)
end
end
end
I have the above concern in the models/concerns directory.
In movies.rb I have:
class Movie < ActiveRecord::Base
include Searchable
end
In movies_controller.rb I am doing searching on the index action and the action looks like this:
def index
options = {
actor: params[:taxon],
sort: params[:sort]
}
#movies = Movie.search(params[q], options).records
end
Now when i go to http://localhost:3000/movies?q=future&actor=Christopher I get all records which have the word future on their title and has an actor with a name Christopher. You can have more than one filter as shown by the expert template of the example application templates found here .
You can try add method search to your model like this:
class Movie < ActiveRecord::Base
include Elasticsearch::Model
include Elasticsearch::Model::Callbacks
# ...
def self.search(query, options = {})
es_options =
{
query: {
query_string: {
query: query,
default_operator: 'AND',
}
},
sort: '_score',
}.merge!(options)
__elasticsearch__.search(es_options)
end
# ...
end
Here is some examples of method search: http://www.sitepoint.com/full-text-search-rails-elasticsearch/
And now you can search in all your indexed fields.
You need to specify the fields in the the search method, like:
def self.search query
__elasticsearch__.search(
query: {
multi_match: {
query: query,
fields: %w[title actor.name]
}
}
)
end
Try this
indexes :actors do
indexes :name, type: "string"
end
I am trying to match a search query against two fields, as well as filter by facets if selected from dropdowns on the page.
When the user enters keywords it should match if found in two database fields: Title and Description. The dropdowns filter by a status, and a type.
Here is my Tire search configuration:
def self.search(params)
tire.search(load: true, page: params[:page], per_page: 25) do
query do
boolean do
should { string "title:#{params[:query]}", default_operator: "OR" } if params[:query].present?
should { string "description:#{params[:query]}", default_operator: "OR" } if params[:query].present?
must { term :status_id, params[:status_id] } if params[:status_id].present?
must { term :type_id, params[:type_id] } if params[:type_id].present?
end
end
sort { by :updated_at, "desc" } if params[:query].blank?
facet "status" do
terms :status_id
end
facet "type" do
terms :type_id
end
end
end
Indexing settings:
settings :analysis => {
:filter => {
:my_ngram => {
"type" => "nGram",
"max_gram" => 10,
"min_gram" => 3}
},
:analyzer => {
:my_analyzer => {
"type" => "custom",
"tokenizer" => "lowercase",
"filter" => ["my_ngram"]
}
}
} do
mapping do
indexes :title, boost: 10, analyzer: 'my_analyzer'
indexes :description, boost: 5, analyzer: 'my_analyzer'
indexes :status_id, :type => 'integer'
indexes :type_id, :type => 'integer'
end
end
I originally only had the title and description fields, which was working fine. I am now trying to add the ability to filter by status and type.
What is the proper way to configure this? If status is selected, it should only return records with that status. The same follows for type, and if both are selected.
Any help is appreciated.
It's not that errors occur, but the results no longer filter at all by either keywords or facets:
curl -X GET 'http://localhost:9200/projects/project/_search?load=true&size=25&pretty' -d '{"query":{"bool":{"should":[{"query_string":{"query":"title:test","default_operator":"OR"}},{"query_string":{"query":"description:test","default_operator":"OR"}}],"must":[{"term":{"status_id":{"term":"1"}}},{"term":{"type_id":{"term":"1"}}}]}},"facets":{"status":{"terms":{"field":"status_id","size":10,"all_terms":false}},"type":{"terms":{"field":"type_id","size":10,"all_terms":false}}},"size":25}'
# 2013-08-16 12:08:34:791 [200] (31 msec)
#
# {"took":31,"timed_out":false,"_shards":{"total":5,"successful":5,"failed":0},"hits":{"total":0,"max_score":null,"hits":[]},"facets":{"status":{"_type":"terms","missing":0,"total":0,"other":0,"terms":[]},"type":{"_type":"terms","missing":0,"total":0,"other":0,"terms":[]}}}
If you could create the equivalent of this I think you would get the desired results. (Please excuse the lack of quotes on the JSON keys!)
{
query: {
multi_match: {
query: "test",
fields: ["title", "description"]
}
},
filter: {
and: [
{
term: { status_id: 123 }
},
{
term: { type_id: 456 }
}
]
},
facets: {
type: {
terms: {
field: "type_id",
size: 10
}
},
status: {
terms: {
field: "status_id",
size: 10
}
}
}
}
Update
I don't know tire but will try to write something!
def self.search(params)
tire.search(load: true, page: params[:page], per_page: 25) do
query do
boolean do
should { match :title params[:query] } if params[:query].present?
should { match :description params[:query] } if params[:query].present?
end
end
sort { by :updated_at, "desc" } if params[:query].blank?
filter :and, { :term => { :status_id => params[:status_id] } } if params[:status_id].present?
{ :term => { :type_id => params[:type_id] } } if params[:type_id].present?
end
end
You will probably have to fix the ruby, but a few things to note. Match queries are the recommended default string search, they are faster than query_string ones (though you have slightly less control). Also