activesearch 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: c0e0ed236897226e3b2101e34ebed32ca97248f5
4
- data.tar.gz: d5b3cee180db19ef832b071c174701a7a039741e
3
+ metadata.gz: b24050eefed5b21eb1b0d085fa964effdc06c2d8
4
+ data.tar.gz: 780df5ddeb72407c972869ab11a9a6e015018554
5
5
  SHA512:
6
- metadata.gz: 41303c2b0d475fc75f348d889e21b57d3c3add61d254fa947b9e137535a8ca65efe63379bd56c135ccba8749b894fd0ebcecd73f9cbe5c6f483c7f5efd9751e8
7
- data.tar.gz: 6e638fb66e68d4521299cb10a13f8b740b50888f8f2fbf4c9f800f11758b45a578bc5efba7af1faeb29731acb696dc6d13c8babb9c7e904b40601f186c65e2b9
6
+ metadata.gz: c1e5576e03f3f0ae6a610ddef63bf3c661e68520ef8fdae55ca4e98c9676f14fb038de8088b416c59b085515ea4568aa5b58500085003bae87ee80c375e66cc7
7
+ data.tar.gz: f1caac8c129064500ec93a3f13f7854ab83f698ed7e27f8729ae9fc21cf2a2a999a23fd75663304590bb1490ef0e979e2f3a2fff4ff7abd71b4aa884eb7e23c1
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --colour
2
+ --backtrace
data/Rakefile CHANGED
@@ -1 +1,19 @@
1
- require "bundler/gem_tasks"
1
+ #!/usr/bin/env rake
2
+ # encoding: utf-8
3
+
4
+ require 'rubygems'
5
+ require 'bundler/setup'
6
+
7
+ require 'rake'
8
+ require 'rspec'
9
+ require 'rspec/core/rake_task'
10
+ require 'rubygems/package_task'
11
+
12
+ # === Gems install tasks ===
13
+ Bundler::GemHelper.install_tasks
14
+
15
+ RSpec::Core::RakeTask.new('spec') do |spec|
16
+ spec.pattern = 'spec/**/*_spec.rb'
17
+ end
18
+
19
+ task :default => :spec
data/activesearch.gemspec CHANGED
@@ -6,8 +6,8 @@ require 'activesearch/version'
6
6
  Gem::Specification.new do |gem|
7
7
  gem.name = "activesearch"
8
8
  gem.version = ActiveSearch::VERSION
9
- gem.authors = ["Rodrigo Alvarez"]
10
- gem.email = ["papipo@gmail.com"]
9
+ gem.authors = ['Rodrigo Alvarez', 'Didier Lafforgue']
10
+ gem.email = ['papipo@gmail.com', 'didier.lafforgue@gmail.com']
11
11
  gem.description = %q{ORM agnostic full text search}
12
12
  gem.summary = %q{ActiveSearch lets you plug in a ruby module in any class that will allow you to do full text searches.}
13
13
  gem.homepage = ""
@@ -16,15 +16,16 @@ Gem::Specification.new do |gem|
16
16
  gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
17
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
18
  gem.require_paths = ["lib"]
19
-
19
+
20
20
  gem.add_dependency "activesupport"
21
21
  gem.add_dependency "sucker_punch"
22
22
  gem.add_dependency "actionpack"
23
-
23
+
24
24
  gem.add_development_dependency "rspec"
25
25
  gem.add_development_dependency "rspec-mocks"
26
26
  gem.add_development_dependency "active_attr"
27
27
  gem.add_development_dependency "mongoid", "~> 3"
28
+ gem.add_development_dependency "database_cleaner", "~> 0.9.1"
28
29
  gem.add_development_dependency "tire"
29
30
  gem.add_development_dependency "parallel_tests"
30
31
  gem.add_development_dependency "httparty"
@@ -2,7 +2,7 @@ require "sucker_punch"
2
2
 
3
3
  class ActiveSearch::Algolia::Worker
4
4
  include SuckerPunch::Job
5
-
5
+
6
6
  def perform(msg)
7
7
  begin
8
8
  case msg[:task]
@@ -10,7 +10,7 @@ class ActiveSearch::Algolia::Worker
10
10
  ::ActiveSearch::Algolia::Client.new.save(msg[:id], msg[:doc])
