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.
@@ -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,3 @@
1
+ module Suggester::Handlers::Helpers::Normalization
2
+
3
+ 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
@@ -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
+