suggester 0.1.0

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