11
11
  when :deindex
12
12
  client = ::ActiveSearch::Algolia::Client.new
13
- client.query("", tags: "original_id:#{msg[:id]}")["hits"].each do |hit|
13
+ client.query("", tags: "original_type:#{msg[:type]},original_id:#{msg[:id]}")["hits"].each do |hit|
14
14
  client.delete(hit["objectID"])
15
15
  end
16
16
  end
@@ -4,13 +4,21 @@ require "activesearch/base"
4
4
  require "activesearch/proxy"
5
5
 
6
6
  module ActiveSearch
7
+
7
8
  def self.search(text, conditions = {}, options = {})
9
+ locale = options[:locale] || I18n.locale
10
+ conditions[:locale] ||= locale
11
+
8
12
  Proxy.new(text, conditions, options) do |text, conditions|
9
13
  Algolia::Client.new.query(text, tags: conditions_to_tags(conditions))["hits"].map! do |hit|
10
14
  if hit["_tags"]
11
15
  hit["_tags"].each do |tag|
12
- k, v = tag.split(':')
13
- hit[k] = v
16
+ # preserve other ":" characters
17
+ _segments = tag.split(':')
18
+
19
+ unless _segments.empty? || _segments[1..-1].empty?
20
+ hit[_segments.first] = _segments[1..-1].join(':')
21
+ end
14
22
  end
15
23
  hit.delete("_tags")
16
24
  end
@@ -20,8 +28,9 @@ module ActiveSearch
20
28
  end
21
29
 
22
30
  protected
31
+
23
32
  def self.conditions_to_tags(conditions)
24
- conditions.merge(locale: I18n.locale).map { |c| c.join(':') }.join(',')
33
+ conditions.map { |c| c.join(':') }.join(',')
25
34
  end
26
35
 
27
36
  module Algolia
@@ -33,32 +42,35 @@ module ActiveSearch
33
42
 
34
43
  protected
35
44
  def reindex
36
- Worker.new.async.perform(task: :reindex, id: "#{indexable_id}_#{I18n.locale}", doc: to_indexable)
45
+ Worker.new.async.perform(task: :reindex, id: "#{indexable_id}_#{search_locale}", doc: to_indexable)
37
46
  end
38
47
 
39
48
  def deindex
40
- Worker.new.async.perform(task: :deindex, id: indexable_id)
49
+ Worker.new.async.perform(task: :deindex, id: self.id, type: self.class.to_s)
41
50
  end
42
51
 
43
52
  def to_indexable
44
- doc = {}
45
- search_fields.each do |field|
46
- if send(field)
47
- doc[field.to_s] = if send(field).is_a?(Hash) && send(field).has_key?(I18n.locale.to_s)
48
- ActiveSearch.strip_tags(send(field)[I18n.locale.to_s])
49
- else
50
- ActiveSearch.strip_tags(send(field))
53
+ {}.tap do |doc|
54
+ _locale = search_locale
55
+
56
+ search_fields.each do |field|
57
+ if content = send(field)
58
+ doc[field.to_s] = if content.is_a?(Hash) && content.has_key?(_locale)
59
+ ActiveSearch.strip_tags(content[_locale])
60
+ else
61
+ ActiveSearch.strip_tags(content)
62
+ end
51
63
  end
52
64
  end
53
- end
54
65
 
55
- (Array(search_options[:store]) - search_fields).each do |field|
56
- doc["_tags"] ||= []
57
- doc["_tags"] << "#{field}:#{self.send(field)}"
66
+ (Array(search_options[:store]) - search_fields).each do |field|
67
+ doc["_tags"] ||= []
68
+ doc["_tags"] << "#{field}:#{self.send(field)}"
69
+ end
70
+ doc["_tags"] << "locale:#{_locale}"
71
+ doc["_tags"] << "original_type:#{self.class.to_s}"
72
+ doc["_tags"] << "original_id:#{self.id}"
58
73
  end
59
- doc["_tags"] << "locale:#{I18n.locale}"
60
- doc["_tags"] << "original_id:#{indexable_id}"
61
- doc
62
74
  end
63
75
  end
64
76
  end
@@ -7,25 +7,37 @@ module ActiveSearch
7
7
  value.gsub(/<\/?[^>]*>/, '')
