soulheart 0.0.2 → 0.0.4
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.
- checksums.yaml +4 -4
- data/{README.markdown → README.md} +49 -43
- data/Rakefile +15 -16
- data/bin/soulheart +24 -74
- data/bin/soulheart-web +5 -11
- data/lib/soulheart/base.rb +13 -11
- data/lib/soulheart/config.rb +4 -6
- data/lib/soulheart/helpers.rb +2 -6
- data/lib/soulheart/loader.rb +20 -17
- data/lib/soulheart/matcher.rb +13 -13
- data/lib/soulheart/server.rb +9 -12
- data/lib/soulheart/version.rb +2 -2
- metadata +3 -30
- data/.gitignore +0 -21
- data/.rspec +0 -2
- data/.travis.yml +0 -12
- data/Gemfile +0 -22
- data/Guardfile +0 -9
- data/logo.png +0 -0
- data/soulheart.gemspec +0 -24
- data/spec/fixtures/multiple_categories.json +0 -15
- data/spec/soulheart/loader_spec.rb +0 -96
- data/spec/soulheart/matcher_spec.rb +0 -104
- data/spec/soulheart/server_spec.rb +0 -26
- data/spec/soulheart_spec.rb +0 -29
- data/spec/spec_helper.rb +0 -31
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b170565afcbbfc53d9ab5f0a46713af88803a3a4
|
4
|
+
data.tar.gz: 58445b7ecdf5713487ba1af3539ad06137a04877
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 169f0de49bcd01b4ec5b2182f0075e220ea25b0059f16bffb9347567a1012d2925c6efef9a35b212825ba6989a97e60f18970b47f8d5df2e9bed2ea6dedf7fb6
|
7
|
+
data.tar.gz: b5b5de55c31b6cd03be318f05c08780302b3e1eee20b124e4eda1b737ffba4eee526b051d6969643ee7709b9f90123c485b21f9efdcdf2c9677c6fe0e66201be
|
@@ -1,70 +1,76 @@
|
|
1
|
-
Soulheart [](https://travis-ci.org/sethherr/soulheart) [](https://codeclimate.com/github/sethherr/soulheart) [](https://codeclimate.com/github/sethherr/soulheart/coverage)
|
2
|
-
========
|
1
|
+
# <img src="https://raw.githubusercontent.com/sethherr/soulheart/master/logo.png" alt="Soulheart" width="200"> Soulheart [](https://travis-ci.org/sethherr/soulheart) [](https://codeclimate.com/github/sethherr/soulheart) [](https://codeclimate.com/github/sethherr/soulheart/coverage)
|
3
2
|
|
4
|
-
This is an updated fork of [Seatgeek/Soulmate](https://github.com/seatgeek/soulmate) to address a few issues - namely [CORS support](../../issues/2), [minimum entry length](../../issues/3) and [playing better with Selectize & Select2](../../issues/4) - also the future.
|
5
3
|
|
6
|
-
|
4
|
+
**Soulheart is a ready to use remote data source for autocomplete**. It supports:
|
7
5
|
|
8
|
-
|
6
|
+
- pagination
|
7
|
+
- categories
|
8
|
+
- sorting by priority (not just alphabetically)
|
9
|
+
- arbitrary returns
|
10
|
+
- loading data via gists
|
11
|
+
- mounting standalone or inside of a rails app
|
9
12
|
|
10
|
-
|
13
|
+
... and is [instantly deployable to heroku](https://heroku.com/deploy) (for free).
|
11
14
|
|
12
|
-
|
15
|
+
To get started, check out examples and documentation at [sethherr.github.io/soulheart](https://sethherr.github.io/soulheart).
|
13
16
|
|
14
|
-
Pushing new options via rake task. Use a gist url! Do it with .csvs!
|
15
17
|
|
16
|
-
|
18
|
+
---
|
17
19
|
|
18
|
-
|
20
|
+
### Adding data
|
19
21
|
|
20
|
-
|
21
|
-
[
|
22
|
-
{ "term": "Something sweet" },
|
23
|
-
{ "term": "The color blue" }
|
24
|
-
]
|
25
|
-
```
|
22
|
+
You can add data from json, CSV and TSV files.
|
26
23
|
|
27
|
-
|
24
|
+
Adding data is very simple - all you need is a `text` value.
|
28
25
|
|
26
|
+
Soulheart uses [line delineated JSON streams](https://en.wikipedia.org/wiki/JSON_Streaming#Line_delimited_JSON), so it doesn't have to load the whole file into memory. Which just means - put each object onto a seperate line.
|
29
27
|
|
30
|
-
|
31
|
-
| ----------- | ----- | ---------- |
|
32
|
-
| `priority` | integer | 100 |
|
33
|
-
| `types` | string | 'default' |
|
34
|
-
| `data` | hash | {} |
|
28
|
+
For the simplest case, with just text values in JSON:
|
35
29
|
|
36
|
-
|
30
|
+
{ "text": "Jamis" }
|
31
|
+
{ "text": "Specialized" }
|
32
|
+
{ "text": "Trek" }
|
37
33
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
}
|
49
|
-
},
|
50
|
-
{ "term": "The color blue" }
|
51
|
-
]
|
52
|
-
```
|
34
|
+
It accepts local files:
|
35
|
+
|
36
|
+
soulheart load my_json_file.json
|
37
|
+
|
38
|
+
or remote files:
|
39
|
+
|
40
|
+
soulheart load https://gist.githubusercontent.com/sethherr/96dbc011e508330ceec4/raw/95122b1fc9de85f241cd048f32b94568f54134e0/manufacturers.tsv
|
41
|
+
|
42
|
+
|
43
|
+
In addition to term, there are a few optional values -
|
53
44
|
|
54
|
-
|
45
|
+
| Key | Default | What it does |
|
46
|
+
| ------------ | ----------- | ------------ |
|
47
|
+
| `priority` | `100` | Higher numbers come first |
|
48
|
+
| `category` | `'default'` | Sets the category |
|
49
|
+
| `data` | `{}` | Returned object from search - the text and category will be added to this if you don't specify them. |
|
50
|
+
|
51
|
+
Here is an example of what a possible hash you could pass is
|
52
|
+
|
53
|
+
{ "text": "Jamis", "category": "Bike Manufacturer" }
|
54
|
+
{ "text": "Specialized" }
|
55
|
+
{ "text": "Trek" }
|
56
|
+
|
57
|
+
*If you set `text` in `data`, it will respond with that rather than the term it searches by. I haven't figured out a use case for this yet, but I'm sure one exists.*
|
55
58
|
|
56
59
|
======
|
57
60
|
|
58
|
-
I'm testing with:
|
61
|
+
I'm testing with: `ruby >= 2.1` and `redis >= 3`.
|
59
62
|
|
60
|
-
|
61
|
-
- redis >= 3
|
63
|
+
Run `bundle exec guard` to run the specs while you work, it will just test the files you change.
|
62
64
|
|
63
|
-
|
65
|
+
This repo includes a `config.ru` and a `Gemfile.lock` so it (and any forks of it) can be deployed to heroku. They shouldn't be in the Gem itself.
|
64
66
|
|
65
67
|
|
66
68
|
======
|
67
69
|
|
70
|
+
This is an updated fork of [Seatgeek/Soulmate](https://github.com/seatgeek/soulmate) to address a few issues - namely [CORS support](../../issues/2), [minimum entry length](../../issues/3) and [playing better with Selectize & Select2](../../issues/4) - also the future.
|
71
|
+
|
72
|
+
Since [Seatgeek no longer uses Soulmate](https://news.ycombinator.com/item?id=9317891), and this isn't backward compatible it's a new project and gem.
|
73
|
+
|
68
74
|
:x::o::x::o::x::o: *Soulmate's README follows ([issue for making new documentation](../../issues/1))*
|
69
75
|
|
70
76
|
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 partially completed words and the corresponding top matching items, and provides a simple sinatra app to query them. Soulmate finishes your sentences.
|
data/Rakefile
CHANGED
@@ -1,29 +1,29 @@
|
|
1
1
|
#!/usr/bin/env rake
|
2
2
|
|
3
|
-
require
|
3
|
+
require 'bundler/gem_helper'
|
4
4
|
|
5
5
|
begin
|
6
6
|
Bundler.setup(:default, :development)
|
7
7
|
rescue Bundler::BundlerError => e
|
8
8
|
$stderr.puts e.message
|
9
|
-
$stderr.puts
|
9
|
+
$stderr.puts 'Run `bundle install` to install missing gems'
|
10
10
|
exit e.status_code
|
11
11
|
end
|
12
12
|
require 'rake'
|
13
13
|
|
14
|
-
REDIS_DIR = File.expand_path(File.join(
|
15
|
-
REDIS_CNF = File.join(REDIS_DIR,
|
16
|
-
REDIS_PID = File.join(REDIS_DIR,
|
14
|
+
REDIS_DIR = File.expand_path(File.join('..', 'test'), __FILE__)
|
15
|
+
REDIS_CNF = File.join(REDIS_DIR, 'test.conf')
|
16
|
+
REDIS_PID = File.join(REDIS_DIR, 'db', 'redis.pid')
|
17
17
|
REDIS_LOCATION = ENV['REDIS_LOCATION']
|
18
18
|
|
19
|
-
desc
|
20
|
-
task :
|
19
|
+
desc 'Run rcov and manage server start/stop'
|
20
|
+
task rcoverage: [:start, :rcov, :stop]
|
21
21
|
|
22
|
-
desc
|
22
|
+
desc 'Start the Redis server'
|
23
23
|
task :start do
|
24
24
|
redis_running = \
|
25
25
|
begin
|
26
|
-
File.
|
26
|
+
File.exist?(REDIS_PID) && Process.kill(0, File.read(REDIS_PID).to_i)
|
27
27
|
rescue Errno::ESRCH
|
28
28
|
FileUtils.rm REDIS_PID
|
29
29
|
false
|
@@ -36,21 +36,20 @@ task :start do
|
|
36
36
|
end
|
37
37
|
end
|
38
38
|
|
39
|
-
desc
|
39
|
+
desc 'Stop the Redis server'
|
40
40
|
task :stop do
|
41
|
-
if File.
|
42
|
-
Process.kill
|
41
|
+
if File.exist?(REDIS_PID)
|
42
|
+
Process.kill 'INT', File.read(REDIS_PID).to_i
|
43
43
|
FileUtils.rm REDIS_PID
|
44
44
|
end
|
45
45
|
end
|
46
46
|
|
47
|
+
require 'rspec/core/rake_task'
|
47
48
|
|
48
|
-
|
49
|
-
|
50
|
-
desc "Run all specs"
|
49
|
+
desc 'Run all specs'
|
51
50
|
RSpec::Core::RakeTask.new(:spec) do |t|
|
52
51
|
t.rspec_opts = %w(--color)
|
53
52
|
t.verbose = false
|
54
53
|
end
|
55
54
|
|
56
|
-
task :
|
55
|
+
task default: :spec
|
data/bin/soulheart
CHANGED
@@ -12,95 +12,45 @@ require 'optparse'
|
|
12
12
|
require 'tempfile'
|
13
13
|
|
14
14
|
parser = OptionParser.new do |opts|
|
15
|
-
opts.banner =
|
15
|
+
opts.banner = 'Usage: soulheart [options] COMMAND'
|
16
16
|
|
17
|
-
opts.separator
|
18
|
-
opts.separator
|
17
|
+
opts.separator ''
|
18
|
+
opts.separator 'Options:'
|
19
19
|
|
20
|
-
opts.on(
|
20
|
+
opts.on('-r', '--redis [HOST:PORT]', 'Redis connection string') do |host|
|
21
21
|
Soulheart.redis = host
|
22
22
|
end
|
23
23
|
|
24
|
-
opts.on(
|
24
|
+
opts.on('-s', '--stop-words [FILE]', 'Path to file containing a list of stop words') do |fn|
|
25
25
|
File.open(fn) do |file|
|
26
|
-
Soulheart.stop_words = file.readlines.map
|
26
|
+
Soulheart.stop_words = file.readlines.map(&:strip).reject(&:empty?)
|
27
27
|
end
|
28
28
|
end
|
29
29
|
|
30
|
-
opts.on(
|
30
|
+
opts.on('-h', '--help', 'Show this message') do
|
31
31
|
puts opts
|
32
32
|
exit
|
33
33
|
end
|
34
34
|
|
35
|
-
opts.on(
|
35
|
+
opts.on('-b', '--batch-size', 'Number of lines to read at a time') do |size|
|
36
36
|
BATCH_SIZE = size
|
37
37
|
end
|
38
38
|
|
39
|
-
opts.separator
|
40
|
-
opts.separator
|
41
|
-
opts.separator
|
42
|
-
opts.separator
|
39
|
+
opts.separator ''
|
40
|
+
opts.separator 'Commands:'
|
41
|
+
opts.separator ' load TYPE FILE Replaces collection specified by TYPE with items read from FILE in the JSON lines format.'
|
42
|
+
opts.separator ' add TYPE Adds items to collection specified by TYPE read from stdin in the JSON lines format.'
|
43
43
|
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."
|
44
|
-
opts.separator
|
44
|
+
opts.separator ' query TYPE QUERY Queries for items from collection specified by TYPE.'
|
45
45
|
end
|
46
46
|
|
47
|
-
def generate(type, file)
|
48
|
-
include Soulheart::Helpers
|
49
|
-
|
50
|
-
begin
|
51
|
-
temp = Tempfile.new("soulheart")
|
52
|
-
|
53
|
-
if File.exists?(file)
|
54
|
-
start_time = Time.now.to_i
|
55
|
-
base = "soulheart-index:#{type}"
|
56
|
-
database = "soulheart-data:#{type}"
|
57
|
-
# hset = "*4\r\n$4\r\nHSET\r\n$#{database.length}\r\n#{database}\r\n$"
|
58
|
-
# del = "*2\r\n$3\r\nDEL\r\n$"
|
59
|
-
begin
|
60
|
-
f = File.open(file)
|
61
|
-
# cleanup
|
62
|
-
phrases = Soulheart.redis.smembers(base)
|
63
|
-
phrases.each do |phrase|
|
64
|
-
temp << gen_redis_proto("DEL", phrase)
|
65
|
-
# temp << del + phrase.length.to_s + "\r\n" + phrase + "\r\n"
|
66
|
-
end
|
67
|
-
temp << gen_redis_proto("DEL", base)
|
68
|
-
# temp << del + base.length.to_s + "\r\n" + base + "\r\n"
|
69
|
-
while !f.eof?
|
70
|
-
line = f.gets.chomp
|
71
|
-
line =~ /"id":(\d+)/
|
72
|
-
id = $1
|
73
|
-
line =~ /"score":(\d+)/
|
74
|
-
score = $1
|
75
|
-
json = MultiJson.decode(line)
|
76
|
-
temp << gen_redis_proto("HSET", database, id, line)
|
77
|
-
# temp << hset + $1.length.to_s + "\r\n" + $1 + "\r\n$" + line.length.to_s + "\r\n" + line + "\r\n"
|
78
|
-
phrase = json.key?("aliases") ? json["term"] + " " + json["aliases"] : json["term"]
|
79
|
-
prefixes_for_phrase(phrase).each do |p|
|
80
|
-
temp << gen_redis_proto("SADD", base, p)
|
81
|
-
temp << gen_redis_proto("ZADD", base + ":" + p, score, id)
|
82
|
-
end
|
83
|
-
end
|
84
|
-
ensure
|
85
|
-
f.close
|
86
|
-
end
|
87
|
-
puts "Converted in #{Time.now.to_i - start_time} second(s)"
|
88
|
-
puts "Importing into redis ..."
|
89
|
-
`time redis-cli --pipe < #{temp.path}`
|
90
|
-
else
|
91
|
-
puts "Couldn't open file: #{file}"
|
92
|
-
end
|
93
|
-
ensure
|
94
|
-
temp.close
|
95
|
-
end
|
96
|
-
end
|
97
47
|
|
98
48
|
def load(file)
|
99
49
|
require 'uri'
|
100
|
-
if file =~ URI
|
50
|
+
if file =~ URI.regexp
|
101
51
|
require 'open-uri'
|
102
52
|
f = open(file)
|
103
|
-
elsif File.
|
53
|
+
elsif File.exist?(file)
|
104
54
|
f = File.open(file)
|
105
55
|
else
|
106
56
|
puts "Couldn't open file: #{file}"
|
@@ -113,17 +63,17 @@ def load(file)
|
|
113
63
|
lines = []
|
114
64
|
begin
|
115
65
|
if file.match(/(c|t)sv\z/i)
|
116
|
-
puts
|
66
|
+
puts 'Reading a CSV'
|
117
67
|
require 'csv'
|
118
|
-
sep = file.match(/tsv\z/i) ? "\t" :
|
68
|
+
sep = file.match(/tsv\z/i) ? "\t" : ','
|
119
69
|
CSV.foreach(f, headers: true, col_sep: sep) do |row|
|
120
70
|
lines << row.to_hash
|
121
71
|
count += 1
|
122
72
|
end
|
123
73
|
elsif file.match(/json\z/i)
|
124
|
-
puts
|
74
|
+
puts 'Reading JSON'
|
125
75
|
puts "Loading items in batches of #{BATCH_SIZE} ..."
|
126
|
-
|
76
|
+
until f.eof?
|
127
77
|
lines = []
|
128
78
|
BATCH_SIZE.times do
|
129
79
|
break if f.eof?
|
@@ -132,7 +82,7 @@ def load(file)
|
|
132
82
|
end
|
133
83
|
end
|
134
84
|
else
|
135
|
-
puts
|
85
|
+
puts 'unknown File type'
|
136
86
|
end
|
137
87
|
ensure
|
138
88
|
f.close
|
@@ -164,7 +114,7 @@ end
|
|
164
114
|
def query(type, query)
|
165
115
|
puts "> Querying '#{type}' for '#{query}'"
|
166
116
|
matcher = Soulheart::Matcher.new(type)
|
167
|
-
results = matcher.matches_for_term(query, :
|
117
|
+
results = matcher.matches_for_term(query, limit: 0)
|
168
118
|
results.each do |item|
|
169
119
|
puts MultiJson.encode(item)
|
170
120
|
end
|
@@ -172,10 +122,10 @@ def query(type, query)
|
|
172
122
|
end
|
173
123
|
|
174
124
|
def gen_redis_proto(*cmd)
|
175
|
-
proto =
|
125
|
+
proto = '*' + cmd.length.to_s + "\r\n"
|
176
126
|
cmd.each{|arg|
|
177
|
-
proto <<
|
178
|
-
proto << arg+"\r\n"
|
127
|
+
proto << '$' + arg.bytesize.to_s + "\r\n"
|
128
|
+
proto << arg + "\r\n"
|
179
129
|
}
|
180
130
|
proto
|
181
131
|
end
|
data/bin/soulheart-web
CHANGED
@@ -6,23 +6,17 @@ begin
|
|
6
6
|
rescue LoadError
|
7
7
|
require 'rubygems'
|
8
8
|
require 'vegas'
|
9
|
-
end
|
9
|
+
end
|
10
10
|
require 'soulheart/server'
|
11
11
|
|
12
|
-
|
13
|
-
|
14
|
-
:before_run => lambda {|v|
|
15
|
-
# path = (ENV['RESQUECONFIG'] || v.args.first)
|
16
|
-
# load path.to_s.strip if path
|
17
|
-
}
|
18
|
-
}) do |runner, opts, app|
|
19
|
-
opts.on("-r", "--redis [HOST:PORT]", "Redis connection string") do |host|
|
12
|
+
Vegas::Runner.new(Soulheart::Server, 'soulheart-web') do |runner, opts|
|
13
|
+
opts.on('-r', '--redis [HOST:PORT]', 'Redis connection string') do |host|
|
20
14
|
runner.logger.info "Using Redis connection string '#{host}'"
|
21
15
|
Soulheart.redis = host
|
22
16
|
end
|
23
|
-
opts.on(
|
17
|
+
opts.on('-s', '--stop-words [FILE]', 'Path to file containing a list of stop words') do |fn|
|
24
18
|
File.open(fn) do |file|
|
25
|
-
Soulheart.stop_words = file.readlines.map
|
19
|
+
Soulheart.stop_words = file.readlines.map(&:strip).reject(&:empty?)
|
26
20
|
end
|
27
21
|
end
|
28
22
|
end
|
data/lib/soulheart/base.rb
CHANGED
@@ -1,9 +1,7 @@
|
|
1
1
|
module Soulheart
|
2
|
-
|
3
2
|
class Base
|
4
|
-
|
5
3
|
include Helpers
|
6
|
-
|
4
|
+
|
7
5
|
attr_accessor :type
|
8
6
|
|
9
7
|
def redis
|
@@ -13,15 +11,15 @@ module Soulheart
|
|
13
11
|
def cache_length
|
14
12
|
10 * 60 # Setting to 10 minutes, but making it possible to edit down the line
|
15
13
|
end
|
16
|
-
|
14
|
+
|
17
15
|
def base_id
|
18
|
-
ENV['RACK_ENV'] != 'test' ?
|
16
|
+
ENV['RACK_ENV'] != 'test' ? 'soulheart:' : 'soulheart_test:'
|
19
17
|
end
|
20
18
|
|
21
19
|
def set_category_combos_array
|
22
20
|
redis.expire category_combos_id, 0
|
23
|
-
ar = redis.smembers(categories_id).map{ |c| normalize(c) }.uniq.sort
|
24
|
-
ar = 1.upto(ar.size).flat_map {|n| ar.combination(n).map{|el| el.join('')}}
|
21
|
+
ar = redis.smembers(categories_id).map { |c| normalize(c) }.uniq.sort
|
22
|
+
ar = 1.upto(ar.size).flat_map { |n| ar.combination(n).map { |el| el.join('') } }
|
25
23
|
ar.last.replace('all')
|
26
24
|
redis.sadd category_combos_id, ar
|
27
25
|
ar
|
@@ -31,15 +29,19 @@ module Soulheart
|
|
31
29
|
"#{base_id}category_combos:"
|
32
30
|
end
|
33
31
|
|
32
|
+
def category_combos
|
33
|
+
redis.smembers(category_combos_id)
|
34
|
+
end
|
35
|
+
|
34
36
|
def categories_id
|
35
37
|
"#{base_id}categories:"
|
36
38
|
end
|
37
39
|
|
38
|
-
def category_id(name='all')
|
40
|
+
def category_id(name = 'all')
|
39
41
|
"#{categories_id}#{name}:"
|
40
42
|
end
|
41
43
|
|
42
|
-
def no_query_id(category=category_id)
|
44
|
+
def no_query_id(category = category_id)
|
43
45
|
"all:#{category}"
|
44
46
|
end
|
45
47
|
|
@@ -47,8 +49,8 @@ module Soulheart
|
|
47
49
|
"#{base_id}database:"
|
48
50
|
end
|
49
51
|
|
50
|
-
def cache_id(type='all')
|
52
|
+
def cache_id(type = 'all')
|
51
53
|
"#{base_id}cache:#{type}:"
|
52
54
|
end
|
53
55
|
end
|
54
|
-
end
|
56
|
+
end
|
data/lib/soulheart/config.rb
CHANGED
@@ -3,7 +3,7 @@ require 'redis'
|
|
3
3
|
|
4
4
|
module Soulheart
|
5
5
|
module Config
|
6
|
-
DEFAULT_STOP_WORDS =
|
6
|
+
DEFAULT_STOP_WORDS = %w(vs at the)
|
7
7
|
|
8
8
|
# Accepts:
|
9
9
|
# 1. A Redis URL String 'redis://host:port/db'
|
@@ -23,14 +23,12 @@ module Soulheart
|
|
23
23
|
# create a new one.
|
24
24
|
def redis
|
25
25
|
@redis ||= (
|
26
|
-
url = URI(@redis_url || ENV[
|
27
|
-
::Redis.new(
|
28
|
-
# driver: :hiredis,
|
26
|
+
url = URI(@redis_url || ENV['REDIS_URL'] || 'redis://127.0.0.1:6379/0')
|
27
|
+
::Redis.new( # driver: :hiredis,
|
29
28
|
host: url.host,
|
30
29
|
port: url.port,
|
31
30
|
db: url.path[1..-1],
|
32
|
-
password: url.password
|
33
|
-
})
|
31
|
+
password: url.password)
|
34
32
|
)
|
35
33
|
end
|
36
34
|
|
data/lib/soulheart/helpers.rb
CHANGED
@@ -2,21 +2,17 @@
|
|
2
2
|
|
3
3
|
module Soulheart
|
4
4
|
module Helpers
|
5
|
-
def blank?
|
6
|
-
respond_to?(:empty?) ? !!empty? : !self
|
7
|
-
end
|
8
|
-
|
9
5
|
def normalize(str)
|
10
6
|
# Letter, Mark, Number, Connector_Punctuation (Chinese, Japanese, etc.)
|
11
7
|
str.downcase.gsub(/[^\p{Word}\ ]/i, '').strip
|
12
8
|
end
|
13
|
-
|
9
|
+
|
14
10
|
def prefixes_for_phrase(phrase)
|
15
11
|
words = normalize(phrase).split(' ').reject do |w|
|
16
12
|
Soulheart.stop_words.include?(w)
|
17
13
|
end
|
18
14
|
words.map do |w|
|
19
|
-
(0..(w.length-1)).map{ |l| w[0..l] }
|
15
|
+
(0..(w.length - 1)).map { |l| w[0..l] }
|
20
16
|
end.flatten.uniq
|
21
17
|
end
|
22
18
|
end
|
data/lib/soulheart/loader.rb
CHANGED
@@ -1,7 +1,5 @@
|
|
1
1
|
module Soulheart
|
2
|
-
|
3
2
|
class Loader < Base
|
4
|
-
|
5
3
|
def default_items_hash(text, category)
|
6
4
|
category ||= 'default'
|
7
5
|
{
|
@@ -12,7 +10,7 @@ module Soulheart
|
|
12
10
|
'data' => {
|
13
11
|
'text' => text,
|
14
12
|
'category' => category
|
15
|
-
}
|
13
|
+
}
|
16
14
|
}
|
17
15
|
end
|
18
16
|
|
@@ -57,31 +55,36 @@ module Soulheart
|
|
57
55
|
end
|
58
56
|
|
59
57
|
def clean(item)
|
60
|
-
|
61
|
-
default_items_hash(item.delete('text'), item.delete('category'))
|
62
|
-
tap{ |i| i['data'].merge!(item.delete('data')) if item['data'] }
|
63
|
-
tap{ |i| i['priority'] = item.delete('priority').to_f if item['priority'] }
|
64
|
-
merge item
|
58
|
+
fail ArgumentError, 'Items must have text' unless item['text']
|
59
|
+
default_items_hash(item.delete('text'), item.delete('category'))
|
60
|
+
.tap { |i| i['data'].merge!(item.delete('data')) if item['data'] }
|
61
|
+
.tap { |i| i['priority'] = item.delete('priority').to_f if item['priority'] }
|
62
|
+
.merge item
|
65
63
|
end
|
66
64
|
|
67
|
-
def add_item(item, category_base_id=nil, cleaned: false)
|
65
|
+
def add_item(item, category_base_id = nil, cleaned: false)
|
68
66
|
unless cleaned
|
69
67
|
item = clean(item)
|
70
68
|
category_base_id ||= category_id(item['category'])
|
69
|
+
item.keys.select{ |k| k[/data-/i] }.each do |key|
|
70
|
+
item['data'].merge!({
|
71
|
+
"#{key.gsub(/data-/i,'')}" => item.delete(key)
|
72
|
+
})
|
73
|
+
end
|
71
74
|
unless redis.smembers(categories_id).include?(item['category'])
|
72
75
|
redis.sadd categories_id, item['category']
|
73
76
|
end
|
74
77
|
end
|
75
78
|
redis.pipelined do
|
76
|
-
redis.zadd(no_query_id(category_base_id), item[
|
79
|
+
redis.zadd(no_query_id(category_base_id), item['priority'], item['term']) # Add to master set for queryless searches
|
77
80
|
# store the raw data in a separate key to reduce memory usage, if it's cleaned it's done
|
78
81
|
redis.hset(results_hashes_id, item['term'], MultiJson.encode(item['data'])) unless cleaned
|
79
|
-
phrase = ([item[
|
82
|
+
phrase = ([item['term']] + (item['aliases'] || [])).join(' ')
|
80
83
|
# Store all the prefixes
|
81
84
|
prefixes_for_phrase(phrase).each do |p|
|
82
85
|
redis.sadd(base_id, p) unless cleaned # remember prefix in a master set
|
83
86
|
# store the normalized term in the index for each of the categories
|
84
|
-
redis.zadd("#{category_base_id}#{p}", item[
|
87
|
+
redis.zadd("#{category_base_id}#{p}", item['priority'], item['term'])
|
85
88
|
end
|
86
89
|
end
|
87
90
|
item
|
@@ -89,19 +92,19 @@ module Soulheart
|
|
89
92
|
|
90
93
|
# remove only cares about an item's id, but for consistency takes an object
|
91
94
|
def remove(item)
|
92
|
-
prev_item = Soulheart.redis.hget(base_id, item[
|
95
|
+
prev_item = Soulheart.redis.hget(base_id, item['term'])
|
93
96
|
if prev_item
|
94
97
|
prev_item = MultiJson.decode(prev_item)
|
95
98
|
# undo the operations done in add
|
96
99
|
Soulheart.redis.pipelined do
|
97
|
-
Soulheart.redis.hdel(base_id, prev_item[
|
98
|
-
phrase = ([prev_item[
|
100
|
+
Soulheart.redis.hdel(base_id, prev_item['term'])
|
101
|
+
phrase = ([prev_item['term']] + (prev_item['aliases'] || [])).join(' ')
|
99
102
|
prefixes_for_phrase(phrase).each do |p|
|
100
103
|
Soulheart.redis.srem(base_id, p)
|
101
|
-
Soulheart.redis.zrem("#{base_id}:#{p}", prev_item[
|
104
|
+
Soulheart.redis.zrem("#{base_id}:#{p}", prev_item['term'])
|
102
105
|
end
|
103
106
|
end
|
104
107
|
end
|
105
108
|
end
|
106
109
|
end
|
107
|
-
end
|
110
|
+
end
|
data/lib/soulheart/matcher.rb
CHANGED
@@ -1,28 +1,29 @@
|
|
1
1
|
module Soulheart
|
2
|
-
|
3
2
|
class Matcher < Base
|
4
|
-
def initialize(params={})
|
3
|
+
def initialize(params = {})
|
5
4
|
@opts = self.class.default_params_hash.merge params
|
6
5
|
clean_opts
|
7
6
|
end
|
8
7
|
|
8
|
+
attr_accessor :opts
|
9
|
+
|
9
10
|
def self.default_params_hash
|
10
11
|
{
|
11
12
|
'page' => 1,
|
12
13
|
'per_page' => 5,
|
13
14
|
'categories' => [],
|
14
|
-
'
|
15
|
+
'q' => '', # Query
|
15
16
|
'cache' => true
|
16
17
|
}
|
17
18
|
end
|
18
19
|
|
19
20
|
def clean_opts
|
20
21
|
unless @opts['categories'] == '' || @opts['categories'] == []
|
21
|
-
@opts['categories'] = @opts['categories'].split(/,|\+/) unless @opts['categories'].
|
22
|
-
@opts['categories'] = @opts['categories'].map{ |s| normalize(s) }.uniq.sort
|
22
|
+
@opts['categories'] = @opts['categories'].split(/,|\+/) unless @opts['categories'].is_a?(Array)
|
23
|
+
@opts['categories'] = @opts['categories'].map { |s| normalize(s) }.uniq.sort
|
23
24
|
@opts['categories'] = [] if @opts['categories'].length == redis.scard(categories_id)
|
24
25
|
end
|
25
|
-
@opts['
|
26
|
+
@opts['q'] = normalize(@opts['q']).split(' ') unless @opts['q'].is_a?(Array)
|
26
27
|
# .reject{ |i| i && i.length > 0 } .split(' ').reject{ Soulmate.stop_words.include?(w) }
|
27
28
|
@opts
|
28
29
|
end
|
@@ -36,12 +37,12 @@ module Soulheart
|
|
36
37
|
end
|
37
38
|
|
38
39
|
def cache_id_from_opts
|
39
|
-
"#{cache_id(categories_string)}#{@opts['
|
40
|
+
"#{cache_id(categories_string)}#{@opts['q'].join(':')}"
|
40
41
|
end
|
41
42
|
|
42
43
|
def interkeys_from_opts(cid)
|
43
44
|
# If there isn't a query, we use a special key in redis
|
44
|
-
@opts['
|
45
|
+
@opts['q'].empty? ? [no_query_id(cid)] : @opts['q'].map { |w| "#{cid}#{w}" }
|
45
46
|
end
|
46
47
|
|
47
48
|
def matches
|
@@ -55,17 +56,16 @@ module Soulheart
|
|
55
56
|
end
|
56
57
|
offset = (@opts['page'].to_i - 1) * @opts['per_page'].to_i
|
57
58
|
limit = @opts['per_page'].to_i + offset - 1
|
58
|
-
|
59
|
+
|
59
60
|
limit = 0 if limit < 0
|
60
61
|
ids = redis.zrevrange(cachekey, offset, limit) # Using 'ids', even though keys are now terms
|
61
62
|
if ids.size > 0
|
62
63
|
results = redis.hmget(results_hashes_id, *ids)
|
63
|
-
results = results.reject
|
64
|
+
results = results.reject(&:nil?) # handle cached results for ids which have since been deleted
|
64
65
|
results.map { |r| MultiJson.decode(r) }
|
65
66
|
else
|
66
67
|
[]
|
67
|
-
end
|
68
|
+
end
|
68
69
|
end
|
69
|
-
|
70
70
|
end
|
71
|
-
end
|
71
|
+
end
|
data/lib/soulheart/server.rb
CHANGED
@@ -1,33 +1,30 @@
|
|
1
1
|
require 'sinatra/base'
|
2
2
|
require 'soulheart'
|
3
|
-
require 'rack/contrib'
|
4
3
|
|
5
4
|
module Soulheart
|
6
|
-
|
7
5
|
class Server < Sinatra::Base
|
8
6
|
include Helpers
|
9
|
-
|
7
|
+
|
10
8
|
before do
|
11
|
-
content_type 'application/json', :
|
9
|
+
content_type 'application/json', charset: 'utf-8'
|
12
10
|
headers['Access-Control-Allow-Origin'] = '*'
|
13
11
|
headers['Access-Control-Allow-Methods'] = 'POST, PUT, GET, OPTIONS'
|
14
12
|
headers['Access-Control-Request-Method'] = '*'
|
15
13
|
headers['Access-Control-Allow-Headers'] = 'Origin, X-Requested-With, Content-Type, Accept, Authorization'
|
16
14
|
end
|
17
|
-
|
15
|
+
|
18
16
|
get '/' do
|
19
17
|
matches = Matcher.new(params).matches
|
20
|
-
MultiJson.encode(
|
18
|
+
MultiJson.encode(matches: matches)
|
21
19
|
end
|
22
20
|
|
23
|
-
get '/status' do
|
24
|
-
MultiJson.encode(
|
21
|
+
get '/status' do
|
22
|
+
MultiJson.encode(soulheart: Soulheart::VERSION, status: 'ok')
|
25
23
|
end
|
26
|
-
|
24
|
+
|
27
25
|
not_found do
|
28
|
-
content_type 'application/json', :
|
29
|
-
MultiJson.encode(
|
26
|
+
content_type 'application/json', charset: 'utf-8'
|
27
|
+
MultiJson.encode(error: 'not found')
|
30
28
|
end
|
31
|
-
|
32
29
|
end
|
33
30
|
end
|
data/lib/soulheart/version.rb
CHANGED
@@ -1,3 +1,3 @@
|
|
1
1
|
module Soulheart
|
2
|
-
VERSION =
|
3
|
-
end
|
2
|
+
VERSION = '0.0.4'
|
3
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: soulheart
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Seth Herr
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-06-
|
11
|
+
date: 2015-06-23 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: hiredis
|
@@ -80,20 +80,6 @@ dependencies:
|
|
80
80
|
- - ">="
|
81
81
|
- !ruby/object:Gem::Version
|
82
82
|
version: '0'
|
83
|
-
- !ruby/object:Gem::Dependency
|
84
|
-
name: rack-contrib
|
85
|
-
requirement: !ruby/object:Gem::Requirement
|
86
|
-
requirements:
|
87
|
-
- - ">="
|
88
|
-
- !ruby/object:Gem::Version
|
89
|
-
version: '0'
|
90
|
-
type: :development
|
91
|
-
prerelease: false
|
92
|
-
version_requirements: !ruby/object:Gem::Requirement
|
93
|
-
requirements:
|
94
|
-
- - ">="
|
95
|
-
- !ruby/object:Gem::Version
|
96
|
-
version: '0'
|
97
83
|
- !ruby/object:Gem::Dependency
|
98
84
|
name: rake
|
99
85
|
requirement: !ruby/object:Gem::Requirement
|
@@ -145,13 +131,8 @@ executables:
|
|
145
131
|
extensions: []
|
146
132
|
extra_rdoc_files: []
|
147
133
|
files:
|
148
|
-
- ".gitignore"
|
149
|
-
- ".rspec"
|
150
|
-
- ".travis.yml"
|
151
|
-
- Gemfile
|
152
|
-
- Guardfile
|
153
134
|
- LICENSE.md
|
154
|
-
- README.
|
135
|
+
- README.md
|
155
136
|
- Rakefile
|
156
137
|
- bin/soulheart
|
157
138
|
- bin/soulheart-web
|
@@ -163,14 +144,6 @@ files:
|
|
163
144
|
- lib/soulheart/matcher.rb
|
164
145
|
- lib/soulheart/server.rb
|
165
146
|
- lib/soulheart/version.rb
|
166
|
-
- logo.png
|
167
|
-
- soulheart.gemspec
|
168
|
-
- spec/fixtures/multiple_categories.json
|
169
|
-
- spec/soulheart/loader_spec.rb
|
170
|
-
- spec/soulheart/matcher_spec.rb
|
171
|
-
- spec/soulheart/server_spec.rb
|
172
|
-
- spec/soulheart_spec.rb
|
173
|
-
- spec/spec_helper.rb
|
174
147
|
homepage: https://github.com/sethherr/soulheart
|
175
148
|
licenses:
|
176
149
|
- MIT
|
data/.gitignore
DELETED
data/.rspec
DELETED
data/.travis.yml
DELETED
@@ -1,12 +0,0 @@
|
|
1
|
-
language: ruby
|
2
|
-
rvm:
|
3
|
-
- 2.1.0
|
4
|
-
bundler_args: "--without=guard"
|
5
|
-
notifications:
|
6
|
-
disabled: true
|
7
|
-
script:
|
8
|
-
- bundle exec rake spec
|
9
|
-
# - bundle exec rubocop
|
10
|
-
addons:
|
11
|
-
code_climate:
|
12
|
-
repo_token: e72d8fa152922cef05c311cd49d3db9016b82486b712399a8e7c7da2af5e071e
|
data/Gemfile
DELETED
@@ -1,22 +0,0 @@
|
|
1
|
-
source 'http://rubygems.org'
|
2
|
-
|
3
|
-
group :development do
|
4
|
-
gem 'rspec', '~> 2.14.1'
|
5
|
-
gem 'bundler'
|
6
|
-
gem 'guard'
|
7
|
-
gem 'guard-rspec', '~> 4.2.8'
|
8
|
-
gem 'rack-contrib'
|
9
|
-
gem 'rubocop'
|
10
|
-
end
|
11
|
-
|
12
|
-
group :test do
|
13
|
-
gem 'rack-test'
|
14
|
-
gem "codeclimate-test-reporter", require: nil
|
15
|
-
end
|
16
|
-
|
17
|
-
gem 'rake'
|
18
|
-
gem 'redis', '>= 3.0.1'
|
19
|
-
gem 'hiredis', '~> 0.4.5'
|
20
|
-
gem 'vegas', '>= 0.1.0'
|
21
|
-
gem 'sinatra'
|
22
|
-
gem 'multi_json', '>= 1.11.0'
|
data/Guardfile
DELETED
data/logo.png
DELETED
Binary file
|
data/soulheart.gemspec
DELETED
@@ -1,24 +0,0 @@
|
|
1
|
-
# -*- encoding: utf-8 -*-
|
2
|
-
require File.expand_path('../lib/soulheart/version', __FILE__)
|
3
|
-
|
4
|
-
Gem::Specification.new do |gem|
|
5
|
-
gem.version = Soulheart::VERSION
|
6
|
-
gem.authors = ["Seth Herr"]
|
7
|
-
gem.email = ["seth.william.herr@gmail.com"]
|
8
|
-
gem.description = gem.summary = "Simple, fast autocomplete server for Ruby and Rails"
|
9
|
-
gem.homepage = "https://github.com/sethherr/soulheart"
|
10
|
-
gem.license = "MIT"
|
11
|
-
gem.executables = ["soulheart", "soulheart-web"]
|
12
|
-
gem.files = `git ls-files | grep -Ev '^(test)'`.split("\n")
|
13
|
-
gem.name = "soulheart"
|
14
|
-
gem.require_paths = ["lib"]
|
15
|
-
gem.add_dependency 'hiredis', '~> 0.4.5'
|
16
|
-
gem.add_dependency 'redis', '>= 3.0.6'
|
17
|
-
gem.add_dependency 'vegas', '>= 0.1.0'
|
18
|
-
gem.add_dependency 'json'
|
19
|
-
gem.add_dependency 'sinatra'
|
20
|
-
gem.add_development_dependency 'rack-contrib'
|
21
|
-
gem.add_development_dependency 'rake'
|
22
|
-
gem.add_development_dependency 'rspec'
|
23
|
-
gem.add_development_dependency 'rubocop'
|
24
|
-
end
|
@@ -1,15 +0,0 @@
|
|
1
|
-
{"text":"Steel","category":"Frame Materials" }
|
2
|
-
{"text":"Brompton Bicycle","priority":51,"category":"Frame Manufacturer","data":{"id":8,"url":"http://www.brompton.com"}}
|
3
|
-
{"text":"Jamis","priority":75,"category":"Frame Manufacturer","data":{"id":2222,"url":"http://jamisbikes.com"}}
|
4
|
-
{"text":"Surly","priority":102,"category":"Frame Manufacturer"}
|
5
|
-
{"text":"Jagwire","priority":40,"category":"Manufacturer"}
|
6
|
-
{"text":"Jannd","priority":41,"category":"Manufacturer"}
|
7
|
-
{"text":"Sram","priority":50,"category":"Manufacturer","data":{"id":8,"url":"http://sram.com"}}
|
8
|
-
{"text":"Brooks England LTD.","priority":50,"category":"Manufacturer","data":{"id":200,"url":"http://www.brooksengland.com/"}}
|
9
|
-
{"text":"Dodger Stadium","priority":84,"data":{"id":1,"url":"\/dodger-stadium-tickets\/","subtitle":"Los Angeles, CA"},"aliases":["Chavez Ravine"]}
|
10
|
-
{"text":"Angel Stadium","priority":90,"data":{"id":28,"url":"\/angel-stadium-tickets\/","subtitle":"Anaheim, CA"},"aliases":["Edison International Field of Anaheim"]}
|
11
|
-
{"text":"Chase Field ","priority":80,"data":{"id":30,"url":"\/chase-field-tickets\/","subtitle":"Phoenix, AZ"},"aliases":["Bank One Ballpark", "Bank One Stadium"]}
|
12
|
-
{"text":"Sun Life Stadium","priority":75,"data":{"id":29,"url":"\/sun-life-stadium-tickets\/","subtitle":"Miami, FL"},"aliases":["Dolphins Stadium","Land Shark Stadium"]}
|
13
|
-
{"text":"Turner Field","priority":50,"data":{"id":2,"url":"\/turner-field-tickets\/","subtitle":"Atlanta, GA"}}
|
14
|
-
{"text":"Citi Field","priority":92,"data":{"id":3,"url":"\/citi-field-tickets\/","subtitle":"Atlanta, GA"},"aliases":["Shea Stadium"]}
|
15
|
-
{"text":"中国佛山 李小龙","priority":94,"data":{"id":8,"url":"\/Bruce Lee\/","subtitle":"Chinese Foshan"},"aliases":["Li XiaoLong"]}
|
@@ -1,96 +0,0 @@
|
|
1
|
-
require 'spec_helper'
|
2
|
-
|
3
|
-
describe Soulheart::Loader do
|
4
|
-
|
5
|
-
describe :clean_data do
|
6
|
-
it "sets the default category, priority and normalizes term" do
|
7
|
-
item = { 'text' => ' FooBar' }
|
8
|
-
result = Soulheart::Loader.new.clean(item)
|
9
|
-
expect(result['priority']).to eq(100)
|
10
|
-
expect(result['term']).to eq('foobar')
|
11
|
-
expect(result['category']).to eq('default')
|
12
|
-
expect(result['data']['text']).to eq(' FooBar')
|
13
|
-
end
|
14
|
-
|
15
|
-
it "doesn't overwrite the submitted params" do
|
16
|
-
item = {
|
17
|
-
'text' => 'Cool ',
|
18
|
-
'priority' => '50',
|
19
|
-
'category' => 'Gooble',
|
20
|
-
'data' => {
|
21
|
-
'id' => 199
|
22
|
-
}
|
23
|
-
}
|
24
|
-
result = Soulheart::Loader.new.clean(item)
|
25
|
-
expect(result['term']).to eq('cool')
|
26
|
-
expect(result['priority']).to eq(50)
|
27
|
-
expect(result['data']['text']).to eq('Cool ')
|
28
|
-
expect(result['data']['id']).to eq(199)
|
29
|
-
expect(result['category']).to eq('gooble')
|
30
|
-
expect(result['data']['category']).to eq('Gooble')
|
31
|
-
end
|
32
|
-
|
33
|
-
it "raises argument error if text is passed" do
|
34
|
-
expect{
|
35
|
-
Soulheart::Loader.new.clean({'name' => 'stuff'})
|
36
|
-
}.to raise_error(/must have/i)
|
37
|
-
end
|
38
|
-
end
|
39
|
-
|
40
|
-
describe :add_item do
|
41
|
-
it "adds an item, adds prefix scopes, adds category" do
|
42
|
-
item = {
|
43
|
-
'text' => 'Brompton Bicycle',
|
44
|
-
'priority' => 50,
|
45
|
-
'category' => 'Gooble',
|
46
|
-
'data' => {
|
47
|
-
'id' => 199
|
48
|
-
}
|
49
|
-
}
|
50
|
-
loader = Soulheart::Loader.new
|
51
|
-
redis = loader.redis
|
52
|
-
redis.expire loader.results_hashes_id, 0
|
53
|
-
loader.add_item(item)
|
54
|
-
redis = loader.redis
|
55
|
-
target = "{\"text\":\"Brompton Bicycle\",\"category\":\"Gooble\",\"id\":199}"
|
56
|
-
result = redis.hget(loader.results_hashes_id, 'brompton bicycle')
|
57
|
-
expect(result).to eq(target)
|
58
|
-
prefixed = redis.zrevrange "#{loader.category_id('gooble')}:brom", 0, -1
|
59
|
-
expect(prefixed[0]).to eq('brompton bicycle')
|
60
|
-
expect(redis.smembers(loader.categories_id).include?('gooble')).to be_true
|
61
|
-
end
|
62
|
-
end
|
63
|
-
|
64
|
-
describe :store_terms do
|
65
|
-
it "stores terms by priority and adds categories for each possible category combination" do
|
66
|
-
items = []
|
67
|
-
file = File.read('spec/fixtures/multiple_categories.json')
|
68
|
-
file.each_line { |l| items << MultiJson.decode(l) }
|
69
|
-
loader = Soulheart::Loader.new
|
70
|
-
redis = loader.redis
|
71
|
-
loader.delete_categories
|
72
|
-
loader.load(items)
|
73
|
-
|
74
|
-
cat_prefixed = redis.zrevrange "#{loader.category_id('frame manufacturermanufacturer')}:brom", 0, -1
|
75
|
-
expect(cat_prefixed.count).to eq(1)
|
76
|
-
expect(redis.smembers(loader.categories_id).count).to be > 3
|
77
|
-
|
78
|
-
prefixed = redis.zrevrange "#{loader.category_id('all')}:bro", 0, -1
|
79
|
-
expect(prefixed.count).to eq(2)
|
80
|
-
expect(prefixed[0]).to eq('brompton bicycle')
|
81
|
-
end
|
82
|
-
|
83
|
-
it "stores terms by priority and doesn't add run categories if none are present" do
|
84
|
-
items = [
|
85
|
-
{'text' => 'cool thing', 'category' => 'AWESOME'},
|
86
|
-
{'text' => 'Sweet', 'category' => ' awesome'}
|
87
|
-
]
|
88
|
-
loader = Soulheart::Loader.new
|
89
|
-
redis = loader.redis
|
90
|
-
loader.delete_categories
|
91
|
-
loader.load(items)
|
92
|
-
expect(redis.smembers(loader.category_combos_id).count).to eq(1)
|
93
|
-
end
|
94
|
-
end
|
95
|
-
|
96
|
-
end
|
@@ -1,104 +0,0 @@
|
|
1
|
-
require 'spec_helper'
|
2
|
-
|
3
|
-
describe Soulheart::Matcher do
|
4
|
-
|
5
|
-
describe :clean_opts do
|
6
|
-
it "Has the keys we need" do
|
7
|
-
target_keys = ['categories', 'query', 'page', 'per_page']
|
8
|
-
keys = Soulheart::Matcher.default_params_hash.keys
|
9
|
-
expect((target_keys - keys).count).to eq(0)
|
10
|
-
end
|
11
|
-
|
12
|
-
it "obeys stop words"
|
13
|
-
end
|
14
|
-
|
15
|
-
describe :category_id_from_opts do
|
16
|
-
it 'gets the id for one' do
|
17
|
-
Soulheart::Loader.new.reset_categories(['cool', 'test'])
|
18
|
-
matcher = Soulheart::Matcher.new('categories' => ['some_category'])
|
19
|
-
expect(matcher.category_id_from_opts).to eq(matcher.category_id('some_category'))
|
20
|
-
end
|
21
|
-
|
22
|
-
it 'gets the id for all of them' do
|
23
|
-
Soulheart::Loader.new.reset_categories(['cool', 'test', 'boo'])
|
24
|
-
matcher = Soulheart::Matcher.new('categories' => 'cool, boo, test')
|
25
|
-
expect(matcher.category_id_from_opts).to eq(matcher.category_id('all'))
|
26
|
-
end
|
27
|
-
end
|
28
|
-
|
29
|
-
describe :categories_string do
|
30
|
-
it 'does all if none' do
|
31
|
-
Soulheart::Loader.new.reset_categories(['cool', 'test'])
|
32
|
-
matcher = Soulheart::Matcher.new('categories' => '')
|
33
|
-
expect(matcher.categories_string).to eq('all')
|
34
|
-
end
|
35
|
-
it "correctly concats a string of categories" do
|
36
|
-
Soulheart::Loader.new.reset_categories(['cool', 'some_category', 'another cat', 'z9', 'stuff'])
|
37
|
-
matcher = Soulheart::Matcher.new({'categories' => 'some_category, another cat, z9'})
|
38
|
-
expect(matcher.categories_string).to eq('another catsome_categoryz9')
|
39
|
-
end
|
40
|
-
end
|
41
|
-
|
42
|
-
|
43
|
-
describe :matches do
|
44
|
-
it "With no params, gets all the matches, ordered by priority and name" do
|
45
|
-
store_terms_fixture
|
46
|
-
opts = { 'per_page' => 100, 'cache' => false }
|
47
|
-
matches = Soulheart::Matcher.new(opts).matches
|
48
|
-
expect(matches.count).to be > 10
|
49
|
-
expect(matches[0]['text']).to eq('Surly')
|
50
|
-
end
|
51
|
-
|
52
|
-
it "With no query but with categories, matches categories" do
|
53
|
-
store_terms_fixture
|
54
|
-
opts = { 'per_page' => 100, 'cache' => false, 'categories' => 'manufacturer' }
|
55
|
-
matches = Soulheart::Matcher.new(opts).matches
|
56
|
-
expect(matches.count).to eq(4)
|
57
|
-
expect(matches[0]['text']).to eq('Sram')
|
58
|
-
end
|
59
|
-
|
60
|
-
it "Gets the matches matching query and priority for one item in query, all categories" do
|
61
|
-
store_terms_fixture
|
62
|
-
opts = { 'per_page' => 100, 'query' => 'j', 'cache' => false }
|
63
|
-
matches = Soulheart::Matcher.new(opts).matches
|
64
|
-
expect(matches.count).to eq(3)
|
65
|
-
expect(matches[0]['text']).to eq('Jamis')
|
66
|
-
end
|
67
|
-
|
68
|
-
it "Gets the matches matching query and priority for one item in query, one category" do
|
69
|
-
store_terms_fixture
|
70
|
-
opts = { 'per_page' => 100, 'query' => 'j', 'cache' => false, 'categories' => 'manufacturer' }
|
71
|
-
matches = Soulheart::Matcher.new(opts).matches
|
72
|
-
expect(matches.count).to eq(2)
|
73
|
-
expect(matches[0]['text']).to eq('Jannd')
|
74
|
-
end
|
75
|
-
|
76
|
-
it "Gets pages and uses them" do
|
77
|
-
# Pagination wrecked my mind, hence the multitude of tests
|
78
|
-
items = [
|
79
|
-
{"text" => 'First item', 'priority' => '11000' },
|
80
|
-
{"text" => 'Second item', 'priority' => '1999' },
|
81
|
-
{"text" => 'Third item', 'priority' => 1900 },
|
82
|
-
{"text" => 'Fourth item', 'priority' => 1800 },
|
83
|
-
{"text" => 'Fifth item', 'priority' => 1750 },
|
84
|
-
{"text" => 'Sixth item', 'priority' => 1700 },
|
85
|
-
{"text" => 'Seventh item', 'priority' => 1699 }
|
86
|
-
]
|
87
|
-
loader = Soulheart::Loader.new
|
88
|
-
loader.delete_categories
|
89
|
-
loader.load(items)
|
90
|
-
|
91
|
-
page1 = Soulheart::Matcher.new({'per_page' => 1, 'cache' => false}).matches
|
92
|
-
expect(page1[0]['text']).to eq('First item')
|
93
|
-
|
94
|
-
page2 = Soulheart::Matcher.new({'per_page' => 1, 'page' => 2, 'cache' => false}).matches
|
95
|
-
expect(page2.count).to eq(1)
|
96
|
-
expect(page2[0]['text']).to eq('Second item')
|
97
|
-
|
98
|
-
page3 = Soulheart::Matcher.new({'per_page' => 2, 'page' => 3, 'cache' => false}).matches
|
99
|
-
expect(page3[0]['text']).to eq('Fifth item')
|
100
|
-
expect(page3[1]['text']).to eq('Sixth item')
|
101
|
-
end
|
102
|
-
end
|
103
|
-
|
104
|
-
end
|
@@ -1,26 +0,0 @@
|
|
1
|
-
require 'spec_helper'
|
2
|
-
|
3
|
-
describe Soulheart::Server do
|
4
|
-
|
5
|
-
describe :search do
|
6
|
-
it "Has CORS headers, JSON Content-Type and it succeeds" do
|
7
|
-
get '/'
|
8
|
-
expect(last_response.headers['Access-Control-Allow-Origin']).to eq('*')
|
9
|
-
expect(last_response.headers['Access-Control-Request-Method']).to eq('*')
|
10
|
-
expect(last_response.headers['Content-Type']).to match('json')
|
11
|
-
expect(last_response.status).to eq(200)
|
12
|
-
end
|
13
|
-
end
|
14
|
-
|
15
|
-
describe :status do
|
16
|
-
it "Has cors headers and is valid JSON" do
|
17
|
-
get '/status'
|
18
|
-
expect(last_response.headers['Access-Control-Allow-Origin']).to eq('*')
|
19
|
-
expect(last_response.headers['Access-Control-Request-Method']).to eq('*')
|
20
|
-
expect(last_response.headers['Content-Type']).to match('json')
|
21
|
-
expect(JSON.parse(last_response.body)['soulheart']).to match(/\d/)
|
22
|
-
end
|
23
|
-
end
|
24
|
-
|
25
|
-
|
26
|
-
end
|
data/spec/soulheart_spec.rb
DELETED
@@ -1,29 +0,0 @@
|
|
1
|
-
require 'spec_helper'
|
2
|
-
|
3
|
-
describe Soulheart do
|
4
|
-
|
5
|
-
it 'has a version number' do
|
6
|
-
expect(Soulheart::VERSION).not_to be nil
|
7
|
-
end
|
8
|
-
|
9
|
-
it "has a test base_id" do
|
10
|
-
expect(Soulheart::Base.new.base_id).to eq('soulheart_test:')
|
11
|
-
end
|
12
|
-
|
13
|
-
it "collates (? probably not the word I'm looking for) all the things" do
|
14
|
-
base = Soulheart::Base.new
|
15
|
-
base.redis.expire base.categories_id, 0
|
16
|
-
base.redis.sadd base.categories_id, ['George', 'category one', 'other thing ']
|
17
|
-
result = base.set_category_combos_array
|
18
|
-
expect(result.include?("category one")).to be_true
|
19
|
-
expect(result.include?("george")).to be_true
|
20
|
-
expect(result.include?("other thing")).to be_true
|
21
|
-
expect(result.include?("georgeother thing")).to be_true
|
22
|
-
expect(result.include?("category oneother thing")).to be_true
|
23
|
-
expect(result.include?("category onegeorge")).to be_true
|
24
|
-
expect(result.include?("georgecategory one")).to be_false
|
25
|
-
expect(result.include?("all")).to be_true
|
26
|
-
expect(result.include?("category onegeorgeother thing")).to be_false
|
27
|
-
end
|
28
|
-
|
29
|
-
end
|
data/spec/spec_helper.rb
DELETED
@@ -1,31 +0,0 @@
|
|
1
|
-
if ENV['CODECLIMATE_REPO_TOKEN']
|
2
|
-
require "codeclimate-test-reporter"
|
3
|
-
CodeClimate::TestReporter.start
|
4
|
-
end
|
5
|
-
require 'rack/test'
|
6
|
-
require 'rspec'
|
7
|
-
|
8
|
-
$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
|
9
|
-
require 'soulheart'
|
10
|
-
require 'soulheart/server'
|
11
|
-
|
12
|
-
ENV['RACK_ENV'] = 'test'
|
13
|
-
|
14
|
-
module RSpecMixin
|
15
|
-
include Rack::Test::Methods
|
16
|
-
def app() Soulheart::Server end
|
17
|
-
end
|
18
|
-
|
19
|
-
RSpec.configure do |config|
|
20
|
-
config.treat_symbols_as_metadata_keys_with_true_values = true
|
21
|
-
config.include RSpecMixin
|
22
|
-
end
|
23
|
-
|
24
|
-
def store_terms_fixture
|
25
|
-
items = []
|
26
|
-
file = File.read('spec/fixtures/multiple_categories.json')
|
27
|
-
file.each_line { |l| items << MultiJson.decode(l) }
|
28
|
-
loader = Soulheart::Loader.new
|
29
|
-
loader.delete_categories
|
30
|
-
loader.load(items)
|
31
|
-
end
|