activesearch 0.1.3 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,15 +1,7 @@
1
1
  ---
2
- !binary "U0hBMQ==":
3
- metadata.gz: !binary |-
4
- NWZiODUwMjI1M2M2OTQwZTBjOWI4ZWIzZTRiODFhYmEyMzg5MjAwNw==
5
- data.tar.gz: !binary |-
6
- NmRmNTIwNjA1M2Y4NTJhZGQ2NzYyMTAwOTdlZmQyM2YyYzA4YmRkOA==
7
- !binary "U0hBNTEy":
8
- metadata.gz: !binary |-
9
- M2M1ZGNmMTVjYjA1YmM1YTUxNDk5ZWIxNzI0MTZjOWE4ZjE5ZTJmZmEwOWYx
10
- OTNkZDFlZDM2NDA0NWYzMGE5ZjYxMGI3OGNmMzExMGE5YTkwNmM5Y2JjNWFh
11
- OTY5ZDZiNzA2YWYzYWQ2M2I3MDc5NTBiZmVlZjY1MzY5NGIyNzI=
12
- data.tar.gz: !binary |-
13
- NzdhYjBmNDNkYzFhMjdmNDhlMzczOWViOWEzMzFjMTAyMDVhNjY2MjMzMjI0
14
- ZDA2MmY0YmNjY2E5MzEzYzZlNWQxMDc4OWNiMjc0OGQ3N2RlNDYxMWFkNDQy
15
- Yzg3Yjg4NzQzMjBlOTgzOTE0ZjQ5ZmUyYjFiM2E5Zjg2MWUxOWI=
2
+ SHA1:
3
+ metadata.gz: c0e0ed236897226e3b2101e34ebed32ca97248f5
4
+ data.tar.gz: d5b3cee180db19ef832b071c174701a7a039741e
5
+ SHA512:
6
+ metadata.gz: 41303c2b0d475fc75f348d889e21b57d3c3add61d254fa947b9e137535a8ca65efe63379bd56c135ccba8749b894fd0ebcecd73f9cbe5c6f483c7f5efd9751e8
7
+ data.tar.gz: 6e638fb66e68d4521299cb10a13f8b740b50888f8f2fbf4c9f800f11758b45a578bc5efba7af1faeb29731acb696dc6d13c8babb9c7e904b40601f186c65e2b9
data/activesearch.gemspec CHANGED
@@ -19,6 +19,7 @@ Gem::Specification.new do |gem|
19
19
 
20
20
  gem.add_dependency "activesupport"
21
21
  gem.add_dependency "sucker_punch"
22
+ gem.add_dependency "actionpack"
22
23
 
23
24
  gem.add_development_dependency "rspec"
24
25
  gem.add_development_dependency "rspec-mocks"
@@ -31,6 +31,10 @@ module ActiveSearch
31
31
  def query(text, extras = {})
32
32
  self.class.get("", query: extras.merge!(query: text))
33
33
  end
34
+
35
+ def get(id)
36
+ self.class.get("/#{id}")
37
+ end
34
38
  end
35
39
  end
36
40
  end
@@ -9,7 +9,10 @@ class ActiveSearch::Algolia::Worker
9
9
  when :reindex
10
10
  ::ActiveSearch::Algolia::Client.new.save(msg[:id], msg[:doc])
11
11
  when :deindex
12
- ::ActiveSearch::Algolia::Client.new.delete(msg[:id])
12
+ client = ::ActiveSearch::Algolia::Client.new
13
+ client.query("", tags: "original_id:#{msg[:id]}")["hits"].each do |hit|
14
+ client.delete(hit["objectID"])
15
+ end
13
16
  end
14
17
  rescue Exception => e
15
18
  perform(msg.merge!(retries: msg[:retries].to_i + 1)) unless msg[:retries].to_i >= 3
@@ -4,12 +4,9 @@ require "activesearch/base"
4
4
  require "activesearch/proxy"
5
5
 
6
6
  module ActiveSearch
