soulmate 0.0.4 → 0.0.5

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 CHANGED
@@ -13,6 +13,7 @@ group :development do
13
13
  end
14
14
 
15
15
  gem 'redis', "~> 2.1.1"
16
+ gem 'multi_json', "~> 1.0.3"
16
17
  gem 'rack-contrib'
17
18
  gem 'vegas', "~> 0.1.8"
18
19
  gem 'sinatra', "~> 1.2.3"
data/Gemfile.lock CHANGED
@@ -6,6 +6,7 @@ GEM
6
6
  bundler (~> 1.0.0)
7
7
  git (>= 1.2.5)
8
8
  rake
9
+ multi_json (1.0.3)
9
10
  rack (1.2.2)
10
11
  rack-contrib (1.1.0)
11
12
  rack (>= 0.9.1)
@@ -26,6 +27,7 @@ PLATFORMS
26
27
  DEPENDENCIES
27
28
  bundler (~> 1.0.0)
28
29
  jeweler (~> 1.5.2)
30
+ multi_json (~> 1.0.3)
29
31
  rack-contrib
30
32
  rcov
31
33
  redis (~> 2.1.1)
data/README.markdown CHANGED
@@ -56,7 +56,7 @@ Once it's loaded, we can query this data by starting `soulmate-web`:
56
56
 
57
57
  $ soulmate-web --foreground --no-launch --redis=redis://localhost:6379/0
58
58
 
59
- And viewing the service in your browser: <a href="http://localhost:5678/search?types[]=venue&term=stad">http://localhost:5678/search?types[]=venue&term=stad</a>. You should see something like:
59
+ And viewing the service in your browser: http://localhost:5678/search?types[]=venue&term=stad. You should see something like:
60
60
 
