suggester 0.1.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/Gemfile +18 -0
- data/Gemfile.lock +43 -0
- data/README.md +127 -0
- data/Rakefile +70 -0
- data/VERSION +1 -0
- data/bin/suggester_server +18 -0
- data/config/database.yml.example +6 -0
- data/lib/array_bsearch.rb +82 -0
- data/lib/suggester.rb +3 -0
- data/lib/suggester/client.rb +109 -0
- data/lib/suggester/handlers.rb +15 -0
- data/lib/suggester/handlers/active_record.rb +51 -0
- data/lib/suggester/handlers/base.rb +107 -0
- data/lib/suggester/handlers/helpers/normalization.rb +3 -0
- data/lib/suggester/handlers/helpers/refresh.rb +37 -0
- data/lib/suggester/handlers/marshal.rb +22 -0
- data/lib/suggester/handlers/yaml.rb +22 -0
- data/lib/suggester/server.rb +130 -0
- data/pkg/suggester-0.0.1.gem +0 -0
- data/pkg/suggester-0.0.2.gem +0 -0
- data/suggester.gemspec +92 -0
- data/test/fixtures/books.sql +15 -0
- data/test/fixtures/marshal.marshal +0 -0
- data/test/fixtures/yaml.yml +25 -0
- data/test/functional/basic_test.rb +98 -0
- data/test/test_helper.rb +26 -0
- data/test/unit/active_record_handler_test.rb +74 -0
- data/test/unit/marshal_handler_test.rb +65 -0
- data/test/unit/yaml_handler_test.rb +66 -0
- metadata +178 -0
@@ -0,0 +1,15 @@
|
|
1
|
+
# Defines the module for the basic handle namespace
|
2
|
+
#
|
3
|
+
# Author:: Jeff Ching
|
4
|
+
# Copyright:: Copyright (c) 2010
|
5
|
+
# License:: Distributes under the same terms as Ruby
|
6
|
+
module Suggester
|
7
|
+
module Handlers
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
# require all the provided base handlers
|
12
|
+
list = Dir.glob(File.expand_path(File.dirname(__FILE__) + "/handlers/*.rb")).sort
|
13
|
+
list.each do |f|
|
14
|
+
require f
|
15
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/base')
|
2
|
+
|
3
|
+
module Suggester
|
4
|
+
module Handlers
|
5
|
+
class ActiveRecord < Base
|
6
|
+
|
7
|
+
def initialize(params = {})
|
8
|
+
@klass = params[:class] || raise("must specify a class")
|
9
|
+
@klass = @klass.constantize if @klass.is_a?(String)
|
10
|
+
@id_field = params[:id_field] || :id
|
11
|
+
@name_field = params[:name_field] || :name
|
12
|
+
@conditions = params[:conditions] || {}
|
13
|
+
@include = params[:include] || {}
|
14
|
+
super(params)
|
15
|
+
end
|
16
|
+
|
17
|
+
protected
|
18
|
+
|
19
|
+
def all_records
|
20
|
+
@klass.find(:all, :include => @include, :conditions => @conditions)
|
21
|
+
end
|
22
|
+
|
23
|
+
def build_cache
|
24
|
+
cache = []
|
25
|
+
all_records.each do |entry|
|
26
|
+
cache << build_entry(entry)
|
27
|
+
end
|
28
|
+
cache.sort{|x,y| x[:search_term] <=> y[:search_term]}
|
29
|
+
end
|
30
|
+
|
31
|
+
def build_entry(record)
|
32
|
+
{
|
33
|
+
:search_term => search_term(record),
|
34
|
+
:data => entry_data(record)
|
35
|
+
}
|
36
|
+
end
|
37
|
+
|
38
|
+
def search_term(record)
|
39
|
+
record.send(@name_field).downcase
|
40
|
+
end
|
41
|
+
|
42
|
+
def entry_data(record)
|
43
|
+
{
|
44
|
+
@unique_field_name => record.send(@name_field),
|
45
|
+
:id => record.send(@id_field),
|
46
|
+
}
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
# Base abstract class for the handlers. Provides basic functionality allowing
|
2
|
+
# the caches to be searched. Provides basic functionality allowing the
|
3
|
+
# cache to be refreshed.
|
4
|
+
#
|
5
|
+
# Author:: Jeff Ching
|
6
|
+
# Copyright:: Copyright (c) 2010
|
7
|
+
# License:: Distributes under the same terms as Ruby
|
8
|
+
require 'suggester/handlers/helpers/refresh'
|
9
|
+
|
10
|
+
module Suggester
|
11
|
+
module Handlers
|
12
|
+
class Base
|
13
|
+
# name of the field in the data hash that should be unique
|
14
|
+
attr_accessor :unique_field_name
|
15
|
+
|
16
|
+
include Suggester::Handlers::Helpers::Refresh
|
17
|
+
def initialize(params = {})
|
18
|
+
@unique_field_name = params[:unique_field_name] || :display_string
|
19
|
+
@refresh_interval = params.delete(:refresh_interval)
|
20
|
+
@last_refreshed_at = Time.now
|
21
|
+
@cache = build_cache
|
22
|
+
end
|
23
|
+
|
24
|
+
# Returns an array of hashes with the following format:
|
25
|
+
# [
|
26
|
+
# :search_term => <string>,
|
27
|
+
# :data => {
|
28
|
+
# <unique_field_name> => <anything>
|
29
|
+
# ...other data to be returned
|
30
|
+
# }
|
31
|
+
# ]
|
32
|
+
# NOTE: must be sorted by :search_term
|
33
|
+
def cache
|
34
|
+
@cache
|
35
|
+
end
|
36
|
+
|
37
|
+
# Returns an array of data hashes that are an exact match for params[:query]
|
38
|
+
def match(params)
|
39
|
+
query = params[:query].downcase
|
40
|
+
limit = params[:limit]
|
41
|
+
limit = limit.to_i unless limit.nil?
|
42
|
+
results = find_exact_matches(query, limit, params)
|
43
|
+
end
|
44
|
+
|
45
|
+
# Returns an array of data hashes that begin with params[:query]
|
46
|
+
def find(params)
|
47
|
+
query = params[:query].downcase
|
48
|
+
limit = params[:limit]
|
49
|
+
limit = limit.to_i unless limit.nil?
|
50
|
+
results = find_begin_matches(query, limit, params)
|
51
|
+
end
|
52
|
+
|
53
|
+
protected
|
54
|
+
|
55
|
+
# Build a copy of the cache (needs to be specified by subclasses)
|
56
|
+
def build_cache
|
57
|
+
[]
|
58
|
+
end
|
59
|
+
|
60
|
+
# do a binary search through the cache to find the lowest index
|
61
|
+
def find_lower_bound(string)
|
62
|
+
@cache.bsearch_lower_boundary {|x| x[:search_term] <=> string}
|
63
|
+
end
|
64
|
+
|
65
|
+
# returns an array of begins with matches as:
|
66
|
+
# [
|
67
|
+
# {
|
68
|
+
# <unique_field_name> => <anything>
|
69
|
+
# ...other data
|
70
|
+
# }
|
71
|
+
# ]
|
72
|
+
def find_begin_matches(search_string, limit, params)
|
73
|
+
results = []
|
74
|
+
lower_bound = find_lower_bound(search_string)
|
75
|
+
|
76
|
+
for i in lower_bound...@cache.length
|
77
|
+
# stop looking if we are no longer matching OR we have found enough matches
|
78
|
+
break if @cache[i][:search_term].index(search_string) != 0 || (limit && results.length >= limit)
|
79
|
+
results << @cache[i]
|
80
|
+
end
|
81
|
+
|
82
|
+
results.map{|r| r[:data]}
|
83
|
+
end
|
84
|
+
|
85
|
+
# returns an array of exact matches as:
|
86
|
+
# [
|
87
|
+
# {
|
88
|
+
# <unique_field_name> => <anything>
|
89
|
+
# ...other data
|
90
|
+
# }
|
91
|
+
# ]
|
92
|
+
def find_exact_matches(search_string, limit, params)
|
93
|
+
results = []
|
94
|
+
lower_bound = find_lower_bound(search_string)
|
95
|
+
|
96
|
+
for i in lower_bound...@cache.length
|
97
|
+
# stop looking if we are no longer matching OR we have found enough matches
|
98
|
+
break if @cache[i][:search_term] != search_string || (limit && results.length >= limit)
|
99
|
+
results << @cache[i]
|
100
|
+
end
|
101
|
+
|
102
|
+
results.map{|r| r[:data]}
|
103
|
+
end
|
104
|
+
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Suggester
|
2
|
+
module Handlers
|
3
|
+
module Helpers
|
4
|
+
module Refresh
|
5
|
+
|
6
|
+
def refresh!
|
7
|
+
# assumption: assignment is atomic in ruby
|
8
|
+
@cache = build_cache
|
9
|
+
@last_refreshed_at = Time.now
|
10
|
+
end
|
11
|
+
|
12
|
+
def force_refresh!
|
13
|
+
@last_refreshed_at = nil
|
14
|
+
end
|
15
|
+
|
16
|
+
def needs_refresh?
|
17
|
+
return true if last_refreshed_at.nil?
|
18
|
+
refresh_interval && last_refreshed_at + refresh_interval.minutes < Time.now
|
19
|
+
end
|
20
|
+
|
21
|
+
def refresh_interval
|
22
|
+
@refresh_interval
|
23
|
+
end
|
24
|
+
|
25
|
+
def refresh_interval=(value)
|
26
|
+
@refresh_interval = value
|
27
|
+
@refresh_interval = @refresh_interval.to_i unless value.nil?
|
28
|
+
end
|
29
|
+
|
30
|
+
def last_refreshed_at
|
31
|
+
@last_refreshed_at
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/base')
|
2
|
+
|
3
|
+
module Suggester
|
4
|
+
module Handlers
|
5
|
+
class Marshal < Base
|
6
|
+
|
7
|
+
def initialize(params = {})
|
8
|
+
@file = params.delete(:file) || raise("must specify a file")
|
9
|
+
super(params)
|
10
|
+
end
|
11
|
+
|
12
|
+
protected
|
13
|
+
|
14
|
+
def build_cache()
|
15
|
+
io = open(@file)
|
16
|
+
cache = ::Marshal.load(io.read)
|
17
|
+
cache.sort{|x,y| x[:search_term] <=> y[:search_term]}
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/base')
|
2
|
+
|
3
|
+
module Suggester
|
4
|
+
module Handlers
|
5
|
+
class Yaml < Base
|
6
|
+
def initialize(params = {})
|
7
|
+
@file = params.delete(:file) || raise("must specify a file")
|
8
|
+
super(params)
|
9
|
+
end
|
10
|
+
|
11
|
+
protected
|
12
|
+
|
13
|
+
def build_cache()
|
14
|
+
io = open(@file)
|
15
|
+
cache = YAML::load(io.read)
|
16
|
+
cache.sort{|x,y| x[:search_term] <=> y[:search_term]}
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
|
@@ -0,0 +1,130 @@
|
|
1
|
+
# This is the core server for the Suggester gem. It extends the basic sinatra server
|
2
|
+
# and provides a basic interface to browse and query the server.
|
3
|
+
#
|
4
|
+
# The server supports output in HTML, YAML, JSON, and Marshal output
|
5
|
+
#
|
6
|
+
# Author:: Jeff Ching
|
7
|
+
# Copyright:: Copyright(c) 2010
|
8
|
+
# License:: Distribues under the same terms as Ruby
|
9
|
+
require 'sinatra/base'
|
10
|
+
require 'yaml'
|
11
|
+
require 'json'
|
12
|
+
require 'open-uri'
|
13
|
+
require 'array_bsearch'
|
14
|
+
require File.expand_path(File.join(File.dirname(__FILE__), 'handlers.rb'))
|
15
|
+
|
16
|
+
module Suggester
|
17
|
+
# Core server class
|
18
|
+
class Server < Sinatra::Base
|
19
|
+
|
20
|
+
# Create an instance of the server. At this time, we spawn a separate thread
|
21
|
+
# that will reload handlers as needed to prevent locking the server thread.
|
22
|
+
def initialize(*args)
|
23
|
+
super(*args)
|
24
|
+
spawn_refresh_thread!
|
25
|
+
end
|
26
|
+
|
27
|
+
# list all handlers currently registered
|
28
|
+
get "/" do
|
29
|
+
output = "<html><body><h1>Suggester Handlers</h1>"
|
30
|
+
output << self.class.handlers.keys.sort.map{|n| "<a href='/#{n}/dump.html'>#{n} (#{self.class.handler(n).cache.size})</a>"}.join('<br/>')
|
31
|
+
output << "</body></html>"
|
32
|
+
end
|
33
|
+
|
34
|
+
# dump out the contents of the handler's cache
|
35
|
+
get "/:handler/dump.:format" do
|
36
|
+
format = params.delete("format")
|
37
|
+
handler = params.delete("handler")
|
38
|
+
|
39
|
+
data = self.class.handler(handler).cache
|
40
|
+
case(format)
|
41
|
+
when 'yml'
|
42
|
+
data.to_yaml
|
43
|
+
when 'json'
|
44
|
+
data.to_json
|
45
|
+
when 'marshal'
|
46
|
+
Marshal.dump(data)
|
47
|
+
when 'html'
|
48
|
+
output = "<html><body>"
|
49
|
+
output << data.map{|r| r.inspect}.join('<br/>')
|
50
|
+
output << "</body></html>"
|
51
|
+
else
|
52
|
+
"BAD FORMAT"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# find exact matches for the query string
|
57
|
+
get "/:handler/match/:query.:format" do
|
58
|
+
format = params.delete("format")
|
59
|
+
handler = params.delete("handler")
|
60
|
+
matches = self.class.handler(handler).match(params)
|
61
|
+
case(format)
|
62
|
+
when 'yml'
|
63
|
+
matches.to_yaml
|
64
|
+
when 'json'
|
65
|
+
matches.to_json
|
66
|
+
else
|
67
|
+
matches.inspect
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# find matches that begin with the query string
|
72
|
+
get "/:handler/find/:query.:format" do
|
73
|
+
format = params.delete("format")
|
74
|
+
handler = params.delete("handler")
|
75
|
+
matches = self.class.handler(handler).find(params)
|
76
|
+
case(format)
|
77
|
+
when 'yml'
|
78
|
+
matches.to_yaml
|
79
|
+
when 'json'
|
80
|
+
matches.to_json
|
81
|
+
else
|
82
|
+
matches.inspect
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# force a refresh of the specified handler
|
87
|
+
get "/:handler/refresh" do
|
88
|
+
handler = params.delete("handler")
|
89
|
+
|
90
|
+
if current_handler = self.class.handler(handler)
|
91
|
+
current_handler.force_refresh!
|
92
|
+
"OK"
|
93
|
+
else
|
94
|
+
"FAIL"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# Returns the hash of all handler names to their instances
|
99
|
+
def self.handlers
|
100
|
+
@handlers
|
101
|
+
end
|
102
|
+
|
103
|
+
# Returns the handler instance given the handler name
|
104
|
+
def self.handler(name)
|
105
|
+
@handlers ||= {}
|
106
|
+
@handlers[name]
|
107
|
+
end
|
108
|
+
|
109
|
+
# Register a handler instance to its handler name
|
110
|
+
def self.add_handler(name, handler)
|
111
|
+
@handlers ||= {}
|
112
|
+
@handlers[name] = handler
|
113
|
+
end
|
114
|
+
|
115
|
+
private
|
116
|
+
|
117
|
+
def spawn_refresh_thread! #:nodoc:
|
118
|
+
Thread.new do
|
119
|
+
loop do
|
120
|
+
sleep(10)
|
121
|
+
self.class.handlers.each do |name, handler|
|
122
|
+
handler.refresh! if handler.needs_refresh?
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
Binary file
|
Binary file
|
data/suggester.gemspec
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{suggester}
|
8
|
+
s.version = "0.1.0"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Jeff Ching"]
|
12
|
+
s.date = %q{2010-12-09}
|
13
|
+
s.default_executable = %q{suggester_server}
|
14
|
+
s.description = %q{Extensible, cache-based auto-suggest server for ruby. Includes refresh and replication support out of the box.}
|
15
|
+
s.email = %q{ching.jeff@gmail.com}
|
16
|
+
s.executables = ["suggester_server"]
|
17
|
+
s.extra_rdoc_files = [
|
18
|
+
"README.md"
|
19
|
+
]
|
20
|
+
s.files = [
|
21
|
+
"Gemfile",
|
22
|
+
"Gemfile.lock",
|
23
|
+
"README.md",
|
24
|
+
"Rakefile",
|
25
|
+
"VERSION",
|
26
|
+
"bin/suggester_server",
|
27
|
+
"config/database.yml.example",
|
28
|
+
"lib/array_bsearch.rb",
|
29
|
+
"lib/suggester.rb",
|
30
|
+
"lib/suggester/client.rb",
|
31
|
+
"lib/suggester/handlers.rb",
|
32
|
+
"lib/suggester/handlers/active_record.rb",
|
33
|
+
"lib/suggester/handlers/base.rb",
|
34
|
+
"lib/suggester/handlers/helpers/normalization.rb",
|
35
|
+
"lib/suggester/handlers/helpers/refresh.rb",
|
36
|
+
"lib/suggester/handlers/marshal.rb",
|
37
|
+
"lib/suggester/handlers/yaml.rb",
|
38
|
+
"lib/suggester/server.rb",
|
39
|
+
"pkg/suggester-0.0.1.gem",
|
40
|
+
"pkg/suggester-0.0.2.gem",
|
41
|
+
"suggester.gemspec",
|
42
|
+
"test/fixtures/books.sql",
|
43
|
+
"test/fixtures/marshal.marshal",
|
44
|
+
"test/fixtures/yaml.yml",
|
45
|
+
"test/functional/basic_test.rb",
|
46
|
+
"test/test_helper.rb",
|
47
|
+
"test/unit/active_record_handler_test.rb",
|
48
|
+
"test/unit/marshal_handler_test.rb",
|
49
|
+
"test/unit/yaml_handler_test.rb"
|
50
|
+
]
|
51
|
+
s.homepage = %q{http://github.com/chingor13/suggester}
|
52
|
+
s.licenses = ["MIT"]
|
53
|
+
s.require_paths = ["lib"]
|
54
|
+
s.rubygems_version = %q{1.3.7}
|
55
|
+
s.summary = %q{Extensible, cache-based auto-suggest server for ruby.}
|
56
|
+
s.test_files = [
|
57
|
+
"test/functional/basic_test.rb",
|
58
|
+
"test/test_helper.rb",
|
59
|
+
"test/unit/active_record_handler_test.rb",
|
60
|
+
"test/unit/marshal_handler_test.rb",
|
61
|
+
"test/unit/yaml_handler_test.rb"
|
62
|
+
]
|
63
|
+
|
64
|
+
if s.respond_to? :specification_version then
|
65
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
66
|
+
s.specification_version = 3
|
67
|
+
|
68
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
69
|
+
s.add_runtime_dependency(%q<sinatra>, [">= 0"])
|
70
|
+
s.add_runtime_dependency(%q<json>, [">= 0"])
|
71
|
+
s.add_runtime_dependency(%q<vegas>, [">= 0"])
|
72
|
+
s.add_development_dependency(%q<bundler>, ["~> 1.0.0"])
|
73
|
+
s.add_development_dependency(%q<jeweler>, ["~> 1.5.1"])
|
74
|
+
s.add_development_dependency(%q<rcov>, [">= 0"])
|
75
|
+
else
|
76
|
+
s.add_dependency(%q<sinatra>, [">= 0"])
|
77
|
+
s.add_dependency(%q<json>, [">= 0"])
|
78
|
+
s.add_dependency(%q<vegas>, [">= 0"])
|
79
|
+
s.add_dependency(%q<bundler>, ["~> 1.0.0"])
|
80
|
+
s.add_dependency(%q<jeweler>, ["~> 1.5.1"])
|
81
|
+
s.add_dependency(%q<rcov>, [">= 0"])
|
82
|
+
end
|
83
|
+
else
|
84
|
+
s.add_dependency(%q<sinatra>, [">= 0"])
|
85
|
+
s.add_dependency(%q<json>, [">= 0"])
|
86
|
+
s.add_dependency(%q<vegas>, [">= 0"])
|
87
|
+
s.add_dependency(%q<bundler>, ["~> 1.0.0"])
|
88
|
+
s.add_dependency(%q<jeweler>, ["~> 1.5.1"])
|
89
|
+
s.add_dependency(%q<rcov>, [">= 0"])
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|