redis-autosuggest 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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
-