spinel 0.2.0 → 0.3.0
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/.gitignore +1 -1
- data/CHANGELOG.md +4 -0
- data/README.md +6 -2
- data/Rakefile +5 -0
- data/lib/spinel/config.rb +7 -8
- data/lib/spinel/helper.rb +10 -6
- data/lib/spinel/indexer.rb +5 -5
- data/lib/spinel/searcher.rb +20 -8
- data/lib/spinel/version.rb +1 -1
- data/spinel.gemspec +6 -0
- data/test/spinel_test.rb +120 -0
- data/test/test_helper.rb +39 -0
- metadata +91 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 15242d0a0aa570e1cdb2833f088015b8a123f728
|
4
|
+
data.tar.gz: 065227c4062d41de4e360ce359ad6d18a2119b41
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0cedf931a094181409bf4bda8fd05267b3da5aefa860f3d3c78fda7ba61575cebaacc871bab8cc8247991a00e85884887374eff45b2bdfbe2fbe805239573c98
|
7
|
+
data.tar.gz: a7e79c12d900d2f6030c6d95366aea711b1dea944d8d5649ecc583e76443e174e2de1d5e853ba5e3f1fb5a0270ab3aa7da39ed3c26c9fa5cadea12d2d03b4fc5
|
data/.gitignore
CHANGED
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -1,5 +1,9 @@
|
|
1
1
|
# Spinel
|
2
2
|
|
3
|
+
[](http://badge.fury.io/rb/spinel)
|
4
|
+
[](https://codeclimate.com/github/k-shogo/spinel)
|
5
|
+
[](https://codeclimate.com/github/k-shogo/spinel)
|
6
|
+
|
3
7
|
Spinel is Redis based lightweight text search engine.
|
4
8
|
SpinelはRedisをバックエンドに用いた軽量なテキスト検索システムです。
|
5
9
|
|
@@ -72,9 +76,9 @@ spinel.search 'usu'
|
|
72
76
|
```ruby
|
73
77
|
Spinel.configure do |config|
|
74
78
|
config.redis = 'redis://127.0.0.1:6379/0'
|
75
|
-
config.
|
79
|
+
config.minimal_word = 2
|
76
80
|
config.cache_expire = 600
|
77
|
-
config.
|
81
|
+
config.search_limit = 10
|
78
82
|
config.document_key = :body
|
79
83
|
config.namespace = 'spinel'
|
80
84
|
end
|
data/Rakefile
CHANGED
data/lib/spinel/config.rb
CHANGED
@@ -1,23 +1,23 @@
|
|
1
1
|
module Spinel
|
2
2
|
module Config
|
3
|
-
|
3
|
+
DEFAULT_MINIMAL_WORD = 2
|
4
4
|
DEFAULT_CACHE_EXPIRE = 600
|
5
|
-
|
5
|
+
DEFAULT_SEARCH_LIMIT = 10
|
6
6
|
DEFAULT_DOCUMENT_KEY = :body
|
7
7
|
DEFAULT_NAMESPACE = 'spinel'
|
8
8
|
|
9
|
-
attr_writer :
|
9
|
+
attr_writer :minimal_word, :cache_expire, :search_limit, :document_key, :namespace
|
10
10
|
|
11
|
-
def
|
12
|
-
@
|
11
|
+
def minimal_word
|
12
|
+
@minimal_word ||= DEFAULT_MINIMAL_WORD
|
13
13
|
end
|
14
14
|
|
15
15
|
def cache_expire
|
16
16
|
@cache_expire ||= DEFAULT_CACHE_EXPIRE
|
17
17
|
end
|
18
18
|
|
19
|
-
def
|
20
|
-
@
|
19
|
+
def search_limit
|
20
|
+
@search_limit ||= DEFAULT_SEARCH_LIMIT
|
21
21
|
end
|
22
22
|
|
23
23
|
def document_key
|
@@ -35,7 +35,6 @@ module Spinel
|
|
35
35
|
else
|
36
36
|
@redis = server
|
37
37
|
end
|
38
|
-
|
39
38
|
redis
|
40
39
|
end
|
41
40
|
|
data/lib/spinel/helper.rb
CHANGED
@@ -1,9 +1,9 @@
|
|
1
1
|
module Spinel
|
2
2
|
module Helper
|
3
3
|
|
4
|
-
def
|
5
|
-
squish(
|
6
|
-
(Spinel.
|
4
|
+
def prefixes str
|
5
|
+
squish(str).split.flat_map do |w|
|
6
|
+
(Spinel.minimal_word-1..(w.length-1)).map{ |l| w[0..l] }
|
7
7
|
end.uniq
|
8
8
|
end
|
9
9
|
|
@@ -11,8 +11,12 @@ module Spinel
|
|
11
11
|
str.to_s.gsub(/[[:space:]]+/, ' ').strip
|
12
12
|
end
|
13
13
|
|
14
|
-
def
|
15
|
-
|
14
|
+
def get_valid_document doc
|
15
|
+
id = document_id doc
|
16
|
+
body = document_body doc
|
17
|
+
score = document_score doc
|
18
|
+
raise ArgumentError, "documents must specify both an id and a body" unless id && body
|
19
|
+
[id, body, score]
|
16
20
|
end
|
17
21
|
|
18
22
|
def document_id doc
|
@@ -20,7 +24,7 @@ module Spinel
|
|
20
24
|
end
|
21
25
|
|
22
26
|
def document_score doc
|
23
|
-
doc[:score] || doc["score"]
|
27
|
+
(doc[:score] || doc["score"]).to_f
|
24
28
|
end
|
25
29
|
|
26
30
|
def document_body doc
|
data/lib/spinel/indexer.rb
CHANGED
@@ -3,17 +3,17 @@ module Spinel
|
|
3
3
|
|
4
4
|
def store doc, opts = {}
|
5
5
|
opts = { skip_duplicate_check: false }.merge(opts)
|
6
|
-
|
7
|
-
id = document_id doc
|
6
|
+
id, body, score = get_valid_document doc
|
8
7
|
|
9
8
|
remove(id: id) unless opts[:skip_duplicate_check]
|
10
9
|
|
11
10
|
Spinel.redis.pipelined do
|
12
11
|
Spinel.redis.hset(database, id, MultiJson.encode(doc))
|
13
|
-
|
14
|
-
Spinel.redis.zadd(index(p),
|
12
|
+
prefixes(body).each do |p|
|
13
|
+
Spinel.redis.zadd(index(p), score, id)
|
15
14
|
end
|
16
15
|
end
|
16
|
+
doc
|
17
17
|
end
|
18
18
|
|
19
19
|
def get id
|
@@ -28,7 +28,7 @@ module Spinel
|
|
28
28
|
prev_id = document_id prev_doc
|
29
29
|
Spinel.redis.pipelined do
|
30
30
|
Spinel.redis.hdel(database, prev_id)
|
31
|
-
|
31
|
+
prefixes(document_body(prev_doc)).each do |p|
|
32
32
|
Spinel.redis.zrem(index(p), prev_id)
|
33
33
|
end
|
34
34
|
end
|
data/lib/spinel/searcher.rb
CHANGED
@@ -2,22 +2,34 @@ module Spinel
|
|
2
2
|
module Searcher
|
3
3
|
|
4
4
|
def search term, options = {}
|
5
|
-
options =
|
6
|
-
|
7
|
-
words = squish(term).split.reject{|w| w.size < Spinel.min_complete}.sort
|
5
|
+
options = search_option options
|
6
|
+
words = search_word_split term
|
8
7
|
return [] if words.empty?
|
9
8
|
|
10
9
|
tmp_cachekey = cachekey(words)
|
11
10
|
|
12
|
-
unless options[:cache] &&
|
13
|
-
interkeys = words.map{ |w| index w }
|
14
|
-
Spinel.redis.zinterstore(tmp_cachekey, interkeys)
|
15
|
-
Spinel.redis.expire(tmp_cachekey, Spinel.cache_expire)
|
16
|
-
end
|
11
|
+
set_cache(tmp_cachekey, words) unless options[:cache] && cache_enable(tmp_cachekey)
|
17
12
|
|
18
13
|
ids = Spinel.redis.zrevrange(tmp_cachekey, 0, options[:limit] - 1)
|
19
14
|
ids.empty? ? [] : Spinel.redis.hmget(database, *ids).compact.map{|json| MultiJson.decode(json)}
|
15
|
+
end
|
16
|
+
|
17
|
+
def search_option options = {}
|
18
|
+
{ limit: Spinel.minimal_word, cache: true }.merge(options)
|
19
|
+
end
|
20
|
+
|
21
|
+
def search_word_split word
|
22
|
+
squish(word).split.reject{|w| w.size < Spinel.minimal_word}.sort
|
23
|
+
end
|
24
|
+
|
25
|
+
def set_cache key, words
|
26
|
+
interkeys = words.map{ |w| index w }
|
27
|
+
Spinel.redis.zinterstore(key, interkeys)
|
28
|
+
Spinel.redis.expire(key, Spinel.cache_expire)
|
29
|
+
end
|
20
30
|
|
31
|
+
def cache_enable key
|
32
|
+
Spinel.redis.exists(key)
|
21
33
|
end
|
22
34
|
|
23
35
|
end
|
data/lib/spinel/version.rb
CHANGED
data/spinel.gemspec
CHANGED
@@ -23,4 +23,10 @@ Gem::Specification.new do |spec|
|
|
23
23
|
|
24
24
|
spec.add_development_dependency "bundler", "~> 1.7"
|
25
25
|
spec.add_development_dependency "rake", "~> 10.0"
|
26
|
+
spec.add_development_dependency "minitest", ">= 5.4"
|
27
|
+
spec.add_development_dependency "minitest-power_assert"
|
28
|
+
spec.add_development_dependency "simplecov"
|
29
|
+
spec.add_development_dependency "codeclimate-test-reporter"
|
30
|
+
spec.add_development_dependency "dotenv"
|
31
|
+
spec.add_development_dependency "mock_redis"
|
26
32
|
end
|
data/test/spinel_test.rb
ADDED
@@ -0,0 +1,120 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class SpinelTest < Minitest::Test
|
4
|
+
def setup
|
5
|
+
@spinel = Spinel.new
|
6
|
+
end
|
7
|
+
|
8
|
+
def test_redis
|
9
|
+
assert { 'PONG' == Spinel.redis.ping }
|
10
|
+
|
11
|
+
Spinel.redis = 'redis://127.0.0.1:6379/0'
|
12
|
+
assert { 'PONG' == Spinel.redis.ping }
|
13
|
+
|
14
|
+
mock_redis = MockRedis.new
|
15
|
+
Spinel.redis = mock_redis
|
16
|
+
assert { mock_redis.object_id == Spinel.redis.object_id }
|
17
|
+
end
|
18
|
+
|
19
|
+
def test_respond_to?
|
20
|
+
assert Spinel.respond_to? :store
|
21
|
+
assert Spinel.respond_to? :remove
|
22
|
+
assert Spinel.respond_to? :get
|
23
|
+
assert Spinel.respond_to? :search
|
24
|
+
end
|
25
|
+
|
26
|
+
def test_method_missing
|
27
|
+
Spinel.redis.flushall
|
28
|
+
Spinel.store({id: 1, body: 'test'},{})
|
29
|
+
assert { 1 == Spinel.redis.hlen(@spinel.database) }
|
30
|
+
end
|
31
|
+
|
32
|
+
def test_database_index
|
33
|
+
assert { 'spinel:data:default' == @spinel.database }
|
34
|
+
end
|
35
|
+
|
36
|
+
def test_index
|
37
|
+
assert { 'spinel:index:default:term' == @spinel.index(:term) }
|
38
|
+
end
|
39
|
+
|
40
|
+
def test_cachekey_index
|
41
|
+
assert { 'spinel:cache:default:word1|word2' == @spinel.cachekey(%w(word1 word2)) }
|
42
|
+
end
|
43
|
+
|
44
|
+
def test_prefixes
|
45
|
+
assert { ["ta", "tak", "take", "tal", "talk"] == @spinel.prefixes("take talk") }
|
46
|
+
end
|
47
|
+
|
48
|
+
def test_squish
|
49
|
+
assert { "quick brown fox" == @spinel.squish(" quick \t brown\n fox ") }
|
50
|
+
end
|
51
|
+
|
52
|
+
def test_get_valid_document
|
53
|
+
assert_raises(ArgumentError){ @spinel.get_valid_document({id: 1}) }
|
54
|
+
assert_raises(ArgumentError){ @spinel.get_valid_document({body: 'body'}) }
|
55
|
+
assert { [1, 'body', 0.0] == @spinel.get_valid_document({id: 1, body: 'body'}) }
|
56
|
+
end
|
57
|
+
|
58
|
+
def test_document_id
|
59
|
+
assert { "doc_id" == @spinel.document_id({id: "doc_id"}) }
|
60
|
+
assert { "doc_id" == @spinel.document_id({"id" => "doc_id"}) }
|
61
|
+
end
|
62
|
+
|
63
|
+
def test_document_score
|
64
|
+
assert { 1.0 == @spinel.document_score({score: 1}) }
|
65
|
+
assert { 1.0 == @spinel.document_score({"score" => 1}) }
|
66
|
+
assert { 0.0 == @spinel.document_score({}) }
|
67
|
+
end
|
68
|
+
|
69
|
+
def test_document_body
|
70
|
+
assert { "doc_body" == @spinel.document_body({body: "doc_body"}) }
|
71
|
+
assert { "doc_body" == @spinel.document_body({"body" => "doc_body"}) }
|
72
|
+
end
|
73
|
+
|
74
|
+
def test_minimal_word
|
75
|
+
assert { 2 == Spinel.minimal_word }
|
76
|
+
end
|
77
|
+
|
78
|
+
def test_cache_expire
|
79
|
+
assert { 600 == Spinel.cache_expire }
|
80
|
+
end
|
81
|
+
|
82
|
+
def test_search_limit
|
83
|
+
assert { 10 == Spinel.search_limit }
|
84
|
+
end
|
85
|
+
|
86
|
+
def test_document_key
|
87
|
+
assert { 'body' == Spinel.document_key }
|
88
|
+
end
|
89
|
+
|
90
|
+
def test_namespace
|
91
|
+
assert { 'spinel' == Spinel.namespace }
|
92
|
+
end
|
93
|
+
|
94
|
+
def test_store
|
95
|
+
Spinel.redis.flushall
|
96
|
+
@spinel.store id: 1, body: 'test'
|
97
|
+
assert { 1 == Spinel.redis.hlen(@spinel.database) }
|
98
|
+
end
|
99
|
+
|
100
|
+
def test_get
|
101
|
+
Spinel.redis.flushall
|
102
|
+
@spinel.store id: 1, body: 'test'
|
103
|
+
assert { {"id" => 1, "body" => 'test'} == @spinel.get(1) }
|
104
|
+
end
|
105
|
+
|
106
|
+
def test_remove
|
107
|
+
Spinel.redis.flushall
|
108
|
+
@spinel.store id: 1, body: 'test'
|
109
|
+
@spinel.remove id: 1
|
110
|
+
assert { 0 == Spinel.redis.hlen(@spinel.database) }
|
111
|
+
end
|
112
|
+
|
113
|
+
def test_search
|
114
|
+
Spinel.redis.flushall
|
115
|
+
@spinel.store id: 1, body: 'ruby'
|
116
|
+
@spinel.store id: 2, body: 'rubx'
|
117
|
+
assert { 2 == @spinel.search('rub').size }
|
118
|
+
assert { 1 == @spinel.search('ruby').size }
|
119
|
+
end
|
120
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'dotenv'
|
2
|
+
require "codeclimate-test-reporter"
|
3
|
+
require 'simplecov'
|
4
|
+
|
5
|
+
Dotenv.load
|
6
|
+
CodeClimate::TestReporter.start if ENV['CCR']
|
7
|
+
|
8
|
+
SimpleCov.start do
|
9
|
+
add_filter "vendor"
|
10
|
+
end
|
11
|
+
|
12
|
+
require 'spinel'
|
13
|
+
require 'mock_redis'
|
14
|
+
require 'minitest/autorun'
|
15
|
+
require 'minitest/unit'
|
16
|
+
require 'minitest-power_assert'
|
17
|
+
|
18
|
+
Spinel.configure do |config|
|
19
|
+
config.redis = MockRedis.new
|
20
|
+
config.minimal_word = 2
|
21
|
+
config.cache_expire = 600
|
22
|
+
config.search_limit = 10
|
23
|
+
config.document_key = :body
|
24
|
+
config.namespace = 'spinel'
|
25
|
+
end
|
26
|
+
|
27
|
+
def test_data
|
28
|
+
[
|
29
|
+
{id: 1, body: 'and all with pearl and ruby glowing'},
|
30
|
+
{id: 2, body: 'yellow or orange variety of ruby spinel'},
|
31
|
+
{id: 3, body: 'colour called pearl yellow'},
|
32
|
+
{id: 4, body: 'mandarin orange net sack'},
|
33
|
+
{id: 5, body: 'spinel used as a gemstone usually dark red'},
|
34
|
+
{id: 6, body: 'today is hotter than usual'},
|
35
|
+
{id: 7, body: 'call on a person'},
|
36
|
+
{id: 8, body: 'that gem is shining'},
|
37
|
+
{id: 9, body: 'polish shoes to a bright shine'}
|
38
|
+
]
|
39
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: spinel
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- k-shogo
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-02-
|
11
|
+
date: 2015-02-04 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: redis
|
@@ -66,6 +66,90 @@ dependencies:
|
|
66
66
|
- - "~>"
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: '10.0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: minitest
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '5.4'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '5.4'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: minitest-power_assert
|
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
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: simplecov
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: codeclimate-test-reporter
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: dotenv
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: mock_redis
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - ">="
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '0'
|
146
|
+
type: :development
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - ">="
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '0'
|
69
153
|
description: lightweight text search system on Redis
|
70
154
|
email:
|
71
155
|
- platycod0n.ramosa@gmail.com
|
@@ -87,6 +171,8 @@ files:
|
|
87
171
|
- lib/spinel/searcher.rb
|
88
172
|
- lib/spinel/version.rb
|
89
173
|
- spinel.gemspec
|
174
|
+
- test/spinel_test.rb
|
175
|
+
- test/test_helper.rb
|
90
176
|
homepage: ''
|
91
177
|
licenses:
|
92
178
|
- MIT
|
@@ -111,4 +197,6 @@ rubygems_version: 2.2.2
|
|
111
197
|
signing_key:
|
112
198
|
specification_version: 4
|
113
199
|
summary: lightweight text search system on Redis
|
114
|
-
test_files:
|
200
|
+
test_files:
|
201
|
+
- test/spinel_test.rb
|
202
|
+
- test/test_helper.rb
|