7
- def self.search(text, conditions = {})
8
- Proxy.new(text, conditions) do |text, conditions|
9
- options = {}
10
- tags = conditions_to_tags(conditions)
11
- options.merge!(tags: tags) if tags != ""
12
- Algolia::Client.new.query(text, options)["hits"].map! do |hit|
7
+ def self.search(text, conditions = {}, options = {})
8
+ Proxy.new(text, conditions, options) do |text, conditions|
9
+ Algolia::Client.new.query(text, tags: conditions_to_tags(conditions))["hits"].map! do |hit|
13
10
  if hit["_tags"]
14
11
  hit["_tags"].each do |tag|
15
12
  k, v = tag.split(':')
@@ -21,38 +18,46 @@ module ActiveSearch
21
18
  end
22
19
  end
23
20
  end
24
-
21
+
25
22
  protected
26
23
  def self.conditions_to_tags(conditions)
27
- conditions.map { |c| c.join(':') }.join(',')
24
+ conditions.merge(locale: I18n.locale).map { |c| c.join(':') }.join(',')
28
25
  end
29
-
30
- module Algolia
26
+
27
+ module Algolia
31
28
  def self.included(base)
32
29
  base.class_eval do
33
30
  include Base
34
31
  end
35
32
  end
36
-
33
+
37
34
  protected
38
35
  def reindex
39
- Worker.new.async.perform(task: :reindex, id: indexable_id, doc: self.to_indexable)
36
+ Worker.new.async.perform(task: :reindex, id: "#{indexable_id}_#{I18n.locale}", doc: to_indexable)
40
37
  end
41
-
38
+
42
39
  def deindex
43
40
  Worker.new.async.perform(task: :deindex, id: indexable_id)
44
41
  end
45
-
42
+
46
43
  def to_indexable
47
44
  doc = {}
48
45
  search_fields.each do |field|
49
- doc[field.to_s] = attributes[field.to_s] if attributes[field.to_s]
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))
51
+ end
52
+ end
50
53
  end
51
-
54
+
52
55
  (Array(search_options[:store]) - search_fields).each do |field|
53
56
  doc["_tags"] ||= []
54
57
  doc["_tags"] << "#{field}:#{self.send(field)}"
55
58
  end
59
+ doc["_tags"] << "locale:#{I18n.locale}"
60
+ doc["_tags"] << "original_id:#{indexable_id}"
56
61
  doc
57
62
  end
58
63
  end
@@ -6,7 +6,9 @@ module ActiveSearch
6
6
  when String
7
7
  value.gsub(/<\/?[^>]*>/, '')
8
8
  when Hash
9
- value.each { |k,v| value[k] = strip_tags(value[k]) }
9
+ value.each_with_object({}) { |(k,v),h| h[k] = strip_tags(v) }
10
+ else
11
+ value
10
12
  end
11
13
  end
12
14
 
@@ -3,28 +3,28 @@ require 'activesearch/proxy'
3
3
  require 'activesearch/mongoid/model'
4
4
 
5
5
  module ActiveSearch
6
-
7
- def self.search(text, conditions = {})
8
- Proxy.new(text, conditions) do |text, conditions|
6
+
7
+ def self.search(text, conditions = {}, options = {})
8
+ Proxy.new(text, conditions, options) do |text, conditions|
9
9
  text = text.downcase.split(/\s+/)
10
10
  conditions.keys.each { |k| conditions["_stored.#{k}"] = conditions.delete(k) }
11
11
  conditions.merge!(:_keywords.in => text + text.map { |word| "#{I18n.locale}:#{word}"})
12
12
  Mongoid::Model.where(conditions)
13
13
  end
14
14
  end
15
-
15
+
16
16
  module Mongoid
17
17
  def self.included(base)
18
18
  base.class_eval do
19
19
  include Base
20
20
  end
21
21
  end
22
-
22
+
23
23
  protected
24
24
  def reindex
25
25
  ActiveSearch::Mongoid::Model.reindex(self, self.search_fields, self.search_options)
26
26
  end
27
-
27
+
28
28
  def deindex
29
29
  ActiveSearch::Mongoid::Model.deindex(self)
30
30
  end
@@ -3,15 +3,18 @@ require "activesearch/result"
3
3
  module ActiveSearch
4
4
  class Proxy
5
5
  include Enumerable
6
-
7
- def initialize(text, conditions, &implementation)
8
- @text = text
9
- @conditions = conditions
6
+
7
+ def initialize(text, conditions, options = {}, &implementation)
8
+ @text = text
9
+ @conditions = conditions
10
+ @options = options
10
11
  @implementation = implementation
