picky 2.6.0 → 2.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (90) hide show
  1. data/lib/picky/analyzer.rb +4 -4
  2. data/lib/picky/application.rb +6 -7
  3. data/lib/picky/backend/{backend.rb → base.rb} +31 -14
  4. data/lib/picky/backend/file/basic.rb +12 -4
  5. data/lib/picky/backend/file/json.rb +5 -5
  6. data/lib/picky/backend/file/text.rb +1 -1
  7. data/lib/picky/backend/files.rb +3 -9
  8. data/lib/picky/backend/redis/basic.rb +8 -0
  9. data/lib/picky/backend/redis/list_hash.rb +5 -5
  10. data/lib/picky/backend/redis/string_hash.rb +5 -5
  11. data/lib/picky/backend/redis.rb +5 -5
  12. data/lib/picky/bundle.rb +62 -0
  13. data/lib/picky/categories.rb +10 -9
  14. data/lib/picky/categories_indexed.rb +12 -7
  15. data/lib/picky/categories_indexing.rb +7 -9
  16. data/lib/picky/category.rb +38 -26
  17. data/lib/picky/category_indexed.rb +4 -20
  18. data/lib/picky/category_indexing.rb +71 -68
  19. data/lib/picky/generators/base.rb +6 -6
  20. data/lib/picky/generators/partial/substring.rb +28 -26
  21. data/lib/picky/generators/partial_generator.rb +3 -3
  22. data/lib/picky/generators/similarity/phonetic.rb +5 -5
  23. data/lib/picky/generators/similarity_generator.rb +2 -2
  24. data/lib/picky/generators/weights/logarithmic.rb +3 -3
  25. data/lib/picky/generators/weights_generator.rb +2 -2
  26. data/lib/picky/index/base.rb +13 -10
  27. data/lib/picky/index/base_indexed.rb +2 -0
  28. data/lib/picky/index/base_indexing.rb +65 -57
  29. data/lib/picky/indexed/bundle/base.rb +21 -86
  30. data/lib/picky/indexed/bundle/memory.rb +5 -12
  31. data/lib/picky/indexed/bundle/redis.rb +42 -0
  32. data/lib/picky/indexed/wrappers/bundle/wrapper.rb +3 -3
  33. data/lib/picky/indexers/base.rb +20 -3
  34. data/lib/picky/indexers/parallel.rb +32 -14
  35. data/lib/picky/indexers/serial.rb +29 -26
  36. data/lib/picky/indexes.rb +5 -3
  37. data/lib/picky/indexes_indexed.rb +3 -15
  38. data/lib/picky/indexes_indexing.rb +18 -21
  39. data/lib/picky/indexing/bundle/base.rb +64 -45
  40. data/lib/picky/indexing/bundle/memory.rb +0 -4
  41. data/lib/picky/loader.rb +7 -6
  42. data/lib/picky/query/allocation.rb +3 -3
  43. data/lib/picky/query/token.rb +5 -1
  44. data/lib/picky/search.rb +5 -0
  45. data/lib/picky/sources/base.rb +21 -2
  46. data/lib/picky/sources/db.rb +0 -7
  47. data/lib/picky/statistics.rb +9 -12
  48. data/lib/picky/tokenizers/location.rb +1 -1
  49. data/lib/tasks/checks.rake +8 -6
  50. data/lib/tasks/index.rake +14 -20
  51. data/lib/tasks/server.rake +18 -2
  52. data/lib/tasks/statistics.rake +27 -14
  53. data/lib/tasks/todo.rake +2 -2
  54. data/lib/tasks/try.rake +12 -27
  55. data/spec/lib/application_spec.rb +1 -1
  56. data/spec/lib/backend/file/basic_spec.rb +6 -6
  57. data/spec/lib/backend/file/json_spec.rb +11 -6
  58. data/spec/lib/backend/file/marshal_spec.rb +11 -6
  59. data/spec/lib/backend/files_spec.rb +21 -7
  60. data/spec/lib/backend/redis/basic_spec.rb +6 -0
  61. data/spec/lib/backend/redis/list_hash_spec.rb +9 -3
  62. data/spec/lib/backend/redis/string_hash_spec.rb +7 -1
  63. data/spec/lib/backend/redis_spec.rb +22 -12
  64. data/spec/lib/categories_indexed_spec.rb +2 -2
  65. data/spec/lib/category_indexing_spec.rb +12 -33
  66. data/spec/lib/category_spec.rb +22 -0
  67. data/spec/lib/index/base_indexing_spec.rb +30 -0
  68. data/spec/lib/indexed/bundle/memory_spec.rb +13 -20
  69. data/spec/lib/indexers/base_spec.rb +39 -4
  70. data/spec/lib/indexers/parallel_spec.rb +2 -10
  71. data/spec/lib/indexers/serial_spec.rb +11 -26
  72. data/spec/lib/indexes_class_spec.rb +4 -4
  73. data/spec/lib/indexes_indexed_spec.rb +2 -2
  74. data/spec/lib/indexes_indexing_spec.rb +6 -10
  75. data/spec/lib/indexes_spec.rb +3 -3
  76. data/spec/lib/indexing/bundle/{super_base_spec.rb → base_spec.rb} +2 -2
  77. data/spec/lib/indexing/bundle/memory_partial_generation_speed_spec.rb +3 -3
  78. data/spec/lib/indexing/bundle/memory_spec.rb +16 -14
  79. data/spec/lib/indexing/bundle/redis_spec.rb +18 -16
  80. data/spec/lib/query/allocation_spec.rb +1 -1
  81. data/spec/lib/query/token_spec.rb +5 -7
  82. data/spec/lib/sources/base_spec.rb +53 -0
  83. data/spec/lib/sources/db_spec.rb +0 -7
  84. metadata +11 -12
  85. data/lib/picky/indexers/solr.rb +0 -56
  86. data/lib/picky/indexing/bundle/super_base.rb +0 -61
  87. data/lib/picky/solr/schema_generator.rb +0 -74
  88. data/lib/tasks/search.rake +0 -9
  89. data/lib/tasks/shortcuts.rake +0 -32
  90. data/lib/tasks/solr.rake +0 -36