8
8
  when Hash
9
9
  value.each_with_object({}) { |(k,v),h| h[k] = strip_tags(v) }
10
+ when Array
11
+ value.map { |v| strip_tags(v) }
10
12
  else
11
13
  value
12
14
  end
13
15
  end
14
-
16
+
15
17
  module Base
16
18
  def self.included(parent)
17
19
  parent.extend ClassMethods
18
20
  parent.class_attribute :search_parameters, instance_reader: false
19
21
  end
20
-
22
+
21
23
  def search_options
22
24
  search_parameters.last.is_a?(Hash) ? search_parameters.last : {}
23
25
  end
24
-
26
+
25
27
  def search_fields
26
28
  search_parameters.last.is_a?(Hash) ? search_parameters[0...-1] : search_parameters
27
29
  end
28
-
30
+
31
+ def search_locale
32
+ search_locale = search_options[:locale] || I18n.locale.to_s
33
+
34
+ if search_locale.respond_to?(:call)
35
+ search_locale.call
36
+ else
37
+ search_locale
38
+ end
39
+ end
40
+
29
41
  def search_parameters
30
42
  if self.class.search_parameters.is_a?(Symbol)
31
43
  self.send(self.class.search_parameters)
@@ -33,14 +45,14 @@ module ActiveSearch
33
45
  self.class.search_parameters
34
46
  end
35
47
  end
36
-
48
+
37
49
  module ClassMethods
38
50
  def search_by(params, conditions = {})
39
51
  after_save :reindex, conditions
40
52
  after_destroy :deindex, conditions
41
53
  self.search_parameters = params
42
54
  end
43
-
55
+
44
56
  end
45
57
  end
46
58
  end
@@ -0,0 +1,115 @@
1
+ module ActiveSearch
2
+ module Mongoid
3
+ class Index
4
+
5
+ include ::Mongoid::Document
6
+
7
+ ## fields ##
8
+ field :original_type, type: String
9
+ field :original_id, type: Moped::BSON::ObjectId
10
+ field :language, type: String
11
+ field :locale, type: String
12
+ field :content, type: Array
13
+ field :stored, type: Hash, default: {}
14
+ alias_method :to_hash, :stored
15
+
16
+ ## indexes ##
17
+ index({ content: 'text', locale: 1 })
18
+ index({ original_type: 1, original_id: 1, locale: 1 }, unique: true)
19
+
20
+ ## methods ##
21
+
22
+ def store_language(original)
23
+ self.locale = original.search_locale
24
+ self.language = self.class.locale_to_language(self.locale)
25
+ end
26
+
27
+ def store_fields(original)
28
+ self.stored = {}
29
+
30
+ fields = (original.search_fields + (original.search_options[:store] || [])).uniq
31
+
32
+ fields.each do |f|
33
+ if original.send(f).present?
34
+ self.stored[f] = ActiveSearch.strip_tags(original.send(f))
35
+ end
36
+ end
37
+ end
38
+
39
+ def refresh_content(original)
40
+ self.content = original.to_indexable.values.flatten
41
+ end
42
+
43
+ ## class methods ##
44
+
45
+ def self.search(query, conditions = {})
46
+ language = self.locale_to_language(conditions[:locale] || I18n.locale)
47
+
48
+ filter = {}
49
+ conditions.each do |key, value|
50
+ if key == :locale
51
+ filter['locale'] = value.to_s
52
+ else
53
+ filter["stored.#{key}"] = value
54
+ end
55
+ end
56
+
57
+ session = ::Mongoid.session('default')
58
+ results = session.command({
59
+ 'text' => collection.name,
60
+ 'search' => query,
61
+ 'language' => language,
62
+ 'filter' => filter
63
+ })
64
+ if results.has_key?('results')
65
+ results['results'].map do |result|
66
+ result['obj']['stored'].merge(result['obj'].slice('locale', 'original_type', 'original_id'))
67
+ end
68
+ else
69
+ []
70
+ end
71
+ end
72
+
73
+ def self.deindex(original)
74
+ # delete the records in all the locales
75
+ self.where(original_type: original.class.to_s, original_id: original.id).destroy
76
+ end
77
+
78
+ def self.reindex(original, fields, options)
79
+ # re-index only in the current locale (unless another locale has been specified)
80
+ locale = original.search_locale
81
+
82
+ # find the exact index scoped by the locale or build a new one
83
+ doc = find_or_initialize_by(original_type: original.class.to_s, original_id: original.id, locale: locale)
84
+
85
+ doc.store_language(original)
86
+ doc.store_fields(original) #, fields, options)
87
+ doc.refresh_content(original)
88
+
89
+ # save it (create or update it)
90
+ doc.save
91
+ end
92
+
93
+ def self.locale_to_language(locale)
94
+ {
95
+ dk: 'danish',
96
+ nl: 'dutch',
97
+ en: 'english',
98
+ fi: 'finnish',
99
+ fr: 'french',
100
+ de: 'german',
101
+ hu: 'hungarian',
102
+ it: 'italian',
103
+ nb: 'norwegian',
104
+ br: 'portuguese',
105
+ pt: 'portuguese',
106
+ ro: 'romanian',
107
+ ru: 'russian',
108
+ es: 'spanish',
109
+ se: 'swedish',
110
+ tr: 'turkish'
111
+ }[locale.to_sym] || 'english'
112
+ end
113
+ end
114
+ end
115
+ end
@@ -1,15 +1,15 @@
1
1
  require 'activesearch/base'