61
61
  {
62
62
  "term": "stad",
data/bin/soulmate CHANGED
@@ -27,14 +27,47 @@ parser = OptionParser.new do |opts|
27
27
 
28
28
  opts.separator ""
29
29
  opts.separator "Commands:"
30
- opts.separator " load TYPE Loads items of specified type read from stdin in the JSON lines format"
30
+ opts.separator " load TYPE Replaces collection specified by TYPE with items read from stdin in the JSON lines format."
31
+ opts.separator " add TYPE Adds items to collection specified by TYPE read from stdin in the JSON lines format."
32
+ opts.separator " remove TYPE Removes items from collection specified by TYPE read from stdin in the JSON lines format. Items only require an 'id', all other fields are ignored."
33
+ opts.separator " query TYPE QUERY Queries for items from collection specified by TYPE."
31
34
  end
32
35
 
33
36
  def load(type)
34
37
  puts "Loading items of type #{type}..."
35
- items = $stdin.read.split("\n").map { |l| JSON.parse(l) }
36
- total = Soulmate::Loader.new(type).load(items)
37
- puts "Loaded a total of #{total} items"
38
+ items = $stdin.read.split("\n").map { |l| MultiJson.decode(l) }
39
+ loaded = Soulmate::Loader.new(type).load(items)
40
+ puts "Loaded a total of #{loaded.size} items"
41
+ end
42
+
43
+ def add(type)
44
+ puts "Adding items of type #{type}..."
45
+ loader = Soulmate::Loader.new(type)
46
+ items = $stdin.read.split("\n").map { |l| MultiJson.decode(l) }
47
+ items.each do |item|
48
+ loader.add(item)
49
+ end
50
+ puts "Loaded a total of #{items.size} items"
51
+ end
52
+
53
+ def remove(type)
54
+ puts "Removing items of type #{type}..."
55
+ loader = Soulmate::Loader.new(type)
56
+ items = $stdin.read.split("\n").map { |l| MultiJson.decode(l) }
57
+ items.each do |item|
58
+ loader.remove(item)
59
+ end
60
+ puts "Removed a total of #{items.size} items"
61
+ end
62
+
63
+ def query(type, query)
64
+ puts "> Querying '#{type}' for '#{query}'"
65
+ matcher = Soulmate::Matcher.new(type)
66
+ results = matcher.matches_for_term(query, :limit => 0)
67
+ results.each do |item|
68
+ puts MultiJson.encode(item)
69
+ end
70
+ puts "> Found #{results.size} matches"
38
71
  end
39
72
 
40
73
  parser.parse!
@@ -42,6 +75,12 @@ parser.parse!
42
75
  case ARGV[0]
43
76
  when 'load'
44
77
  load ARGV[1]
78
+ when 'add'
79
+ add ARGV[1]
80
+ when 'remove'
81
+ remove ARGV[1]
82
+ when 'query'
83
+ query ARGV[1], ARGV[2]
45
84
  else
46
85
  puts parser.help
47
86
  end
data/lib/soulmate.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  require 'uri'
2
- require 'json'
2
+ require 'multi_json'
3
3
  require 'redis'
4
4
 
5
5
  require 'soulmate/version'
@@ -5,7 +5,7 @@ module Soulmate
5
5
  words = normalize(phrase).split(' ')
6
6
  words.map do |w|
7
7
  (MIN_COMPLETE-1..(w.length-1)).map{ |l| w[0..l] }
8
- end.flatten
8
+ end.flatten.uniq
9
9
  end
10
10
 
11
11
  def normalize(str)
@@ -19,27 +19,41 @@ module Soulmate
19
19
  # delete the data stored for this type
20
20
  Soulmate.redis.del(database)
21
21
 
22
- items_loaded = 0
23
22
  items.each_with_index do |item, i|
24
- id = item["id"]
25
- term = item["term"]
26
- score = item["score"]
23
+ add(item, :skip_duplicate_check => true)
24
+ end
25
+ end
27
26
 
28
- if id and term
29
- # store the raw data in a separate key to reduce memory usage
30
- Soulmate.redis.hset(database, id, JSON.dump(item))
27
+ # "id", "term", "score", "aliases", "data"
28
+ def add(item, opts = {})
29
+ opts = { :skip_duplicate_check => false }.merge(opts)
30
+ raise ArgumentError unless item["id"] && item["term"]
31
+
32
+ # kill any old items with this id
33
+ remove("id" => item["id"]) unless opts[:skip_duplicate_check]
34
+
35
+ # store the raw data in a separate key to reduce memory usage
36
+ Soulmate.redis.hset(database, item["id"], MultiJson.encode(item))
37
+ phrase = ([item["term"]] + (item["aliases"] || [])).join(' ')
38
+ prefixes_for_phrase(phrase).each do |p|
39
+ Soulmate.redis.sadd(base, p) # remember this prefix in a master set
40
+ Soulmate.redis.zadd("#{base}:#{p}", item["score"], item["id"]) # store the id of this term in the index
41
+ end
42
+ end
31
43
 
32
- phrase = ([term] + (item["aliases"] || [])).join(' ')
33
- prefixes_for_phrase(phrase).uniq.each do |p|
34
- Soulmate.redis.sadd(base, p) # remember this prefix in a master set
35
- Soulmate.redis.zadd("#{base}:#{p}", score, id) # store the id of this term in the index
36
- end
37
- items_loaded += 1
44
+ # remove only cares about an item's id, but for consistency takes an object
45
+ def remove(item)
46
+ prev_item = Soulmate.redis.hget(database, item["id"])
47
+ if prev_item
48
+ prev_item = MultiJson.decode(prev_item)
49
+ # undo the operations done in add
50
+ Soulmate.redis.hdel(database, prev_item["id"])
51
+ phrase = ([prev_item["term"]] + (prev_item["aliases"] || [])).join(' ')
52
+ prefixes_for_phrase(phrase).each do |p|
53
+ Soulmate.redis.srem(base, p)
54
+ Soulmate.redis.zrem("#{base}:#{p}", prev_item["id"])
38
55
  end
39
- puts "added #{i} entries" if i % 100 == 0 and i != 0
40
56
  end
41
-
42
- items_loaded
43
57
  end
44
58
  end
45
59
  end
@@ -3,22 +3,30 @@ module Soulmate
3
3
  class Matcher < Base
4
4
 
5
5
  def matches_for_term(term, options = {})
6
+ options = { :limit => 5, :cache => true }.merge(options)
7
+
6
8
  words = normalize(term).split(' ').reject do |w|
7
9
  w.size < MIN_COMPLETE or STOP_WORDS.include?(w)
8
10
  end.sort
9
11
 
10
- options[:limit] ||= 5
12
+ return [] if words.empty?
11
13
 
12
14
  cachekey = "#{cachebase}:" + words.join('|')
13
15
 
14
- if !Soulmate.redis.exists(cachekey)
16
+ if !options[:cache] || !Soulmate.redis.exists(cachekey)
15
17
  interkeys = words.map { |w| "#{base}:#{w}" }
16
18
  Soulmate.redis.zinterstore(cachekey, interkeys)
17
19
  Soulmate.redis.expire(cachekey, 10 * 60) # expire after 10 minutes
18
20
  end
19
21
 
20
22
  ids = Soulmate.redis.zrevrange(cachekey, 0, options[:limit] - 1)
21
- ids.size > 0 ? Soulmate.redis.hmget(database, *ids).map { |r| JSON.parse(r) } : []
23
+ if ids.size > 0
24
+ Soulmate.redis.hmget(database, *ids)
25
+ .reject{ |r| r.nil? } # handle cached results for ids which have since been deleted
26
+ .map { |r| MultiJson.decode(r) }
27
+ else
28
+ []
29
+ end
22
30
  end
23
31
  end
24
32
  end
@@ -14,7 +14,7 @@ module Soulmate
14
14
  end
15
15
 
16
16
  get '/' do
17
- JSON.pretty_generate({ :soulmate => Soulmate::Version::STRING, :status => "ok" })
17
+ MultiJson.encode({ :soulmate => Soulmate::Version::STRING, :status => "ok" })
18
18
  end
19
19
 
20
20
  get '/search' do
@@ -30,7 +30,7 @@ module Soulmate
30
30
  results[type] = matcher.matches_for_term(term, :limit => limit)
31
31
  end
32
32
 
33
- JSON.pretty_generate({
33
+ MultiJson.encode({
34
34
  :term => params[:term],
35
35
  :results => results
36
36
  })
@@ -38,7 +38,7 @@ module Soulmate
38
38
 
39
39
  not_found do
40
40
  content_type 'application/json', :charset => 'utf-8'
41
- JSON.pretty_generate({ :error => "not found" })
41
+ MultiJson.encode({ :error => "not found" })
42
42
  end
43
43
 
44
44
  end
@@ -2,7 +2,7 @@ module Soulmate
2
2
  module Version
3
3
  MAJOR = 0
4
4
  MINOR = 0
5
- PATCH = 4
5
+ PATCH = 5
6
6
 
7
7
  STRING = [MAJOR, MINOR, PATCH].compact.join('.')
8
8
  end
data/soulmate.gemspec CHANGED
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{soulmate}
8
- s.version = "0.0.4"
8
+ s.version = "0.0.5"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Eric Waller"]
12
- s.date = %q{2011-07-07}
12
+ s.date = %q{2011-10-12}
13
13
  s.description = %q{Soulmate is a tool to help solve the common problem of developing a fast autocomplete feature. It uses Redis's sorted sets to build an index of partial words and corresponding top matches, and provides a simple sinatra app to query them. Soulmate finishes your sentences.}
14
14
  s.email = %q{eric@seatgeek.com}
15
15
  s.executables = ["soulmate", "soulmate-web"]
@@ -55,6 +55,7 @@ Gem::Specification.new do |s|
55
55
 
56
56
  if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
57
57
  s.add_runtime_dependency(%q<redis>, ["~> 2.1.1"])
58
+ s.add_runtime_dependency(%q<multi_json>, ["~> 1.0.3"])
58
59
  s.add_runtime_dependency(%q<rack-contrib>, [">= 0"])
59
60
  s.add_runtime_dependency(%q<vegas>, ["~> 0.1.8"])
60
61
  s.add_runtime_dependency(%q<sinatra>, ["~> 1.2.3"])
@@ -68,6 +69,7 @@ Gem::Specification.new do |s|
68
69
  s.add_runtime_dependency(%q<json>, ["~> 1.4.6"])
69
70
  else
70
71
  s.add_dependency(%q<redis>, ["~> 2.1.1"])
72
+ s.add_dependency(%q<multi_json>, ["~> 1.0.3"])
71
73
  s.add_dependency(%q<rack-contrib>, [">= 0"])
72
74
  s.add_dependency(%q<vegas>, ["~> 0.1.8"])
73
75
  s.add_dependency(%q<sinatra>, ["~> 1.2.3"])
@@ -82,6 +84,7 @@ Gem::Specification.new do |s|
82
84
  end
83
85
  else
84
86
  s.add_dependency(%q<redis>, ["~> 2.1.1"])
87
+ s.add_dependency(%q<multi_json>, ["~> 1.0.3"])
85
88
  s.add_dependency(%q<rack-contrib>, [">= 0"])
86
89
  s.add_dependency(%q<vegas>, ["~> 0.1.8"])
87
90
  s.add_dependency(%q<sinatra>, ["~> 1.2.3"])
@@ -5,12 +5,12 @@ class TestSoulmate < Test::Unit::TestCase
5
5
  items = []
6
6
  venues = File.open(File.expand_path(File.dirname(__FILE__)) + '/samples/venues.json', "r")
7
7
  venues.each_line do |venue|
8
- items << JSON.parse(venue)
8
+ items << MultiJson.decode(venue)
9
9
  end
10
10
 
11
11
  items_loaded = Soulmate::Loader.new('venues').load(items)
12
12
 
13
- assert_equal 6, items_loaded
13
+ assert_equal 6, items_loaded.size
14
14
 
15
15
  matcher = Soulmate::Matcher.new('venues')
16
16
  results = matcher.matches_for_term('stad', :limit => 5)
@@ -23,12 +23,12 @@ class TestSoulmate < Test::Unit::TestCase
23
23
  items = []
24
24
  venues = File.open(File.expand_path(File.dirname(__FILE__)) + '/samples/venues.json', "r")
25
25
  venues.each_line do |venue|
26
- items << JSON.parse(venue)
26
+ items << MultiJson.decode(venue)
27
27
  end
28
28
 
29
29
  items_loaded = Soulmate::Loader.new('venues').load(items)
30
30
 
31
- assert_equal 6, items_loaded
31
+ assert_equal 6, items_loaded.size
32
32
 
33
33
  matcher = Soulmate::Matcher.new('venues')
34
34
  results = matcher.matches_for_term('land shark stadium', :limit => 5)
@@ -42,4 +42,60 @@ class TestSoulmate < Test::Unit::TestCase
42
42
  results = matcher.matches_for_term('stadium', :limit => 5)
43
43
  assert_equal 5, results.size
44
44
  end
45
+
46
+ def test_can_remove_items
47
+
48
+ loader = Soulmate::Loader.new('venues')
49
+ matcher = Soulmate::Matcher.new('venues')
50
+
51
+ # empty the collection
52
+ loader.load([])
53
+ results = matcher.matches_for_term("te", :cache => false)
54
+ assert_equal 0, results.size
55
+
56
+ loader.add("id" => 1, "term" => "Testing this", "score" => 10)
57
+ results = matcher.matches_for_term("te", :cache => false)
58
+ assert_equal 1, results.size
59
+
60
+ loader.remove("id" => 1)
61
+ results = matcher.matches_for_term("te", :cache => false)
62
+ assert_equal 0, results.size
63
+
64
+ end
65
+
66
+ def test_can_update_items
67
+
68
+ loader = Soulmate::Loader.new('venues')
69
+ matcher = Soulmate::Matcher.new('venues')
70
+
71
+ # empty the collection
72
+ loader.load([])
73
+
74
+ # initial data
75
+ loader.add("id" => 1, "term" => "Testing this", "score" => 10)
76
+ loader.add("id" => 2, "term" => "Another Term", "score" => 9)
77
+ loader.add("id" => 3, "term" => "Something different", "score" => 5)
78
+
79
+ results = matcher.matches_for_term("te", :cache => false)
80
+ assert_equal 2, results.size
81
+ assert_equal "Testing this", results.first["term"]
82
+ assert_equal 10, results.first["score"]
83
+
84
+ # update id:1
85
+ loader.add("id" => 1, "term" => "Updated", "score" => 5)
86
+
87
+ results = matcher.matches_for_term("te", :cache => false)
88
+ assert_equal 1, results.size
89
+ assert_equal "Another Term", results.first["term"]
90
+ assert_equal 9, results.first["score"]
91
+
92
+ end
93
+
94
+ def test_prefixes_for_phrase
95
+ loader = Soulmate::Loader.new('venues')
96
+
97
+ assert_equal ["te", "tes", "test", "testi", "testin", "th", "thi", "this"], loader.prefixes_for_phrase("testin' this")
98
+ assert_equal ["te", "tes", "test"], loader.prefixes_for_phrase("test test")
99
+ assert_equal ["so", "sou", "soul", "soulm", "soulma", "soulmat", "soulmate"], loader.prefixes_for_phrase("SoUlmATE")
100
+ end
45
101
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: soulmate
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.4
4
+ version: 0.0.5
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,12 +9,12 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2011-07-07 00:00:00.000000000 -04:00
12
+ date: 2011-10-12 00:00:00.000000000 -04:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: redis
17
- requirement: &2164813260 !ruby/object:Gem::Requirement
17
+ requirement: &2151919160 !ruby/object:Gem::Requirement
18
18
  none: false
19
19
  requirements:
20
20
  - - ~>
@@ -22,10 +22,21 @@ dependencies:
22
22
  version: 2.1.1
23
23
  type: :runtime
24
24
  prerelease: false
25
- version_requirements: *2164813260
25
+ version_requirements: *2151919160
26
+ - !ruby/object:Gem::Dependency
27
+ name: multi_json
28
+ requirement: &2151917560 !ruby/object:Gem::Requirement
29
+ none: false
30
+ requirements:
31
+ - - ~>
32
+ - !ruby/object:Gem::Version
33
+ version: 1.0.3
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: *2151917560
26
37
  - !ruby/object:Gem::Dependency
27
38
  name: rack-contrib
28
- requirement: &2164811620 !ruby/object:Gem::Requirement
39
+ requirement: &2151916600 !ruby/object:Gem::Requirement
29
40
  none: false
30
41
  requirements:
31
42
  - - ! '>='
@@ -33,10 +44,10 @@ dependencies:
33
44
  version: '0'
34
45
  type: :runtime
35
46
  prerelease: false
36
- version_requirements: *2164811620
47
+ version_requirements: *2151916600
37
48
  - !ruby/object:Gem::Dependency
38
49
  name: vegas
39
- requirement: &2164810860 !ruby/object:Gem::Requirement
50
+ requirement: &2151915480 !ruby/object:Gem::Requirement
40
51
  none: false
41
52
  requirements:
42
53
  - - ~>
@@ -44,10 +55,10 @@ dependencies:
44
55
  version: 0.1.8
45
56
  type: :runtime
46
57
  prerelease: false
47
- version_requirements: *2164810860
58
+ version_requirements: *2151915480
48
59
  - !ruby/object:Gem::Dependency
49
60
  name: sinatra
50
- requirement: &2164809840 !ruby/object:Gem::Requirement
61
+ requirement: &2151914480 !ruby/object:Gem::Requirement
51
62
  none: false
52
63
  requirements:
53
64
  - - ~>
@@ -55,10 +66,10 @@ dependencies:
55
66
  version: 1.2.3
56
67
  type: :runtime
57
68
  prerelease: false
58
- version_requirements: *2164809840
69
+ version_requirements: *2151914480
59
70
  - !ruby/object:Gem::Dependency
60
71
  name: shoulda
61
- requirement: &2164808840 !ruby/object:Gem::Requirement
72
+ requirement: &2151913720 !ruby/object:Gem::Requirement
62
73
  none: false
63
74
  requirements:
64
75
  - - ! '>='
@@ -66,10 +77,10 @@ dependencies:
66
77
  version: '0'
67
78
  type: :development
68
79
  prerelease: false
69
- version_requirements: *2164808840
80
+ version_requirements: *2151913720
70
81
  - !ruby/object:Gem::Dependency
71
82
  name: bundler
72
- requirement: &2164807780 !ruby/object:Gem::Requirement
83
+ requirement: &2151912740 !ruby/object:Gem::Requirement
73
84
  none: false
74
85
  requirements:
75
86
  - - ~>
@@ -77,10 +88,10 @@ dependencies:
77
88
  version: 1.0.0
78
89
  type: :development
79
90
  prerelease: false
80
- version_requirements: *2164807780
91
+ version_requirements: *2151912740
81
92
  - !ruby/object:Gem::Dependency
82
93
  name: jeweler
83
- requirement: &2164807160 !ruby/object:Gem::Requirement
94
+ requirement: &2151911840 !ruby/object:Gem::Requirement
84
95
  none: false
85
96
  requirements:
86
97
  - - ~>
@@ -88,10 +99,10 @@ dependencies:
88
99
  version: 1.5.2
89
100
  type: :development
90
101
  prerelease: false
91
- version_requirements: *2164807160
102
+ version_requirements: *2151911840
92
103
  - !ruby/object:Gem::Dependency
93
104
  name: rcov
94
- requirement: &2164806540 !ruby/object:Gem::Requirement
105
+ requirement: &2151910900 !ruby/object:Gem::Requirement
95
106
  none: false
96
107
  requirements:
97
108
  - - ! '>='
@@ -99,10 +110,10 @@ dependencies:
99
110
  version: '0'
100
111
  type: :development
101
112
  prerelease: false
102
- version_requirements: *2164806540
113
+ version_requirements: *2151910900
103
114
  - !ruby/object:Gem::Dependency
104
115
  name: redis
105
- requirement: &2164805780 !ruby/object:Gem::Requirement
116
+ requirement: &2151909380 !ruby/object:Gem::Requirement
106
117
  none: false
107
118
  requirements:
108
119
  - - ! '>='
@@ -110,10 +121,10 @@ dependencies:
110
121
  version: '2.0'
111
122
  type: :runtime
112
123
  prerelease: false
113
- version_requirements: *2164805780
124
+ version_requirements: *2151909380
114
125
  - !ruby/object:Gem::Dependency
115
126
  name: vegas
116
- requirement: &2164805020 !ruby/object:Gem::Requirement
127
+ requirement: &2151907400 !ruby/object:Gem::Requirement
117
128
  none: false
118
129
  requirements:
119
130
  - - ! '>='
@@ -121,10 +132,10 @@ dependencies:
121
132
  version: 0.1.0
122
133
  type: :runtime
123
134
  prerelease: false
124
- version_requirements: *2164805020
135
+ version_requirements: *2151907400
125
136
  - !ruby/object:Gem::Dependency
126
137
  name: sinatra
127
- requirement: &2164804440 !ruby/object:Gem::Requirement
138
+ requirement: &2151904740 !ruby/object:Gem::Requirement
128
139
  none: false
129
140
  requirements:
130
141
  - - ! '>='
@@ -132,10 +143,10 @@ dependencies:
132
143
  version: '1.0'
133
144
  type: :runtime
134
145
  prerelease: false
135
- version_requirements: *2164804440
146
+ version_requirements: *2151904740
136
147
  - !ruby/object:Gem::Dependency
137
148
  name: json
138
- requirement: &2164803800 !ruby/object:Gem::Requirement
149
+ requirement: &2151901600 !ruby/object:Gem::Requirement
139
150
  none: false
140
151
  requirements:
141
152
  - - ~>
@@ -143,7 +154,7 @@ dependencies:
143
154
  version: 1.4.6
144
155
  type: :runtime
145
156
  prerelease: false
146
- version_requirements: *2164803800
157
+ version_requirements: *2151901600
147
158
  description: Soulmate is a tool to help solve the common problem of developing a fast
148
159
  autocomplete feature. It uses Redis's sorted sets to build an index of partial words
149
160
  and corresponding top matches, and provides a simple sinatra app to query them.
@@ -194,7 +205,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
194
205
  version: '0'
195
206
  segments:
196
207
  - 0
197
- hash: 1504170323034549202
208
+ hash: -3865399172879930147
198
209
  required_rubygems_version: !ruby/object:Gem::Requirement
199
210
  none: false
200
211
  requirements: