spinel 0.1.0 → 0.2.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/CHANGELOG.md +10 -0
- data/README.md +141 -14
- data/lib/spinel.rb +13 -7
- data/lib/spinel/{base.rb → client.rb} +6 -12
- data/lib/spinel/helper.rb +6 -3
- data/lib/spinel/{backend.rb → indexer.rb} +5 -5
- data/lib/spinel/searcher.rb +24 -0
- data/lib/spinel/version.rb +1 -1
- metadata +6 -5
- data/lib/spinel/matcher.rb +0 -32
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bfc0aae3285cb2efbb58eea915cde7314614c945
|
4
|
+
data.tar.gz: fa7fb254ea04ddf0c93ff28f6a5fc10f637d40f3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 744409a98574cbf84ea2e22e7442296c7322a1ced06047a4b6346df7a9b1d8db95fa340553beb501ee3812b83f53de28935278daa0e7f112a58d377cc9f9ee83
|
7
|
+
data.tar.gz: db068029c776eb8d28e5dcc1d45fe58e09ca1fe95f9c99a26bc6063cbb58eba5d96f2524451a898b19f2b3559471b1ff4cce18b785e5061f1d633010c11ffbda
|
data/CHANGELOG.md
ADDED
data/README.md
CHANGED
@@ -34,16 +34,16 @@ $ gem install spinel
|
|
34
34
|
### データ登録 / registration
|
35
35
|
|
36
36
|
```ruby
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
37
|
+
spinel = Spinel.new
|
38
|
+
spinel.store id: 1, body: 'and all with pearl and ruby glowing'
|
39
|
+
spinel.store id: 2, body: 'a yellow or orange variety of ruby spinel'
|
40
|
+
spinel.store id: 3, body: 'a colour called pearl yellow'
|
41
|
+
spinel.store id: 4, body: 'a mandarin orange net sack'
|
42
|
+
spinel.store id: 5, body: 'a spinel used as a gemstone usually dark red'
|
43
|
+
spinel.store id: 6, body: 'today is hotter than usual'
|
44
|
+
spinel.store id: 7, body: 'call on a person'
|
45
|
+
spinel.store id: 8, body: 'that gem is shining'
|
46
|
+
spinel.store id: 9, body: 'polish shoes to a bright shine'
|
47
47
|
```
|
48
48
|
|
49
49
|
データの登録時には最低限の要素として `id` 及び `body` が必要になります。
|
@@ -52,13 +52,15 @@ backend.add id: 9, body: 'polish shoes to a bright shine'
|
|
52
52
|
`score` がキー含まれていた場合は特別に処理されます。
|
53
53
|
`score`はドキュメントの優先度を指定できるキーであり、検索結果の順序に関係します。
|
54
54
|
|
55
|
+
`id`, `body`, `score` 以外のキーには特殊な処理は行われません、JSONに変換された後、そのまま保存されます。
|
56
|
+
|
55
57
|
### 検索 / search
|
56
58
|
|
57
59
|
```ruby
|
58
|
-
|
59
|
-
|
60
|
+
spinel = Spinel.new
|
61
|
+
spinel.search 'ruby'
|
60
62
|
# => [{"id"=>2, "body"=>"a yellow or orange variety of ruby spinel"}, {"id"=>1, "body"=>"and all with pearl and ruby glowing"}]
|
61
|
-
|
63
|
+
spinel.search 'usu'
|
62
64
|
# => [{"id"=>6, "body"=>"today is hotter than usual"}, {"id"=>5, "body"=>"a spinel used as a gemstone usually dark red"}]
|
63
65
|
```
|
64
66
|
|
@@ -102,15 +104,140 @@ end
|
|
102
104
|
キャッシュの使用と検索候補数は検索時にオプションとして値を指定することも可能です。
|
103
105
|
|
104
106
|
```ruby
|
105
|
-
|
107
|
+
spinel.search 'ruby', cache: false, limit: 5
|
108
|
+
```
|
109
|
+
|
110
|
+
#### 名前空間
|
111
|
+
|
112
|
+
Spinelは複数階層の名前空間をサポートします。
|
113
|
+
SpinelはRedisへのアクセスに `spinel:index:default` のようなキーを用います。
|
114
|
+
これを `#{spinel_namespaace}:index:#{index_type}` と見なしたとき、
|
115
|
+
`#{spinel_namespaace}` 及び `#{index_type}` は変更可能です。
|
116
|
+
|
117
|
+
上位の `#{spinel_namespaace}` は configure によって指定可能です。
|
118
|
+
|
119
|
+
```
|
120
|
+
Spinel.configure do |config|
|
121
|
+
config.namespace = 'spinel'
|
122
|
+
end
|
123
|
+
```
|
124
|
+
|
125
|
+
下位の `#{index_type}` はデータ登録時及び検索時に指定を変更することが可能です。
|
126
|
+
|
127
|
+
```
|
128
|
+
spinel = Spinel.new(:another_type)
|
106
129
|
```
|
107
130
|
|
131
|
+
`#{spinel_namespaace}` よりも上位で名前空間を分割したい場合には [resque/redis-namespace](https://github.com/resque/redis-namespace) を併用してください。
|
132
|
+
|
108
133
|
## バージョニング / Versioning
|
109
134
|
|
110
135
|
Spinelのバージョニングは[Semantic Versioning 2.0.0](http://semver.org/)に基づいて採番されます。
|
111
136
|
現在Spinelは開発初期段階です。
|
112
137
|
いつでも、いかなる変更も起こりうります。
|
113
138
|
|
139
|
+
## どのように活用できるか
|
140
|
+
|
141
|
+
例えばあなたが住所入力のフォームにインクリメンタルサーチを導入しようとしたとき、Spinelは良い選択肢になり得ます。
|
142
|
+
以下では、[郵便番号データのダウンロード - zipcloud](http://zipcloud.ibsnet.co.jp/)の都道府県データを検索する例を示しています。
|
143
|
+
|
144
|
+
この例では、Spinel以外に半角カタカナをひらがなに変換するために[gimite/moji](https://github.com/gimite/moji)ライブラリを使用しています。
|
145
|
+
インデキシングさせる情報(`body`キー)には、郵便番号、都道府県及び都道府県の読み仮名を含めており、
|
146
|
+
データは12万5094件存在します。
|
147
|
+
|
148
|
+
```ruby
|
149
|
+
require 'spinel'
|
150
|
+
require 'moji'
|
151
|
+
require 'csv'
|
152
|
+
|
153
|
+
header = [
|
154
|
+
:jis_x0401, # 全国地方公共団体コード
|
155
|
+
:old_code, # (旧)郵便番号(5桁)
|
156
|
+
:code, # 郵便番号(7桁)
|
157
|
+
:pref_kana, # 都道府県名カタカナ
|
158
|
+
:city_kana, # 市区町村名カタカナ
|
159
|
+
:town_kana, # 町域名カタカナ
|
160
|
+
:pref, # 都道府県名
|
161
|
+
:city, # 市区町村名
|
162
|
+
:town, # 町域名
|
163
|
+
:flag1, # 一町域が二以上の郵便番号で表される場合の表示
|
164
|
+
:flag2, # 小字毎に番地が起番されている町域の表示
|
165
|
+
:flag3, # 丁目を有する町域の場合の表示
|
166
|
+
:flag4, # 一つの郵便番号で二以上の町域を表す場合の表示
|
167
|
+
:flag5, # 更新の表示
|
168
|
+
:flag6 # 変更理由
|
169
|
+
]
|
170
|
+
|
171
|
+
import_data = []
|
172
|
+
|
173
|
+
puts 'data converting...'
|
174
|
+
t1 = Time.now
|
175
|
+
CSV.foreach('x-ken-all.csv') do |row|
|
176
|
+
hash = header.zip(row).to_h
|
177
|
+
hash[:pref_kana] = Moji.kata_to_hira(Moji.han_to_zen(hash[:pref_kana]))
|
178
|
+
hash[:city_kana] = Moji.kata_to_hira(Moji.han_to_zen(hash[:city_kana]))
|
179
|
+
hash[:town_kana] = Moji.kata_to_hira(Moji.han_to_zen(hash[:town_kana]))
|
180
|
+
doc = {
|
181
|
+
id: hash[:code],
|
182
|
+
body: [hash[:code], hash[:pref], hash[:city], hash[:town], hash[:pref_kana], hash[:city_kana], hash[:town_kana]].join(' '),
|
183
|
+
raw_data: hash
|
184
|
+
}
|
185
|
+
import_data << doc
|
186
|
+
end
|
187
|
+
t2 = Time.now
|
188
|
+
|
189
|
+
puts "convert done #{t2 - t1}s"
|
190
|
+
puts "data importing..."
|
191
|
+
|
192
|
+
spinel = Spinel.new
|
193
|
+
import_data.each do |doc|
|
194
|
+
spinel.store doc
|
195
|
+
end
|
196
|
+
|
197
|
+
t3 = Time.now
|
198
|
+
puts "import done #{t3 - t2}s"
|
199
|
+
|
200
|
+
# data converting...
|
201
|
+
# convert done 53.303588s
|
202
|
+
# data importing...
|
203
|
+
# import done 188.305489s
|
204
|
+
```
|
205
|
+
|
206
|
+
MacBook Air(1.7 GHz Intel Core i7, 8 GB 1600 MHz DDR3) でデータを投入したとき、
|
207
|
+
12万件のインポートに約3分かかりました。
|
208
|
+
検索には郵便番号、都道府県、読み仮名を組み合わせることが可能で、検索は非常に軽快です。
|
209
|
+
|
210
|
+
```ruby
|
211
|
+
> spinel.search '014'
|
212
|
+
=> [{"id"=>"0141413",
|
213
|
+
"body"=>"0141413 秋田県 大仙市 角間川町 あきたけん だいせんし かくまがわまち", ...
|
214
|
+
|
215
|
+
> spinel.search '014 ろくごう'
|
216
|
+
=> [{"id"=>"0141411",
|
217
|
+
"body"=>"0141411 秋田県 大仙市 六郷西根 あきたけん だいせんし ろくごうにしね", ...
|
218
|
+
|
219
|
+
> spinel.search 'とうき'
|
220
|
+
=> [
|
221
|
+
{"id"=>"5998242",
|
222
|
+
"body"=>"5998242 大阪府 堺市中区 陶器北 おおさかふ さかいしなかく とうききた", ...
|
223
|
+
{"id"=>"2892254",
|
224
|
+
"body"=>"2892254 千葉県 香取郡多古町 東輝 ちばけん かとりぐんたこまち とうき", ...
|
225
|
+
{"id"=>"2080035",
|
226
|
+
"body"=>"2080035 東京都 武蔵村山市 中原 とうきょうと むさしむらやまし なかはら", ...
|
227
|
+
|
228
|
+
> spinel.search 'とうきょう'
|
229
|
+
=> [{"id"=>"2080035",
|
230
|
+
"body"=>"2080035 東京都 武蔵村山市 中原 とうきょうと むさしむらやまし なかはら", ...
|
231
|
+
|
232
|
+
> spinel.search 'とうきょう しぶや'
|
233
|
+
=> [{"id"=>"1510073",
|
234
|
+
"body"=>"1510073 東京都 渋谷区 笹塚 とうきょうと しぶやく ささづか", ...
|
235
|
+
|
236
|
+
> spinel.search 'とうきょう しぶや よよぎ'
|
237
|
+
=> [{"id"=>"1510053",
|
238
|
+
"body"=>"1510053 東京都 渋谷区 代々木 とうきょうと しぶやく よよぎ", ...
|
239
|
+
```
|
240
|
+
|
114
241
|
## Contributing
|
115
242
|
|
116
243
|
1. Fork it ( https://github.com/k-shogo/spinel/fork )
|
data/lib/spinel.rb
CHANGED
@@ -3,18 +3,24 @@ require 'multi_json'
|
|
3
3
|
require "spinel/version"
|
4
4
|
require "spinel/config"
|
5
5
|
require "spinel/helper"
|
6
|
-
require "spinel/
|
7
|
-
require "spinel/
|
8
|
-
require "spinel/
|
6
|
+
require "spinel/indexer"
|
7
|
+
require "spinel/searcher"
|
8
|
+
require "spinel/client"
|
9
9
|
|
10
10
|
module Spinel
|
11
11
|
extend Config
|
12
12
|
|
13
|
-
def self.
|
14
|
-
|
13
|
+
def self.new type = :default
|
14
|
+
Client.new type
|
15
15
|
end
|
16
16
|
|
17
|
-
def self.
|
18
|
-
|
17
|
+
def self.method_missing(method_name, *args, type: :default, &block)
|
18
|
+
return super unless new(type).respond_to?(method_name)
|
19
|
+
new(type).send(method_name, *args, &block)
|
19
20
|
end
|
21
|
+
|
22
|
+
def self.respond_to?(method_name, include_private = false)
|
23
|
+
new.respond_to?(method_name, include_private) || super
|
24
|
+
end
|
25
|
+
|
20
26
|
end
|
@@ -1,6 +1,8 @@
|
|
1
1
|
module Spinel
|
2
|
-
class
|
2
|
+
class Client
|
3
3
|
include Helper
|
4
|
+
include Indexer
|
5
|
+
include Searcher
|
4
6
|
|
5
7
|
attr_accessor :type
|
6
8
|
|
@@ -8,24 +10,16 @@ module Spinel
|
|
8
10
|
@type = type
|
9
11
|
end
|
10
12
|
|
11
|
-
def
|
12
|
-
"#{Spinel.namespace}:index:#{type}"
|
13
|
-
end
|
14
|
-
|
15
|
-
def base_and p
|
16
|
-
"#{base}:#{p}"
|
13
|
+
def index p
|
14
|
+
"#{Spinel.namespace}:index:#{type}:#{p}"
|
17
15
|
end
|
18
16
|
|
19
17
|
def database
|
20
18
|
"#{Spinel.namespace}:data:#{type}"
|
21
19
|
end
|
22
20
|
|
23
|
-
def cachebase
|
24
|
-
"#{Spinel.namespace}:cache:#{type}"
|
25
|
-
end
|
26
|
-
|
27
21
|
def cachekey words
|
28
|
-
"#{
|
22
|
+
"#{Spinel.namespace}:cache:#{type}:#{words.join('|')}"
|
29
23
|
end
|
30
24
|
end
|
31
25
|
end
|
data/lib/spinel/helper.rb
CHANGED
@@ -2,10 +2,13 @@ module Spinel
|
|
2
2
|
module Helper
|
3
3
|
|
4
4
|
def prefixes_for_phrase(phrase)
|
5
|
-
|
6
|
-
words.map do |w|
|
5
|
+
squish(phrase).split.flat_map do |w|
|
7
6
|
(Spinel.min_complete-1..(w.length-1)).map{ |l| w[0..l] }
|
8
|
-
end.
|
7
|
+
end.uniq
|
8
|
+
end
|
9
|
+
|
10
|
+
def squish str
|
11
|
+
str.to_s.gsub(/[[:space:]]+/, ' ').strip
|
9
12
|
end
|
10
13
|
|
11
14
|
def document_validate doc
|
@@ -1,7 +1,7 @@
|
|
1
1
|
module Spinel
|
2
|
-
|
2
|
+
module Indexer
|
3
3
|
|
4
|
-
def
|
4
|
+
def store doc, opts = {}
|
5
5
|
opts = { skip_duplicate_check: false }.merge(opts)
|
6
6
|
document_validate doc
|
7
7
|
id = document_id doc
|
@@ -11,7 +11,7 @@ module Spinel
|
|
11
11
|
Spinel.redis.pipelined do
|
12
12
|
Spinel.redis.hset(database, id, MultiJson.encode(doc))
|
13
13
|
prefixes_for_phrase(document_body(doc)).each do |p|
|
14
|
-
Spinel.redis.zadd(
|
14
|
+
Spinel.redis.zadd(index(p), document_score(doc), id)
|
15
15
|
end
|
16
16
|
end
|
17
17
|
end
|
@@ -22,14 +22,14 @@ module Spinel
|
|
22
22
|
end
|
23
23
|
end
|
24
24
|
|
25
|
-
def remove
|
25
|
+
def remove doc
|
26
26
|
if prev_doc = Spinel.redis.hget(database, document_id(doc))
|
27
27
|
prev_doc = MultiJson.decode(prev_doc)
|
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_for_phrase(document_body(prev_doc)).each do |p|
|
32
|
-
Spinel.redis.zrem(
|
32
|
+
Spinel.redis.zrem(index(p), prev_id)
|
33
33
|
end
|
34
34
|
end
|
35
35
|
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Spinel
|
2
|
+
module Searcher
|
3
|
+
|
4
|
+
def search term, options = {}
|
5
|
+
options = { limit: Spinel.match_limit, cache: true }.merge(options)
|
6
|
+
|
7
|
+
words = squish(term).split.reject{|w| w.size < Spinel.min_complete}.sort
|
8
|
+
return [] if words.empty?
|
9
|
+
|
10
|
+
tmp_cachekey = cachekey(words)
|
11
|
+
|
12
|
+
unless options[:cache] && Spinel.redis.exists(tmp_cachekey)
|
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
|
17
|
+
|
18
|
+
ids = Spinel.redis.zrevrange(tmp_cachekey, 0, options[:limit] - 1)
|
19
|
+
ids.empty? ? [] : Spinel.redis.hmget(database, *ids).compact.map{|json| MultiJson.decode(json)}
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
data/lib/spinel/version.rb
CHANGED
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.2.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-03 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: redis
|
@@ -74,16 +74,17 @@ extensions: []
|
|
74
74
|
extra_rdoc_files: []
|
75
75
|
files:
|
76
76
|
- ".gitignore"
|
77
|
+
- CHANGELOG.md
|
77
78
|
- Gemfile
|
78
79
|
- LICENSE.txt
|
79
80
|
- README.md
|
80
81
|
- Rakefile
|
81
82
|
- lib/spinel.rb
|
82
|
-
- lib/spinel/
|
83
|
-
- lib/spinel/base.rb
|
83
|
+
- lib/spinel/client.rb
|
84
84
|
- lib/spinel/config.rb
|
85
85
|
- lib/spinel/helper.rb
|
86
|
-
- lib/spinel/
|
86
|
+
- lib/spinel/indexer.rb
|
87
|
+
- lib/spinel/searcher.rb
|
87
88
|
- lib/spinel/version.rb
|
88
89
|
- spinel.gemspec
|
89
90
|
homepage: ''
|
data/lib/spinel/matcher.rb
DELETED
@@ -1,32 +0,0 @@
|
|
1
|
-
module Spinel
|
2
|
-
class Matcher < Base
|
3
|
-
|
4
|
-
def matches(term, options = {})
|
5
|
-
options = { limit: Spinel.match_limit, cache: true }.merge(options)
|
6
|
-
|
7
|
-
words = term.split(' ').reject do |w|
|
8
|
-
w.size < Spinel.min_complete
|
9
|
-
end.sort
|
10
|
-
|
11
|
-
return [] if words.empty?
|
12
|
-
|
13
|
-
tmp_cachekey = cachekey(words)
|
14
|
-
|
15
|
-
if !options[:cache] || !Spinel.redis.exists(tmp_cachekey) || Spinel.redis.exists(tmp_cachekey) == 0
|
16
|
-
interkeys = words.map { |w| base_and w }
|
17
|
-
Spinel.redis.zinterstore(tmp_cachekey, interkeys)
|
18
|
-
Spinel.redis.expire(tmp_cachekey, Spinel.cache_expire)
|
19
|
-
end
|
20
|
-
|
21
|
-
ids = Spinel.redis.zrevrange(tmp_cachekey, 0, options[:limit] - 1)
|
22
|
-
if ids.size > 0
|
23
|
-
results = Spinel.redis.hmget(database, *ids)
|
24
|
-
results = results.reject{ |r| r.nil? }
|
25
|
-
results.map { |r| MultiJson.decode(r) }
|
26
|
-
else
|
27
|
-
[]
|
28
|
-
end
|
29
|
-
end
|
30
|
-
|
31
|
-
end
|
32
|
-
end
|