11
12
  end
12
-
13
+
13
14
  def each(&block)
14
- @implementation.call(@text, @conditions).each { |result| block.call(Result.new(result)) }
15
+ @implementation.call(@text, @conditions).each do |result|
16
+ block.call(Result.new(result, @text, @options))
17
+ end
15
18
  end
16
19
  end
17
20
  end
@@ -1,11 +1,39 @@
1
+ require 'action_view'
2
+ require 'active_support/core_ext'
3
+
1
4
  module ActiveSearch
2
5
  class Result < Hash
3
- def initialize(result)
6
+
7
+ DEFAULT_HIGHLIGHT_RADIUS = 50
8
+
9
+ include ActionView::Helpers::TextHelper
10
+
11
+ def initialize(result, text, options = {})
12
+ @text = text
4
13
  result.to_hash.each do |k,v|
5
- unless v.nil?
14
+ unless v.nil? || k.to_s.start_with?('_')
6
15
  self[k.to_s] = v.respond_to?(:has_key?) && v.has_key?(I18n.locale.to_s) ? v[I18n.locale.to_s] : v
7
16
  end
8
17
  end
18
+
19
+ self.build_highlighted_fields(options[:radius])
20
+ end
21
+
22
+ protected
23
+
24
+ def build_highlighted_fields(radius = nil)
25
+ radius = radius || DEFAULT_HIGHLIGHT_RADIUS
26
+
27
+ self["highlighted"] = self.each_with_object({}) do |(k,v), h|
28
+ if v.is_a?(String)
29
+ h[k] = excerpt(v, text_words.first, radius: radius)
30
+ h[k] = highlight(h[k], text_words, highlighter: '<em>\1</em>') unless h[k].nil?
31
+ end
32
+ end
33
+ end
34
+
35
+ def text_words
36
+ @text_words ||= @text.scan(/\w+|\n/)
9
37
  end
10
38
  end
11
39
  end
@@ -1,3 +1,3 @@
1
1
  module ActiveSearch
2
- VERSION = "0.1.3"
2
+ VERSION = "0.2.0"
3
3
  end
data/spec/engines_spec.rb CHANGED
@@ -19,9 +19,10 @@ end
19
19
 
20
20
  Dir[File.join(File.dirname(__FILE__), 'models', '*.rb')].map { |f| File.basename(f, '.rb') }.each do |filename|
21
21
  engine = filename.split('_').collect { |w| w.capitalize }.join
22
-
22
+ next unless engine == "Algolia"
23
23
  describe "ActiveSearch::#{engine}" do
24
24
  before(:all) do
25
+ I18n.locale = :en
25
26
  require File.join(File.dirname(__FILE__), 'models', filename)
26
27
  end
27
28
 
