picky 4.3.0 → 4.3.1

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.
@@ -1,6 +1,8 @@
1
1
  module Picky
2
2
 
3
3
  class Category
4
+
5
+ class Picky::IdNotGivenException < StandardError; end
4
6
 
5
7
  # Removes an indexed object with the
6
8
  # given id.
@@ -31,11 +33,9 @@ module Picky
31
33
  # Note: Takes a hash as opposed to the add/replace method.
32
34
  #
33
35
  def replace_from hash
34
- # TODO Decide on a format.
35
- #
36
36
  return unless text = hash[from] || hash[from.to_s]
37
37
 
38
- id = hash[:id] || hash['id']
38
+ raise IdNotGivenException.new unless id = hash[:id] || hash['id']
39
39
  id = id.send key_format
40
40
 
41
41
  remove id
@@ -4,18 +4,44 @@ module Picky
4
4
  module IndexActions
5
5
 
6
6
  def self.extended base
7
+ # Updates the given item and returns HTTP codes:
8
+ # * 200 if the index has been updated or no error case has occurred.
9
+ # * 404 if the index cannot be found.
10
+ # * 400 if no data or item id has been provided in the data.
11
+ #
12
+ # Note: 200 returns no data yet.
13
+ #
7
14
  base.post '/' do
8
15
  index_name = params['index']
9
- index = Picky::Indexes[index_name.to_sym]
10
- data = params['data']
11
- index.replace_from Yajl::Parser.parse(data) if data
16
+ begin
17
+ index = Picky::Indexes[index_name.to_sym]
18
+ data = params['data']
19
+ return 400 unless data
20
+ data && index.replace_from(Yajl::Parser.parse data) && 200
21
+ rescue IdNotGivenException
22
+ 400
23
+ rescue StandardError
24
+ 404
25
+ end
12
26
  end
27
+
28
+ # Deletes the given item and returns:
29
+ # * 200 if the index has been updated or no error case has occurred.
30
+ # * 404 if the index cannot be found.
31
+ # * 400 if no data or item id has been provided in the data.
32
+ #
33
+ # Note: 200 returns no data yet.
34
+ #
13
35
  base.delete '/' do
14
36
  index_name = params['index']
15
- index = Picky::Indexes[index_name.to_sym]
16
- data = Yajl::Parser.parse params['data']
17
- id = data['id']
18
- index.remove id if id
37
+ begin
38
+ index = Picky::Indexes[index_name.to_sym]
39
+ data = Yajl::Parser.parse params['data']
40
+ id = data['id']
41
+ id ? index.remove(id) && 200 : 400
42
+ rescue StandardError
43
+ 404
44
+ end
19
45
  end
20
46
  end
21
47
 
@@ -29,5 +29,16 @@ describe Picky::Category, "Realtime API" do
29
29
  it 'offers an unshift method' do
30
30
  category.unshift Thing.new(1, 'text')
31
31
  end
32
+ it 'offers a replace_from method' do
33
+ category.replace_from id: 1, text: "some text"
34
+ end
35
+ it 'raises on no id given' do
36
+ expect {
37
+ category.replace_from text: "some text"
38
+ }.to raise_error
39
+ end
40
+ it 'shrugs off no data given' do
41
+ category.replace_from id: 1
42
+ end
32
43
 
33
44
  end