2
2
  require 'activesearch/proxy'
3
- require 'activesearch/mongoid/model'
3
+ require 'activesearch/mongoid/index'
4
4
 
5
5
  module ActiveSearch
6
6
 
7
7
  def self.search(text, conditions = {}, options = {})
8
+ locale = options[:locale] || I18n.locale
9
+ conditions[:locale] ||= locale
10
+
8
11
  Proxy.new(text, conditions, options) do |text, conditions|
9
- text = text.downcase.split(/\s+/)
10
- conditions.keys.each { |k| conditions["_stored.#{k}"] = conditions.delete(k) }
11
- conditions.merge!(:_keywords.in => text + text.map { |word| "#{I18n.locale}:#{word}"})
12
- Mongoid::Model.where(conditions)
12
+ ActiveSearch::Mongoid::Index.search(text, conditions)
13
13
  end
14
14
  end
15
15
 
@@ -20,13 +20,30 @@ module ActiveSearch
20
20
  end
21
21
  end
22
22
 
23
+ def to_indexable
24
+ {}.tap do |doc|
25
+ _locale = search_locale
26
+
27
+ search_fields.each do |field|
28
+ if content = send(field)
29
+ doc[field.to_s] = if content.is_a?(Hash) && content.has_key?(_locale)
30
+ ActiveSearch.strip_tags(content[_locale])
31
+ else
32
+ ActiveSearch.strip_tags(content)
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+
23
39
  protected
40
+
24
41
  def reindex
25
- ActiveSearch::Mongoid::Model.reindex(self, self.search_fields, self.search_options)
42
+ ActiveSearch::Mongoid::Index.reindex(self, self.search_fields, self.search_options)
26
43
  end
27
44
 
28
45
  def deindex
29
- ActiveSearch::Mongoid::Model.deindex(self)
46
+ ActiveSearch::Mongoid::Index.deindex(self)
30
47
  end
31
48
  end
32
49
  end
@@ -1,5 +1,6 @@
1
1
  require 'action_view'
2
2
  require 'active_support/core_ext'
3
+ require 'active_support/core_ext/hash/slice'
3
4
 
4
5
  module ActiveSearch
5
6
  class Result < Hash
@@ -9,16 +10,22 @@ module ActiveSearch
9
10
  include ActionView::Helpers::TextHelper
10
11
 
11
12
  def initialize(result, text, options = {})
13
+ locale = (options[:locale] || I18n.locale).to_s
14
+
12
15
  @text = text
13
16
  result.to_hash.each do |k,v|
14
- unless v.nil? || k.to_s.start_with?('_')
15
- self[k.to_s] = v.respond_to?(:has_key?) && v.has_key?(I18n.locale.to_s) ? v[I18n.locale.to_s] : v
17
+ unless v.nil?
18
+ self[k.to_s] = v.respond_to?(:has_key?) && v.has_key?(locale) ? v[locale] : v
16
19
  end
17
20
  end
18
21
 
