redis-autosuggest 0.0.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.
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in redis-autosuggest.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Adam Phan
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,29 @@
1
+ # Redis::Autosuggest
2
+
3
+ TODO: Write a gem description
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'redis-autosuggest'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install redis-autosuggest
18
+
19
+ ## Usage
20
+
21
+ TODO: Write usage instructions here
22
+
23
+ ## Contributing
24
+
25
+ 1. Fork it
26
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
27
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
28
+ 4. Push to the branch (`git push origin my-new-feature`)
29
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,44 @@
1
+ class Redis
2
+ module Autosuggest
3
+
4
+ # Default Redis server at localhost:6379
5
+ @redis = Redis.new
6
+
7
+ @db = Redis::Namespace.new("autosuggest", :redis => @redis)
8
+
9
+ # Key for a Redis hash mapping ids to items we want to use for autosuggest responses
10
+ @items = "items"
11
+
12
+ # If we want to autosuggest for partial matchings of the word: 'ruby', we would
13
+ # have four sorted sets: 'autosuggest:substring:r', 'autosuggest:substring:ru',
14
+ # 'autosuggest:substring:rub', and 'autosuggest:substring:ruby'.
15
+ # Each sorted set would the id to the word 'ruby'
16
+ @substrings = Redis::Namespace.new("autosuggest:substring", :redis => @redis)
17
+
18
+ # max number of ids to store per substring.
19
+ @max_per_substring = Float::INFINITY
20
+
21
+ # max number of results to return for an autosuggest query
22
+ @max_results = 5
23
+
24
+ # Key to a sorted set holding all id of items in the autosuggest database sorted
25
+ # by their score
26
+ @leaderboard = "leaderboard"
27
+
28
+ # Leaderboard off by default
29
+ @use_leaderboard = false
30
+
31
+ class << self
32
+ attr_reader :redis
33
+ attr_accessor :db, :items, :substrings, :max_per_substring, :max_results,
34
+ :leaderboard, :use_leaderboard
35
+
36
+ def redis=(redis)
37
+ @redis = redis
38
+ @db = Redis::Namespace.new("autosuggest", :redis => redis)
39
+ @substrings = Redis::Namespace.new("autosuggest:substring", :redis => redis)
40
+ end
41
+ end
42
+ end
43
+ end
44
+
@@ -0,0 +1,5 @@
1
+ class Redis
2
+ module Autosuggest
3
+ VERSION = "0.0.1"
4
+ end
5
+ end
@@ -0,0 +1,88 @@
1
+ class Redis
2
+ module Autosuggest
3
+
4
+ class << self
5
+
6
+ # Add item(s) to the pool of items to autosuggest from. Each item's initial
7
+ # rank is 0
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)
13
+ end
14
+ end
15
+
16
+ # Add item(s) along with their scores.
17
+ # add_with_score("item1", 4, "item2", 1, "item3", 0)
18
+ def add_with_score(*fields)
19
+ item_pool = @db.hgetall(@items).values
20
+ fields.each_slice(2) do |f|
21
+ next if item_pool.include?(f[0].downcase)
22
+ add_item(f[0].downcase, f[1])
23
+ end
24
+ end
25
+
26
+ # Remove an item from the pool of items to autosuggest from
27
+ def remove(item)
28
+ item = item.downcase
29
+ id = get_id(item)
30
+ return if id.nil?
31
+ @db.hdel(@items, id)
32
+ remove_substrings(item, id)
33
+ @redis.zrem(@leaderboard, id) if @use_leaderboard
34
+ end
35
+
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
40
+ id = get_id(item)
41
+ each_substring(item) { |sub| @substrings.zincrby(sub, inc, id) }
42
+ @db.zincrby(@leaderboard, inc, id) if @use_leaderboard
43
+ end
44
+
45
+ # 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)
47
+ def suggest(str, results=@max_results)
48
+ suggestion_ids = @substrings.zrevrange(str.downcase, 0, results - 1)
49
+ suggestion_ids.empty? ? [] : @db.hmget(@items, suggestion_ids)
50
+ end
51
+
52
+ # Gets the items with the highest scores from the autosuggest db
53
+ def get_leaderboard(results=@max_results)
54
+ top_ids = @db.zrevrange(@leaderboard, 0, results - 1)
55
+ top_ids.empty? ? [] : @db.hmget(@items, top_ids)
56
+ end
57
+
58
+ private
59
+ def add_item(item, score=0)
60
+ id = self.db.hlen(self.items)
61
+ self.db.hset(self.items, id, item)
62
+ add_substrings(item, score, id)
63
+ self.db.zadd(self.leaderboard, score, id) if self.use_leaderboard
64
+ end
65
+
66
+ # Yield each substring of a complete string
67
+ def each_substring(str)
68
+ (0..str.length - 1).each { |i| yield str[0..i] }
69
+ end
70
+
71
+ # Add all substrings of a string to redis
72
+ def add_substrings(str, score, id)
73
+ each_substring(str) { |sub| @substrings.zadd(sub, score, id) }
74
+ end
75
+
76
+ # Remove all substrings of a string from the db
77
+ def remove_substrings(str, id)
78
+ each_substring(str) { |sub| @substrings.zrem(sub, id) }
79
+ end
80
+
81
+ # Get the id associated with an item in the db
82
+ def get_id(item)
83
+ kv_pair = @db.hgetall(@items).find { |_, v| v == item}
84
+ kv_pair.first unless kv_pair.nil?
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,5 @@
1
+ require 'redis'
2
+ require 'redis-namespace'
3
+ require 'redis/autosuggest'
4
+ require 'redis/autosuggest/version'
5
+ require 'redis/autosuggest/config'
@@ -0,0 +1,26 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'redis/autosuggest/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "redis-autosuggest"
8
+ gem.version = Redis::Autosuggest::VERSION
9
+ gem.authors = ["Adam Phan"]
10
+ gem.email = ["aphansh@gmail.com"]
11
+ gem.description = %q{Provides autocompletions through Redis, with the ability to rank
12
+ results and integrate with Rails}
13
+ gem.summary = %q{Suggestions/autocompletions with Redis and Ruby}
14
+ gem.homepage = "https://github.com/aphan/redis-autosuggest"
15
+
16
+ gem.files = `git ls-files`.split($/)
17
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
18
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
19
+ gem.require_paths = ["lib"]
20
+
21
+ gem.add_dependency("redis", "~> 3.0.1")
22
+ gem.add_dependency("redis-namespace", "~> 1.2.1")
23
+
24
+ gem.add_development_dependency("debugger", "~> 1.2.0")
25
+ gem.add_development_dependency("minitest", "~> 3.5.0")
26
+ end
@@ -0,0 +1,113 @@
1
+ require 'test_helper'
2
+
3
+ class TestAutosuggest < MiniTest::Unit::TestCase
4
+
5
+ def self.unused_db
6
+ @unused_db ||= Redis.new(:db => db_picker)
7
+ end
8
+
9
+ # get an unused db so that we can safely clear all keys
10
+ def self.db_picker
11
+ redis = Redis.new
12
+ (0..15).each do |i|
13
+ redis.select(i)
14
+ return i if redis.keys.empty?
15
+ end
16
+ end
17
+
18
+ def setup
19
+ self.class.unused_db.flushdb
20
+ Redis::Autosuggest.redis = self.class.unused_db
21
+ @db = Redis::Autosuggest.db
22
+ @subs = Redis::Autosuggest.substrings
23
+ @str1 = "Test String"
24
+ end
25
+
26
+ def test_adding_an_item
27
+ Redis::Autosuggest.add(@str1)
28
+ assert @db.hgetall(Redis::Autosuggest.items)["0"] == @str1.downcase
29
+ assert @subs.keys.size == @str1.size
30
+ assert_equal ["0"], @subs.zrevrange("t", 0, -1)
31
+ assert_equal ["0"], @subs.zrevrange("te", 0, -1)
32
+ assert_equal ["0"], @subs.zrevrange("tes", 0, -1)
33
+ assert_equal ["0"], @subs.zrevrange("test", 0, -1)
34
+ assert_equal ["0"], @subs.zrevrange("test ", 0, -1)
35
+ assert_equal ["0"], @subs.zrevrange("test s", 0, -1)
36
+ assert_equal ["0"], @subs.zrevrange("test st", 0, -1)
37
+ assert_equal ["0"], @subs.zrevrange("test str", 0, -1)
38
+ assert_equal ["0"], @subs.zrevrange("test stri", 0, -1)
39
+ assert_equal ["0"], @subs.zrevrange("test strin", 0, -1)
40
+ assert_equal ["0"], @subs.zrevrange("test string", 0, -1)
41
+ end
42
+
43
+ def test_adding_duplicate_item
44
+ Redis::Autosuggest.add(@str1)
45
+ Redis::Autosuggest.add(@str1)
46
+ assert @db.hgetall(Redis::Autosuggest.items).size == 1
47
+ assert @subs.keys.size == @str1.size
48
+ end
49
+
50
+ def test_adding_multiple_items
51
+ Redis::Autosuggest.add("one", "two", "three")
52
+ assert @db.hgetall(Redis::Autosuggest.items).size == 3
53
+ assert @subs.keys.size == 10
54
+ end
55
+
56
+ def test_adding_multiple_items_with_scores
57
+ Redis::Autosuggest.add_with_score("one", 1, "two", 2, "three", 3)
58
+ assert @db.hgetall(Redis::Autosuggest.items).size == 3
59
+ assert @subs.keys.size == 10
60
+ assert_equal 1, @subs.zscore("one", 0)
61
+ assert_equal 2, @subs.zscore("two", 1)
62
+ assert_equal 3, @subs.zscore("three", 2)
63
+ end
64
+
65
+ def test_removing_an_item
66
+ Redis::Autosuggest.add(@str1)
67
+ Redis::Autosuggest.remove(@str1)
68
+ assert @db.hgetall(Redis::Autosuggest.items).empty?
69
+ assert @subs.keys.size == 0
70
+ end
71
+
72
+ def test_removing_a_nonexistent_item
73
+ Redis::Autosuggest.add(@str1)
74
+ Redis::Autosuggest.remove("Second test string")
75
+ assert @db.hgetall(Redis::Autosuggest.items).size == 1
76
+ assert @db.hgetall(Redis::Autosuggest.items)["0"] == @str1.downcase
77
+ assert @subs.keys.size == @str1.size
78
+ end
79
+
80
+ def test_incrementing_an_items_score
81
+ Redis::Autosuggest.add_with_score(@str1, 5)
82
+ Redis::Autosuggest.increment(@str1)
83
+ @subs.keys.each { |k| assert @subs.zscore(k, 0) == 6 }
84
+ Redis::Autosuggest.increment(@str1, 8)
85
+ @subs.keys.each { |k| assert @subs.zscore(k, 0) == 14 }
86
+ Redis::Autosuggest.increment(@str1, -8)
87
+ @subs.keys.each { |k| assert @subs.zscore(k, 0) == 6 }
88
+ end
89
+
90
+ def test_suggesting_items
91
+ Redis::Autosuggest.add_with_score(@str1, 5)
92
+ Redis::Autosuggest.add_with_score("#{@str1} longer", 2)
93
+ suggestions = Redis::Autosuggest.suggest(@str1[0..4])
94
+ assert_equal [@str1.downcase, "#{@str1} longer".downcase], suggestions
95
+ end
96
+
97
+ def test_no_suggestions_found
98
+ Redis::Autosuggest.add(@str1)
99
+ assert Redis::Autosuggest.suggest("nothing here").empty?
100
+ end
101
+
102
+ def test_leaderboard_items
103
+ Redis::Autosuggest.use_leaderboard = true
104
+ Redis::Autosuggest.add_with_score(@str1, 3)
105
+ Redis::Autosuggest.add_with_score("Another item", 5)
106
+ Redis::Autosuggest.add_with_score("Third item", 1)
107
+ top_items = Redis::Autosuggest.get_leaderboard
108
+ assert_equal ["another item", @str1.downcase, "third item"], top_items
109
+ Redis::Autosuggest.use_leaderboard = false
110
+ end
111
+
112
+ MiniTest::Unit.after_tests { self.unused_db.flushdb }
113
+ end
@@ -0,0 +1,6 @@
1
+ $LOAD_PATH.unshift 'lib'
2
+ require 'minitest/autorun'
3
+ require 'debugger'
4
+ require 'redis'
5
+ require 'redis-namespace'
6
+ require 'redis-autosuggest'
metadata ADDED
@@ -0,0 +1,124 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: redis-autosuggest
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Adam Phan
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-09-26 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: redis
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 3.0.1
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: 3.0.1
30
+ - !ruby/object:Gem::Dependency
31
+ name: redis-namespace
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ~>
36
+ - !ruby/object:Gem::Version
37
+ version: 1.2.1
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ version: 1.2.1
46
+ - !ruby/object:Gem::Dependency
47
+ name: debugger
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ~>
52
+ - !ruby/object:Gem::Version
53
+ version: 1.2.0
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: 1.2.0
62
+ - !ruby/object:Gem::Dependency
63
+ name: minitest
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ~>
68
+ - !ruby/object:Gem::Version
69
+ version: 3.5.0
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ~>
76
+ - !ruby/object:Gem::Version
77
+ version: 3.5.0
78
+ description: ! "Provides autocompletions through Redis, with the ability to rank\n
79
+ \ results and integrate with Rails"
80
+ email:
81
+ - aphansh@gmail.com
82
+ executables: []
83
+ extensions: []
84
+ extra_rdoc_files: []
85
+ files:
86
+ - .gitignore
87
+ - Gemfile
88
+ - LICENSE.txt
89
+ - README.md
90
+ - Rakefile
91
+ - lib/redis-autosuggest.rb
92
+ - lib/redis/autosuggest.rb
93
+ - lib/redis/autosuggest/config.rb
94
+ - lib/redis/autosuggest/version.rb
95
+ - redis-autosuggest.gemspec
96
+ - test/autosuggest_test.rb
97
+ - test/test_helper.rb
98
+ homepage: https://github.com/aphan/redis-autosuggest
99
+ licenses: []
100
+ post_install_message:
101
+ rdoc_options: []
102
+ require_paths:
103
+ - lib
104
+ required_ruby_version: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ! '>='
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ required_rubygems_version: !ruby/object:Gem::Requirement
111
+ none: false
112
+ requirements:
113
+ - - ! '>='
114
+ - !ruby/object:Gem::Version
115
+ version: '0'
116
+ requirements: []
117
+ rubyforge_project:
118
+ rubygems_version: 1.8.24
119
+ signing_key:
120
+ specification_version: 3
121
+ summary: Suggestions/autocompletions with Redis and Ruby
122
+ test_files:
123
+ - test/autosuggest_test.rb
124
+ - test/test_helper.rb