@@ -0,0 +1,72 @@
1
+ # encoding: utf-8
2
+ #
3
+ require 'spec_helper'
4
+
5
+ describe Picky::Tokenizer do
6
+ describe 'examples' do
7
+ it 'works correctly' do
8
+ tokenizer = described_class.new(split_words_on: /\&/, normalizes_words: [[/\&/, 'and']])
9
+
10
+ # TODO Is this really correct? Shouldn't we split after normalizing?
11
+ #
12
+ tokenizer.tokenize('M & M').should == [['m', 'and', 'm'], ['m', 'and', 'm']]
13
+ end
14
+ it 'works correctly' do
15
+ tokenizer = described_class.new(stopwords: /\b(and)\b/, normalizes_words: [[/\&/, 'and']])
16
+
17
+ # TODO Is this really correct? Shouldn't we stop words after normalizing?
18
+ #
19
+ tokenizer.tokenize('M & M').should == [['m', 'and', 'm'], ['m', 'and', 'm']]
20
+ end
21
+ it 'removes all stopwords if they do not occur alone' do
22
+ tokenizer = described_class.new(stopwords: /\b(and|then)\b/)
23
+ tokenizer.tokenize('and then').should == [[], []]
24
+ end
25
+ it 'does not remove a stopword if it occurs alone' do
26
+ tokenizer = described_class.new(stopwords: /\b(and|then)\b/)
27
+ tokenizer.tokenize('and').should == [['and'], ['and']]
28
+ end
29
+ it 'does not remove a stopword if it occurs alone (even with qualifier)' do
30
+ tokenizer = described_class.new(stopwords: /\b(and|then)\b/)
31
+ tokenizer.tokenize('title:and').should == [['title:and'], ['title:and']]
32
+ end
33
+ it 'does not remove a stopword if it occurs alone (even with qualifier)' do
34
+ tokenizer = described_class.new(stopwords: /\b(and|then)\b/)
35
+ tokenizer.tokenize('title:and').should == [['title:and'], ['title:and']]
36
+ end
37
+ it 'removes stopwords, then only lets through max words words' do
38
+ tokenizer = described_class.new(stopwords: /\b(and|then|to|the)\b/, max_words: 2)
39
+ tokenizer.tokenize('and then they went to the house').should == [['they', 'went'], ['they', 'went']]
40
+ end
41
+ it 'can take freaky splits_text_on options' do
42
+ tokenizer = described_class.new(splits_text_on: /([A-Z]?[a-z]+)/, case_sensitive: false) # Explicit case, is false by default.
43
+ tokenizer.tokenize('TOTALCamelCaseExample').should == [
44
+ ["total", "camel", "case", "example"],
45
+ ["total", "camel", "case", "example"]
46
+ ]
47
+ end
48
+ it 'substitutes and removes in the right order' do
49
+ tokenizer = described_class.new(
50
+ substitutes_characters_with: Picky::CharacterSubstituters::WestEuropean.new,
51
+ removes_characters: /e/
52
+ )
53
+
54
+ # Ä -> Ae -> A
55
+ #
56
+ tokenizer.tokenize('Ä ä').should == [['a', 'a'], ['a', 'a']]
57
+ end
58
+ it 'removes characters, then only lets through an ok sized token' do
59
+ tokenizer = described_class.new(rejects_token_if: ->(token){ token.size >= 5 }, removes_characters: /e/)
60
+ tokenizer.tokenize('hullo').should == [[], []]
61
+ tokenizer.tokenize('hello').should == [['hllo'], ['hllo']]
62
+ end
63
+ it 'is case sensitive' do
64
+ tokenizer = described_class.new(case_sensitive: true)
65
+ tokenizer.tokenize('Kaspar codes').should == [['Kaspar', 'codes'], ['Kaspar', 'codes']]
66
+ end
67
+ it 'is case sensitive, also for removing characters' do
68
+ tokenizer = described_class.new(case_sensitive: true, removes_characters: /K/)
69
+ tokenizer.tokenize('Kaspar codes').should == [['aspar', 'codes'], ['aspar', 'codes']]
70
+ end
71
+ end
72
+ end
@@ -1,61 +1,196 @@
1
1
  require 'yajl'
2
2
  require 'sinatra'
3
- require 'picky-client'
3
+ require_relative '../../../client/lib/picky-client'
4
+ require_relative '../../../client/lib/picky-client/spec'
4
5
  require 'spec_helper'
5
6
 
6
7
  describe 'Sinatra Index Actions' do
7
8
 
8
- # This is the application that is tested.
9
- #
10
- class MyPickyServer < Sinatra::Base
11
- extend Picky::Sinatra::IndexActions
9
+ before(:all) do
12
10
 
13
- data = Picky::Index.new :index do
14
- category :name
15
- category :surname
16
- end
11
+ # This is the application that is tested.
12
+ #
13
+ class MyIndexActionsPickyServer < Sinatra::Base
14
+ extend Picky::Sinatra::IndexActions
15
+
16
+ data = Picky::Index.new :some_index do
17
+ category :name
18
+ category :surname
19
+ end
17
20
 
18
- people = Picky::Search.new data
21
+ people = Picky::Search.new data
19
22
 
20
- get '/people' do
21
- results = people.search params[:query], params[:ids] || 20, params[:offset] || 0
22
- results.to_json
23
+ get '/people' do
24
+ results = people.search params[:query], params[:ids] || 20, params[:offset] || 0
25
+ results.to_json
26
+ end
23
27
  end
28
+
24
29
  end
25
30
 
26
31
  describe 'updating' do
27
32
  before(:each) do
28
33
  Picky::Indexes.clear
29
34
  end
