redis-autosuggest 0.1.0 → 0.2.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.
data/README.md CHANGED
@@ -5,16 +5,6 @@ Provides autocompletions through Redis, with the ability to rank
5
5
 
6
6
  ## Installation
7
7
 
8
- Add this line to your application's Gemfile:
9
-
10
- gem 'redis-autosuggest'
11
-
12
- And then execute:
13
-
14
- $ bundle
15
-
16
- Or install it yourself as:
17
-
18
8
  $ gem install redis-autosuggest
19
9
 
20
10
  ## Usage
@@ -42,9 +32,9 @@ Autocompletions will be ordered their score value (descending).
42
32
  Some other usage examples:
43
33
  ```ruby
44
34
  # Add items with initial scores
45
- Redis::Autosuggest.suggest("North By Northwest", 9, Northern Exposure, 3)
35
+ Redis::Autosuggest.add_with_score("North By Northwest", 9, Northern Exposure, 3)
46
36
  # Increment an item's score
47
- Redis::Autosuggest.suggest("North By Northwest", 1)
37
+ Redis::Autosuggest.increment("North By Northwest", 1)
48
38
  ```
49
39
 
50
40
  ## Rails support
@@ -61,19 +51,14 @@ end
61
51
 
62
52
  For first time usage, seed the Redis db with the autosuggest sources:
63
53
  ```ruby
64
- Redis::Autosuggest.init_rails_sources
54
+ rake autosuggest:init
65
55
  ```
66
56
 
67
57
  You can optionally specify a numeric field to be used as the initial score for an item
68
- when it is added:
58
+ when it is added and a limit of how many items maximum to keep:
69
59
  ```ruby
70
- autosuggest :movie_title, :rank_by => imdb_rating
60
+ autosuggest :movie_title, :rank_by => imdb_rating, limit => 10000
71
61
  ```
72
62
 
