soulheart 0.0.5 → 0.0.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +10 -185
- data/bin/soulheart +7 -49
- data/lib/soulheart/loader.rb +20 -24
- data/lib/soulheart/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0ee2b8a0da3cca62bb1236f16a0537e3b6d153e9
|
4
|
+
data.tar.gz: bc09f384eb18f2162d3eb7b852e137ecfe3a48d0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b1cc93669b128ddd68f594124ec0e9bee2b7ee7fa441a8c135c099f00513e288f37ccc0cd02efb67918b101b84220c8c9cc098d10f435678cabb6a4fbb17da42
|
7
|
+
data.tar.gz: 2de3adf63591024964833931ddfd2bff99573711509e633618b7e6a842ca4b2907ade4e91e836ee9e41a8ca5b902cb29ba20db90a2824b2f9b2e969fe4acfa34
|
data/README.md
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
# <img src="https://raw.githubusercontent.com/sethherr/soulheart/master/logo.png" alt="Soulheart" width="200"> Soulheart [![Build Status](https://travis-ci.org/sethherr/soulheart.svg)](https://travis-ci.org/sethherr/soulheart) [![Code Climate](https://codeclimate.com/github/sethherr/soulheart/badges/gpa.svg)](https://codeclimate.com/github/sethherr/soulheart) [![Test Coverage](https://codeclimate.com/github/sethherr/soulheart/badges/coverage.svg)](https://codeclimate.com/github/sethherr/soulheart/coverage)
|
2
2
|
|
3
|
+
To get started, check out examples and documentation at [sethherr.github.io/soulheart/](https://sethherr.github.io/soulheart/).
|
4
|
+
|
3
5
|
**Soulheart is a ready-to-use remote data source for autocomplete**. It supports:
|
4
6
|
|
5
7
|
- pagination
|
@@ -11,199 +13,22 @@
|
|
11
13
|
|
12
14
|
... and is [instantly deployable to heroku](https://heroku.com/deploy) (for free).
|
13
15
|
|
14
|
-
To get started, check out examples and documentation at [sethherr.github.io/soulheart/](https://sethherr.github.io/soulheart/).
|
15
|
-
|
16
|
-
----
|
17
|
-
|
18
|
-
Soulheart is in Beta. It's probably appropriate to use in production... but maybe wait? There are a few more changes coming, and some documentation improvements to be made.
|
19
|
-
|
20
|
-
### Adding data
|
21
|
-
|
22
|
-
You can add data from json, CSV and TSV files.
|
23
|
-
|
24
|
-
Adding data is very simple - all you need is a `text` value.
|
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.
|
27
|
-
|
28
|
-
For the simplest case, with just text values in JSON:
|
29
|
-
|
30
|
-
{ "text": "Jamis" }
|
31
|
-
{ "text": "Specialized" }
|
32
|
-
{ "text": "Trek" }
|
33
|
-
|
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
16
|
|
43
|
-
|
17
|
+
This project is in Beta. It's probably appropriate to use in production... but maybe wait? There are a few more changes coming, and some documentation improvements to be made.
|
44
18
|
|
45
|
-
|
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. |
|
19
|
+
=======
|
50
20
|
|
51
|
-
|
21
|
+
I'm testing with: `ruby 2.1` and `redis 3.0`.
|
52
22
|
|
53
|
-
|
54
|
-
{ "text": "Specialized" }
|
55
|
-
{ "text": "Trek" }
|
23
|
+
Run `bundle exec guard` to run the specs when they change as you work.
|
56
24
|
|
57
|
-
|
25
|
+
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. if you build it separately.
|
58
26
|
|
59
27
|
======
|
60
28
|
|
61
|
-
|
62
|
-
|
63
|
-
Run `bundle exec guard` to run the specs while you work, it will just test the files you change.
|
64
|
-
|
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.
|
66
|
-
|
67
|
-
|
68
|
-
======
|
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
|
-
|
74
|
-
:x::o::x::o::x::o: *Soulmate's README follows ([issue for making new documentation](../../issues/1))*
|
75
|
-
|
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.
|
77
|
-
|
78
|
-
Soulmate was designed to be simple and fast, and offers the following:
|
79
|
-
|
80
|
-
* Provide suggestions for multiple types of items in a single query (at SeatGeek we're autocompleting for performers, events, and venues)
|
81
|
-
* Results are ordered by a user-specified score
|
82
|
-
* Arbitrary metadata for each item (at SeatGeek we're storing both a url and a subtitle)
|
83
|
-
|
84
|
-
An item is a simple JSON object that looks like:
|
85
|
-
|
86
|
-
{
|
87
|
-
"id": 3,
|
88
|
-
"term": "Citi Field",
|
89
|
-
"score": 81,
|
90
|
-
"data": {
|
91
|
-
"url": "/citi-field-tickets/",
|
92
|
-
"subtitle": "Flushing, NY"
|
93
|
-
}
|
94
|
-
}
|
95
|
-
|
96
|
-
Where `id` is a unique identifier (within the specific type), `term` is the phrase you wish to provide completions for, `score` is a user-specified ranking metric (redis will order things lexicographically for items with the same score), and `data` is an optional container for metadata you'd like to return when this item is matched (at SeatGeek we're including a url for the item as well as a subtitle for when we present it in an autocomplete dropdown).
|
97
|
-
|
98
|
-
Getting Started
|
99
|
-
---------------
|
100
|
-
|
101
|
-
As always, kick things off with a `gem install`:
|
102
|
-
|
103
|
-
gem install soulmate
|
104
|
-
|
105
|
-
### Loading Items
|
106
|
-
|
107
|
-
You can load data into Soulmate by piping items in the JSON lines format into `soulmate load TYPE`.
|
108
|
-
|
109
|
-
Here's a sample `venues.json` (one JSON item per line):
|
110
|
-
|
111
|
-
{"id":1,"term":"Dodger Stadium","score":85,"data":{"url":"\/dodger-stadium-tickets\/","subtitle":"Los Angeles, CA"}}
|
112
|
-
{"id":28,"term":"Angel Stadium","score":85,"data":{"url":"\/angel-stadium-tickets\/","subtitle":"Anaheim, CA"}}
|
113
|
-
{"id":30,"term":"Chase Field ","score":85,"data":{"url":"\/chase-field-tickets\/","subtitle":"Phoenix, AZ"}}
|
114
|
-
{"id":29,"term":"Sun Life Stadium","score":84,"data":{"url":"\/sun-life-stadium-tickets\/","subtitle":"Miami, FL"}}
|
115
|
-
{"id":2,"term":"Turner Field","score":83,"data":{"url":"\/turner-field-tickets\/","subtitle":"Atlanta, GA"}}
|
116
|
-
|
117
|
-
And here's the load command (Soulmate assumes redis is running locally on the default port, or you can specify a redis connection string with the `--redis` argument):
|
118
|
-
|
119
|
-
$ soulmate load venue --redis=redis://localhost:6379/0 < venues.json
|
120
|
-
|
121
|
-
You can also provide an array of strings under the `aliases` key that will also be added to the index for this item.
|
122
|
-
|
123
|
-
### Querying for Data
|
124
|
-
|
125
|
-
Once it's loaded, we can query this data by starting `soulmate-web`:
|
126
|
-
|
127
|
-
$ soulmate-web --foreground --no-launch --redis=redis://localhost:6379/0
|
128
|
-
|
129
|
-
And viewing the service in your browser: http://localhost:5678/search?types[]=venue&term=stad. You should see something like:
|
130
|
-
|
131
|
-
{
|
132
|
-
"term": "stad",
|
133
|
-
"results": {
|
134
|
-
"venue": [
|
135
|
-
{
|
136
|
-
"id": 28,
|
137
|
-
"term": "Angel Stadium",
|
138
|
-
"score": 85,
|
139
|
-
"data": {
|
140
|
-
"url": "/angel-stadium-tickets/",
|
141
|
-
"subtitle": "Anaheim, CA"
|
142
|
-
}
|
143
|
-
},
|
144
|
-
{
|
145
|
-
"id": 1,
|
146
|
-
"term": "Dodger Stadium",
|
147
|
-
"score": 85,
|
148
|
-
"data": {
|
149
|
-
"url": "/dodger-stadium-tickets/",
|
150
|
-
"subtitle": "Los Angeles, CA"
|
151
|
-
}
|
152
|
-
},
|
153
|
-
{
|
154
|
-
"id": 29,
|
155
|
-
"term": "Sun Life Stadium",
|
156
|
-
"score": 84,
|
157
|
-
"data": {
|
158
|
-
"url": "/sun-life-stadium-tickets/",
|
159
|
-
"subtitle": "Miami, FL"
|
160
|
-
}
|
161
|
-
}
|
162
|
-
]
|
163
|
-
}
|
164
|
-
}
|
165
|
-
|
166
|
-
The `/search` method supports multiple `types` as well as an optional `limit`. For example: `http://localhost:5678/search?types[]=event&types[]=venue&types[]=performer&limit=3&term=yank`. You can also add the `callback` parameter to enable JSONP output.
|
167
|
-
|
168
|
-
### Mounting soulmate into a rails app
|
169
|
-
|
170
|
-
If you are integrating Soulmate into a rails app, an alternative to launching a separate 'soulmate-web' server is to mount the sinatra app inside of rails.
|
171
|
-
|
172
|
-
Add this to routes.rb:
|
173
|
-
|
174
|
-
mount Soulmate::Server, :at => "/sm"
|
175
|
-
|
176
|
-
Add this to gemfile:
|
177
|
-
|
178
|
-
gem 'rack-contrib'
|
179
|
-
gem 'soulmate', :require => 'soulmate/server'
|
180
|
-
|
181
|
-
Then you can query soulmate at the /sm url, for example: http://localhost:3000/sm/search?types[]=venues&limit=6&term=kitten
|
182
|
-
|
183
|
-
You can also config your redis instance:
|
184
|
-
|
185
|
-
# config/initializers/soulmate.rb
|
186
|
-
|
187
|
-
Soulmate.redis = 'redis://127.0.0.1:6379/0'
|
188
|
-
# or you can asign an existing instance of Redis, Redis::Namespace, etc.
|
189
|
-
# Soulmate.redis = $redis
|
190
|
-
|
191
|
-
### Rendering an autocompleter
|
192
|
-
|
193
|
-
Soulmate doesn't include any client-side code necessary to render an autocompleter, but Mitch Crowe put together a pretty cool looking jquery plugin designed for exactly that: <a href="https://github.com/mcrowe/soulmate.js">soulmate.js</a>.
|
194
|
-
|
195
|
-
Contributing to soulmate
|
196
|
-
------------------------
|
197
|
-
|
198
|
-
* Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
|
199
|
-
* Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
|
200
|
-
* Fork the project
|
201
|
-
* Start a feature/bugfix branch
|
202
|
-
* Commit and push until you are happy with your contribution
|
203
|
-
* 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.
|
29
|
+
[There is issue for improving documentation](../../issues/1) because the documentation needs to be improved. Also, for serious, check out [sethherr.github.io/soulheart/](https://sethherr.github.io/soulheart/).
|
204
30
|
|
205
|
-
|
206
|
-
---------
|
31
|
+
=====
|
207
32
|
|
208
|
-
Copyright (c) 2011 Eric Waller. See LICENSE.txt for further details.
|
209
33
|
|
34
|
+
This is a fork of [Soulmate](https://github.com/seatgeek/soulmate), it's goal isn't backward compatibility.
|
data/bin/soulheart
CHANGED
@@ -38,10 +38,8 @@ parser = OptionParser.new do |opts|
|
|
38
38
|
|
39
39
|
opts.separator ''
|
40
40
|
opts.separator 'Commands:'
|
41
|
-
opts.separator ' load
|
42
|
-
opts.separator
|
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 ' query TYPE QUERY Queries for items from collection specified by TYPE.'
|
41
|
+
opts.separator ' load FILE Loads data from a FILE - can be a local file or a url. Accepted formats are .json, .tsv and .csv'
|
42
|
+
opts.separator " reset FILE Removes all existing data, then runs load on the FILE."
|
45
43
|
end
|
46
44
|
|
47
45
|
|
@@ -91,59 +89,19 @@ def load(file)
|
|
91
89
|
puts "Loaded a total of #{count} items in #{Time.now.to_i - start_time} second(s)"
|
92
90
|
end
|
93
91
|
|
94
|
-
def
|
95
|
-
|
96
|
-
loader = Soulheart::Loader.new(type)
|
97
|
-
items = $stdin.read.split("\n").map { |l| MultiJson.decode(l) }
|
98
|
-
items.each do |item|
|
99
|
-
loader.add(item)
|
100
|
-
end
|
101
|
-
puts "Loaded a total of #{items.size} items"
|
102
|
-
end
|
103
|
-
|
104
|
-
def remove(type)
|
105
|
-
puts "Removing items of type #{type}..."
|
106
|
-
loader = Soulheart::Loader.new(type)
|
107
|
-
items = $stdin.read.split("\n").map { |l| MultiJson.decode(l) }
|
108
|
-
items.each do |item|
|
109
|
-
loader.remove(item)
|
110
|
-
end
|
111
|
-
puts "Removed a total of #{items.size} items"
|
112
|
-
end
|
113
|
-
|
114
|
-
def query(type, query)
|
115
|
-
puts "> Querying '#{type}' for '#{query}'"
|
116
|
-
matcher = Soulheart::Matcher.new(type)
|
117
|
-
results = matcher.matches_for_term(query, limit: 0)
|
118
|
-
results.each do |item|
|
119
|
-
puts MultiJson.encode(item)
|
120
|
-
end
|
121
|
-
puts "> Found #{results.size} matches"
|
122
|
-
end
|
123
|
-
|
124
|
-
def gen_redis_proto(*cmd)
|
125
|
-
proto = '*' + cmd.length.to_s + "\r\n"
|
126
|
-
cmd.each{|arg|
|
127
|
-
proto << '$' + arg.bytesize.to_s + "\r\n"
|
128
|
-
proto << arg + "\r\n"
|
129
|
-
}
|
130
|
-
proto
|
92
|
+
def clear
|
93
|
+
Soulheart::Loader.new.clear
|
131
94
|
end
|
132
95
|
|
133
96
|
parser.parse!
|
134
97
|
BATCH_SIZE ||= 1000
|
135
98
|
|
136
99
|
case ARGV[0]
|
137
|
-
when 'generate'
|
138
|
-
generate ARGV[1], ARGV[2]
|
139
100
|
when 'load'
|
140
101
|
load ARGV[1]
|
141
|
-
when '
|
142
|
-
|
143
|
-
|
144
|
-
remove ARGV[1]
|
145
|
-
when 'query'
|
146
|
-
query ARGV[1], ARGV[2]
|
102
|
+
when 'reset'
|
103
|
+
clear
|
104
|
+
load ARGV[1]
|
147
105
|
else
|
148
106
|
puts parser.help
|
149
107
|
end
|
data/lib/soulheart/loader.rb
CHANGED
@@ -15,6 +15,7 @@ module Soulheart
|
|
15
15
|
end
|
16
16
|
|
17
17
|
def delete_categories
|
18
|
+
redis.expire category_combos_id, 0
|
18
19
|
redis.expire categories_id, 0
|
19
20
|
end
|
20
21
|
|
@@ -23,27 +24,39 @@ module Soulheart
|
|
23
24
|
redis.sadd categories_id, categories
|
24
25
|
end
|
25
26
|
|
26
|
-
def delete_data
|
27
|
+
def delete_data(id="#{base_id}:")
|
27
28
|
# delete the sorted sets for this type
|
28
29
|
phrases = redis.smembers(base_id)
|
29
30
|
redis.pipelined do
|
30
31
|
phrases.each do |p|
|
31
|
-
redis.del("#{
|
32
|
+
redis.del("#{id}#{p}")
|
32
33
|
end
|
33
|
-
redis.del(
|
34
|
+
redis.del(id)
|
34
35
|
end
|
35
36
|
|
36
|
-
# Redis can continue serving cached requests
|
37
|
-
#
|
37
|
+
# Redis can continue serving cached requests while the reload is
|
38
|
+
# occurring. Some requests may be cached incorrectly as empty set (for requests
|
38
39
|
# which come in after the above delete, but before the loading completes). But
|
39
40
|
# everything will work itself out as soon as the cache expires again.
|
41
|
+
end
|
40
42
|
|
41
|
-
|
43
|
+
def remove_results_hash
|
44
|
+
# delete the data store
|
45
|
+
# We don't do this every time we clear because because it breaks the caching feature.
|
46
|
+
# The option to clear this is only called in testing right now.
|
47
|
+
# There should be an option to clear it other times though.
|
48
|
+
redis.expire results_hashes_id, 0
|
42
49
|
redis.del(results_hashes_id)
|
43
50
|
end
|
44
51
|
|
45
|
-
def
|
52
|
+
def clear(remove_results: false)
|
53
|
+
category_combos.each {|cat| delete_data(category_id(cat)) }
|
54
|
+
delete_categories
|
46
55
|
delete_data
|
56
|
+
remove_results_hash if remove_results
|
57
|
+
end
|
58
|
+
|
59
|
+
def load(items)
|
47
60
|
# Replace with item return so we know we have category_id
|
48
61
|
items.each { |item| item.replace(add_item(item)) }
|
49
62
|
set_category_combos_array.each do |category_combo|
|
@@ -87,22 +100,5 @@ module Soulheart
|
|
87
100
|
end
|
88
101
|
item
|
89
102
|
end
|
90
|
-
|
91
|
-
# remove only cares about an item's id, but for consistency takes an object
|
92
|
-
def remove(item)
|
93
|
-
prev_item = Soulheart.redis.hget(base_id, item['term'])
|
94
|
-
if prev_item
|
95
|
-
prev_item = MultiJson.decode(prev_item)
|
96
|
-
# undo the operations done in add
|
97
|
-
Soulheart.redis.pipelined do
|
98
|
-
Soulheart.redis.hdel(base_id, prev_item['term'])
|
99
|
-
phrase = ([prev_item['term']] + (prev_item['aliases'] || [])).join(' ')
|
100
|
-
prefixes_for_phrase(phrase).each do |p|
|
101
|
-
Soulheart.redis.srem(base_id, p)
|
102
|
-
Soulheart.redis.zrem("#{base_id}:#{p}", prev_item['term'])
|
103
|
-
end
|
104
|
-
end
|
105
|
-
end
|
106
|
-
end
|
107
103
|
end
|
108
104
|
end
|
data/lib/soulheart/version.rb
CHANGED