30
- let(:request) { ::Rack::MockRequest.new MyPickyServer }
31
- it 'should update the index correctly' do
32
- request.post('/', params: { index: 'index', data: %Q{{ "id":"1", "name":"Florian", "surname":"Hanke" }} })
35
+ let(:request) { ::Rack::MockRequest.new MyIndexActionsPickyServer }
36
+ context 'return values' do
37
+ describe 'update' do
38
+ it 'returns a correct code after updating without problems' do
39
+ result = request.post('/', params: {
40
+ index: 'some_index',
41
+ data: %Q{{ "id":"1", "name":"Florian", "surname":"Hanke" }}
42
+ })
43
+ result.status.should == 200
44
+ end
45
+ it 'returns a correct code after updating with just the id' do
46
+ result = request.post('/', params: {
47
+ index: 'some_index',
48
+ data: %Q{{ "id":"1" }}
49
+ })
50
+ result.status.should == 200
51
+ end
52
+ it 'returns a correct code after updating without id' do
53
+ result = request.post('/', params: {
54
+ index: 'some_index',
55
+ data: %Q{{ "name":"Florian", "surname":"Hanke" }}
56
+ })
57
+ result.status.should == 400
58
+ end
59
+ it 'returns a correct code after updating with the wrong index' do
60
+ result = request.post('/', params: {
61
+ index: 'some_wrong_index',
62
+ data: %Q{{ "id":"1", "name":"Florian", "surname":"Hanke" }}
63
+ })
64
+ result.status.should == 404
65
+ end
66
+ end
67
+ describe 'delete' do
68
+ before(:each) do
69
+ request.post('/', params: {
70
+ index: 'some_index',
71
+ data: %Q{{ "id":"1", "name":"Florian", "surname":"Hanke" }}
72
+ })
73
+ end
74
+ it 'returns a correct code after deleting without problems' do
75
+ result = request.delete('/', params: {
76
+ index: 'some_index',
77
+ data: %Q{{ "id":"1" }}
78
+ })
79
+ result.status.should == 200
80
+ end
81
+ it 'returns a correct code after deleting twice' do
82
+ result = request.delete('/', params: {
83
+ index: 'some_index',
84
+ data: %Q{{ "id":"1" }}
85
+ })
86
+ result = request.delete('/', params: {
87
+ index: 'some_index',
88
+ data: %Q{{ "id":"1" }}
89
+ })
90
+ result.status.should == 200
91
+ end
92
+ it 'returns a correct code after deleting without id' do
93
+ result = request.delete('/', params: {
94
+ index: 'some_index',
95
+ data: %Q{{}}
96
+ })
97
+ result.status.should == 400
98
+ end
99
+ it 'returns a correct code after deleting with the wrong index' do
100
+ result = request.delete('/', params: {
101
+ index: 'some_wrong_index',
102
+ data: %Q{{ "id":"1" }}
103
+ })
104
+ result.status.should == 404
105
+ end
106
+ end
107
+ end
108
+ context '' do
109
+ it 'updates the index correctly' do
110
+ request.post('/', params: {
111
+ index: 'some_index',
112
+ data: %Q{{ "id":"1", "name":"Florian", "surname":"Hanke" }}
113
+ })
33
114
 
34
- results = Yajl::Parser.parse request.get('/people', params: { query: 'florian' }).body
35
- results['total'].should == 1
115
+ results = Yajl::Parser.parse request.get('/people', params: { query: 'florian' }).body
116
+ results['total'].should == 1
36
117
 
37
- request.post('/', params: { index: 'index', data: %Q{{ "id":"2", "name":"Florian", "surname":"Meier" }} })
118
+ request.post('/', params: {
119
+ index: 'some_index',
120
+ data: %Q{{ "id":"2", "name":"Florian", "surname":"Meier" }}
121
+ })
38
122
 
39
- results = Yajl::Parser.parse request.get('/people', params: { query: 'florian' }).body
40
- results['total'].should == 2
41
- end
42
- it 'should delete entries from the index correctly' do
43
- request.post('/', params: { index: 'index', data: %Q{{ "id":"1", "name":"Florian", "surname":"Hanke" }} })
44
- request.post('/', params: { index: 'index', data: %Q{{ "id":"2", "name":"Florian", "surname":"Meier" }} })
123
+ results = Yajl::Parser.parse request.get('/people', params: { query: 'florian' }).body
124
+ results['total'].should == 2
125
+ end
126
+ it 'updates the index correctly' do
127
+ request.post('/', params: {
128
+ index: 'some_index',
129
+ data: %Q{{ "id":"1", "name":"Flarian", "surname":"Hanke" }}
130
+ })
45
131
 