73
- ## Contributing
74
-
75
- 1. Fork it
76
- 2. Create your feature branch (`git checkout -b my-new-feature`)
77
- 3. Commit your changes (`git commit -am 'Add some feature'`)
78
- 4. Push to the branch (`git push origin my-new-feature`)
79
- 5. Create new Pull Request
63
+ ## Front-end portion
64
+ Jquery plugin for dropdown autocompletions for a from can be found [here](https://github.com/aphan/jquery-rtsuggest)
@@ -3,9 +3,9 @@ require 'redis-namespace'
3
3
  require 'redis/autosuggest'
4
4
  require 'redis/autosuggest/config'
5
5
  require 'redis/autosuggest/file'
6
- require 'redis/autosuggest/init'
7
6
  require 'redis/autosuggest/version'
8
7
 
9
8
  if defined?(Rails)
10
- require 'redis/autosuggest/rails'
9
+ require 'redis/autosuggest/rails/sources'
10
+ require 'redis/autosuggest/rails/railtie'
11
11
  end
@@ -4,48 +4,54 @@ class Redis
4
4
  class << self
5
5
 
6
6
  # Add item(s) to the pool of items to autosuggest from. Each item's initial
7
- # rank is 0
7
+ # rank is 0. Returns true if all items added were new, false otherwise.
8
8
  def add(*items)
9
- item_pool = @db.hgetall(@items).values
10
- items.each do |i|
11
- next if item_pool.include?(i.downcase)
12
- add_item(i.downcase)
9
+ all_new_items = true
10
+ items.each do |item|
11
+ item = item.downcase
12
+ item_exists?(item) ? all_new_items = false : add_item(item)
13
13
  end
14
+ all_new_items
14
15
  end
15
16
 
16
- # Add item(s) along with their scores.
17
+ # Add item(s) along with their initial scores.
18
+ # Returns true if all items added were new, false otherwise.
17
19
  # add_with_score("item1", 4, "item2", 1, "item3", 0)
18
20
  def add_with_score(*fields)
19
- item_pool = @db.hgetall(@items).values
21
+ all_new_items = true
20
22
  fields.each_slice(2) do |f|
21
- next if item_pool.include?(f[0].downcase)
22
- add_item(f[0].downcase, f[1])
23
+ f[0] = normalize(f[0])
24
+ item_exists?(f[0]) ? all_new_items = false : add_item(*f)
23
25
  end
26
+ all_new_items
24
27
  end
25
28
 
26
- # Remove an item from the pool of items to autosuggest from
29
+ # Remove an item from the pool of items to autosuggest from.
30
+ # Returns true if an item was indeed removed, false otherwise.
27
31
  def remove(item)
28
32
  item = item.downcase
29
33
  id = get_id(item)
30
- return if id.nil?
34
+ return false if id.nil?
31
35
  @db.hdel(@items, id)
36
+ @db.hdel(@itemids, item)
32
37
  remove_substrings(item, id)
33
38
  @redis.zrem(@leaderboard, id) if @use_leaderboard
39
+ return true
34
40
  end
35
41
 
36
- # Increment the score (by 1 by default) of an item. Pass in a negative value
37
- # to decrement the score
38
- def increment(item, inc=1)
39
- item = item.downcase
42
+ # Increment the score (by 1 by default) of an item.
43
+ # Pass in a negative value to decrement the score.
44
+ def increment(item, incr=1)
45
+ item = normalize(item)
40
46
  id = get_id(item)
41
- each_substring(item) { |sub| @substrings.zincrby(sub, inc, id) }
42
- @db.zincrby(@leaderboard, inc, id) if @use_leaderboard
47
+ each_substring(item) { |sub| @substrings.zincrby(sub, incr, id) }
48
+ @db.zincrby(@leaderboard, incr, id) if @use_leaderboard
43
49
  end
44
50
 
45
51
  # Suggest items from the database that most closely match the queried string.
46
- # Returns an array of suggestion items (an empty array if nothing found)
52
+ # Returns an array of suggestion items (an empty array if nothing found).
47
53
  def suggest(str, results=@max_results)
48
- suggestion_ids = @substrings.zrevrange(str.downcase, 0, results - 1)
54
+ suggestion_ids = @substrings.zrevrange(normalize(str), 0, results - 1)
49
55
  suggestion_ids.empty? ? [] : @db.hmget(@items, suggestion_ids)
50
56
  end
51
57
 
@@ -57,15 +63,32 @@ class Redis
57
63
 
58
64
  # Get the score of an item
59
65
  def get_score(item)
60
- @substrings.zscore(item.downcase, get_id(item.downcase))
66
+ item = normalize(item)
67
+ @substrings.zscore(item, get_id(item))
68
+ end
69
+
70
+ # Returns whether or not an item is already stored in the db
71
+ def item_exists?(item)
72
+ return !get_id(normalize(item)).nil?
73
+ end
74
+
75
+ # Get the id associated with an item in the db
76
+ def get_id(item)
77
+ return @db.hmget(@itemids, normalize(item)).first
61
78
  end
62
79
 
63
80
  private
81
+
82
+ def normalize(item)
83
+ return item.downcase.strip
84
+ end
85
+
64
86
  def add_item(item, score=0)
65
- id = self.db.hlen(self.items)
66
- self.db.hset(self.items, id, item)
87
+ id = @db.hlen(@items)
88
+ @db.hset(@items, id, item)
89
+ @db.hset(@itemids, item, id)
67
90
  add_substrings(item, score, id)
68
- self.db.zadd(self.leaderboard, score, id) if self.use_leaderboard
91
+ @db.zadd(@leaderboard, score, id) if @use_leaderboard
69
92
  end
70
93
 
71
94
  # Yield each substring of a complete string
@@ -75,19 +98,42 @@ class Redis
75
98
 
76
99
  # Add all substrings of a string to redis
77
100
  def add_substrings(str, score, id)
78
- each_substring(str) { |sub| @substrings.zadd(sub, score, id) }
101
+ each_substring(str) do |sub|
102
+ if @max_per_substring == Float::INFINITY
103
+ add_substring(sub, score, id)
104
+ else
105
+ add_substring_limit(sub, score, id)
106
+ end
107
+ end
108
+ end
109
+
110
+ # Add the id of an item to a substring
111
+ def add_substring(sub, score, id)
112
+ @substrings.zadd(sub, score, id)
113
+ end
114
+
115
+ # Add the id of an item to a substring only when the number of items that
116
+ # substring stores is less then the config value of "max_per_substring".
117
+ # If the substring set is already full, check to see if the item with the
118
+ # lowest score in the substring set has a lower score than the item being added.
119
+ # If yes, remove that item and add this item to the substring set.
120
+ def add_substring_limit(sub, score, id)
121
+ count = @substrings.zcount(sub, "-inf", "inf")
122
+ if count < @max_per_substring
123
+ add_substring(sub, score, id)
124
+ else
125
+ lowest_item = @substrings.zrevrange(sub, -1, -1, { withscores: true }).last
126
+ if score > lowest_item[1]
127
+ @substrings.zrem(sub, lowest_item[0])
128
+ add_substring(sub, score, id)
129
+ end
130
+ end
79
131
  end
80
132
 
81
133
  # Remove all substrings of a string from the db
82
134
  def remove_substrings(str, id)
83
135
  each_substring(str) { |sub| @substrings.zrem(sub, id) }
84
136
  end
85
-
86
- # Get the id associated with an item in the db
87
- def get_id(item)
88
- kv_pair = @db.hgetall(@items).find { |_, v| v == item}
89
- kv_pair.first unless kv_pair.nil?
90
- end
91
137
  end
92
138
  end
93
139
  end
@@ -3,46 +3,55 @@ class Redis
3
3
 
4
4
  # Default Redis server at localhost:6379
5
5
  @redis = Redis.new
6
+
7
+ # Main Redis namespace for this module
8
+ @namespace = "suggest"
6
9
 
7
- @db = Redis::Namespace.new("autosuggest", :redis => @redis)
10
+ @db = Redis::Namespace.new(@namespace, :redis => @redis)
8
11
 
9
12
  # Key for a Redis hash mapping ids to items we want to use for autosuggest responses
10
13
  @items = "items"
11
14
 
15
+ # Key to a Redis hash mapping items to their respective ids
16
+ @itemids = "itemids"
17
+
12
18
  # If we want to autosuggest for partial matchings of the word: 'ruby', we would
13
19
  # have four sorted sets: 'autosuggest:substring:r', 'autosuggest:substring:ru',
14
20
  # 'autosuggest:substring:rub', and 'autosuggest:substring:ruby'.
15
21
  # Each sorted set would the id to the word 'ruby'
16
- @substrings = Redis::Namespace.new("autosuggest:substring", :redis => @redis)
22
+ @substrings = Redis::Namespace.new("#{@namespace}:sub", :redis => @redis)
17
23
 
18
24
  # max number of ids to store per substring.
19
25
  @max_per_substring = Float::INFINITY
20
26
 
21
27
  # max number of results to return for an autosuggest query
22
- @max_results = 5
28
+ @max_results = 5
23
29
 
24
30
  # Key to a sorted set holding all id of items in the autosuggest database sorted
25
31
  # by their score
26
32
  @leaderboard = "lead"
27
33
 
28
- # Leaderboard off by default
34
+ # Leaderboard on by default
29
35
  @use_leaderboard = false
30
36
 
31
37
  # Sources to be used for Autocomplete in rails.
32
38
  # Example: { Movie => :movie_title }
33
39
  @rails_sources = {}
34
40
 
41
+ # Stores the number of items the db has for each rails source
42
+ @rails_source_sizes = Redis::Namespace.new("#{@namespace}:size", :redis => @redis)
43
+
35
44
  class << self
36
45
  attr_reader :redis
37
- attr_accessor :db, :items, :substrings, :max_per_substring, :max_results,
38
- :leaderboard, :use_leaderboard, :rails_sources
46
+ attr_accessor :namespace, :db, :items, :substrings, :max_per_substring, :max_results,
47
+ :leaderboard, :use_leaderboard, :rails_sources, :rails_source_sizes
39
48
 
40
49
  def redis=(redis)
41
50
  @redis = redis
42
- @db = Redis::Namespace.new("autosuggest", :redis => redis)
43
- @substrings = Redis::Namespace.new("autosuggest:substring", :redis => redis)
51
+ @db = Redis::Namespace.new(@namespace, :redis => redis)
52
+ @substrings = Redis::Namespace.new("#{@namespace}:sub", :redis => redis)
53
+ @rails_source_sizes = Redis::Namespace.new("#{@namespace}:size", :redis => redis)
44
54
  end
45
55
  end
46
56
  end
47
57
  end
48
-
@@ -0,0 +1,10 @@
1
+ class Redis
2
+ module Autosuggest
3
+ class Railtie < Rails::Railtie
4
+
5
+ rake_tasks do
6
+ load File.expand_path('../rake_tasks.rb', __FILE__)
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,9 @@
1
+ require "redis-autosuggest"
2
+
3
+ namespace :autosuggest do
4
+
5
+ desc "redis autosuggestions init"
6
+ task :init => :environment do
7
+ Redis::Autosuggest::SuggestRails.init_rails_sources
8
+ end
9
+ end
@@ -0,0 +1,100 @@
1
+ class Redis
2
+ module Autosuggest
3
+ extend ActiveSupport::Concern
4
+ module ClassMethods
5
+
6
+ def autosuggest(column, options={})
7
+ hash = Redis::Autosuggest.rails_sources[self]
8
+ if hash.nil?
9
+ Redis::Autosuggest.rails_sources[self] = { column => options }
10
+ else
11
+ hash[column] = options
12
+ end
13
+
14
+ # Hook onto rails callbacks to update autosuggest db if a source is modified
15
+ class_eval <<-HERE
16
+ after_create :add_to_autosuggest
17
+ def add_to_autosuggest
18
+ Redis::Autosuggest::SuggestRails.add_to_autosuggest(self)
19
+ end
20
+
21
+ after_update :check_if_changed
22
+ def check_if_changed
23
+ Redis::Autosuggest::SuggestRails.check_if_changed(self)
24
+ end
25
+
26
+ before_destroy :remove_from_autosuggest
27
+ def remove_from_autosuggest
28
+ Redis::Autosuggest::SuggestRails.remove_from_autosuggest(self)
29
+ end
30
+ HERE
31
+ end
32
+ end
33
+
34
+ module SuggestRails
35
+ class << self
36
+
37
+ def init_rails_sources
38
+ Rails.application.eager_load!
39
+ Redis::Autosuggest.db.flushdb
40
+ Redis::Autosuggest.rails_sources.each do |model, attrs|
41
+ attrs.each do |column, options|
42
+ order = options[:init_order] || ""
43
+ model.order(order).find_each do |record|
44
+ puts "Adding #{record.send(column)}"
45
+ size = self.add_source_attr(record, column, options)
46
+ break if size >= options[:limit]
47
+ end
48
+ end
49
+ end
50
+ end
51
+
52
+ def add_to_autosuggest(record)
53
+ Redis::Autosuggest.rails_sources[record.class].each do |column, options|
54
+ self.add_source_attr(record, column, options)
55
+ end
56
+ end
57
+
58
+ def add_source_attr(record, column, options)
59
+ item = record.send(column)
60
+ size = self.get_size(record.class, column).to_i
61
+ if size < options[:limit]
62
+ score = record.send(options[:rank_by]) unless options[:rank_by].nil?
63
+ score ||= 0
64
+ is_new_item = Redis::Autosuggest.add_with_score(item, score)
65
+ size = self.incr_size(record.class, column) if is_new_item
66
+ end
67
+ return size
68
+ end
69
+
70
+ def check_if_changed(record)
71
+ Redis::Autosuggest.rails_sources[record.class].each_key do |column|
72
+ next unless record.send("#{column}_changed?")
73
+ old_item = record.send("#{column}_was")
74
+ score = Redis::Autosuggest.get_score(old_item)
75
+ Redis::Autosuggest.remove(old_item)
76
+ Redis::Autosuggest.add_with_score(record.send(column), score)
77
+ end
78
+ end
79
+
80
+ def remove_from_autosuggest(record)
81
+ Redis::Autosuggest.rails_sources[record.class].each_key do |column|
82
+ item = record.send(column)
83
+ item_was_in_db = Redis::Autosuggest.remove(item)
84
+ self.incr_size(record.class, column, -1) if item_was_in_db
85
+ end
86
+ end
87
+
88
+ # Get the size (how many items) of a model/attribute pair
89
+ def get_size(model, attr)
90
+ Redis::Autosuggest.rails_source_sizes.get("#{model}:#{attr}")
91
+ end
92
+
93
+ # Increment the key storing the size of a model/attribute pair
94
+ def incr_size(model, attr, incr=1)
95
+ return Redis::Autosuggest.rails_source_sizes.incrby("#{model}:#{attr}", incr)
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
@@ -1,5 +1,5 @@
1
1
  class Redis
2
2
  module Autosuggest
3
- VERSION = "0.1.0"
3
+ VERSION = "0.2.0"
4
4
  end
5
5
  end
@@ -18,8 +18,8 @@ Gem::Specification.new do |gem|
18
18
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
19
19
  gem.require_paths = ["lib"]
20
20
 
21
- gem.add_dependency("redis", "~> 3.0.1")
21
+ gem.add_dependency("redis", "~> 3.0.2")
22
22
  gem.add_dependency("redis-namespace", "~> 1.2.1")
23
23
 
24
- gem.add_development_dependency("minitest", "~> 3.5.0")
24
+ gem.add_development_dependency("minitest", "~> 4.3.3")
25
25
  end
@@ -1,7 +1,7 @@
1
1
  require 'test_helper'
2
2
 
3
3
  class TestAutosuggest < MiniTest::Unit::TestCase
4
-
4
+
5
5
  def self.unused_db
6
6
  @unused_db ||= Redis.new(:db => TestHelper.db_picker)
7
7
  end
@@ -44,7 +44,7 @@ class TestAutosuggest < MiniTest::Unit::TestCase
44
44
  assert @subs.keys.size == 10
45
45
  end
46
46
 
47
- def test_adding_multiple_items_with_scores
47
+ def test_adding_multiple_items_with_scores
48
48
  Redis::Autosuggest.add_with_score("one", 1, "two", 2, "three", 3)
49
49
  assert @db.hgetall(Redis::Autosuggest.items).size == 3
50
50
  assert @subs.keys.size == 10
@@ -101,8 +101,16 @@ class TestAutosuggest < MiniTest::Unit::TestCase
101
101
  end
102
102
 
103
103
  def test_getting_an_items_score
104
- Redis::Autosuggest.add_with_score(@str1, 3)
105
- assert_equal 3, Redis::Autosuggest.get_score(@str1)
104
+ Redis::Autosuggest.add_with_score(@str1, 3)
105
+ assert_equal 3, Redis::Autosuggest.get_score(@str1)
106
+ end
107
+
108
+ def test_adding_with_substring_limit
109
+ Redis::Autosuggest.max_per_substring = 1
110
+ Redis::Autosuggest.add_with_score(@str1, 1)
111
+ Redis::Autosuggest.add_with_score("Test", 5)
112
+ item_id = Redis::Autosuggest.get_id("Test")
113
+ assert_equal [item_id], @subs.zrevrange("test", 0, -1)
106
114
  end
107
115
 
108
116
  MiniTest::Unit.after_tests { self.unused_db.flushdb }
data/test/file_test.rb CHANGED
@@ -30,4 +30,3 @@ class TestFile < MiniTest::Unit::TestCase
30
30
 
31
31
  MiniTest::Unit.after_tests { self.unused_db.flushdb }
32
32
  end
33
-
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: redis-autosuggest
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-09-26 00:00:00.000000000 Z
12
+ date: 2012-12-18 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: redis
@@ -18,7 +18,7 @@ dependencies:
18
18
  requirements:
19
19
  - - ~>
20
20
  - !ruby/object:Gem::Version
21
- version: 3.0.1
21
+ version: 3.0.2
22
22
  type: :runtime
23
23
  prerelease: false
24
24
  version_requirements: !ruby/object:Gem::Requirement
@@ -26,7 +26,7 @@ dependencies:
26
26
  requirements:
27
27
  - - ~>
28
28
  - !ruby/object:Gem::Version
29
- version: 3.0.1
29
+ version: 3.0.2
30
30
  - !ruby/object:Gem::Dependency
31
31
  name: redis-namespace
32
32
  requirement: !ruby/object:Gem::Requirement
@@ -50,7 +50,7 @@ dependencies:
50
50
  requirements:
51
51
  - - ~>
52
52
  - !ruby/object:Gem::Version
53
- version: 3.5.0
53
+ version: 4.3.3
54
54
  type: :development
55
55
  prerelease: false
56
56
  version_requirements: !ruby/object:Gem::Requirement
@@ -58,7 +58,7 @@ dependencies:
58
58
  requirements:
59
59
  - - ~>
60
60
  - !ruby/object:Gem::Version
61
- version: 3.5.0
61
+ version: 4.3.3
62
62
  description: ! "Provides autocompletions through Redis, with the ability to rank\n
63
63
  \ results and integrate with Rails"
64
64
  email:
@@ -76,8 +76,9 @@ files:
76
76
  - lib/redis/autosuggest.rb
77
77
  - lib/redis/autosuggest/config.rb
78
78
  - lib/redis/autosuggest/file.rb
79
- - lib/redis/autosuggest/init.rb
80
- - lib/redis/autosuggest/rails.rb
79
+ - lib/redis/autosuggest/rails/railtie.rb
80
+ - lib/redis/autosuggest/rails/rake_tasks.rb
81
+ - lib/redis/autosuggest/rails/sources.rb
81
82
  - lib/redis/autosuggest/version.rb
82
83
  - redis-autosuggest.gemspec
83
84
  - test/autosuggest_test.rb
@@ -1,17 +0,0 @@
1
- class Redis
2
- module Autosuggest
3
-
4
- class << self
5
-
6
- def init_rails_sources
7
- self.rails_sources.each_key do |r|
8
- r.all.each do |record|
9
- record.add_to_autosuggest
10
- end
11
- end
12
- end
13
- end
14
- end
15
- end
16
-
17
-
@@ -1,49 +0,0 @@
1
- class Redis
2
- module Autosuggest
3
- extend ActiveSupport::Concern
4
-
5
- module ClassMethods
6
-
7
- def autosuggest(column, options={})
8
- hash = Redis::Autosuggest.rails_sources[self]
9
- if hash.nil?
10
- Redis::Autosuggest.rails_sources[self] = { column => options }
11
- else
12
- hash[column] = options
13
- end
14
-
15
- # hook onto rails callbacks to update autosuggest db if a source is modified
16
- class_eval <<-HERE
17
- after_create :add_to_autosuggest
18
- def add_to_autosuggest
19
- Redis::Autosuggest.rails_sources[self.class].each do |column, options|
20
- score = self.send(options[:rank_by]) if !options[:rank_by].nil?
21
- score ||= 0
22
- Redis::Autosuggest.add_with_score(self.send(column), score)
23
- end
24
- end
25
-
26
- after_update :check_if_changed
27
- def check_if_changed
28
- Redis::Autosuggest.rails_sources[self.class].each_key do |column|
29
- next if !self.send("#{column}_changed?")
30
- old_item = self.send("#{column}_was")
31
- score = Redis::Autosuggest.get_score(old_item)
32
- Redis::Autosuggest.remove(old_item)
33
- Redis::Autosuggest.add_with_score(self.send(column), score)
34
- end
35
- end
36
-
37
- before_destroy :remove_from_autosuggest
38
- def remove_from_autosuggest
39
- Redis::Autosuggest.rails_sources[self.class].each_key do |column|
40
- Redis::Autosuggest.remove(self.send(column))
41
- end
42
- end
43
- HERE
44
- end
45
- end
46
- end
47
- end
48
-
49
-