19
22
  self.build_highlighted_fields(options[:radius])
20
23
  end
21
24
 
25
+ def slice(*keys)
26
+ ::Hash.new.update(self).slice(*keys)
27
+ end
28
+
22
29
  protected
23
30
 
24
31
  def build_highlighted_fields(radius = nil)
@@ -1,3 +1,3 @@
1
1
  module ActiveSearch
2
- VERSION = "0.2.0"
2
+ VERSION = '0.3.0'
3
3
  end
@@ -0,0 +1,32 @@
1
+ #encoding: utf-8
2
+ require 'spec_helper'
3
+
4
+ describe 'ActiveSearch::Algolia' do
5
+
6
+ before(:all) do
7
+ SetupEngine.setup(:algolia)
8
+
9
+ # can not use the rspec let method
10
+ @model = AlgoliaModel
11
+ @another_model = AnotherAlgoliaModel
12
+ end
13
+
14
+ include_examples 'an engine'
15
+
16
+ context 'retry on errors' do
17
+
18
+ before do
19
+ times_called = 0
20
+ @instance = AlgoliaModel.new(title: 'Example')
21
+ ActiveSearch::Algolia::Client.should_receive(:put).exactly(3).times.and_return do
22
+ times_called += 1
23
+ raise Errno::ECONNRESET if times_called <= 2
24
+ end
25
+ end
26
+
27
+ subject { -> { @instance.save } }
28
+
29
+ it { should_not raise_error }
30
+ end
31
+
32
+ end
@@ -1,64 +1,64 @@
1
- require 'activesearch/base'
1
+ require 'spec_helper'
2
2
 
3
3
  describe ActiveSearch::Base do
4
4
  before do
5
5
  @klass = Class.new do
6
6
  include ActiveSearch::Base
7
-
7
+
8
8
  def self.after_save(*args); end
9
9
  def self.after_destroy(*args); end
10
-
10
+
11
11
  end
12
12
  end
13
-
13
+
14
14
  context "search_by" do
15
15
  let(:call_search_by) do
16
16
  @klass.class_eval do
17
17
  search_by [:field], if: :something_happens, unless: :its_friday
18
18
  end
19
19
  end
20
-
20
+
21
21
  it "should rely on after_save and after_destroy callbacks passing conditions" do
22
22
  @klass.should_receive(:after_save).with(:reindex, if: :something_happens, unless: :its_friday)
23
23
  @klass.should_receive(:after_destroy).with(:deindex, if: :something_happens, unless: :its_friday)
24
24
  call_search_by
25
25
  end
26
-
26
+
27
27
  it "should store the parameters in search_parameters" do
28
28
  call_search_by
29
29
  @klass.send(:search_parameters).should == [:field]
30
30
  end
31
31
  end
32
-
32
+
33
33
  context "utility methods with options" do
34
34
  before do
35
35
  @klass.stub(:search_parameters).and_return([:field, store: [:another_field]])
36
36
  end
37
-
37
+
38
38
  it "search_options should return the hash at the end of the parameters" do
39
39
  @klass.new.search_options.should == {store: [:another_field]}
40
40
  end
41
-
41
+
42
42
  it "search_fields should return all parameters except the options" do
43
43
  @klass.new.search_fields.should == [:field]
44
44
  end
45
45
  end
46
-
46
+
47
47
  context "search_by with virtual parameters" do
48
48
  before do
49
49
  @klass.class_eval do
50
50
  def options_for_search
51
51
  [@field]
52
52
  end
53
-
53
+
54
54
  def initialize(field)
55
55
  @field = field
56
56
  end
57
-
57
+
58
58
  search_by :options_for_search
59
59
  end
60
60
  end
61
-
61
+
62
62
  it "should work" do
63
63
  @klass.new(:first).send(:search_parameters).should == [:first]
64
64
  @klass.new(:second).send(:search_parameters).should == [:second]