46
- results = Yajl::Parser.parse request.get('/people', params: { query: 'florian' }).body
47
- results['total'].should == 2
132
+ results = Yajl::Parser.parse request.get('/people', params: { query: 'hanke' }).body
133
+ results['total'].should == 1
48
134
 
49
- request.delete('/', params: { index: 'index', data: %Q{{ "id":"1" }} })
135
+ results = Yajl::Parser.parse request.get('/people', params: { query: 'florian' }).body
136
+ results['total'].should == 0
50
137
 
51
- results = Yajl::Parser.parse request.get('/people', params: { query: 'florian' }).body
52
- results['total'].should == 1
53
- end
54
- it 'should have no problem with a superfluous delete' do
55
- request.delete('/', params: { index: 'index', data: %Q{{ "id":"1" }} })
138
+ # Whoops, typo. Let's fix it.
139
+ #
140
+ request.post('/', params: {
141
+ index: 'some_index',
142
+ data: %Q{{ "id":"1", "name":"Florian", "surname":"Hanke" }}
143
+ })
144
+
145
+ results = Yajl::Parser.parse request.get('/people', params: { query: 'hanke' }).body
146
+ results['total'].should == 1
147
+
148
+ results = Yajl::Parser.parse request.get('/people', params: { query: 'flarian' }).body
149
+ results['total'].should == 0
150
+
151
+ results = Yajl::Parser.parse request.get('/people', params: { query: 'florian' }).body
152
+ results['total'].should == 1
153
+ end
154
+ it 'deletes entries from the index correctly' do
155
+ request.post('/', params: {
156
+ index: 'some_index',
157
+ data: %Q{{ "id":"1", "name":"Florian", "surname":"Hanke" }}
158
+ })
159
+ request.post('/', params: {
160
+ index: 'some_index',
161
+ data: %Q{{ "id":"2", "name":"Florian", "surname":"Meier" }}
162
+ })
163
+
164
+ results = Yajl::Parser.parse request.get('/people', params: { query: 'florian' }).body
165
+ results['total'].should == 2
166
+
167
+ request.delete('/', params: {
168
+ index: 'some_index',
169
+ data: %Q{{ "id":"1" }}
170
+ })
171
+
172
+ results = Yajl::Parser.parse request.get('/people', params: { query: 'florian' }).body
173
+ results['total'].should == 1
174
+ end
175
+ it 'has no problem with a superfluous delete' do
176
+ request.delete('/', params: {
177
+ index: 'some_index',
178
+ data: %Q{{ "id":"1" }}
179
+ })
180
+
181
+ results = Yajl::Parser.parse request.get('/people', params: { query: 'florian' }).body
182
+ results['total'].should == 0
183
+ end
184
+ it 'works with the (test) client' do
185
+ client = Picky::TestClient.new MyIndexActionsPickyServer, :path => '/people'
186
+
187
+ request.post('/', params: {
188
+ index: 'some_index',
189
+ data: %Q{{ "id":"1", "name":"Florian", "surname":"Hanke" }}
190
+ })
56
191
 
57
- results = Yajl::Parser.parse request.get('/people', params: { query: 'florian' }).body
58
- results['total'].should == 0
192
+ client.search('florian').total.should == 1
193
+ end
59
194
  end
60
195
  end
61
196
 
@@ -96,7 +96,9 @@ ERROR
96
96
  before(:each) do
97
97
  tokenizer.normalizes_words([
98
98
  [/st\./, 'sankt'],
99
- [/stras?s?e?/, 'str']
99
+ [/stras?s?e?/, 'str'],
100
+ [/\+/, 'plus'],
101
+ [/\&/, 'and']
100
102
  ])
101
103
  end
102
104
  it "has normalize_with_patterns" do
@@ -105,6 +107,12 @@ ERROR
105
107
  it "normalizes, but just the first one" do
106
108
  tokenizer.normalize_with_patterns('st. wegstrasse').should == 'sankt wegstrasse'
107
109
  end
110
+ it "works correctly" do
111
+ tokenizer.normalize_with_patterns('camera +').should == 'camera plus'
112
+ end
113
+ it "works correctly" do
114
+ tokenizer.normalize_with_patterns('alice & bob').should == 'alice and bob'
115
+ end
108
116
  end
109
117
  end
