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 +17 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +29 -0
- data/Rakefile +1 -0
- data/lib/redis/autosuggest/config.rb +44 -0
- data/lib/redis/autosuggest/version.rb +5 -0
- data/lib/redis/autosuggest.rb +88 -0
- data/lib/redis-autosuggest.rb +5 -0
- data/redis-autosuggest.gemspec +26 -0
- data/test/autosuggest_test.rb +113 -0
- data/test/test_helper.rb +6 -0
- metadata +124 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
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,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,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
|
data/test/test_helper.rb
ADDED
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
|