@@ -17,11 +17,11 @@ class Analyzer # :nodoc:all
17
17
  #
18
18
  #
19
19
  def analyze bundle
20
- bundle.load_index
21
- analysis[:__keys] = bundle.size
22
- cardinality :index, bundle.index
20
+ bundle.load_inverted
21
+ analysis[:__keys] = bundle.inverted.size
22
+ cardinality :index, bundle.inverted
23
23
  index_analysis
24
- bundle.clear_index
24
+ bundle.clear_inverted
25
25
 
26
26
  bundle.load_weights
27
27
  weights bundle.weights
@@ -68,7 +68,7 @@
68
68
  # In full glory:
69
69
  # class MyGreatSearch < Application
70
70
  #
71
- # books = index :books do
71
+ # books = Index::Memory.new :books do
72
72
  # source Sources::CSV.new(:title, :author, :isbn, file:'app/library.csv')
73
73
  # category :title
74
74
  # end
@@ -227,12 +227,6 @@ class Application
227
227
  "WARNING: No routes defined for application configuration in #{self.class}." if rack_adapter.empty?
228
228
  end
229
229
 
230
- def to_s # :nodoc:
231
- <<-APPLICATION
232
- \033[1m#{name}\033[m
233
- #{to_stats.indented_to_s}
234
- APPLICATION
235
- end
236
230
  def to_stats
237
231
  <<-APP
238
232
  \033[1mIndexing (default)\033[m:
@@ -248,10 +242,15 @@ APPLICATION
248
242
  #{to_routes.indented_to_s}
249
243
  APP
250
244
  end
245
+
251
246
  def to_routes
252
247
  rack_adapter.to_s
253
248
  end
254
249
 
250
+ def to_s # :nodoc:
251
+ self.name
252
+ end
253
+
255
254
  end
256
255
 
257
256
  end
@@ -1,13 +1,20 @@
1
1
  module Backend
2
2
 
3
- class Backend
3
+ class Base
4
4
 
5
- attr_reader :bundle_name
6
- attr_reader :prepared, :index, :weights, :similarity, :configuration
5
+ attr_reader :bundle_name,
6
+ :prepared,
7
+ :inverted,
8
+ :weights,
9
+ :similarity,
10
+ :configuration,
11
+ :category,
12
+ :identifier
7
13
 
8
14
  delegate :index_name, :category_name, :to => :@category
9
15
 
10
16
  def initialize bundle_name, category
17
+ @identifier = "#{category.identifier}:#{bundle_name}"
11
18
  @bundle_name = bundle_name
12
19
  @category = category
13
20
  @prepared = File::Text.new category.prepared_index_path
@@ -24,23 +31,27 @@ module Backend
24
31
 
25
32
  # Dumping.
26
33
  #
27
- def dump_index index_hash
28
- index.dump index_hash
34
+ def dump_inverted inverted_hash
35
+ timed_exclaim %Q{"#{identifier}": => #{inverted}.}
36
+ inverted.dump inverted_hash
29
37
  end
30
38
  def dump_weights weights_hash
39
+ timed_exclaim %Q{"#{identifier}": => #{weights}.}
31
40
  weights.dump weights_hash
32
41
  end
33
42
  def dump_similarity similarity_hash
43
+ timed_exclaim %Q{"#{identifier}": => #{similarity}.}
34
44
  similarity.dump similarity_hash
35
45
  end
36
46
  def dump_configuration configuration_hash
47
+ timed_exclaim %Q{"#{identifier}": => #{configuration}.}
37
48
  configuration.dump configuration_hash
38
49
  end
39
50
 
40
51
  # Loading.
41
52
  #
42
- def load_index
43
- index.load
53
+ def load_inverted
54
+ inverted.load
44
55
  end
45
56
  def load_similarity
46
57
  similarity.load
@@ -54,8 +65,8 @@ module Backend
54
65
 
55
66
  # Cache ok?
56
67
  #
57
- def index_cache_ok?
58
- index.cache_ok?
68
+ def inverted_cache_ok?
69
+ inverted.cache_ok?
59
70
  end
60
71
  def similarity_cache_ok?
61
72
  similarity.cache_ok?
@@ -66,8 +77,8 @@ module Backend
66
77
 
67
78
  # Cache small?
68
79
  #
69
- def index_cache_small?
70
- index.cache_small?
80
+ def inverted_cache_small?
81
+ inverted.cache_small?
71
82
  end
72
83
  def similarity_cache_small?
73
84
  similarity.cache_small?
@@ -79,7 +90,7 @@ module Backend
79
90
  # Copies the indexes to the "backup" directory.
80
91
  #
81
92
  def backup
82
- index.backup
93
+ inverted.backup
83
94
  weights.backup
84
95
  similarity.backup
85
96
  configuration.backup
@@ -88,7 +99,7 @@ module Backend
88
99
  # Restores the indexes from the "backup" directory.
89
100
  #
90
101
  def restore
91
- index.restore
102
+ inverted.restore
92
103
  weights.restore
93
104
  similarity.restore
94
105
  configuration.restore
@@ -97,12 +108,18 @@ module Backend
97
108
  # Delete all index files.
98
109
  #
99
110
  def delete
100
- index.delete
111
+ inverted.delete
101
112
  weights.delete
102
113
  similarity.delete
103
114
  configuration.delete
104
115
  end
105
116
 
117
+ #
118
+ #
119
+ def to_s
120
+ "#{self.class}(#{prepared}, #{bundle_name}, #{category.identifier})"
121
+ end
122
+
106
123
  end
107
124
 
108
125
  end
@@ -13,6 +13,8 @@ module Backend
13
13
  #
14
14
  class Basic
15
15
 
16
+ # This file's location.
17
+ #
16
18
  attr_reader :cache_path
17
19
 
18
20
  # An index cache takes a path, without file extension,
@@ -22,10 +24,6 @@ module Backend
22
24
  @cache_path = "#{cache_path}.#{extension}"
23
25
  end
24
26
 
25
- def to_s
26
- cache_path
27
- end
28
-
29
27
  # The default extension for index files is "index".
30
28
  #
31
29
  def extension
@@ -40,12 +38,15 @@ module Backend
40
38
  prepare_backup backup_directory
41
39
  FileUtils.cp cache_path, target, verbose: true
42
40
  end
41
+
43
42
  # The backup directory of this file.
44
43
  # Equal to the file's dirname plus /backup
45
44
  #
45
+
46
46
  def backup_directory
47
47
  ::File.join ::File.dirname(cache_path), 'backup'
48
48
  end
49
+
49
50
  # Prepares the backup directory for the file.
50
51
  #
51
52
  def prepare_backup target
@@ -58,6 +59,7 @@ module Backend
58
59
  def restore
59
60
  FileUtils.cp backup_file_path_of(cache_path), cache_path, verbose: true
60
61
  end
62
+
61
63
  # The backup filename.
62
64
  #
63
65
  def backup_file_path_of path
@@ -94,6 +96,12 @@ module Backend
94
96
  `ls -l #{path} | awk '{print $5}'`.to_i
95
97
  end
96
98
 
99
+ #
100
+ #
101
+ def to_s
102
+ "#{self.class}(#{cache_path})"
103
+ end
104
+
97
105
  end
98
106
 
99
107
  end
@@ -1,11 +1,11 @@
1
1
  module Backend
2
-
2
+
3
3
  module File
4
-
4
+
5
5
  # Index files dumped in the JSON format.
6
6
  #
7
7
  class JSON < Basic
8
-
8
+
9
9
  # Uses the extension "json".
10
10
  #
11
11
  def extension
@@ -26,9 +26,9 @@ module Backend
26
26
  def retrieve
27
27
  raise "Can't retrieve from JSON file. Use text file."
28
28
  end
29
-
29
+
30
30
  end
31
-
31
+
32
32
  end
33
33
 
34
34
  end
@@ -44,7 +44,7 @@ module Backend
44
44
 
45
45
  #
46
46
  #
47
- def open_for_indexing &block
47
+ def open &block
48
48
  ::File.open cache_path, 'w:binary', &block
49
49
  end
50
50
 
@@ -1,6 +1,6 @@
1
1
  module Backend
2
2
 
3
- class Files < Backend
3
+ class Files < Base
4
4
 
5
5
  def initialize bundle_name, category
6
6
  super bundle_name, category
@@ -9,20 +9,14 @@ module Backend
9
9
  # Yajl json lib cannot load symbolized
10
10
  # values, just keys.
11
11
  #
12
- @index = File::JSON.new category.index_path(bundle_name, :index)
12
+ @inverted = File::JSON.new category.index_path(bundle_name, :inverted)
13
13
  @weights = File::JSON.new category.index_path(bundle_name, :weights)
14
14
  @similarity = File::Marshal.new category.index_path(bundle_name, :similarity)
15
15
  @configuration = File::JSON.new category.index_path(bundle_name, :configuration)
16
16
  end
17
17
 
18
18
  def to_s
19
- <<-FILES
20
- Files:
21
- #{"Index: #{@index}".indented_to_s}
22
- #{"Weights: #{@weights}".indented_to_s}
23
- #{"Similarity: #{@similarity}".indented_to_s}
24
- #{"Config: #{@configuration}".indented_to_s}
25
- FILES
19
+ "#{self.class}(#{[@prepared, @inverted, @weights, @similarity, @configuration].join(', ')})"
26
20
  end
27
21
 
28
22
  end
@@ -61,6 +61,7 @@ module Backend
61
61
  def cache_small?
62
62
  size < 1
63
63
  end
64
+
64
65
  # Is the cache ok?
65
66
  #
66
67
  # A small cache is still ok.
@@ -68,6 +69,7 @@ module Backend
68
69
  def cache_ok?
69
70
  size > 0
70
71
  end
72
+
71
73
  # Extracts the size of the file in Bytes.
72
74
  #
73
75
  # Note: This is a very forgiving implementation.
@@ -78,6 +80,12 @@ module Backend
78
80
  backend.dbsize
79
81
  end
80
82
 
83
+ #
84
+ #
85
+ def to_s
86
+ "#{self.class}(#{namespace}:*)"
87
+ end
88
+
81
89
  end
82
90
 
83
91
  end
@@ -17,7 +17,7 @@ module Backend
17
17
  end
18
18
  end
19
19
  end
20
-
20
+
21
21
  # Clear the index for this list.
22
22
  #
23
23
  # Note: Perhaps we can use a server only command.
@@ -32,14 +32,14 @@ module Backend
32
32
 
33
33
  # Get a collection.
34
34
  #
35
- def collection sym
36
- backend.lrange "#{namespace}:#{sym}", 0, -1
35
+ def collection key
36
+ backend.zrange "#{namespace}:#{key}", 0, -1
37
37
  end
38
38
 
39
39
  # Get a single value.
40
40
  #
41
- def member sym
42
- raise "Can't retrieve a single value from a Redis ListHash. Use Index::Redis::StringHash."
41
+ def member key
42
+ raise "Can't retrieve single value :#{key} from a Redis ListHash. Use Index::Redis::StringHash."
43
43
  end
44
44
 
45
45
  end
@@ -14,7 +14,7 @@ module Backend
14
14
  backend.hset namespace, key, value
15
15
  end
16
16
  end
17
-
17
+
18
18
  # Clears the hash.
19
19
  #
20
20
  def clear
@@ -23,14 +23,14 @@ module Backend
23
23
 
24
24
  # Get a collection.
25
25
  #
26
- def collection sym
27
- raise "Can't retrieve a collection from a StringHash. Use Index::Redis::ListHash."
26
+ def collection key
27
+ raise "Can't retrieve collection for :#{key} from a StringHash. Use Index::Redis::ListHash."
28
28
  end
29
29
 
30
30
  # Get a single value.
31
31
  #
32
- def member sym
33
- backend.hget namespace, sym
32
+ def member key
33
+ backend.hget namespace, key
34
34
  end
35
35
 
36
36
  end
@@ -1,24 +1,24 @@
1
1
  module Backend
2
2
 
3
- # TODO Needs a reconnect to be run after forking.
4
3
  #
5
- class Redis < Backend
4
+ #
5
+ class Redis < Base
6
6
 
7
7
  def initialize bundle_name, category
8
8
  super bundle_name, category
9
9
 
10
10
  # Refine a few Redis "types".
11
11
  #
12
- @index = Redis::ListHash.new "#{category.identifier}:#{bundle_name}:index"
12
+ @inverted = Redis::ListHash.new "#{category.identifier}:#{bundle_name}:inverted"
13
13
  @weights = Redis::StringHash.new "#{category.identifier}:#{bundle_name}:weights"
14
- @similarity = Redis::ListHash.new "#{category.identifier}:#{bundle_name}:similarity"
14
+ @similarity = Redis::ListHash.new "#{category.identifier}:#{bundle_name}:similarity"
15
15
  @configuration = Redis::StringHash.new "#{category.identifier}:#{bundle_name}:configuration"
16
16
  end
17
17
 
18
18
  # Delegate to the right collection.
19
19
  #
20
20
  def ids sym
21
- index.collection sym
21
+ inverted.collection sym
22
22
  end
23
23
 
24
24
  # Delegate to the right member value.
@@ -0,0 +1,62 @@
1
+ # A Bundle is a number of indexes
2
+ # per [index, category] combination.
3
+ #
4
+ # At most, there are three indexes:
5
+ # * *core* index (always used)
6
+ # * *weights* index (always used)
7
+ # * *similarity* index (used with similarity)
8
+ #
9
+ # In Picky, indexing is separated from the index
10
+ # handling itself through a parallel structure.
11
+ #
12
+ # Both use methods provided by this base class, but
13
+ # have very different goals:
14
+ #
15
+ # * *Indexing*::*Bundle*::*Base* is just concerned with creating index
16
+ # files / redis entries and providing helper functions to e.g. check
17
+ # the indexes.
18
+ #
19
+ # * *Index*::*Bundle*::*Base* is concerned with loading these index files into
20
+ # memory / redis and looking up search data as fast as possible.
21
+ #
22
+ class Bundle
23
+
24
+ attr_reader :identifier,
25
+ :files
26
+ attr_accessor :inverted,
27
+ :weights,
28
+ :similarity,
29
+ :configuration,
30
+ :similarity_strategy
31
+
32
+ delegate :clear, :to => :inverted
33
+ delegate :[], :[]=, :to => :configuration
34
+
35
+ def initialize name, category, similarity_strategy
36
+ @identifier = "#{category.identifier}:#{name}"
37
+ @files = Backend::Files.new name, category
38
+
39
+ @inverted = {}
40
+ @weights = {}
41
+ @similarity = {}
42
+ @configuration = {} # A hash with config options.
43
+
44
+ @similarity_strategy = similarity_strategy
45
+ end
46
+
47
+ # Get a list of similar texts.
48
+ #
49
+ # Note: Does not return itself.
50
+ #
51
+ def similar text
52
+ code = similarity_strategy.encoded text
53
+ similar_codes = code && @similarity[code]
54
+ similar_codes.delete text if similar_codes
55
+ similar_codes || []
56
+ end
57
+
58
+ def to_s
59
+ "#{self.class}(#{identifier}, #{files})"
60
+ end
61
+
62
+ end
@@ -8,6 +8,7 @@ class Categories
8
8
  :to => :categories
9
9
 
10
10
  each_delegate :reindex,
11
+ :each_category,
11
12
  :to => :categories
12
13
 
13
14
  # A list of indexed categories.
@@ -26,18 +27,25 @@ class Categories
26
27
  # Nifty! :)
27
28
  #
28
29
  def initialize options = {}
29
- clear
30
+ clear_categories
30
31
 
31
32
  @ignore_unassigned_tokens = options[:ignore_unassigned_tokens] || false
32
33
  end
33
34
 
34
35
  # Clears both the array of categories and the hash of categories.
35
36
  #
36
- def clear
37
+ def clear_categories
37
38
  @categories = []
38
39
  @category_hash = {}
39
40
  end
40
41
 
42
+ # Add the given category to the list of categories.
43
+ #
44
+ def << category
45
+ categories << category
46
+ category_hash[category.name] = category
47
+ end
48
+
41
49
  # Find a given category in the categories.
42
50
  #
43
51
  def [] category_name
@@ -48,13 +56,6 @@ class Categories
48
56
  raise %Q{Index category "#{category_name}" not found. Possible categories: "#{categories.map(&:name).join('", "')}".}
49
57
  end
50
58
 
51
- # Add the given category to the list of categories.
52
- #
53
- def << category
54
- categories << category
55
- category_hash[category.name] = category
56
- end
57
-
58
59
  def to_s
59
60
  categories.join(', ')
60
61
  end
@@ -1,5 +1,5 @@
1
1
  class Categories
2
-
2
+
3
3
  attr_reader :ignore_unassigned_tokens
4
4
 
5
5
  each_delegate :load_from_cache,
@@ -15,17 +15,17 @@ class Categories
15
15
  def possible_combinations_for token
16
16
  token.similar? ? similar_possible_for(token) : possible_for(token)
17
17
  end
18
+
18
19
  # Gets all similar tokens and puts together the possible combinations
19
20
  # for each found similar token.
20
21
  #
21
22
  def similar_possible_for token
22
- # Get as many tokens as necessary
23
- #
24
23
  tokens = similar_tokens_for token
25
- # possible combinations
26
- #
27
24
  inject_possible_for tokens
28
25
  end
26
+
27
+ # Returns all possible similar tokens for the given token.
28
+ #
29
29
  def similar_tokens_for token
30
30
  text = token.text
31
31
  categories.inject([]) do |result, category|
@@ -41,6 +41,9 @@ class Categories
41
41
  result
42
42
  end
43
43
  end
44
+
45
+ #
46
+ #
44
47
  def inject_possible_for tokens
45
48
  tokens.inject([]) do |result, token|
46
49
  possible = possible_categories token
@@ -65,8 +68,9 @@ class Categories
65
68
  # This is an optimization to mark tokens that are ignored.
66
69
  #
67
70
  return if ignore_unassigned_tokens && possible.empty?
68
- possible # wrap in combinations
71
+ possible
69
72
  end
73
+
70
74
  # This returns the possible categories for this token.
71
75
  # If the user has already preselected a category for this token,
72
76
  # like "artist:moby", if not just return all for the given token,
@@ -77,6 +81,7 @@ class Categories
77
81
  def possible_categories token
78
82
  user_defined_categories(token) || categories
79
83
  end
84
+
80
85
  # This returns the array of categories if the user has defined
81
86
  # an existing category.
82
87
  #
@@ -89,5 +94,5 @@ class Categories
89
94
  category_hash[name]
90
95
  end.compact
91
96
  end
92
-
97
+
93
98
  end
@@ -1,12 +1,10 @@
1
1
  class Categories
2
-
3
- each_delegate :backup_caches,
4
- :cache,
5
- :check_caches,
6
- :clear_caches,
7
- :create_directory_structure,
8
- :generate_caches,
9
- :restore_caches,
2
+
3
+ each_delegate :cache,
4
+ :check,
5
+ :clear,
6
+ :backup,
7
+ :restore,
10
8
  :to => :categories
11
-
9
+
12
10
  end