elasticsearch_autocomplete 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +22 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +68 -0
- data/Rakefile +6 -0
- data/elasticsearch_autocomplete.gemspec +25 -0
- data/lib/elasticsearch_autocomplete/analyzers.rb +59 -0
- data/lib/elasticsearch_autocomplete/model_addition.rb +125 -0
- data/lib/elasticsearch_autocomplete/railtie.rb +9 -0
- data/lib/elasticsearch_autocomplete/version.rb +3 -0
- data/lib/elasticsearch_autocomplete.rb +26 -0
- data/spec/elasticsearch_autocomplete/base_spec.rb +47 -0
- data/spec/elasticsearch_autocomplete/full_mode_spec.rb +49 -0
- data/spec/elasticsearch_autocomplete/localized_spec.rb +34 -0
- data/spec/elasticsearch_autocomplete/phrase_mode_spec.rb +26 -0
- data/spec/elasticsearch_autocomplete/search_filters_spec.rb +64 -0
- data/spec/elasticsearch_autocomplete/word_mode_spec.rb +31 -0
- data/spec/spec_helper.rb +76 -0
- metadata +141 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Alex Leschenko
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
# ElasticsearchAutocomplete
|
2
|
+
|
3
|
+
Simple autocomplete for rails models using awesome elasticsearch and tire gem
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'elasticsearch_autocomplete'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install elasticsearch_autocomplete
|
18
|
+
|
19
|
+
## Basic Usage
|
20
|
+
|
21
|
+
Specify attributes for autocompletion. By default, this is `name` attribute:
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
class User < ActiveRecord::Base
|
25
|
+
ac_field :full_name
|
26
|
+
end
|
27
|
+
```
|
28
|
+
|
29
|
+
To find suggestions call `ac_search` method on your model. It return `Tire::Results::Collection` instance:
|
30
|
+
|
31
|
+
```ruby
|
32
|
+
User.ac_search('Alex').map(&:full_name)
|
33
|
+
=> ['Alex First', 'Alexandr Second']
|
34
|
+
```
|
35
|
+
|
36
|
+
##
|
37
|
+
|
38
|
+
You can specify fields for suggestions search:
|
39
|
+
|
40
|
+
```ruby
|
41
|
+
class User < ActiveRecord::Base
|
42
|
+
ac_field :full_name, :search_fields => [:full_name, :email]
|
43
|
+
end
|
44
|
+
```
|
45
|
+
|
46
|
+
For search on localized fields such as `name_en`, `name_ru`:
|
47
|
+
|
48
|
+
```ruby
|
49
|
+
class Product < ActiveRecord::Base
|
50
|
+
ac_field :name, :localized => true
|
51
|
+
end
|
52
|
+
```
|
53
|
+
|
54
|
+
If you wand to define settings and mapping for elasticsearch index yourselves:
|
55
|
+
|
56
|
+
```ruby
|
57
|
+
class Product < ActiveRecord::Base
|
58
|
+
ac_field :name, :skip_settings => true
|
59
|
+
end
|
60
|
+
```
|
61
|
+
|
62
|
+
## Contributing
|
63
|
+
|
64
|
+
1. Fork it
|
65
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
66
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
67
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
68
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'elasticsearch_autocomplete/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = 'elasticsearch_autocomplete'
|
8
|
+
gem.version = ElasticsearchAutocomplete::VERSION
|
9
|
+
gem.authors = ['Alex Leschenko']
|
10
|
+
gem.email = %w(leschenko.al@gmail.com)
|
11
|
+
gem.summary = %q{Elasticsearch autocomplete for models}
|
12
|
+
gem.description = %q{Simple autocomplete for your models using awesome elasticsearch and tire gem}
|
13
|
+
gem.homepage = 'https://github.com/leschenko/elasticsearch_autocomplete'
|
14
|
+
|
15
|
+
gem.files = `git ls-files`.split($/)
|
16
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
17
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
18
|
+
gem.require_paths = %w(lib)
|
19
|
+
|
20
|
+
gem.add_dependency 'tire'
|
21
|
+
|
22
|
+
gem.add_development_dependency 'rake'
|
23
|
+
gem.add_development_dependency 'rspec'
|
24
|
+
gem.add_development_dependency 'oj'
|
25
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module ElasticsearchAutocomplete
|
2
|
+
module Analyzers
|
3
|
+
|
4
|
+
AC_TOKENIZERS = {
|
5
|
+
:ac_edge_ngram => {
|
6
|
+
:type => 'edgeNGram',
|
7
|
+
:min_gram => 1,
|
8
|
+
:max_gram => 50,
|
9
|
+
:side => 'front'
|
10
|
+
},
|
11
|
+
:ac_edge_ngram_full => {
|
12
|
+
:type => 'nGram',
|
13
|
+
:min_gram => 1,
|
14
|
+
:max_gram => 50
|
15
|
+
}
|
16
|
+
}
|
17
|
+
|
18
|
+
AC_FILTERS = {
|
19
|
+
:ac_edge_ngram => {
|
20
|
+
:type => 'edgeNGram',
|
21
|
+
:min_gram => 1,
|
22
|
+
:max_gram => 50,
|
23
|
+
:side => 'front'
|
24
|
+
}
|
25
|
+
}
|
26
|
+
|
27
|
+
AC_ANALYZERS = {
|
28
|
+
:ac_edge_ngram => {
|
29
|
+
:type => 'custom',
|
30
|
+
:tokenizer => 'ac_edge_ngram',
|
31
|
+
:filter => %w(lowercase asciifolding)
|
32
|
+
},
|
33
|
+
:ac_edge_ngram_full => {
|
34
|
+
:type => 'custom',
|
35
|
+
:tokenizer => 'ac_edge_ngram_full',
|
36
|
+
:filter => %w(lowercase asciifolding)
|
37
|
+
},
|
38
|
+
:ac_edge_ngram_word => {
|
39
|
+
:type => 'custom',
|
40
|
+
:tokenizer => 'standard',
|
41
|
+
:filter => %w(lowercase asciifolding ac_edge_ngram)
|
42
|
+
},
|
43
|
+
:ac_search => {
|
44
|
+
:type => 'custom',
|
45
|
+
:tokenizer => 'keyword',
|
46
|
+
:filter => %w(lowercase asciifolding)
|
47
|
+
}
|
48
|
+
}
|
49
|
+
|
50
|
+
AC_BASE = {
|
51
|
+
:analysis => {
|
52
|
+
:analyzer => AC_ANALYZERS,
|
53
|
+
:tokenizer => AC_TOKENIZERS,
|
54
|
+
:filter => AC_FILTERS
|
55
|
+
}
|
56
|
+
}
|
57
|
+
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
module ElasticsearchAutocomplete
|
2
|
+
module ModelAddition
|
3
|
+
def self.included(base)
|
4
|
+
base.send :extend, SingletonMethods
|
5
|
+
end
|
6
|
+
|
7
|
+
module SingletonMethods
|
8
|
+
def elasticsearch(options={})
|
9
|
+
include Tire::Model::Search
|
10
|
+
unless options.delete(:skip_after_save)
|
11
|
+
after_save lambda { tire.update_index }
|
12
|
+
end
|
13
|
+
after_destroy lambda { tire.update_index }
|
14
|
+
index_prefix ElasticsearchAutocomplete.defaults[:index_prefix]
|
15
|
+
end
|
16
|
+
|
17
|
+
def ac_field(*args, &block)
|
18
|
+
options = args.extract_options!
|
19
|
+
|
20
|
+
include InstanceMethods
|
21
|
+
extend ClassMethods
|
22
|
+
|
23
|
+
elasticsearch(options)
|
24
|
+
|
25
|
+
class_attribute :ac_opts, :ac_attr, :instance_writer => false
|
26
|
+
self.ac_opts = options.reverse_merge(ElasticsearchAutocomplete.defaults)
|
27
|
+
self.ac_attr = args.first || ElasticsearchAutocomplete.defaults[:attr]
|
28
|
+
|
29
|
+
define_ac_index(ac_opts[:mode]) unless options[:no_settings]
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
module ClassMethods
|
34
|
+
def ac_search(query, options={})
|
35
|
+
options.reverse_merge!({:per_page => 50, :search_fields => ac_search_fields})
|
36
|
+
|
37
|
+
tire.search :per_page => options[:per_page] do
|
38
|
+
query do
|
39
|
+
if query.size.zero?
|
40
|
+
all
|
41
|
+
else
|
42
|
+
match options[:search_fields], query
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
sort { by options[:order], options[:sort_mode] || 'asc' } if options[:order].present?
|
47
|
+
|
48
|
+
filter(:and, :filters => options[:with].map { |k, v| {:terms => {k => ElasticsearchAutocomplete.val_to_array(v)}} }) if options[:with].present?
|
49
|
+
if options[:without].present?
|
50
|
+
options[:without].each do |k, v|
|
51
|
+
filter(:not, {:terms => {k => ElasticsearchAutocomplete.val_to_array(v, true)}})
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def ac_search_attrs
|
58
|
+
@ac_search_attrs ||=
|
59
|
+
if ac_opts[:search_fields]
|
60
|
+
ac_opts[:search_fields]
|
61
|
+
else
|
62
|
+
if ac_opts[:localized]
|
63
|
+
I18n.available_locales.map { |l| "#{ac_attr}_#{l}" }
|
64
|
+
else
|
65
|
+
[ac_attr]
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def for_input_token(r, attr='name_ru')
|
71
|
+
{:name => r[attr], :id => r.id}
|
72
|
+
end
|
73
|
+
|
74
|
+
def ac_mode_config
|
75
|
+
ElasticsearchAutocomplete::MODES[ac_opts[:mode]]
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
def define_ac_index(mode=:word)
|
81
|
+
mode_config = ac_mode_config
|
82
|
+
settings ElasticsearchAutocomplete::Analyzers::AC_BASE do
|
83
|
+
mapping do
|
84
|
+
ac_search_attrs.each do |attr|
|
85
|
+
case mode
|
86
|
+
when :word
|
87
|
+
indexes attr, :type => 'multi_field', :fields => {
|
88
|
+
attr => {:type => 'string'},
|
89
|
+
"#{mode_config[:base]}_#{attr}" => {:type => 'string', :index_analyzer => 'ac_edge_ngram', :search_analyzer => 'ac_search', :include_in_all => false},
|
90
|
+
"#{mode_config[:word]}_#{attr}" => {:type => 'string', :index_analyzer => 'ac_edge_ngram_word', :search_analyzer => 'ac_search', :include_in_all => false}
|
91
|
+
}
|
92
|
+
when :phrase
|
93
|
+
indexes attr, :type => 'multi_field', :fields => {
|
94
|
+
attr => {:type => 'string'},
|
95
|
+
"#{mode_config[:base]}_#{attr}" => {:type => 'string', :index_analyzer => 'ac_edge_ngram', :search_analyzer => 'ac_search', :include_in_all => false},
|
96
|
+
}
|
97
|
+
when :full
|
98
|
+
indexes attr, :type => 'multi_field', :fields => {
|
99
|
+
attr => {:type => 'string'},
|
100
|
+
"#{mode_config[:base]}_#{attr}" => {:type => 'string', :index_analyzer => 'ac_edge_ngram', :search_analyzer => 'ac_search', :include_in_all => false, :boost => 3},
|
101
|
+
"#{mode_config[:full]}_#{attr}" => {:type => 'string', :index_analyzer => 'ac_edge_ngram_full', :search_analyzer => 'ac_search', :include_in_all => false}
|
102
|
+
}
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def ac_search_fields
|
110
|
+
@ac_search_fields ||= ac_search_attrs.map { |attr| ac_mode_config.values.map { |prefix| "#{prefix}_#{attr}" } }.flatten
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
module InstanceMethods
|
115
|
+
def to_indexed_json
|
116
|
+
for_json = {}
|
117
|
+
attrs = [:id, :created_at] + self.class.ac_search_attrs
|
118
|
+
attrs.each do |attr|
|
119
|
+
for_json[attr] = send(attr)
|
120
|
+
end
|
121
|
+
MultiJson.encode(for_json)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'elasticsearch_autocomplete/version'
|
2
|
+
require 'elasticsearch_autocomplete/analyzers'
|
3
|
+
require 'elasticsearch_autocomplete/model_addition'
|
4
|
+
require 'elasticsearch_autocomplete/railtie' if defined? Rails
|
5
|
+
|
6
|
+
module ElasticsearchAutocomplete
|
7
|
+
mattr_accessor :defaults
|
8
|
+
|
9
|
+
def self.default_index_prefix
|
10
|
+
Rails.application.class.name.split('::').first.downcase if Object.const_defined?('Rails')
|
11
|
+
end
|
12
|
+
|
13
|
+
self.defaults = {:attr => :name, :localized => false, :mode => :word, :index_prefix => default_index_prefix}
|
14
|
+
|
15
|
+
MODES = {
|
16
|
+
:word => {:base => 'ac', :word => 'ac_word'},
|
17
|
+
:phrase => {:base => 'ac'},
|
18
|
+
:full => {:base => 'ac', :full => 'ac_full'}
|
19
|
+
}
|
20
|
+
|
21
|
+
def self.val_to_array(val, zero=false)
|
22
|
+
return [] unless val
|
23
|
+
a = val.is_a?(Array) ? val : val.to_s.split(',').map(&:to_i)
|
24
|
+
zero ? a : a.reject(&:zero?)
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
class ActiveModelUser < StubModelBase
|
4
|
+
ac_field :full_name
|
5
|
+
end
|
6
|
+
|
7
|
+
describe ElasticsearchAutocomplete do
|
8
|
+
subject { ActiveModelUser }
|
9
|
+
before :all do
|
10
|
+
ActiveModelUser.setup_index
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'add ac_search method' do
|
14
|
+
should respond_to(:ac_search)
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'define to_indexed_json method' do
|
18
|
+
ActiveModelUser.new(:full_name => 'test').to_indexed_json.should == '{"id":null,"created_at":null,"full_name":"test"}'
|
19
|
+
end
|
20
|
+
|
21
|
+
describe 'default settings' do
|
22
|
+
around do |example|
|
23
|
+
old_settings = ElasticsearchAutocomplete.defaults
|
24
|
+
example.run
|
25
|
+
ElasticsearchAutocomplete.defaults = old_settings
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'allow to change default settings' do
|
29
|
+
ElasticsearchAutocomplete.defaults = {:attr => :test, :localized => true, :mode => :phrase, :index_prefix => 'test'}
|
30
|
+
ElasticsearchAutocomplete.defaults.should == {:attr => :test, :localized => true, :mode => :phrase, :index_prefix => 'test'}
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
shared_examples 'basic autocomplete' do |model|
|
36
|
+
it 'suggest for beginning of the source' do
|
37
|
+
model.ac_search('Joyce Flores').to_a.should_not be_empty
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'suggest for for full match' do
|
41
|
+
model.ac_search('Lau').to_a.should_not be_empty
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'don\'t suggest for unmatched term' do
|
45
|
+
model.ac_search('Lai').to_a.should be_empty
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
class ActiveModelProductFull < StubModelBase
|
4
|
+
ac_field :sku, :mode => :full
|
5
|
+
|
6
|
+
def self.test_data
|
7
|
+
['SAMARA', 'A.3103', 'b A.3611', 'kac12 dk/sm']
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.populate
|
11
|
+
test_data.each_with_index do |name, id|
|
12
|
+
u = new(:sku => name)
|
13
|
+
u.id = id
|
14
|
+
u.save
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
describe ':full mode autocomplete' do
|
20
|
+
let(:model) { ActiveModelProductFull }
|
21
|
+
|
22
|
+
before :all do
|
23
|
+
model.setup_index
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'have :full mode' do
|
27
|
+
model.ac_opts[:mode].should == :full
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'suggest for beginning of the source' do
|
31
|
+
model.ac_search('A.31').to_a.should_not be_empty
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'suggest for for full match' do
|
35
|
+
model.ac_search('SAMARA').to_a.should_not be_empty
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'don\'t suggest for unmatched term' do
|
39
|
+
model.ac_search('kac3').to_a.should be_empty
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'suggest from the middle of the word' do
|
43
|
+
model.ac_search('/sm').to_a.should_not be_empty
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'suggest with relevance order' do
|
47
|
+
model.ac_search('A.3').map(&:sku).should == ['A.3103', 'b A.3611']
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
class ActiveModelProductLocalized < StubModelBase
|
4
|
+
ac_field :name, :localized => true
|
5
|
+
|
6
|
+
def self.test_data
|
7
|
+
[
|
8
|
+
{:name_ru => 'name_ru first', :name_en => 'name_en first'},
|
9
|
+
{:name_ru => 'name_ru second', :name_en => 'name_en second'}
|
10
|
+
]
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.populate
|
14
|
+
test_data.each_with_index do |data, id|
|
15
|
+
u = new(data)
|
16
|
+
u.id = id
|
17
|
+
u.save
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
describe 'suggestions for localized attributes' do
|
23
|
+
let(:model) { ActiveModelProductLocalized }
|
24
|
+
|
25
|
+
before :all do
|
26
|
+
model.setup_index
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'don\'t suggest from all locales' do
|
30
|
+
model.ac_search('name_en first').to_a.should have(1).results
|
31
|
+
model.ac_search('name_ru first').to_a.should have(1).results
|
32
|
+
model.ac_search('name_ru').to_a.should have(2).results
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
class ActiveModelUserPhrase < StubModelBase
|
4
|
+
ac_field :full_name, :mode => :phrase
|
5
|
+
end
|
6
|
+
|
7
|
+
describe ':phrase mode autocomplete' do
|
8
|
+
let(:model) { ActiveModelUserPhrase }
|
9
|
+
before :all do
|
10
|
+
model.setup_index
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'have :phrase mode' do
|
14
|
+
model.ac_opts[:mode].should == :phrase
|
15
|
+
end
|
16
|
+
|
17
|
+
it_behaves_like 'basic autocomplete', ActiveModelUserPhrase
|
18
|
+
|
19
|
+
it 'don\'t suggest from the middle of the word' do
|
20
|
+
model.ac_search('becca').to_a.should be_empty
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'don\'t for each word of the source' do
|
24
|
+
model.ac_search('Flores').map(&:full_name).should be_empty
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
class ActiveModelUserFilter < StubModelBase
|
4
|
+
ac_field :full_name
|
5
|
+
|
6
|
+
def self.test_data
|
7
|
+
[
|
8
|
+
{:full_name => 'Laura Nelson', :interest_ids => [1, 2]},
|
9
|
+
{:full_name => 'Laura Flores', :interest_ids => [2, 3]},
|
10
|
+
{:full_name => 'Laura Larson', :interest_ids => [3, 4]}
|
11
|
+
]
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.populate
|
15
|
+
test_data.each_with_index do |data, id|
|
16
|
+
u = new(data)
|
17
|
+
u.id = id
|
18
|
+
u.save
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_indexed_json
|
23
|
+
for_json = {}
|
24
|
+
attrs = [:id, :created_at, :interest_ids] + self.class.ac_search_attrs
|
25
|
+
attrs.each do |attr|
|
26
|
+
for_json[attr] = send(attr)
|
27
|
+
end
|
28
|
+
MultiJson.encode(for_json)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
describe 'search filters' do
|
33
|
+
let(:model) { ActiveModelUserFilter }
|
34
|
+
|
35
|
+
before :all do
|
36
|
+
model.setup_index
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'filter suggestions with terms' do
|
40
|
+
model.ac_search('Laura', :with => {:interest_ids => [2]}).map(&:full_name).should =~ ['Laura Nelson', 'Laura Flores']
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'accept coma separated string for filter' do
|
44
|
+
model.ac_search('Laura', :with => {:interest_ids => '1,4'}).map(&:full_name).should =~ ['Laura Nelson', 'Laura Larson']
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'filter suggestions without terms' do
|
48
|
+
model.ac_search('Laura', :without => {:interest_ids => [2]}).map(&:full_name).should =~ ['Laura Larson']
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'can order suggestions desc' do
|
52
|
+
res = model.ac_search('Laura', :order => :id, :sort_mode => 'desc').map(&:id)
|
53
|
+
res.should == res.sort.reverse
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'can order suggestions asc' do
|
57
|
+
res = model.ac_search('Laura', :order => :id, :sort_mode => 'asc').map(&:id)
|
58
|
+
res.should == res.sort
|
59
|
+
end
|
60
|
+
|
61
|
+
it 'limit suggestions collection size' do
|
62
|
+
model.ac_search('Laura', :per_page => 1).to_a.should have(1).result
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
class ActiveModelUserWord < StubModelBase
|
4
|
+
ac_field :full_name, :mode => :word
|
5
|
+
end
|
6
|
+
|
7
|
+
describe ':word mode autocomplete' do
|
8
|
+
let(:model) { ActiveModelUserWord }
|
9
|
+
|
10
|
+
before :all do
|
11
|
+
model.setup_index
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'have :word mode' do
|
15
|
+
model.ac_opts[:mode].should == :word
|
16
|
+
end
|
17
|
+
|
18
|
+
it_behaves_like 'basic autocomplete', ActiveModelUserWord
|
19
|
+
|
20
|
+
it 'don\'t suggest from the middle of the word' do
|
21
|
+
model.ac_search('becca').to_a.should be_empty
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'suggest for each word of the source' do
|
25
|
+
model.ac_search('Flores').map(&:full_name).should == ['Joyce Flores']
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'suggest with relevance order' do
|
29
|
+
model.ac_search('Lau').map(&:full_name).should == ['Laura Larson', 'Larson Laura']
|
30
|
+
end
|
31
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'active_model'
|
2
|
+
require 'tire'
|
3
|
+
require 'active_support/core_ext'
|
4
|
+
require 'elasticsearch_autocomplete'
|
5
|
+
|
6
|
+
Tire.configure do
|
7
|
+
logger 'tmp/elasticsearch.log'
|
8
|
+
url 'http://localhost:9200'
|
9
|
+
pretty 1
|
10
|
+
end
|
11
|
+
|
12
|
+
I18n.available_locales = [:en, :ru]
|
13
|
+
|
14
|
+
class ActiveModelBase
|
15
|
+
include ActiveModel::AttributeMethods
|
16
|
+
include ActiveModel::Serialization
|
17
|
+
include ActiveModel::Serializers::JSON
|
18
|
+
include ActiveModel::Naming
|
19
|
+
|
20
|
+
extend ActiveModel::Callbacks
|
21
|
+
define_model_callbacks :save, :destroy
|
22
|
+
|
23
|
+
include ElasticsearchAutocomplete::ModelAddition
|
24
|
+
|
25
|
+
attr_reader :attributes
|
26
|
+
attr_accessor :id, :created_at
|
27
|
+
|
28
|
+
def initialize(attributes = {})
|
29
|
+
@attributes = attributes
|
30
|
+
end
|
31
|
+
|
32
|
+
def method_missing(id, *args, &block)
|
33
|
+
attributes[id.to_sym] || attributes[id.to_s] || super
|
34
|
+
end
|
35
|
+
|
36
|
+
def persisted?
|
37
|
+
true
|
38
|
+
end
|
39
|
+
|
40
|
+
def save
|
41
|
+
_run_save_callbacks do
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def destroy
|
46
|
+
_run_destroy_callbacks do
|
47
|
+
@destroyed = true
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def destroyed?
|
52
|
+
!!@destroyed
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.setup_index
|
56
|
+
tire.index.delete
|
57
|
+
tire.create_elasticsearch_index
|
58
|
+
populate
|
59
|
+
tire.index.refresh
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
class StubModelBase < ActiveModelBase
|
64
|
+
|
65
|
+
def self.test_data
|
66
|
+
['Joyce Flores', 'Rebecca Nelson', 'Larson Laura', 'Laura Larson']
|
67
|
+
end
|
68
|
+
|
69
|
+
def self.populate
|
70
|
+
test_data.each_with_index do |name, id|
|
71
|
+
u = new(:full_name => name)
|
72
|
+
u.id = id
|
73
|
+
u.save
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
metadata
ADDED
@@ -0,0 +1,141 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: elasticsearch_autocomplete
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease:
|
5
|
+
version: 0.0.1
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Alex Leschenko
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-01-08 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
version_requirements: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ! '>='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
none: false
|
21
|
+
prerelease: false
|
22
|
+
name: tire
|
23
|
+
requirement: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - ! '>='
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: '0'
|
28
|
+
none: false
|
29
|
+
type: :runtime
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
version_requirements: !ruby/object:Gem::Requirement
|
32
|
+
requirements:
|
33
|
+
- - ! '>='
|
34
|
+
- !ruby/object:Gem::Version
|
35
|
+
version: '0'
|
36
|
+
none: false
|
37
|
+
prerelease: false
|
38
|
+
name: rake
|
39
|
+
requirement: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - ! '>='
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '0'
|
44
|
+
none: false
|
45
|
+
type: :development
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
version_requirements: !ruby/object:Gem::Requirement
|
48
|
+
requirements:
|
49
|
+
- - ! '>='
|
50
|
+
- !ruby/object:Gem::Version
|
51
|
+
version: '0'
|
52
|
+
none: false
|
53
|
+
prerelease: false
|
54
|
+
name: rspec
|
55
|
+
requirement: !ruby/object:Gem::Requirement
|
56
|
+
requirements:
|
57
|
+
- - ! '>='
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: '0'
|
60
|
+
none: false
|
61
|
+
type: :development
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ! '>='
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0'
|
68
|
+
none: false
|
69
|
+
prerelease: false
|
70
|
+
name: oj
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ! '>='
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
none: false
|
77
|
+
type: :development
|
78
|
+
description: Simple autocomplete for your models using awesome elasticsearch and tire
|
79
|
+
gem
|
80
|
+
email:
|
81
|
+
- leschenko.al@gmail.com
|
82
|
+
executables: []
|
83
|
+
extensions: []
|
84
|
+
extra_rdoc_files: []
|
85
|
+
files:
|
86
|
+
- .gitignore
|
87
|
+
- Gemfile
|
88
|
+
- LICENSE.txt
|
89
|
+
- README.md
|
90
|
+
- Rakefile
|
91
|
+
- elasticsearch_autocomplete.gemspec
|
92
|
+
- lib/elasticsearch_autocomplete.rb
|
93
|
+
- lib/elasticsearch_autocomplete/analyzers.rb
|
94
|
+
- lib/elasticsearch_autocomplete/model_addition.rb
|
95
|
+
- lib/elasticsearch_autocomplete/railtie.rb
|
96
|
+
- lib/elasticsearch_autocomplete/version.rb
|
97
|
+
- spec/elasticsearch_autocomplete/base_spec.rb
|
98
|
+
- spec/elasticsearch_autocomplete/full_mode_spec.rb
|
99
|
+
- spec/elasticsearch_autocomplete/localized_spec.rb
|
100
|
+
- spec/elasticsearch_autocomplete/phrase_mode_spec.rb
|
101
|
+
- spec/elasticsearch_autocomplete/search_filters_spec.rb
|
102
|
+
- spec/elasticsearch_autocomplete/word_mode_spec.rb
|
103
|
+
- spec/spec_helper.rb
|
104
|
+
homepage: https://github.com/leschenko/elasticsearch_autocomplete
|
105
|
+
licenses: []
|
106
|
+
post_install_message:
|
107
|
+
rdoc_options: []
|
108
|
+
require_paths:
|
109
|
+
- lib
|
110
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
111
|
+
requirements:
|
112
|
+
- - ! '>='
|
113
|
+
- !ruby/object:Gem::Version
|
114
|
+
version: '0'
|
115
|
+
segments:
|
116
|
+
- 0
|
117
|
+
hash: 4533165797483734826
|
118
|
+
none: false
|
119
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
120
|
+
requirements:
|
121
|
+
- - ! '>='
|
122
|
+
- !ruby/object:Gem::Version
|
123
|
+
version: '0'
|
124
|
+
segments:
|
125
|
+
- 0
|
126
|
+
hash: 4533165797483734826
|
127
|
+
none: false
|
128
|
+
requirements: []
|
129
|
+
rubyforge_project:
|
130
|
+
rubygems_version: 1.8.24
|
131
|
+
signing_key:
|
132
|
+
specification_version: 3
|
133
|
+
summary: Elasticsearch autocomplete for models
|
134
|
+
test_files:
|
135
|
+
- spec/elasticsearch_autocomplete/base_spec.rb
|
136
|
+
- spec/elasticsearch_autocomplete/full_mode_spec.rb
|
137
|
+
- spec/elasticsearch_autocomplete/localized_spec.rb
|
138
|
+
- spec/elasticsearch_autocomplete/phrase_mode_spec.rb
|
139
|
+
- spec/elasticsearch_autocomplete/search_filters_spec.rb
|
140
|
+
- spec/elasticsearch_autocomplete/word_mode_spec.rb
|
141
|
+
- spec/spec_helper.rb
|