silver 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.rspec +1 -0
- data/Gemfile +18 -0
- data/Gemfile.lock +51 -0
- data/LICENSE.txt +20 -0
- data/README.markdown +92 -0
- data/Rakefile +50 -0
- data/VERSION +1 -0
- data/lib/silver/cache.rb +152 -0
- data/lib/silver/common_words.rb +1 -0
- data/lib/silver/indexer.rb +126 -0
- data/lib/silver/search.rb +71 -0
- data/lib/silver.rb +7 -0
- data/silver.gemspec +102 -0
- data/spec/silver_spec.rb +263 -0
- data/spec/spec_helper.rb +13 -0
- data/spec/support/db.rb +29 -0
- data/spec/support/spec.db +0 -0
- metadata +308 -0
data/.document
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/Gemfile
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
source "http://rubygems.org"
|
2
|
+
# Add dependencies required to use your gem here.
|
3
|
+
# Example:
|
4
|
+
# gem "activesupport", ">= 2.3.5"
|
5
|
+
|
6
|
+
# Add dependencies to develop your gem here.
|
7
|
+
# Include everything needed to run rake, tests, features, etc.
|
8
|
+
group :development do
|
9
|
+
gem "rspec", "~> 2.3.0"
|
10
|
+
gem "bundler", "~> 1.0.0"
|
11
|
+
gem "jeweler", "~> 1.5.2"
|
12
|
+
gem "rcov", ">= 0"
|
13
|
+
gem "redis", "~> 2.1.1"
|
14
|
+
gem "yajl-ruby", ">= 0.7.7"
|
15
|
+
gem "text", "~> 0.2.0"
|
16
|
+
gem "dm-core", "~> 1.0.0"
|
17
|
+
gem "dm-sqlite-adapter", "~> 1.0.0"
|
18
|
+
end
|
data/Gemfile.lock
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
GEM
|
2
|
+
remote: http://rubygems.org/
|
3
|
+
specs:
|
4
|
+
addressable (2.2.3)
|
5
|
+
data_objects (0.10.2)
|
6
|
+
addressable (~> 2.1)
|
7
|
+
diff-lcs (1.1.2)
|
8
|
+
dm-core (1.0.2)
|
9
|
+
addressable (~> 2.2)
|
10
|
+
extlib (~> 0.9.15)
|
11
|
+
dm-do-adapter (1.0.2)
|
12
|
+
data_objects (~> 0.10.2)
|
13
|
+
dm-core (~> 1.0.2)
|
14
|
+
dm-sqlite-adapter (1.0.2)
|
15
|
+
dm-do-adapter (~> 1.0.2)
|
16
|
+
do_sqlite3 (~> 0.10.2)
|
17
|
+
do_sqlite3 (0.10.2)
|
18
|
+
data_objects (= 0.10.2)
|
19
|
+
extlib (0.9.15)
|
20
|
+
git (1.2.5)
|
21
|
+
jeweler (1.5.2)
|
22
|
+
bundler (~> 1.0.0)
|
23
|
+
git (>= 1.2.5)
|
24
|
+
rake
|
25
|
+
rake (0.8.7)
|
26
|
+
rcov (0.9.9)
|
27
|
+
redis (2.1.1)
|
28
|
+
rspec (2.3.0)
|
29
|
+
rspec-core (~> 2.3.0)
|
30
|
+
rspec-expectations (~> 2.3.0)
|
31
|
+
rspec-mocks (~> 2.3.0)
|
32
|
+
rspec-core (2.3.1)
|
33
|
+
rspec-expectations (2.3.0)
|
34
|
+
diff-lcs (~> 1.1.2)
|
35
|
+
rspec-mocks (2.3.0)
|
36
|
+
text (0.2.0)
|
37
|
+
yajl-ruby (0.7.9)
|
38
|
+
|
39
|
+
PLATFORMS
|
40
|
+
ruby
|
41
|
+
|
42
|
+
DEPENDENCIES
|
43
|
+
bundler (~> 1.0.0)
|
44
|
+
dm-core (~> 1.0.0)
|
45
|
+
dm-sqlite-adapter (~> 1.0.0)
|
46
|
+
jeweler (~> 1.5.2)
|
47
|
+
rcov
|
48
|
+
redis (~> 2.1.1)
|
49
|
+
rspec (~> 2.3.0)
|
50
|
+
text (~> 0.2.0)
|
51
|
+
yajl-ruby (>= 0.7.7)
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2011 Erik Hin-tone
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.markdown
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
# Silver
|
2
|
+
|
3
|
+
Silver is a lightweight, redis-backed database cacher and indexer.
|
4
|
+
|
5
|
+
## Getting Started
|
6
|
+
|
7
|
+
As it says on the tin, Silver is going to make your database queries much faster. Now it is no secret that Redis is fantastic to use as a cache/index. However, you have to write the same boilerplate to use it as cache over and over: find most recent cached entry, look for newer entries in the database, cache it to redis, combine with old results. Rinse, repeat.
|
8
|
+
The goal of Silver is so that you never have to do that again. Rather than connecting to the database/service for you, you simply wrap your calls in Silver and it does the rest. This means you can use silver to speed up calls to databases, calls APIs, CURLS, really whatever you want.
|
9
|
+
|
10
|
+
### A Simple Caching Example
|
11
|
+
|
12
|
+
First make sure you have Silver installed.
|
13
|
+
gem install silver
|
14
|
+
Now, let's pretend you have an app that queries your database for entries frequently. Entries are added frequently. Furthermore, you only want Entries that come from a specific blog, blog #12. Also you want to grab something from an association of the Entry row in the database. Let's say the author's name.
|
15
|
+
First, instantiate a new cache object.
|
16
|
+
cache = Silver::Cache.new("12_entries","created_time") do |date|
|
17
|
+
Entry.all(:order => :created_time.desc, :created_time.gt => date, :blog_id => 12)
|
18
|
+
end
|
19
|
+
The first paramater passed to the constructor is the name you want to give to this cache in Redis as Silver allows to creates as many caches for different queries as you would like. The second paramater is the name of the field that you will be using to determine if there are new entries. Finally, you pass the constructor a block that will receive the date of the newest cached entry from Redis. You must return the entries in reverse chronological order for Silver to be able to keep them in order. Silver will then query the database/service for newer entries when the instance's find method is called.
|
20
|
+
results = cache.find do |entry|
|
21
|
+
attrs = entry.attributes
|
22
|
+
author = {:author_name => entry.author[:name]}
|
23
|
+
attrs.merge author
|
24
|
+
end
|
25
|
+
The find method of a cache instance takes a block that will be called for every new entry. The results of the block call should be a hash that will be stored in the cache. The whole thing will be converted into JSON and stashed in the Redis cache. From now on the database will never have to be hit again to return this value. The find method returns an array of all the results old and new from the Redis cache.
|
26
|
+
If you just want to read from the cache without hitting the database, simply call find without a block and with a single param: false
|
27
|
+
results = cache.find(false)
|
28
|
+
Currently, the cache does not support the changing of cached entries and is, thus, intended for data that is unlikely to change once it has been written to the database. This feature will be included in future releases of Silver.
|
29
|
+
Finally, Silver provides a cull method.
|
30
|
+
cache.cull(30)
|
31
|
+
This will cut the Redis cache down to the 30 most recent items.
|
32
|
+
|
33
|
+
### A Simple Indexing Example
|
34
|
+
|
35
|
+
However, Silver is not just a simple cache. It can also be used to index a database. It is optimized to index based on short text, such as names, captions, tag lists, excerpts, tweets etc. There is nothing stopping you from using on longer fields such as body text except the size of your memory alloted to Redis. Silver uses a stupidly simple fuzzy text search. The search will likely be augmented in the future.
|
36
|
+
Here's how you would index a mess of photos by their captions, falling back on their filename if no caption is given.
|
37
|
+
First, instantiate a new index object.
|
38
|
+
index = Silver::Index.new("blog_pictures","created_time") do |date|
|
39
|
+
Picture.all(:order => :created_time.desc, :created_time.gt => date)
|
40
|
+
end
|
41
|
+
This is the same deal as before with Silver::Cache: redis key name, time field, ordering block.
|
42
|
+
Next, call the find_and_update method of the instance.
|
43
|
+
index.find_and_update do |result|
|
44
|
+
output = result.label || result.filename || ""
|
45
|
+
id = result.id
|
46
|
+
[id,output]
|
47
|
+
end
|
48
|
+
Find_and_update takes a block that will be called for each db-fetched result. This block should return a two item array of the row's id, first, and the value we are using for indexing second. As you can see in the example, Silver allows you to mix fields to use to index. It let's you do anything you want actually as long as an id and a corresponding value are returned. After calling find_and_update, your database is indexed and ready to be searched. Say, we wanted to search for photos of "Barack Obama":
|
49
|
+
search = Silver::Search.new("Barack Obama","blog_pictures")
|
50
|
+
The constructor takes a string to search for and the name of Redis key storing the index. To actually perform the search:
|
51
|
+
search.perform{|id| Picture.get(id)}
|
52
|
+
The perform method takes a block that will be passed the ids of all the id's whose indexes match the query. Perform will return an array of database/service objects for you to then interact with as you please.
|
53
|
+
|
54
|
+
### A note about the shortcoming of the search.
|
55
|
+
|
56
|
+
As Silver is currently in beta, it's search could use some work (feel free to contribute code). Currently, it does a fuzzy search based on the double-metaphone of each word in the search and then interects the results. This means several things. First, a search for "barack Obama" will only return those entries indexed with "Barack Obama"(case-insensitive) in their indexed field. It will excludes those with just, say, "obama". Second, no levensteining is done yet. It should be.
|
57
|
+
|
58
|
+
### Non-standard configurations
|
59
|
+
|
60
|
+
Every initializer in Silver takes, in addition to the parameters shown above, an optional options hash for Redis as the third parameter.
|
61
|
+
cache = Silver::Cache.new("12_entries","created_time",{:host => "127.0.0.1",:port => "6969"})
|
62
|
+
Also, the search initializer for Silver's indexing takes optional number and offset paramater for pagination. Default is no offest and 30 results returned.
|
63
|
+
search = Silver::Search.new("Barack Obama","blog_pictures",{:host => "127.0.0.1",:port => "6969"},50,10)
|
64
|
+
This will return results 10-60.
|
65
|
+
|
66
|
+
### Rocco Annotated Source
|
67
|
+
|
68
|
+
* [Cacher](http://tpm.github.com/Silver/cache.html)
|
69
|
+
* [Indexer](http://tpm.github.com/Silver/indexer.html)
|
70
|
+
* [Searcher](http://tpm.github.com/Silver/search.html)
|
71
|
+
|
72
|
+
## Improvements to be made
|
73
|
+
|
74
|
+
* Better Search
|
75
|
+
* Allow for changes to be made in the database for already cached values. (Make an uncache function that fetches that ID again and replaces it in the cache)
|
76
|
+
* Etc.
|
77
|
+
|
78
|
+
## Contributing to silver
|
79
|
+
|
80
|
+
* Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
|
81
|
+
* Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
|
82
|
+
* Fork the project
|
83
|
+
* Start a feature/bugfix branch
|
84
|
+
* Commit and push until you are happy with your contribution
|
85
|
+
* Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
|
86
|
+
* Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
|
87
|
+
|
88
|
+
## Copyright
|
89
|
+
|
90
|
+
Copyright (c) 2011 Erik Hin-tone. See LICENSE.txt for
|
91
|
+
further details.
|
92
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler'
|
3
|
+
begin
|
4
|
+
Bundler.setup(:default, :development)
|
5
|
+
rescue Bundler::BundlerError => e
|
6
|
+
$stderr.puts e.message
|
7
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
8
|
+
exit e.status_code
|
9
|
+
end
|
10
|
+
require 'rake'
|
11
|
+
|
12
|
+
require 'jeweler'
|
13
|
+
Jeweler::Tasks.new do |gem|
|
14
|
+
gem.name = "silver"
|
15
|
+
gem.homepage = "http://github.com/tpm/silver"
|
16
|
+
gem.license = "MIT"
|
17
|
+
gem.summary = %Q{Makes your queries faster with the power of Redis.}
|
18
|
+
gem.description = %Q{A lightweight, Redis-backed cacher and indexer for databases, REST API's, really anything you can query.}
|
19
|
+
gem.email = "hinton.erik@gmail.com"
|
20
|
+
gem.authors = ["Erik Hin-tone"]
|
21
|
+
gem.add_runtime_dependency "redis", "~> 2.1.1"
|
22
|
+
gem.add_runtime_dependency "yajl-ruby", ">= 0.7.7"
|
23
|
+
gem.add_runtime_dependency "text", "~> 0.2.0"
|
24
|
+
gem.add_development_dependency "dm-core", "~> 1.0.0"
|
25
|
+
gem.add_development_dependency "dm-sqlite-adapter", "~> 1.0.0"
|
26
|
+
end
|
27
|
+
Jeweler::RubygemsDotOrgTasks.new
|
28
|
+
|
29
|
+
require 'rspec/core'
|
30
|
+
require 'rspec/core/rake_task'
|
31
|
+
RSpec::Core::RakeTask.new(:spec) do |spec|
|
32
|
+
spec.pattern = FileList['spec/**/*_spec.rb']
|
33
|
+
end
|
34
|
+
|
35
|
+
RSpec::Core::RakeTask.new(:rcov) do |spec|
|
36
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
37
|
+
spec.rcov = true
|
38
|
+
end
|
39
|
+
|
40
|
+
task :default => :spec
|
41
|
+
|
42
|
+
require 'rake/rdoctask'
|
43
|
+
Rake::RDocTask.new do |rdoc|
|
44
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
45
|
+
|
46
|
+
rdoc.rdoc_dir = 'rdoc'
|
47
|
+
rdoc.title = "silver #{version}"
|
48
|
+
rdoc.rdoc_files.include('README*')
|
49
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
50
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.2.0
|
data/lib/silver/cache.rb
ADDED
@@ -0,0 +1,152 @@
|
|
1
|
+
#### Hash extensions
|
2
|
+
|
3
|
+
# Creates a new class, BareHash, that is alike a Hash in every way except that
|
4
|
+
# it may be accessed by a symbol or a string for every key. Really the same thing
|
5
|
+
# as HashWithIndifferentAccess but without ActiveSupport
|
6
|
+
|
7
|
+
class BareHash < Hash
|
8
|
+
|
9
|
+
def [](key)
|
10
|
+
|
11
|
+
if self.include? key
|
12
|
+
self.fetch(key)
|
13
|
+
else
|
14
|
+
key.class == String ? self.fetch(key.to_sym,nil) : self.fetch(key.to_s, nil)
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
|
21
|
+
# Monkey patches Hash to allow for conversion to a BareHash where all values are strings. This
|
22
|
+
# is good for mixing results with Redis results which are always stored as strings.
|
23
|
+
|
24
|
+
class Hash
|
25
|
+
|
26
|
+
def to_bare
|
27
|
+
|
28
|
+
bhash = BareHash.new
|
29
|
+
self.each{|k,v| bhash[k] = v}
|
30
|
+
bhash
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
#### Caching
|
37
|
+
|
38
|
+
module Silver
|
39
|
+
|
40
|
+
class Cache
|
41
|
+
|
42
|
+
attr_reader :key, :time_field, :query
|
43
|
+
|
44
|
+
# Creates a new cached search object.
|
45
|
+
#
|
46
|
+
# Silver does not connect to a database, the query is passed as a block. This means you can use
|
47
|
+
# Silver to cache databases, REST APIs, or anything else that can be queried.
|
48
|
+
#
|
49
|
+
# key is string to identify this and successive queries to Redis
|
50
|
+
# time_field is the name of a field used to determine whether or not there are new entries that
|
51
|
+
# are not yet cached.
|
52
|
+
# query is a block that take a date and queries the database for all entries after that date. The results
|
53
|
+
# must be returned in descending order.
|
54
|
+
#
|
55
|
+
# Example to prepare a cache and query for the database of new stories in blog #2:
|
56
|
+
#
|
57
|
+
# cache = Silver::Cache.new("news_stories",
|
58
|
+
# "created_time") do |date|
|
59
|
+
#
|
60
|
+
# Stories.all(:order => :created_time.desc,
|
61
|
+
# :created_time.gt => date
|
62
|
+
# :blog_id => 2)
|
63
|
+
#
|
64
|
+
# end
|
65
|
+
|
66
|
+
|
67
|
+
def initialize(key,time_field,redis_options={},&query)
|
68
|
+
|
69
|
+
@key = key
|
70
|
+
@time_field = time_field
|
71
|
+
@query = query
|
72
|
+
@r = Redis.new(redis_options)
|
73
|
+
@r.select 12
|
74
|
+
|
75
|
+
end
|
76
|
+
|
77
|
+
# Queries Redis, returns new entries and inserts them into Redis.
|
78
|
+
#
|
79
|
+
# update is an optional parameter that, if false, will just return cached results and not look for new ones.
|
80
|
+
# callback is block that gets called for every new results, receives the result
|
81
|
+
# and returns the hash to be cached. This can used to query associations.
|
82
|
+
#
|
83
|
+
# Example to cache and the query the database and include any categories the entry might have:
|
84
|
+
#
|
85
|
+
# cache.find do |entry|
|
86
|
+
# attrs = entry.attributes
|
87
|
+
# cats = {:categories => entry.categories}
|
88
|
+
# attrs.merge cats
|
89
|
+
# end
|
90
|
+
|
91
|
+
def find(update=true,&callback)
|
92
|
+
|
93
|
+
old_results = @r.lrange(@key,0,-1).map{|q| JSON.parse(q)}
|
94
|
+
|
95
|
+
if update
|
96
|
+
last_date = @r.get("#{@key}:last") || "1970-01-01"
|
97
|
+
new_results = @query.call(DateTime.parse(last_date))
|
98
|
+
results = new_results.map do |result|
|
99
|
+
callback.call(result)
|
100
|
+
end
|
101
|
+
|
102
|
+
if results.empty?
|
103
|
+
final_results = old_results
|
104
|
+
else
|
105
|
+
write_new(results)
|
106
|
+
|
107
|
+
# Why do we go back to Redis here instead of just merging old and new? Because it's faster and cleaner than
|
108
|
+
# selectively determining which types are changed by the to_json (like Dates) and which are preservered (like
|
109
|
+
# Hashes).
|
110
|
+
|
111
|
+
final_results = @r.lrange(@key,0,-1).map{|q| JSON.parse(q)}
|
112
|
+
end
|
113
|
+
else
|
114
|
+
final_results = old_results
|
115
|
+
end
|
116
|
+
|
117
|
+
final_results = final_results.map do |result|
|
118
|
+
result.to_bare
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
# A helper method to keep the redis list at a reasonable size.
|
123
|
+
#
|
124
|
+
# length is the number of entries to reduce the redis to
|
125
|
+
|
126
|
+
def cull(length)
|
127
|
+
|
128
|
+
@r.ltrim(@key,0,length-1)
|
129
|
+
|
130
|
+
end
|
131
|
+
|
132
|
+
private
|
133
|
+
|
134
|
+
# Writes the results to redis by pushing them in reverse order on the head
|
135
|
+
# of the redis list. This ensures that the first result will always be the newest.
|
136
|
+
# Also turns every result hash into JSON before writing because Redis is string based.
|
137
|
+
# Find will automatically parse these JSON strings upon retrieval.
|
138
|
+
|
139
|
+
def write_new(results)
|
140
|
+
|
141
|
+
new_date = results[0][@time_field].to_s
|
142
|
+
@r.set("#{@key}:last",new_date)
|
143
|
+
|
144
|
+
results.reverse.each do |result|
|
145
|
+
@r.lpush(@key,result.to_json)
|
146
|
+
end
|
147
|
+
|
148
|
+
end
|
149
|
+
|
150
|
+
end
|
151
|
+
|
152
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
COMMON_WORDS = %w[about, after, again, air, all, along, also, an, and, another, any, are, around, as, at, away, back, be, because, been, before, below, between, both, but, by, came, can, come, could, day, did, different, do, does, don't, down, each, end, even, every, few, find, first, for, found, from, get, give, go, good, great, had, has, have, he, help, her, here, him, his, home, house, how, I, if, in, into, is, it, its, just, know, large, last, left, like, line, little, long, look, made, make, man, many, may, me, men, might, more, most, Mr., must, my, name, never, new, next, no, not, now, number, of, off, old, on, one, only, or, other, our, out, over, own, part, people, place, put, read, right, said, same, saw, say, see, she, should, show, small, so, some, something, sound, still, such, take, tell, than, that, the, them, then, there, these, they, thing, think, this, those, thought, three, through, time, to, together, too, two, under, up, us, use, very, want, water, way, we, well, went, were, what, when, where, which, while, who, why, will, with, word, work, world, would, write, year, you, your, was]
|
@@ -0,0 +1,126 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/common_words.rb')
|
2
|
+
|
3
|
+
#### Boring new error
|
4
|
+
|
5
|
+
# Just makes a new sublcass of RuntimeError so we can raise database
|
6
|
+
# errors later
|
7
|
+
|
8
|
+
class AttrError < RuntimeError
|
9
|
+
end
|
10
|
+
|
11
|
+
#### Indexing
|
12
|
+
|
13
|
+
module Silver
|
14
|
+
|
15
|
+
class Index
|
16
|
+
|
17
|
+
# Create a new index for some given database or web service. Silver
|
18
|
+
# doesn't do any connecting to a database, it requires you to pass
|
19
|
+
# a query to it as a block. Use whatever ORM you like best.
|
20
|
+
#
|
21
|
+
# key is the name this index will be stored with in Redis.
|
22
|
+
# time_field is the name of the field used for determining whether or not
|
23
|
+
# there are new entries.
|
24
|
+
# query is a block that takes the date of the most recently indexed item
|
25
|
+
# as a parameter. Query should always sort entries in descending order.
|
26
|
+
#
|
27
|
+
# Example to index all large pictures based on their captions:
|
28
|
+
#
|
29
|
+
# index = Silver::Index.new("picturecaptions",
|
30
|
+
# "created_time") do |time|
|
31
|
+
#
|
32
|
+
# Pictures.all(:order => :created_time.desc,
|
33
|
+
# :created_time.gt => time,
|
34
|
+
# :size => "large")
|
35
|
+
# end
|
36
|
+
|
37
|
+
def initialize(key,time_field,redis_options={},&query)
|
38
|
+
@r = Redis.new(redis_options)
|
39
|
+
@r.select 12
|
40
|
+
@key = key
|
41
|
+
@time_field = time_field
|
42
|
+
@query = query
|
43
|
+
@new_results = []
|
44
|
+
@indexed_items = []
|
45
|
+
end
|
46
|
+
|
47
|
+
# Looks for the most recent date indexed and only fetch
|
48
|
+
# entries newer than that date from the database.
|
49
|
+
#
|
50
|
+
# Takes a block that takes a results from the database and returns the value by which to index.
|
51
|
+
# accessor should be a block that takes an individual results and returns and array containing, first,
|
52
|
+
# the item's id and, second, the value by which to index.
|
53
|
+
#
|
54
|
+
# Ex:
|
55
|
+
#
|
56
|
+
# index.find_and_update do |result|
|
57
|
+
# output = result.caption || result.label || ""
|
58
|
+
# id = result.id
|
59
|
+
# [id,output]
|
60
|
+
# end
|
61
|
+
|
62
|
+
def find_and_update(&accessor)
|
63
|
+
|
64
|
+
last_date = @r.get("#{@key}:last") || "Jan. 1, 1970"
|
65
|
+
new_results = @query.call(DateTime.parse(last_date))
|
66
|
+
if new_results.empty?
|
67
|
+
false
|
68
|
+
else
|
69
|
+
new_date = new_results[0][@time_field].to_s
|
70
|
+
@r.set("#{@key}:last",new_date)
|
71
|
+
@new_results = new_results
|
72
|
+
parse(accessor)
|
73
|
+
true
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
# Sends results off to a double metaphone method to allow for fuzzy searching.
|
81
|
+
|
82
|
+
def parse(accessor)
|
83
|
+
|
84
|
+
@new_results.each do |result|
|
85
|
+
begin
|
86
|
+
value = accessor.call(result)
|
87
|
+
time = Time.parse(result[@time_field].to_s).to_i
|
88
|
+
raise AttrError, "Specified attribute not found in item #{value[0]}" if !value
|
89
|
+
morphed_words = morph(value[1])
|
90
|
+
morphed_words.each{|word| write(word,value[0],time)}
|
91
|
+
rescue AttrError => e
|
92
|
+
puts e
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
98
|
+
|
99
|
+
# Takes a metaphoned string, the id of the row that contains it, and the time created/modified of that row
|
100
|
+
# and writes to the database to phonemes by which that row is now indexed.
|
101
|
+
|
102
|
+
def write(word,id,time)
|
103
|
+
|
104
|
+
text = word[0]
|
105
|
+
phonemes = word[1].compact
|
106
|
+
phonemes.each do |phoneme|
|
107
|
+
@r.zadd "#{@key}:#{phoneme}", time, id
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# Note: this method needs some work/ is not perfect.
|
112
|
+
# Takes a string, removes any file extension, splits it on non-alpha characters, throws out empty words
|
113
|
+
# words smaller than 5 characters that are not capitalized and all common words. Finally it converts each
|
114
|
+
# word to its metaphoned equivalents and returns an array of the original words in its metaphones.
|
115
|
+
|
116
|
+
def morph(string)
|
117
|
+
string.gsub!(/\.[a-zA-Z]{1,4}$/,"")
|
118
|
+
words = string.split(/[^a-zA-Z]/).reject{|q| q == "" || (q.length < 5 && q.capitalize != q)} - COMMON_WORDS
|
119
|
+
morphed_words = words.map{|q| [q,Text::Metaphone.double_metaphone(q)]}
|
120
|
+
morphed_words
|
121
|
+
end
|
122
|
+
|
123
|
+
end
|
124
|
+
|
125
|
+
end
|
126
|
+
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module Silver
|
2
|
+
|
3
|
+
#### Search
|
4
|
+
|
5
|
+
# Searches an indexed database. What else would it do?
|
6
|
+
|
7
|
+
class Search
|
8
|
+
|
9
|
+
# Takes a query and a redis key from a previous indexing. There is an optional offset
|
10
|
+
# that can be used to paginate. It defaults to returning 30 results.
|
11
|
+
#
|
12
|
+
# Example, query picture captions for "Barack Obama":
|
13
|
+
# search = Silver::Search.new("barack obama","picturecaption")
|
14
|
+
|
15
|
+
def initialize(query,key,redis_options={},number=30,offset=0)
|
16
|
+
@query = query
|
17
|
+
@key = key
|
18
|
+
@number = number
|
19
|
+
@offset = offset
|
20
|
+
@r = Redis.new(redis_options)
|
21
|
+
@r.select 12
|
22
|
+
end
|
23
|
+
|
24
|
+
# Send a query to be metaphoned, finds the matching ids for the query and then
|
25
|
+
# returns the results. Finally it only returns entries that are shared by both words.
|
26
|
+
#
|
27
|
+
# Takes a block that takes an id and then queries the database for that row. Again, Silver
|
28
|
+
# can be used for services that "row" is a bad metaphor like REST apis. However, it is easy
|
29
|
+
# to write.
|
30
|
+
#
|
31
|
+
# Ex:
|
32
|
+
# search.perform{|id| Picture.get(id) }
|
33
|
+
|
34
|
+
def perform(&accessor)
|
35
|
+
morphed_words = morph_words
|
36
|
+
morphed_words.map! do |word|
|
37
|
+
phones = word[1]
|
38
|
+
phones = self.find_matching_phones(phones)
|
39
|
+
phones
|
40
|
+
end
|
41
|
+
results = morphed_words.reduce{|memo,obj| memo & obj}.slice(@offset,@offset+@number)
|
42
|
+
results.map{|result| accessor.call(result)}
|
43
|
+
end
|
44
|
+
|
45
|
+
# Takes the instance's query, splits it into words, metaphones each word and returns the array of metaphoned words.
|
46
|
+
|
47
|
+
def morph_words
|
48
|
+
words = @query.split(/[^a-zA-Z0-9]/)
|
49
|
+
morphed_words = words.map{|word| [word,Text::Metaphone.double_metaphone(word)]}
|
50
|
+
morphed_words
|
51
|
+
end
|
52
|
+
|
53
|
+
# Takes an array of metaphones and returns the matching keys in the index. Since we are using double metaphone,
|
54
|
+
# it unions the results for the two possible metaphones.i
|
55
|
+
|
56
|
+
def find_matching_phones(phones)
|
57
|
+
phones.map! do |phone|
|
58
|
+
if phone
|
59
|
+
"#{@key}:#{phone}"
|
60
|
+
else
|
61
|
+
nil
|
62
|
+
end
|
63
|
+
end
|
64
|
+
phones = @r.zunionstore "temp", phones.compact
|
65
|
+
phones = @r.zrevrange "temp", 0, -1
|
66
|
+
phones
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
data/lib/silver.rb
ADDED
data/silver.gemspec
ADDED
@@ -0,0 +1,102 @@
|
|
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{silver}
|
8
|
+
s.version = "0.2.0"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Erik Hin-tone"]
|
12
|
+
s.date = %q{2011-01-31}
|
13
|
+
s.description = %q{A lightweight, Redis-backed cacher and indexer for databases, REST API's, really anything you can query.}
|
14
|
+
s.email = %q{hinton.erik@gmail.com}
|
15
|
+
s.extra_rdoc_files = [
|
16
|
+
"LICENSE.txt",
|
17
|
+
"README.markdown"
|
18
|
+
]
|
19
|
+
s.files = [
|
20
|
+
".document",
|
21
|
+
".rspec",
|
22
|
+
"Gemfile",
|
23
|
+
"Gemfile.lock",
|
24
|
+
"LICENSE.txt",
|
25
|
+
"README.markdown",
|
26
|
+
"Rakefile",
|
27
|
+
"VERSION",
|
28
|
+
"lib/silver.rb",
|
29
|
+
"lib/silver/cache.rb",
|
30
|
+
"lib/silver/common_words.rb",
|
31
|
+
"lib/silver/indexer.rb",
|
32
|
+
"lib/silver/search.rb",
|
33
|
+
"silver.gemspec",
|
34
|
+
"spec/silver_spec.rb",
|
35
|
+
"spec/spec_helper.rb",
|
36
|
+
"spec/support/db.rb",
|
37
|
+
"spec/support/spec.db"
|
38
|
+
]
|
39
|
+
s.homepage = %q{http://github.com/tpm/silver}
|
40
|
+
s.licenses = ["MIT"]
|
41
|
+
s.require_paths = ["lib"]
|
42
|
+
s.rubygems_version = %q{1.3.7}
|
43
|
+
s.summary = %q{Makes your queries faster with the power of Redis.}
|
44
|
+
s.test_files = [
|
45
|
+
"spec/silver_spec.rb",
|
46
|
+
"spec/spec_helper.rb",
|
47
|
+
"spec/support/db.rb"
|
48
|
+
]
|
49
|
+
|
50
|
+
if s.respond_to? :specification_version then
|
51
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
52
|
+
s.specification_version = 3
|
53
|
+
|
54
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
55
|
+
s.add_development_dependency(%q<rspec>, ["~> 2.3.0"])
|
56
|
+
s.add_development_dependency(%q<bundler>, ["~> 1.0.0"])
|
57
|
+
s.add_development_dependency(%q<jeweler>, ["~> 1.5.2"])
|
58
|
+
s.add_development_dependency(%q<rcov>, [">= 0"])
|
59
|
+
s.add_development_dependency(%q<redis>, ["~> 2.1.1"])
|
60
|
+
s.add_development_dependency(%q<yajl-ruby>, [">= 0.7.7"])
|
61
|
+
s.add_development_dependency(%q<text>, ["~> 0.2.0"])
|
62
|
+
s.add_development_dependency(%q<dm-core>, ["~> 1.0.0"])
|
63
|
+
s.add_development_dependency(%q<dm-sqlite-adapter>, ["~> 1.0.0"])
|
64
|
+
s.add_runtime_dependency(%q<redis>, ["~> 2.1.1"])
|
65
|
+
s.add_runtime_dependency(%q<yajl-ruby>, [">= 0.7.7"])
|
66
|
+
s.add_runtime_dependency(%q<text>, ["~> 0.2.0"])
|
67
|
+
s.add_development_dependency(%q<dm-core>, ["~> 1.0.0"])
|
68
|
+
s.add_development_dependency(%q<dm-sqlite-adapter>, ["~> 1.0.0"])
|
69
|
+
else
|
70
|
+
s.add_dependency(%q<rspec>, ["~> 2.3.0"])
|
71
|
+
s.add_dependency(%q<bundler>, ["~> 1.0.0"])
|
72
|
+
s.add_dependency(%q<jeweler>, ["~> 1.5.2"])
|
73
|
+
s.add_dependency(%q<rcov>, [">= 0"])
|
74
|
+
s.add_dependency(%q<redis>, ["~> 2.1.1"])
|
75
|
+
s.add_dependency(%q<yajl-ruby>, [">= 0.7.7"])
|
76
|
+
s.add_dependency(%q<text>, ["~> 0.2.0"])
|
77
|
+
s.add_dependency(%q<dm-core>, ["~> 1.0.0"])
|
78
|
+
s.add_dependency(%q<dm-sqlite-adapter>, ["~> 1.0.0"])
|
79
|
+
s.add_dependency(%q<redis>, ["~> 2.1.1"])
|
80
|
+
s.add_dependency(%q<yajl-ruby>, [">= 0.7.7"])
|
81
|
+
s.add_dependency(%q<text>, ["~> 0.2.0"])
|
82
|
+
s.add_dependency(%q<dm-core>, ["~> 1.0.0"])
|
83
|
+
s.add_dependency(%q<dm-sqlite-adapter>, ["~> 1.0.0"])
|
84
|
+
end
|
85
|
+
else
|
86
|
+
s.add_dependency(%q<rspec>, ["~> 2.3.0"])
|
87
|
+
s.add_dependency(%q<bundler>, ["~> 1.0.0"])
|
88
|
+
s.add_dependency(%q<jeweler>, ["~> 1.5.2"])
|
89
|
+
s.add_dependency(%q<rcov>, [">= 0"])
|
90
|
+
s.add_dependency(%q<redis>, ["~> 2.1.1"])
|
91
|
+
s.add_dependency(%q<yajl-ruby>, [">= 0.7.7"])
|
92
|
+
s.add_dependency(%q<text>, ["~> 0.2.0"])
|
93
|
+
s.add_dependency(%q<dm-core>, ["~> 1.0.0"])
|
94
|
+
s.add_dependency(%q<dm-sqlite-adapter>, ["~> 1.0.0"])
|
95
|
+
s.add_dependency(%q<redis>, ["~> 2.1.1"])
|
96
|
+
s.add_dependency(%q<yajl-ruby>, [">= 0.7.7"])
|
97
|
+
s.add_dependency(%q<text>, ["~> 0.2.0"])
|
98
|
+
s.add_dependency(%q<dm-core>, ["~> 1.0.0"])
|
99
|
+
s.add_dependency(%q<dm-sqlite-adapter>, ["~> 1.0.0"])
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
data/spec/silver_spec.rb
ADDED
@@ -0,0 +1,263 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
|
+
require File.expand_path(File.dirname(__FILE__) + '/support/db.rb')
|
3
|
+
|
4
|
+
describe "indexer" do
|
5
|
+
|
6
|
+
it "correctly metaphones searches" do
|
7
|
+
|
8
|
+
search = Silver::Search.new("Barack Obama","specparents")
|
9
|
+
morph = search.morph_words
|
10
|
+
morph.should eq([["Barack",["PRK",nil]],["Obama",["APM",nil]]])
|
11
|
+
|
12
|
+
end
|
13
|
+
|
14
|
+
it "correctly metaphones items" do
|
15
|
+
|
16
|
+
index = Silver::Index.new("specparents","date") do |date|
|
17
|
+
Parent.all(:order => :date.desc, :date.gt => date)
|
18
|
+
end
|
19
|
+
morph = index.send("morph","barack-obama.JpEg")
|
20
|
+
morph.should eq([["barack",["PRK",nil]],["obama",["APM",nil]]])
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
it "indexes the database" do
|
25
|
+
r = Redis.new
|
26
|
+
r.select 12
|
27
|
+
keys = r.keys("specparents:*")
|
28
|
+
keys.each{|key| r.del key}
|
29
|
+
r.keys("specparents:*").should eq([])
|
30
|
+
|
31
|
+
index = Silver::Index.new("specparents","date") do |date|
|
32
|
+
Parent.all(:order => :date.desc, :date.gt => date)
|
33
|
+
end
|
34
|
+
|
35
|
+
output = index.find_and_update do |result|
|
36
|
+
output = result.name
|
37
|
+
id = result.id
|
38
|
+
[id,output]
|
39
|
+
end
|
40
|
+
|
41
|
+
r.keys("specparents:*").should include("specparents:PS")
|
42
|
+
r.keys("specparents:*").should include("specparents:ARK")
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
|
47
|
+
it "returns false when there are no new updates" do
|
48
|
+
|
49
|
+
index = Silver::Index.new("specparents","date") do |date|
|
50
|
+
Parent.all(:order => :date.desc, :date.gt => date)
|
51
|
+
end
|
52
|
+
|
53
|
+
output = index.find_and_update do |result|
|
54
|
+
output = result.name
|
55
|
+
id = result.id
|
56
|
+
[id,output]
|
57
|
+
end
|
58
|
+
|
59
|
+
output.should eq(false)
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
it "searches the index" do
|
64
|
+
|
65
|
+
search = Silver::Search.new("Erik","specparents")
|
66
|
+
results = search.perform{|id| Parent.get(id)}
|
67
|
+
results[0]["name"].should eq("Erik")
|
68
|
+
results[0]["age"].should eq(24)
|
69
|
+
search = Silver::Search.new("Erik Hinton","specparents")
|
70
|
+
results = search.perform{|id| Parent.get(id)}
|
71
|
+
results[0]["name"].should eq("Erik Hinton")
|
72
|
+
results[0]["age"].should eq(2)
|
73
|
+
|
74
|
+
end
|
75
|
+
|
76
|
+
it "updates the index" do
|
77
|
+
|
78
|
+
r = Redis.new
|
79
|
+
r.select 12
|
80
|
+
|
81
|
+
index = Silver::Index.new("specparents","date") do |date|
|
82
|
+
Parent.all(:order => :date.desc, :date.gt => date)
|
83
|
+
end
|
84
|
+
|
85
|
+
p = Parent.create(:name => "Update",
|
86
|
+
:age => 1,
|
87
|
+
:date => DateTime.now)
|
88
|
+
|
89
|
+
output = index.find_and_update do |result|
|
90
|
+
output = result.name
|
91
|
+
id = result.id
|
92
|
+
[id,output]
|
93
|
+
end
|
94
|
+
|
95
|
+
output.should eq(true)
|
96
|
+
r.keys("specparents:*").should include("specparents:APTT")
|
97
|
+
p.destroy
|
98
|
+
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
102
|
+
|
103
|
+
describe "cacher" do
|
104
|
+
|
105
|
+
it "caches new additions to the database" do
|
106
|
+
r = Redis.new
|
107
|
+
r.select 12
|
108
|
+
r.del "parents"
|
109
|
+
r.del "parents:last"
|
110
|
+
|
111
|
+
cache = Silver::Cache.new("parents","date") do |date|
|
112
|
+
Parent.all(:order => :date.desc, :date.gt => date)
|
113
|
+
end
|
114
|
+
|
115
|
+
results = cache.find do |result|
|
116
|
+
result.attributes
|
117
|
+
end
|
118
|
+
results.should eq([{"name"=>"Baz", "id"=>2, "age"=>33, "date"=>"2011-01-28T10:40:45-05:00"}, {"name"=>"Erik", "id"=>1, "age"=>24, "date"=>"2011-01-28T10:34:10-05:00"}, {"name"=>"Erik Hinton", "id"=>11, "age"=>2, "date"=>"1980-01-02T00:00:00+00:00"}])
|
119
|
+
cached_results = r.lrange "parents",0,-1
|
120
|
+
cached_results.map{|result| JSON.parse(result)}.should eq([{"name"=>"Baz", "id"=>2, "age"=>33, "date"=>"2011-01-28T10:40:45-05:00"}, {"name"=>"Erik", "id"=>1, "age"=>24, "date"=>"2011-01-28T10:34:10-05:00"}, {"name"=>"Erik Hinton", "id"=>11, "age"=>2, "date"=>"1980-01-02T00:00:00+00:00"}])
|
121
|
+
end
|
122
|
+
|
123
|
+
it "caches associations" do
|
124
|
+
r = Redis.new
|
125
|
+
r.select 12
|
126
|
+
r.del "parents"
|
127
|
+
r.del "parents:last"
|
128
|
+
|
129
|
+
cache = Silver::Cache.new("parents","date") do |date|
|
130
|
+
Parent.all(:order => :date.desc, :date.gt => date)
|
131
|
+
end
|
132
|
+
|
133
|
+
results = cache.find do |parent|
|
134
|
+
children = parent.children
|
135
|
+
names = children.map{|child| child["name"]}
|
136
|
+
attrs = parent.attributes
|
137
|
+
c = {:children => names}
|
138
|
+
attrs.merge c
|
139
|
+
end
|
140
|
+
|
141
|
+
results.should eq([{"name"=>"Baz", "id"=>2, "age"=>33, "date"=>"2011-01-28T10:40:45-05:00", "children"=>["Bar"]},
|
142
|
+
{"name"=>"Erik", "id"=>1, "age"=>24, "date"=>"2011-01-28T10:34:10-05:00", "children"=>["Foo"]},
|
143
|
+
{"name"=>"Erik Hinton", "id"=>11, "age"=>2, "date"=>"1980-01-02T00:00:00+00:00", "children"=>[]}])
|
144
|
+
cached_results = r.lrange "parents",0,-1
|
145
|
+
cached_results.map{|result| JSON.parse(result)}.should eq([{"name"=>"Baz", "id"=>2, "age"=>33, "date"=>"2011-01-28T10:40:45-05:00", "children"=>["Bar"]},
|
146
|
+
{"name"=>"Erik", "id"=>1, "age"=>24, "date"=>"2011-01-28T10:34:10-05:00", "children"=>["Foo"]},
|
147
|
+
{"name"=>"Erik Hinton", "id"=>11, "age"=>2, "date"=>"1980-01-02T00:00:00+00:00", "children"=>[]}])
|
148
|
+
|
149
|
+
end
|
150
|
+
|
151
|
+
it "retrieves just old results" do
|
152
|
+
|
153
|
+
cache = Silver::Cache.new("parents","date") do |date|
|
154
|
+
Parent.all(:order => :date.desc, :date.gt => date)
|
155
|
+
end
|
156
|
+
results = cache.find(false)
|
157
|
+
results.should eq([{"name"=>"Baz", "id"=>2, "age"=>33, "date"=>"2011-01-28T10:40:45-05:00", "children"=>["Bar"]},
|
158
|
+
{"name"=>"Erik", "id"=>1, "age"=>24, "date"=>"2011-01-28T10:34:10-05:00", "children"=>["Foo"]},
|
159
|
+
{"name"=>"Erik Hinton", "id"=>11, "age"=>2, "date"=>"1980-01-02T00:00:00+00:00", "children"=>[]}])
|
160
|
+
|
161
|
+
end
|
162
|
+
|
163
|
+
end
|
164
|
+
|
165
|
+
describe "barehash" do
|
166
|
+
|
167
|
+
it "makes an indifferent hash" do
|
168
|
+
a = BareHash.new
|
169
|
+
a[:name] = "Erik"
|
170
|
+
a["age"] = 14
|
171
|
+
a["name"].should eq("Erik")
|
172
|
+
a[:age].should eq(14)
|
173
|
+
end
|
174
|
+
|
175
|
+
it "converts a hash to a barehash" do
|
176
|
+
|
177
|
+
a = {:name => "Erik", "age" => 24, "DOB" => DateTime.parse("Jan. 3, 1987")}
|
178
|
+
b = a.to_bare
|
179
|
+
b["name"].should eq("Erik")
|
180
|
+
b["age"].should eq(24)
|
181
|
+
b["DOB"].should eq(DateTime.parse("Jan. 3, 1987"))
|
182
|
+
|
183
|
+
end
|
184
|
+
|
185
|
+
end
|
186
|
+
|
187
|
+
#describe "reader" do
|
188
|
+
|
189
|
+
#it "reads the database" do
|
190
|
+
#index = Silver::Index.new("Asset","label",{:id => 15})
|
191
|
+
#items = index.read
|
192
|
+
#items[0].label.should eq("petraeus-testify-med.jpg")
|
193
|
+
#end
|
194
|
+
|
195
|
+
#end
|
196
|
+
|
197
|
+
#describe "parser" do
|
198
|
+
#it "registers the correct time" do
|
199
|
+
#index = Silver::Index.new("Asset","label",{:id => 15})
|
200
|
+
#items = index.read
|
201
|
+
#parsed_item = index.parse(items[0])
|
202
|
+
#parsed_item[0].should eq(1207685164)
|
203
|
+
#end
|
204
|
+
|
205
|
+
#it "parses the string" do
|
206
|
+
#index = Silver::Index.new("Asset","label",{:id => 15})
|
207
|
+
#items = index.read
|
208
|
+
#parsed_item = index.parse(items[0])
|
209
|
+
#parsed_item[1].should eq([["petraeus", ["PTRS", nil]], ["testify", ["TSTF", nil]], ["med", ["MT", nil]]])
|
210
|
+
#end
|
211
|
+
#end
|
212
|
+
|
213
|
+
#describe "morpher" do
|
214
|
+
#it "breaks strings into words" do
|
215
|
+
#string = "barack-obama-happy.jpg"
|
216
|
+
#parsed_string = Silver::Index.morph(string)
|
217
|
+
#parsed_string[0][0].should eq("barack")
|
218
|
+
#parsed_string[2][0].should eq("happy")
|
219
|
+
#end
|
220
|
+
#it "correctly sounds out words" do
|
221
|
+
#string = "cat-hard-tack.jpg"
|
222
|
+
#parsed_string = Silver::Index.morph(string)
|
223
|
+
#parsed_string[0][1].should eq(["KT",nil])
|
224
|
+
#parsed_string[2][1].should eq(["TK",nil])
|
225
|
+
#end
|
226
|
+
#end
|
227
|
+
|
228
|
+
#describe "writer" do
|
229
|
+
|
230
|
+
#it "writes to redis" do
|
231
|
+
#index = Silver::Index.new("Asset","caption/label",{:parent => nil, :limit => 2})
|
232
|
+
#r = Redis.new
|
233
|
+
#r.select 4
|
234
|
+
#items = index.read
|
235
|
+
#if items == []
|
236
|
+
#puts "no new items to test"
|
237
|
+
#else
|
238
|
+
#parsed_item = index.parse(items[1])
|
239
|
+
#index.process
|
240
|
+
#r.keys.should include("captionlabel:#{parsed_item[1][0][1][0]}")
|
241
|
+
#end
|
242
|
+
#end
|
243
|
+
|
244
|
+
#end
|
245
|
+
|
246
|
+
#describe "searcher" do
|
247
|
+
|
248
|
+
#it "finds matching phones" do
|
249
|
+
#search = Silver::Search.new("obama","Asset","captionlabel",0)
|
250
|
+
#results = search.find_matching_phones(["SMT","XMT"])
|
251
|
+
#results.should include("36662")
|
252
|
+
#end
|
253
|
+
|
254
|
+
#it "perform searches" do
|
255
|
+
#search = Silver::Search.new("barack obama","Asset","captionlabel",0)
|
256
|
+
#results = search.perform
|
257
|
+
#results.length.should eq(30)
|
258
|
+
#words = (results[0].caption || "")+" "+(results[0].label || "")
|
259
|
+
#results[0].class.should eq(Asset)
|
260
|
+
#words.should =~ /obama/i
|
261
|
+
#end
|
262
|
+
|
263
|
+
#end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
2
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
3
|
+
require 'rubygems'
|
4
|
+
require 'rspec'
|
5
|
+
require 'silver'
|
6
|
+
|
7
|
+
# Requires supporting files with custom matchers and macros, etc,
|
8
|
+
# in ./support/ and its subdirectories.
|
9
|
+
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
|
10
|
+
|
11
|
+
RSpec.configure do |config|
|
12
|
+
|
13
|
+
end
|
data/spec/support/db.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'dm-core'
|
3
|
+
|
4
|
+
DB = "sqlite://#{File.expand_path(File.dirname(__FILE__))}/spec.db"
|
5
|
+
|
6
|
+
DataMapper.setup(:default,DB)
|
7
|
+
DataMapper.finalize
|
8
|
+
|
9
|
+
class Parent
|
10
|
+
include DataMapper::Resource
|
11
|
+
|
12
|
+
property :id, Serial
|
13
|
+
property :name, String
|
14
|
+
property :date, DateTime
|
15
|
+
property :age, Integer
|
16
|
+
|
17
|
+
has n, :children
|
18
|
+
end
|
19
|
+
|
20
|
+
class Child
|
21
|
+
include DataMapper::Resource
|
22
|
+
|
23
|
+
property :id, Serial
|
24
|
+
property :name, String
|
25
|
+
property :wow_factor, Integer
|
26
|
+
|
27
|
+
belongs_to :parent
|
28
|
+
end
|
29
|
+
|
Binary file
|
metadata
ADDED
@@ -0,0 +1,308 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: silver
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 23
|
5
|
+
prerelease: false
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 2
|
9
|
+
- 0
|
10
|
+
version: 0.2.0
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Erik Hin-tone
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2011-01-31 00:00:00 -05:00
|
19
|
+
default_executable:
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: &id001 !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
hash: 3
|
30
|
+
segments:
|
31
|
+
- 2
|
32
|
+
- 3
|
33
|
+
- 0
|
34
|
+
version: 2.3.0
|
35
|
+
name: rspec
|
36
|
+
requirement: *id001
|
37
|
+
- !ruby/object:Gem::Dependency
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: &id002 !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ~>
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
hash: 23
|
46
|
+
segments:
|
47
|
+
- 1
|
48
|
+
- 0
|
49
|
+
- 0
|
50
|
+
version: 1.0.0
|
51
|
+
name: bundler
|
52
|
+
requirement: *id002
|
53
|
+
- !ruby/object:Gem::Dependency
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: &id003 !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
hash: 7
|
62
|
+
segments:
|
63
|
+
- 1
|
64
|
+
- 5
|
65
|
+
- 2
|
66
|
+
version: 1.5.2
|
67
|
+
name: jeweler
|
68
|
+
requirement: *id003
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
type: :development
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: &id004 !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ">="
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
hash: 3
|
78
|
+
segments:
|
79
|
+
- 0
|
80
|
+
version: "0"
|
81
|
+
name: rcov
|
82
|
+
requirement: *id004
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
type: :development
|
85
|
+
prerelease: false
|
86
|
+
version_requirements: &id005 !ruby/object:Gem::Requirement
|
87
|
+
none: false
|
88
|
+
requirements:
|
89
|
+
- - ~>
|
90
|
+
- !ruby/object:Gem::Version
|
91
|
+
hash: 9
|
92
|
+
segments:
|
93
|
+
- 2
|
94
|
+
- 1
|
95
|
+
- 1
|
96
|
+
version: 2.1.1
|
97
|
+
name: redis
|
98
|
+
requirement: *id005
|
99
|
+
- !ruby/object:Gem::Dependency
|
100
|
+
type: :development
|
101
|
+
prerelease: false
|
102
|
+
version_requirements: &id006 !ruby/object:Gem::Requirement
|
103
|
+
none: false
|
104
|
+
requirements:
|
105
|
+
- - ">="
|
106
|
+
- !ruby/object:Gem::Version
|
107
|
+
hash: 13
|
108
|
+
segments:
|
109
|
+
- 0
|
110
|
+
- 7
|
111
|
+
- 7
|
112
|
+
version: 0.7.7
|
113
|
+
name: yajl-ruby
|
114
|
+
requirement: *id006
|
115
|
+
- !ruby/object:Gem::Dependency
|
116
|
+
type: :development
|
117
|
+
prerelease: false
|
118
|
+
version_requirements: &id007 !ruby/object:Gem::Requirement
|
119
|
+
none: false
|
120
|
+
requirements:
|
121
|
+
- - ~>
|
122
|
+
- !ruby/object:Gem::Version
|
123
|
+
hash: 23
|
124
|
+
segments:
|
125
|
+
- 0
|
126
|
+
- 2
|
127
|
+
- 0
|
128
|
+
version: 0.2.0
|
129
|
+
name: text
|
130
|
+
requirement: *id007
|
131
|
+
- !ruby/object:Gem::Dependency
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: &id008 !ruby/object:Gem::Requirement
|
135
|
+
none: false
|
136
|
+
requirements:
|
137
|
+
- - ~>
|
138
|
+
- !ruby/object:Gem::Version
|
139
|
+
hash: 23
|
140
|
+
segments:
|
141
|
+
- 1
|
142
|
+
- 0
|
143
|
+
- 0
|
144
|
+
version: 1.0.0
|
145
|
+
name: dm-core
|
146
|
+
requirement: *id008
|
147
|
+
- !ruby/object:Gem::Dependency
|
148
|
+
type: :development
|
149
|
+
prerelease: false
|
150
|
+
version_requirements: &id009 !ruby/object:Gem::Requirement
|
151
|
+
none: false
|
152
|
+
requirements:
|
153
|
+
- - ~>
|
154
|
+
- !ruby/object:Gem::Version
|
155
|
+
hash: 23
|
156
|
+
segments:
|
157
|
+
- 1
|
158
|
+
- 0
|
159
|
+
- 0
|
160
|
+
version: 1.0.0
|
161
|
+
name: dm-sqlite-adapter
|
162
|
+
requirement: *id009
|
163
|
+
- !ruby/object:Gem::Dependency
|
164
|
+
type: :runtime
|
165
|
+
prerelease: false
|
166
|
+
version_requirements: &id010 !ruby/object:Gem::Requirement
|
167
|
+
none: false
|
168
|
+
requirements:
|
169
|
+
- - ~>
|
170
|
+
- !ruby/object:Gem::Version
|
171
|
+
hash: 9
|
172
|
+
segments:
|
173
|
+
- 2
|
174
|
+
- 1
|
175
|
+
- 1
|
176
|
+
version: 2.1.1
|
177
|
+
name: redis
|
178
|
+
requirement: *id010
|
179
|
+
- !ruby/object:Gem::Dependency
|
180
|
+
type: :runtime
|
181
|
+
prerelease: false
|
182
|
+
version_requirements: &id011 !ruby/object:Gem::Requirement
|
183
|
+
none: false
|
184
|
+
requirements:
|
185
|
+
- - ">="
|
186
|
+
- !ruby/object:Gem::Version
|
187
|
+
hash: 13
|
188
|
+
segments:
|
189
|
+
- 0
|
190
|
+
- 7
|
191
|
+
- 7
|
192
|
+
version: 0.7.7
|
193
|
+
name: yajl-ruby
|
194
|
+
requirement: *id011
|
195
|
+
- !ruby/object:Gem::Dependency
|
196
|
+
type: :runtime
|
197
|
+
prerelease: false
|
198
|
+
version_requirements: &id012 !ruby/object:Gem::Requirement
|
199
|
+
none: false
|
200
|
+
requirements:
|
201
|
+
- - ~>
|
202
|
+
- !ruby/object:Gem::Version
|
203
|
+
hash: 23
|
204
|
+
segments:
|
205
|
+
- 0
|
206
|
+
- 2
|
207
|
+
- 0
|
208
|
+
version: 0.2.0
|
209
|
+
name: text
|
210
|
+
requirement: *id012
|
211
|
+
- !ruby/object:Gem::Dependency
|
212
|
+
type: :development
|
213
|
+
prerelease: false
|
214
|
+
version_requirements: &id013 !ruby/object:Gem::Requirement
|
215
|
+
none: false
|
216
|
+
requirements:
|
217
|
+
- - ~>
|
218
|
+
- !ruby/object:Gem::Version
|
219
|
+
hash: 23
|
220
|
+
segments:
|
221
|
+
- 1
|
222
|
+
- 0
|
223
|
+
- 0
|
224
|
+
version: 1.0.0
|
225
|
+
name: dm-core
|
226
|
+
requirement: *id013
|
227
|
+
- !ruby/object:Gem::Dependency
|
228
|
+
type: :development
|
229
|
+
prerelease: false
|
230
|
+
version_requirements: &id014 !ruby/object:Gem::Requirement
|
231
|
+
none: false
|
232
|
+
requirements:
|
233
|
+
- - ~>
|
234
|
+
- !ruby/object:Gem::Version
|
235
|
+
hash: 23
|
236
|
+
segments:
|
237
|
+
- 1
|
238
|
+
- 0
|
239
|
+
- 0
|
240
|
+
version: 1.0.0
|
241
|
+
name: dm-sqlite-adapter
|
242
|
+
requirement: *id014
|
243
|
+
description: A lightweight, Redis-backed cacher and indexer for databases, REST API's, really anything you can query.
|
244
|
+
email: hinton.erik@gmail.com
|
245
|
+
executables: []
|
246
|
+
|
247
|
+
extensions: []
|
248
|
+
|
249
|
+
extra_rdoc_files:
|
250
|
+
- LICENSE.txt
|
251
|
+
- README.markdown
|
252
|
+
files:
|
253
|
+
- .document
|
254
|
+
- .rspec
|
255
|
+
- Gemfile
|
256
|
+
- Gemfile.lock
|
257
|
+
- LICENSE.txt
|
258
|
+
- README.markdown
|
259
|
+
- Rakefile
|
260
|
+
- VERSION
|
261
|
+
- lib/silver.rb
|
262
|
+
- lib/silver/cache.rb
|
263
|
+
- lib/silver/common_words.rb
|
264
|
+
- lib/silver/indexer.rb
|
265
|
+
- lib/silver/search.rb
|
266
|
+
- silver.gemspec
|
267
|
+
- spec/silver_spec.rb
|
268
|
+
- spec/spec_helper.rb
|
269
|
+
- spec/support/db.rb
|
270
|
+
- spec/support/spec.db
|
271
|
+
has_rdoc: true
|
272
|
+
homepage: http://github.com/tpm/silver
|
273
|
+
licenses:
|
274
|
+
- MIT
|
275
|
+
post_install_message:
|
276
|
+
rdoc_options: []
|
277
|
+
|
278
|
+
require_paths:
|
279
|
+
- lib
|
280
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
281
|
+
none: false
|
282
|
+
requirements:
|
283
|
+
- - ">="
|
284
|
+
- !ruby/object:Gem::Version
|
285
|
+
hash: 3
|
286
|
+
segments:
|
287
|
+
- 0
|
288
|
+
version: "0"
|
289
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
290
|
+
none: false
|
291
|
+
requirements:
|
292
|
+
- - ">="
|
293
|
+
- !ruby/object:Gem::Version
|
294
|
+
hash: 3
|
295
|
+
segments:
|
296
|
+
- 0
|
297
|
+
version: "0"
|
298
|
+
requirements: []
|
299
|
+
|
300
|
+
rubyforge_project:
|
301
|
+
rubygems_version: 1.3.7
|
302
|
+
signing_key:
|
303
|
+
specification_version: 3
|
304
|
+
summary: Makes your queries faster with the power of Redis.
|
305
|
+
test_files:
|
306
|
+
- spec/silver_spec.rb
|
307
|
+
- spec/spec_helper.rb
|
308
|
+
- spec/support/db.rb
|