110
118
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: picky
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.3.0
4
+ version: 4.3.1
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-03-13 00:00:00.000000000 Z
12
+ date: 2012-03-24 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rspec
16
- requirement: &70126521728720 !ruby/object:Gem::Requirement
16
+ requirement: &70193563400740 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ! '>='
@@ -21,21 +21,21 @@ dependencies:
21
21
  version: '0'
22
22
  type: :development
23
23
  prerelease: false
24
- version_requirements: *70126521728720
24
+ version_requirements: *70193563400740
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: picky-client
27
- requirement: &70126521728220 !ruby/object:Gem::Requirement
27
+ requirement: &70193563400240 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
30
  - - ~>
31
31
  - !ruby/object:Gem::Version
32
- version: 4.3.0
32
+ version: 4.3.1
33
33
  type: :development
34
34
  prerelease: false
35
- version_requirements: *70126521728220
35
+ version_requirements: *70193563400240
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: text
38
- requirement: &70126521727800 !ruby/object:Gem::Requirement
38
+ requirement: &70193563399820 !ruby/object:Gem::Requirement
39
39
  none: false
40
40
  requirements:
41
41
  - - ! '>='
@@ -43,10 +43,10 @@ dependencies:
43
43
  version: '0'
44
44
  type: :runtime
45
45
  prerelease: false
46
- version_requirements: *70126521727800
46
+ version_requirements: *70193563399820
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: yajl-ruby
49
- requirement: &70126521727340 !ruby/object:Gem::Requirement
49
+ requirement: &70193563399360 !ruby/object:Gem::Requirement
50
50
  none: false
51
51
  requirements:
52
52
  - - ! '>='
@@ -54,10 +54,10 @@ dependencies:
54
54
  version: '0'
55
55
  type: :runtime
56
56
  prerelease: false
57
- version_requirements: *70126521727340
57
+ version_requirements: *70193563399360
58
58
  - !ruby/object:Gem::Dependency
59
59
  name: activesupport
60
- requirement: &70126521726800 !ruby/object:Gem::Requirement
60
+ requirement: &70193563415200 !ruby/object:Gem::Requirement
61
61
  none: false
62
62
  requirements:
63
63
  - - ~>
@@ -65,10 +65,10 @@ dependencies:
65
65
  version: '3.0'
66
66
  type: :runtime
67
67
  prerelease: false
68
- version_requirements: *70126521726800
68
+ version_requirements: *70193563415200
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: procrastinate
71
- requirement: &70126521726280 !ruby/object:Gem::Requirement
71
+ requirement: &70193563414580 !ruby/object:Gem::Requirement
72
72
  none: false
73
73
  requirements:
74
74
  - - ~>
@@ -76,10 +76,10 @@ dependencies:
76
76
  version: '0.4'
77
77
  type: :runtime
78
78
  prerelease: false
79
- version_requirements: *70126521726280
79
+ version_requirements: *70193563414580
80
80
  - !ruby/object:Gem::Dependency
81
81
  name: rack_fast_escape
82
- requirement: &70126521725880 !ruby/object:Gem::Requirement
82
+ requirement: &70193563414120 !ruby/object:Gem::Requirement
83
83
  none: false
84
84
  requirements:
85
85
  - - ! '>='
@@ -87,7 +87,7 @@ dependencies:
87
87
  version: '0'
88
88
  type: :runtime
89
89
  prerelease: false
90
- version_requirements: *70126521725880
90
+ version_requirements: *70193563414120
91
91
  description: Fast Ruby semantic text search engine with comfortable single field interface.
92
92
  email: florian.hanke+picky@gmail.com
93
93
  executables:
@@ -259,6 +259,7 @@ files:
259
259
  - spec/functional/regression_spec.rb
260
260
  - spec/functional/speed_spec.rb
261
261
  - spec/functional/terminate_early_spec.rb
262
+ - spec/functional/tokenizer_spec.rb
262
263
  - spec/integration/sinatra_index_actions_spec.rb
263
264
  - spec/lib/analytics_spec.rb
264
265
  - spec/lib/analyzer_spec.rb
@@ -408,6 +409,7 @@ test_files:
408
409
  - spec/functional/regression_spec.rb
409
410
  - spec/functional/speed_spec.rb
410
411
  - spec/functional/terminate_early_spec.rb
412
+ - spec/functional/tokenizer_spec.rb
411
413
  - spec/integration/sinatra_index_actions_spec.rb
412
414
  - spec/lib/analytics_spec.rb
413
415
  - spec/lib/analyzer_spec.rb