@@ -34,13 +35,18 @@ Dir[File.join(File.dirname(__FILE__), 'models', '*.rb')].map { |f| File.basename
34
35
  @special = Object.const_get("#{engine}Model").create(title: "Not findable because it's special", special: true, scope_id: 1)
35
36
  @foreign = Object.const_get("#{engine}Model").create(title: "Findable", scope_id: 2)
36
37
  @tagged = Object.const_get("#{engine}Model").create(title: "Tagged document", tags: ['findable'], scope_id: 1)
38
+ @localized = Object.const_get("#{engine}Model").create(title: "Localized", color: "Red")
39
+ I18n.with_locale :es do
40
+ @localized.color = "Rojo"
41
+ @localized.save
42
+ end
37
43
  end
38
44
 
39
45
  it "should find the expected documents" do
40
46
  results = ActiveSearch.search("findable", scope_id: 1).map { |doc| doc.select { |k,v| %w[title junk virtual].include?(k.to_s) } }
41
47
  results.sort_by { |result| result["title"] }.should == [
42
48
  {
43
- "title" => "Another <strong>findable</strong> title with tags",
49
+ "title" => "Another findable title with tags",
44
50
  "virtual" => "virtual"
45
51
  },
46
52
  {
@@ -54,8 +60,15 @@ Dir[File.join(File.dirname(__FILE__), 'models', '*.rb')].map { |f| File.basename
54
60
  "title" => "Tagged document"
55
61
  }
56
62
  ]
57
- ActiveSearch.search("some text").first.to_hash["title"].should == "Some title"
58
- ActiveSearch.search("junk").first.to_hash["title"].should == "Junk"
63
+ ActiveSearch.search("some text").first["title"].should == "Some title"
64
+ ActiveSearch.search("junk").first["title"].should == "Junk"
65
+ end
66
+
67
+ it "should handle localized fields" do
68
+ ActiveSearch.search("Localized").first["color"].should == "Red"
69
+ I18n.with_locale :es do
70
+ ActiveSearch.search("Localized").first["color"].should == "Rojo"
71
+ end
59
72
  end
60
73
 
61
74
  it "should find docs even with upcase searches" do
@@ -66,5 +79,12 @@ Dir[File.join(File.dirname(__FILE__), 'models', '*.rb')].map { |f| File.basename
66
79
  @findable.destroy
67
80
  ActiveSearch.search("findable").count.should == 4
68
81
  end
82
+
83
+ it "should excerpt and highlight" do
84
+ Object.const_get("#{engine}Model").create(title: <<-LIPSUM, junk: "Junk field", scope_id: 1)
85
+ Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy findable text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.
86
+ LIPSUM
87
+ ActiveSearch.search("dummy findable").first["highlighted"]["title"].should == "Lorem Ipsum is simply <em>dummy</em> text of the printing and typesetting industry. Lo..."
88
+ end
69
89
  end
70
90
  end
@@ -9,8 +9,9 @@ class AlgoliaModel < ActiveMimic
9
9
  attribute :special, default: false
10
10
  attribute :scope_id, type: Integer
11
11
  attribute :tags, type: Array
12
+ localized_attribute :color
12
13
 
13
- search_by [:title, :text, :tags, store: [:title, :junk, :scope_id]], if: lambda { !self.special }
14
+ search_by [:title, :text, :tags, :color, store: [:title, :junk, :scope_id]], if: lambda { !self.special }
14
15
 
15
16
  end
16
17
 
@@ -19,7 +20,8 @@ class AnotherAlgoliaModel < ActiveMimic
19
20
 
20
21
  attribute :title, type: String
21
22
  attribute :scope_id, type: Integer
22
- search_by [:title, store: [:title, :virtual, :scope_id]]
23
+ localized_attribute :color
24
+ search_by [:title, store: [:title, :virtual, :scope_id, :color]]
23
25
 
24
26
  def virtual
25
27
  "virtual"
data/spec/spec_helper.rb CHANGED
@@ -29,7 +29,7 @@ class ActiveMimic
29
29
  end
30
30
 
31
31
  def save
32
- self.id = self.class.next_id
32
+ self.id ||= self.class.next_id
33
33
  run_callbacks :save do
34
34
  true
35
35
  end
@@ -45,4 +45,17 @@ class ActiveMimic
45
45
  @next_id ||= 0
46
46
  @next_id += 1
47
47
  end
48
+
49
+ def self.localized_attribute(name)
50
+ attribute "#{name}_translations", type: Hash
51
+
52
+ define_method name do
53
+ send("#{name}_translations") && send("#{name}_translations")[I18n.locale.to_s]
54
+ end
55
+
56
+ define_method "#{name}=" do |value|
57
+ send("#{name}_translations=", {}) if send("#{name}_translations").nil?
58
+ send("#{name}_translations").merge!(I18n.locale.to_s => value)
59
+ end
60
+ end
48
61
  end
metadata CHANGED
@@ -1,83 +1,97 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activesearch
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rodrigo Alvarez
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2013-10-16 00:00:00.000000000 Z
11
+ date: 2013-11-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - ! '>='
17
+ - - '>='
18
18
  - !ruby/object:Gem::Version
19
19
  version: '0'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - ! '>='
24
+ - - '>='
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: sucker_punch
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - ! '>='
31
+ - - '>='
32
32
  - !ruby/object:Gem::Version
33
33
  version: '0'
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - ! '>='
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: actionpack
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '>='
39
53
  - !ruby/object:Gem::Version
40
54
  version: '0'
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: rspec
43
57
  requirement: !ruby/object:Gem::Requirement
44
58
  requirements:
45
- - - ! '>='
59
+ - - '>='
46
60
  - !ruby/object:Gem::Version
47
61
  version: '0'
48
62
  type: :development
49
63
  prerelease: false
50
64
  version_requirements: !ruby/object:Gem::Requirement
51
65
  requirements:
52
- - - ! '>='
66
+ - - '>='
53
67
  - !ruby/object:Gem::Version
54
68
  version: '0'
55
69
  - !ruby/object:Gem::Dependency
56
70
  name: rspec-mocks
57
71
  requirement: !ruby/object:Gem::Requirement
58
72
  requirements:
59
- - - ! '>='
73
+ - - '>='
60
74
  - !ruby/object:Gem::Version
61
75
  version: '0'
62
76
  type: :development
63
77
  prerelease: false
64
78
  version_requirements: !ruby/object:Gem::Requirement
65
79
  requirements:
66
- - - ! '>='
80
+ - - '>='
67
81
  - !ruby/object:Gem::Version
68
82
  version: '0'
69
83
  - !ruby/object:Gem::Dependency
70
84
  name: active_attr
71
85
  requirement: !ruby/object:Gem::Requirement
72
86
  requirements:
73
- - - ! '>='
87
+ - - '>='
74
88
  - !ruby/object:Gem::Version
75
89
  version: '0'
76
90
  type: :development
77
91
  prerelease: false
78
92
  version_requirements: !ruby/object:Gem::Requirement
79
93
  requirements:
80
- - - ! '>='
94
+ - - '>='
81
95
  - !ruby/object:Gem::Version
82
96
  version: '0'
83
97
  - !ruby/object:Gem::Dependency
@@ -98,42 +112,42 @@ dependencies:
98
112
  name: tire
99
113
  requirement: !ruby/object:Gem::Requirement
100
114
  requirements:
101
- - - ! '>='
115
+ - - '>='
102
116
  - !ruby/object:Gem::Version
103
117
  version: '0'
104
118
  type: :development
105
119
  prerelease: false
106
120
  version_requirements: !ruby/object:Gem::Requirement
107
121
  requirements:
108
- - - ! '>='
122
+ - - '>='
109
123
  - !ruby/object:Gem::Version
110
124
  version: '0'
111
125
  - !ruby/object:Gem::Dependency
112
126
  name: parallel_tests
113
127
  requirement: !ruby/object:Gem::Requirement
114
128
  requirements:
115
- - - ! '>='
129
+ - - '>='
116
130
  - !ruby/object:Gem::Version
117
131
  version: '0'
118
132
  type: :development
119
133
  prerelease: false
120
134
  version_requirements: !ruby/object:Gem::Requirement
121
135
  requirements:
122
- - - ! '>='
136
+ - - '>='
123
137
  - !ruby/object:Gem::Version
124
138
  version: '0'
125
139
  - !ruby/object:Gem::Dependency
126
140
  name: httparty
127
141
  requirement: !ruby/object:Gem::Requirement
128
142
  requirements:
129
- - - ! '>='
143
+ - - '>='
130
144
  - !ruby/object:Gem::Version
131
145
  version: '0'
132
146
  type: :development
133
147
  prerelease: false
134
148
  version_requirements: !ruby/object:Gem::Requirement
135
149
  requirements:
136
- - - ! '>='
150
+ - - '>='
137
151
  - !ruby/object:Gem::Version
138
152
  version: '0'
139
153
  - !ruby/object:Gem::Dependency
@@ -194,17 +208,17 @@ require_paths:
194
208
  - lib
195
209
  required_ruby_version: !ruby/object:Gem::Requirement
196
210
  requirements:
197
- - - ! '>='
211
+ - - '>='
198
212
  - !ruby/object:Gem::Version
199
213
  version: '0'
200
214
  required_rubygems_version: !ruby/object:Gem::Requirement
201
215
  requirements:
202
- - - ! '>='
216
+ - - '>='
203
217
  - !ruby/object:Gem::Version
204
218
  version: '0'
205
219
  requirements: []
206
220
  rubyforge_project:
207
- rubygems_version: 2.0.5
221
+ rubygems_version: 2.0.3
208
222
  signing_key:
209
223
  specification_version: 4
210
224
  summary: ActiveSearch lets you plug in a ruby module in any class that will allow