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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: da5beee409ee144651696f28780f01ac1674568c
4
- data.tar.gz: 95e3ac8b09ad14410d4aa209a3b5403ed886d601
3
+ metadata.gz: bfc0aae3285cb2efbb58eea915cde7314614c945
4
+ data.tar.gz: fa7fb254ea04ddf0c93ff28f6a5fc10f637d40f3
5
5
  SHA512:
6
- metadata.gz: 772acc69c9303650862441440df10b16509c8ed0a3935c00da3a8d80d93c9cf383ee110e7727f85dfd6d9ac0a2e5f272e8fc08f4bd22e8acc91e4826ce591dde
7
- data.tar.gz: 30de09fa646a5229a3efae3c192fa186146aa3cd5b7b732f6e5c5fa606c1a190c52b78c2a944ec5fecfd0281e20e9079220962d44afb6e0326670ce711d9d0a8
6
+ metadata.gz: 744409a98574cbf84ea2e22e7442296c7322a1ced06047a4b6346df7a9b1d8db95fa340553beb501ee3812b83f53de28935278daa0e7f112a58d377cc9f9ee83
7
+ data.tar.gz: db068029c776eb8d28e5dcc1d45fe58e09ca1fe95f9c99a26bc6063cbb58eba5d96f2524451a898b19f2b3559471b1ff4cce18b785e5061f1d633010c11ffbda
@@ -0,0 +1,10 @@
1
+ # 0.2.0
2
+
3
+ * 空白文字列の取り扱いをPOSIX文字クラスで指定
4
+ * 文字列中の連続する空白を単一の空白と見なし、かつ前後の空白を除去
5
+ * データ保存時と検索時で同一のインスタンスを使用できるように変更
6
+ * 保存及び検索メソッド名称を変更
7
+
8
+ # 0.1.0
9
+
10
+ * 基本機能のリリース
data/README.md CHANGED
@@ -34,16 +34,16 @@ $ gem install spinel
34
34
  ### データ登録 / registration
35
35
 
36
36
  ```ruby
37
- backend = Spinel.backend
38
- backend.add id: 1, body: 'and all with pearl and ruby glowing'
39
- backend.add id: 2, body: 'a yellow or orange variety of ruby spinel'
40
- backend.add id: 3, body: 'a colour called pearl yellow'
41
- backend.add id: 4, body: 'a mandarin orange net sack'
42
- backend.add id: 5, body: 'a spinel used as a gemstone usually dark red'
43
- backend.add id: 6, body: 'today is hotter than usual'
44
- backend.add id: 7, body: 'call on a person'
45
- backend.add id: 8, body: 'that gem is shining'
46
- backend.add id: 9, body: 'polish shoes to a bright shine'
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
- matcher = Spinel.matcher
59
- matcher.matches 'ruby'
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
- matcher.matches 'usu'
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
- matcher.matches 'ruby', cache: false, limit: 5
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 )
@@ -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/base"
7
- require "spinel/backend"
8
- require "spinel/matcher"
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.backend type = :default
14
- Backend.new type
13
+ def self.new type = :default
14
+ Client.new type
15
15
  end
16
16
 
17
- def self.matcher type = :default
18
- Matcher.new type
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 Base
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 base
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
- "#{cachebase}:#{words.join('|')}"
22
+ "#{Spinel.namespace}:cache:#{type}:#{words.join('|')}"
29
23
  end
30
24
  end
31
25
  end
@@ -2,10 +2,13 @@ module Spinel
2
2
  module Helper
3
3
 
4
4
  def prefixes_for_phrase(phrase)
5
- words = phrase.split(' ')
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.flatten.uniq
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
- class Backend < Base
2
+ module Indexer
3
3
 
4
- def add(doc, opts = {})
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(base_and(p), document_score(doc), id)
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(doc)
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(base_and(p), prev_id)
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
@@ -1,6 +1,6 @@
1
1
  module Spinel
2
2
  MAJOR = 0
3
- MINOR = 1
3
+ MINOR = 2
4
4
  PATCH = 0
5
5
  VERSION = [MAJOR, MINOR, PATCH].compact.join('.')
6
6
  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.1.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-02 00:00:00.000000000 Z
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/backend.rb
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/matcher.rb
86
+ - lib/spinel/indexer.rb
87
+ - lib/spinel/searcher.rb
87
88
  - lib/spinel/version.rb
88
89
  - spinel.gemspec
89
90
  homepage: ''
@@ -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