@@ -0,0 +1,56 @@
1
+ #encoding: utf-8
2
+ require 'spec_helper'
3
+
4
+ describe 'ActiveSearch::Mongoid' do
5
+
6
+ before(:all) do
7
+ SetupEngine.setup(:mongoid)
8
+
9
+ # can not use the rspec let method
10
+ @model = MongoidModel
11
+ @another_model = AnotherMongoidModel
12
+ end
13
+
14
+ include_examples 'an engine'
15
+
16
+ describe 'localized content' do
17
+
18
+ before(:all) do
19
+ @localized = LocalizedMongoidModel.create!(title: "<strong>English</strong> English")
20
+ I18n.with_locale(:es) do
21
+ @localized.title = "Español Español"
22
+ @localized.save!
23
+ end
24
+ end
25
+
26
+ it "should be able to find by different locales" do
27
+ ActiveSearch.search("english").first["title"].should == "English English"
28
+ I18n.with_locale(:es) do
29
+ ActiveSearch.search("español").first["title"].should == "Español Español"
30
+ end
31
+ end
32
+
33
+ it "finds by a different locale" do
34
+ ActiveSearch.search("español", {}, { locale: 'es'}).first["title"].should == "Español Español"
35
+ end
36
+
37
+ it "should store content with tags stripped" do
38
+ index = ActiveSearch::Mongoid::Index.where(original_type: "LocalizedMongoidModel", original_id: @localized.id, locale: 'en')
39
+ index.first.content.should == ["English English"]
40
+ end
41
+
42
+ it "handles empty translations" do
43
+ lambda { LocalizedMongoidModel.create!(title: nil, not_localized: "example") }.should_not raise_error
44
+ end
45
+
46
+ it "handles empty fields" do
47
+ lambda { LocalizedMongoidModel.create!(title: "Example", not_localized: nil) }.should_not raise_error
48
+ end
49
+
50
+ it "handles nil values in arrays" do
51
+ lambda { LocalizedMongoidModel.create!(title: "Example", not_localized: "example", array: [nil]) }.should_not raise_error
52
+ end
53
+
54
+ end
55
+
56
+ end
@@ -1,8 +1,6 @@
1
- require 'activesearch/algolia'
2
-
3
1
  class AlgoliaModel < ActiveMimic
4
2
  include ActiveSearch::Algolia
5
-
3
+
6
4
  attribute :title
7
5
  attribute :text
8
6
  attribute :junk
@@ -10,19 +8,19 @@ class AlgoliaModel < ActiveMimic
10
8
  attribute :scope_id, type: Integer
11
9
  attribute :tags, type: Array
12
10
  localized_attribute :color
13
-
11
+
14
12
  search_by [:title, :text, :tags, :color, store: [:title, :junk, :scope_id]], if: lambda { !self.special }
15
13
 
16
14
  end
17
15
 
18
16
  class AnotherAlgoliaModel < ActiveMimic
19
17
  include ActiveSearch::Algolia
20
-
18
+
21
19
  attribute :title, type: String
22
20
  attribute :scope_id, type: Integer
23
21
  localized_attribute :color
24
22
  search_by [:title, store: [:title, :virtual, :scope_id, :color]]
25
-
23
+
26
24
  def virtual
27
25
  "virtual"
28
26
  end
@@ -1,11 +1,9 @@
1
- require 'activesearch/elastic_search'
2
-
3
1
  module ElasticSearchRefresh
4
-
2
+
5
3
  def save
6
4
  super.tap { Tire.index('_all') { refresh }}
7
5
  end
8
-
6
+
9
7
  def destroy
10
8
  super.tap { Tire.index('_all') { refresh }}
11
9
  end
@@ -14,13 +12,12 @@ end
14
12
  class ElasticSearchModel < ActiveMimic
15
13
  include ActiveSearch::ElasticSearch
16
14
  include ElasticSearchRefresh
17
-
15
+
18
16
  attribute :title
19
- attribute :text
20
17
  attribute :junk
21
18
  attribute :special, default: false
22
19
  attribute :tags, type: Array
23
-
20
+
24
21
  search_by [:title, :text, :tags, store: [:title, :junk]], if: lambda { !self.special }
25
22
 
26
23
  end
@@ -28,10 +25,10 @@ end
28
25
  class AnotherElasticSearchModel < ActiveMimic
29
26
  include ActiveSearch::ElasticSearch
30
27
  include ElasticSearchRefresh
31
-
28
+
32
29
  attribute :title, type: String
33
30
  search_by [:title, store: [:title, :virtual]]
34
-
31
+
35
32
  def virtual
36
33
  "virtual"
37
34
  end