fluent-plugin-droonga 0.7.0 → 0.8.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.
Files changed (163) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -4
  3. data/benchmark/watch/benchmark-notify.rb +2 -2
  4. data/benchmark/watch/benchmark-scan.rb +3 -0
  5. data/benchmark/watch/fluentd.conf +0 -1
  6. data/fluent-plugin-droonga.gemspec +2 -3
  7. data/lib/droonga/catalog.rb +10 -124
  8. data/lib/droonga/catalog/base.rb +140 -0
  9. data/lib/droonga/catalog/version1.rb +23 -0
  10. data/lib/droonga/catalog_loader.rb +33 -0
  11. data/lib/droonga/collector.rb +2 -71
  12. data/lib/droonga/collector_plugin.rb +2 -34
  13. data/lib/droonga/dispatcher.rb +141 -196
  14. data/lib/droonga/distribution_planner.rb +76 -0
  15. data/lib/droonga/distributor.rb +5 -7
  16. data/lib/droonga/distributor_plugin.rb +23 -15
  17. data/lib/droonga/engine.rb +2 -2
  18. data/lib/droonga/event_loop.rb +46 -0
  19. data/lib/droonga/farm.rb +9 -5
  20. data/lib/droonga/fluent_message_sender.rb +84 -0
  21. data/lib/droonga/forwarder.rb +43 -53
  22. data/lib/droonga/handler.rb +20 -68
  23. data/lib/droonga/handler_message.rb +61 -0
  24. data/lib/droonga/handler_messenger.rb +92 -0
  25. data/lib/droonga/handler_plugin.rb +10 -12
  26. data/lib/droonga/input_adapter.rb +52 -0
  27. data/lib/droonga/{adapter.rb → input_adapter_plugin.rb} +7 -13
  28. data/lib/droonga/input_message.rb +11 -11
  29. data/lib/droonga/logger.rb +4 -3
  30. data/lib/droonga/message_pack_packer.rb +62 -0
  31. data/lib/droonga/message_processing_error.rb +54 -0
  32. data/lib/droonga/message_pusher.rb +60 -0
  33. data/lib/droonga/message_receiver.rb +61 -0
  34. data/lib/droonga/output_adapter.rb +53 -0
  35. data/lib/droonga/{adapter_plugin.rb → output_adapter_plugin.rb} +3 -21
  36. data/lib/droonga/output_message.rb +37 -0
  37. data/lib/droonga/partition.rb +27 -5
  38. data/lib/droonga/pluggable.rb +9 -4
  39. data/lib/droonga/plugin.rb +12 -3
  40. data/lib/droonga/plugin/collector/basic.rb +91 -18
  41. data/lib/droonga/plugin/distributor/crud.rb +9 -9
  42. data/lib/droonga/plugin/distributor/distributed_search_planner.rb +401 -0
  43. data/lib/droonga/plugin/distributor/groonga.rb +5 -5
  44. data/lib/droonga/plugin/distributor/search.rb +4 -246
  45. data/lib/droonga/plugin/distributor/watch.rb +11 -6
  46. data/lib/droonga/plugin/handler/add.rb +69 -7
  47. data/lib/droonga/plugin/handler/groonga.rb +6 -6
  48. data/lib/droonga/plugin/handler/search.rb +5 -3
  49. data/lib/droonga/plugin/handler/watch.rb +19 -13
  50. data/lib/droonga/plugin/{adapter → input_adapter}/groonga.rb +5 -11
  51. data/lib/droonga/plugin/{adapter → input_adapter}/groonga/select.rb +2 -36
  52. data/lib/droonga/plugin/output_adapter/groonga.rb +30 -0
  53. data/lib/droonga/plugin/output_adapter/groonga/select.rb +54 -0
  54. data/lib/droonga/plugin_loader.rb +2 -2
  55. data/lib/droonga/processor.rb +21 -23
  56. data/lib/droonga/replier.rb +40 -0
  57. data/lib/droonga/searcher.rb +298 -174
  58. data/lib/droonga/server.rb +0 -67
  59. data/lib/droonga/session.rb +85 -0
  60. data/lib/droonga/test.rb +21 -0
  61. data/lib/droonga/test/stub_distributor.rb +31 -0
  62. data/lib/droonga/test/stub_handler.rb +37 -0
  63. data/lib/droonga/test/stub_handler_message.rb +35 -0
  64. data/lib/droonga/test/stub_handler_messenger.rb +34 -0
  65. data/lib/droonga/time_formatter.rb +37 -0
  66. data/lib/droonga/watcher.rb +1 -0
  67. data/lib/droonga/worker.rb +16 -19
  68. data/lib/fluent/plugin/out_droonga.rb +9 -9
  69. data/lib/groonga_command_converter.rb +5 -5
  70. data/sample/cluster/catalog.json +1 -1
  71. data/test/command/config/default/catalog.json +19 -1
  72. data/test/command/fixture/event.jsons +41 -0
  73. data/test/command/fixture/user-table.jsons +9 -0
  74. data/test/command/run-test.rb +2 -2
  75. data/test/command/suite/add/error/invalid-integer.expected +20 -0
  76. data/test/command/suite/add/error/invalid-integer.test +12 -0
  77. data/test/command/suite/add/error/invalid-time.expected +20 -0
  78. data/test/command/suite/add/error/invalid-time.test +12 -0
  79. data/test/command/suite/add/error/missing-key.expected +13 -0
  80. data/test/command/suite/add/error/missing-key.test +16 -0
  81. data/test/command/suite/add/error/missing-table.expected +13 -0
  82. data/test/command/suite/add/error/missing-table.test +16 -0
  83. data/test/command/suite/add/error/unknown-column.expected +20 -0
  84. data/test/command/suite/add/error/unknown-column.test +12 -0
  85. data/test/command/suite/add/error/unknown-table.expected +13 -0
  86. data/test/command/suite/add/error/unknown-table.test +17 -0
  87. data/test/command/suite/add/minimum.expected +1 -3
  88. data/test/command/suite/add/with-values.expected +1 -3
  89. data/test/command/suite/add/without-key.expected +1 -3
  90. data/test/command/suite/message/error/missing-dataset.expected +13 -0
  91. data/test/command/suite/message/error/missing-dataset.test +5 -0
  92. data/test/command/suite/message/error/unknown-command.expected +13 -0
  93. data/test/command/suite/message/error/unknown-command.test +6 -0
  94. data/test/command/suite/message/error/unknown-dataset.expected +13 -0
  95. data/test/command/suite/message/error/unknown-dataset.test +6 -0
  96. data/test/command/suite/search/{array-attribute-label.expected → attributes/array.expected} +0 -0
  97. data/test/command/suite/search/{array-attribute-label.test → attributes/array.test} +0 -0
  98. data/test/command/suite/search/{hash-attribute-label.expected → attributes/hash.expected} +0 -0
  99. data/test/command/suite/search/{hash-attribute-label.test → attributes/hash.test} +0 -0
  100. data/test/command/suite/search/{condition-nested.expected → condition/nested.expected} +0 -0
  101. data/test/command/suite/search/{condition-nested.test → condition/nested.test} +0 -0
  102. data/test/command/suite/search/{condition-query.expected → condition/query.expected} +0 -0
  103. data/test/command/suite/search/{condition-query.test → condition/query.test} +0 -0
  104. data/test/command/suite/search/{condition-script.expected → condition/script.expected} +0 -0
  105. data/test/command/suite/search/{condition-script.test → condition/script.test} +0 -0
  106. data/test/command/suite/search/error/cyclic-source.expected +18 -0
  107. data/test/command/suite/search/error/cyclic-source.test +12 -0
  108. data/test/command/suite/search/error/deeply-cyclic-source.expected +21 -0
  109. data/test/command/suite/search/error/deeply-cyclic-source.test +15 -0
  110. data/test/command/suite/search/error/missing-source-parameter.expected +17 -0
  111. data/test/command/suite/search/error/missing-source-parameter.test +11 -0
  112. data/test/command/suite/search/error/unknown-source.expected +18 -0
  113. data/test/command/suite/search/error/unknown-source.test +12 -0
  114. data/test/command/suite/search/{minimum.expected → group/count.expected} +2 -1
  115. data/test/command/suite/search/{minimum.test → group/count.test} +5 -3
  116. data/test/command/suite/search/group/limit.expected +19 -0
  117. data/test/command/suite/search/group/limit.test +20 -0
  118. data/test/command/suite/search/group/string.expected +36 -0
  119. data/test/command/suite/search/group/string.test +44 -0
  120. data/test/command/suite/search/{chained-queries.expected → multiple/chained.expected} +0 -0
  121. data/test/command/suite/search/{chained-queries.test → multiple/chained.test} +0 -0
  122. data/test/command/suite/search/{multiple-queries.expected → multiple/parallel.expected} +0 -0
  123. data/test/command/suite/search/{multiple-queries.test → multiple/parallel.test} +0 -0
  124. data/test/command/suite/search/{output-range.expected → range/only-output.expected} +0 -0
  125. data/test/command/suite/search/{output-range.test → range/only-output.test} +0 -0
  126. data/test/command/suite/search/{sort-range.expected → range/only-sort.expected} +0 -0
  127. data/test/command/suite/search/{sort-range.test → range/only-sort.test} +0 -0
  128. data/test/command/suite/search/{sort-and-output-range.expected → range/sort-and-output.expected} +0 -0
  129. data/test/command/suite/search/{sort-and-output-range.test → range/sort-and-output.test} +0 -0
  130. data/test/command/suite/search/range/too-large-output-offset.expected +16 -0
  131. data/test/command/suite/search/range/too-large-output-offset.test +25 -0
  132. data/test/command/suite/search/range/too-large-sort-offset.expected +16 -0
  133. data/test/command/suite/search/range/too-large-sort-offset.test +28 -0
  134. data/test/command/suite/search/response/records/value/time.expected +24 -0
  135. data/test/command/suite/search/response/records/value/time.test +24 -0
  136. data/test/command/suite/search/sort/default-offset-limit.expected +43 -0
  137. data/test/command/suite/search/sort/default-offset-limit.test +26 -0
  138. data/test/command/suite/search/{sort-with-invisible-column.expected → sort/invisible-column.expected} +0 -0
  139. data/test/command/suite/search/{sort-with-invisible-column.test → sort/invisible-column.test} +0 -0
  140. data/test/command/suite/watch/subscribe.expected +12 -0
  141. data/test/command/suite/watch/subscribe.test +9 -0
  142. data/test/command/suite/watch/unsubscribe.expected +12 -0
  143. data/test/command/suite/watch/unsubscribe.test +9 -0
  144. data/test/unit/{test_catalog.rb → catalog/test_version1.rb} +12 -4
  145. data/test/unit/fixtures/{catalog.json → catalog/version1.json} +0 -0
  146. data/test/unit/helper.rb +2 -0
  147. data/test/unit/plugin/collector/test_basic.rb +289 -33
  148. data/test/unit/plugin/distributor/test_search.rb +176 -861
  149. data/test/unit/plugin/distributor/test_search_planner.rb +1102 -0
  150. data/test/unit/plugin/handler/groonga/test_column_create.rb +17 -13
  151. data/test/unit/plugin/handler/groonga/test_table_create.rb +10 -10
  152. data/test/unit/plugin/handler/test_add.rb +74 -11
  153. data/test/unit/plugin/handler/test_groonga.rb +15 -1
  154. data/test/unit/plugin/handler/test_search.rb +33 -17
  155. data/test/unit/plugin/handler/test_watch.rb +43 -27
  156. data/test/unit/run-test.rb +2 -0
  157. data/test/unit/test_message_pack_packer.rb +51 -0
  158. data/test/unit/test_time_formatter.rb +29 -0
  159. metadata +208 -110
  160. data/lib/droonga/job_queue.rb +0 -87
  161. data/lib/droonga/job_queue_schema.rb +0 -65
  162. data/test/unit/test_adapter.rb +0 -51
  163. data/test/unit/test_job_queue_schema.rb +0 -45
@@ -0,0 +1,1102 @@
1
+ # Copyright (C) 2013 Droonga Project
2
+ #
3
+ # This library is free software; you can redistribute it and/or
4
+ # modify it under the terms of the GNU Lesser General Public
5
+ # License version 2.1 as published by the Free Software Foundation.
6
+ #
7
+ # This library is distributed in the hope that it will be useful,
8
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
9
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
10
+ # Lesser General Public License for more details.
11
+ #
12
+ # You should have received a copy of the GNU Lesser General Public
13
+ # License along with this library; if not, write to the Free Software
14
+ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
15
+
16
+ require "droonga/plugin/distributor/distributed_search_planner"
17
+
18
+ class DistributedSearchPlannerTest < Test::Unit::TestCase
19
+ def assert_planned(expected, search_request_envelope)
20
+ plan = Droonga::DistributedSearchPlanner.new(search_request_envelope)
21
+ actual = plan.messages
22
+ assert_equal(expected, actual)
23
+ end
24
+
25
+ class MultipleQueriesTest < self
26
+ def test_distribute
27
+ envelope = {
28
+ "type" => "search",
29
+ "dataset" => "Droonga",
30
+ "body" => {
31
+ "queries" => {
32
+ "query1" => {
33
+ "source" => "User",
34
+ "output" => {
35
+ "format" => "complex",
36
+ "elements" => ["count", "records"],
37
+ "attributes" => [],
38
+ "offset" => 0,
39
+ "limit" => 10,
40
+ },
41
+ },
42
+ "query2" => {
43
+ "source" => "User",
44
+ "output" => {
45
+ "format" => "complex",
46
+ "elements" => ["count", "records"],
47
+ "attributes" => [],
48
+ "offset" => 0,
49
+ "limit" => 20,
50
+ },
51
+ },
52
+ "query3" => {
53
+ "source" => "User",
54
+ "output" => {
55
+ "format" => "complex",
56
+ "elements" => ["count", "records"],
57
+ "attributes" => [],
58
+ "offset" => 0,
59
+ "limit" => 30,
60
+ },
61
+ },
62
+ },
63
+ },
64
+ }
65
+
66
+ message = []
67
+
68
+ message << {
69
+ "type" => "reduce",
70
+ "body" => {
71
+ "query1" => {
72
+ "query1_reduced" => {
73
+ "count" => {
74
+ "type" => "sum",
75
+ },
76
+ "records" => {
77
+ "type" => "sort",
78
+ "operators" => [],
79
+ "limit" => 10,
80
+ },
81
+ },
82
+ },
83
+ },
84
+ "inputs" => ["query1"],
85
+ "outputs" => ["query1_reduced"],
86
+ }
87
+ message << {
88
+ "type" => "reduce",
89
+ "body" => {
90
+ "query2" => {
91
+ "query2_reduced" => {
92
+ "count" => {
93
+ "type" => "sum",
94
+ },
95
+ "records" => {
96
+ "type" => "sort",
97
+ "operators" => [],
98
+ "limit" => 20,
99
+ },
100
+ },
101
+ },
102
+ },
103
+ "inputs" => ["query2"],
104
+ "outputs" => ["query2_reduced"],
105
+ }
106
+ message << {
107
+ "type" => "reduce",
108
+ "body" => {
109
+ "query3" => {
110
+ "query3_reduced" => {
111
+ "count" => {
112
+ "type" => "sum",
113
+ },
114
+ "records" => {
115
+ "type" => "sort",
116
+ "operators" => [],
117
+ "limit" => 30,
118
+ },
119
+ },
120
+ },
121
+ },
122
+ "inputs" => ["query3"],
123
+ "outputs" => ["query3_reduced"],
124
+ }
125
+
126
+ gatherer = {
127
+ "type" => "gather",
128
+ "body" => {
129
+ "query1_reduced" => {
130
+ "output" => "query1",
131
+ "elements" => {
132
+ "records" => {
133
+ "type" => "sort",
134
+ "offset" => 0,
135
+ "limit" => 10,
136
+ "format" => "complex",
137
+ "attributes" => [],
138
+ },
139
+ },
140
+ },
141
+ "query2_reduced" => {
142
+ "output" => "query2",
143
+ "elements" => {
144
+ "records" => {
145
+ "type" => "sort",
146
+ "offset" => 0,
147
+ "limit" => 20,
148
+ "format" => "complex",
149
+ "attributes" => [],
150
+ },
151
+ },
152
+ },
153
+ "query3_reduced" => {
154
+ "output" => "query3",
155
+ "elements" => {
156
+ "records" => {
157
+ "type" => "sort",
158
+ "offset" => 0,
159
+ "limit" => 30,
160
+ "format" => "complex",
161
+ "attributes" => [],
162
+ },
163
+ },
164
+ },
165
+ },
166
+ "inputs" => [
167
+ "query1_reduced",
168
+ "query2_reduced",
169
+ "query3_reduced",
170
+ ],
171
+ "post" => true,
172
+ }
173
+ message << gatherer
174
+
175
+ searcher = {
176
+ "type" => "broadcast",
177
+ "command" => "search",
178
+ "dataset" => "Droonga",
179
+ "body" => {
180
+ "queries" => {
181
+ "query1" => {
182
+ "source" => "User",
183
+ "output" => {
184
+ "format" => "simple",
185
+ "elements" => ["count", "records"],
186
+ "attributes" => [],
187
+ "offset" => 0,
188
+ "limit" => 10,
189
+ },
190
+ },
191
+ "query2" => {
192
+ "source" => "User",
193
+ "output" => {
194
+ "format" => "simple",
195
+ "elements" => ["count", "records"],
196
+ "attributes" => [],
197
+ "offset" => 0,
198
+ "limit" => 20,
199
+ },
200
+ },
201
+ "query3" => {
202
+ "source" => "User",
203
+ "output" => {
204
+ "format" => "simple",
205
+ "elements" => ["count", "records"],
206
+ "attributes" => [],
207
+ "offset" => 0,
208
+ "limit" => 30,
209
+ },
210
+ },
211
+ },
212
+ },
213
+ "outputs" => [
214
+ "query1",
215
+ "query2",
216
+ "query3",
217
+ ],
218
+ "replica" => "random",
219
+ }
220
+ message << searcher
221
+
222
+ assert_planned(message, envelope)
223
+ end
224
+ end
225
+
226
+ class SingleQueryTest < self
227
+ def test_no_output
228
+ envelope = {
229
+ "type" => "search",
230
+ "dataset" => "Droonga",
231
+ "body" => {
232
+ "queries" => {
233
+ "no_output" => {
234
+ "source" => "User",
235
+ "sortBy" => {
236
+ "keys" => ["name"],
237
+ "offset" => 0,
238
+ "limit" => 1,
239
+ },
240
+ },
241
+ },
242
+ },
243
+ }
244
+
245
+ message = []
246
+ message << gatherer(envelope, :no_output => true)
247
+ message << searcher(envelope, :no_output => true)
248
+ assert_planned(message, envelope)
249
+ end
250
+
251
+ def test_no_records_element
252
+ envelope = {
253
+ "type" => "search",
254
+ "dataset" => "Droonga",
255
+ "body" => {
256
+ "queries" => {
257
+ "no_records" => {
258
+ "source" => "User",
259
+ "sortBy" => {
260
+ "keys" => ["name"],
261
+ "offset" => 0,
262
+ "limit" => 1,
263
+ },
264
+ "output" => {
265
+ "elements" => ["count"],
266
+ },
267
+ },
268
+ },
269
+ },
270
+ }
271
+
272
+ message = []
273
+ message << reducer(envelope, {
274
+ "count" => {
275
+ "type" => "sum",
276
+ },
277
+ })
278
+ message << gatherer(envelope)
279
+ message << searcher(envelope, :sort_limit => 1,
280
+ :output_limit => 0)
281
+ assert_planned(message, envelope)
282
+ end
283
+
284
+ def test_no_output_limit
285
+ envelope = {
286
+ "type" => "search",
287
+ "dataset" => "Droonga",
288
+ "body" => {
289
+ "queries" => {
290
+ "no_limit" => {
291
+ "source" => "User",
292
+ "output" => {
293
+ "format" => "complex",
294
+ "elements" => ["count", "records"],
295
+ },
296
+ },
297
+ },
298
+ },
299
+ }
300
+
301
+ message = []
302
+ message << reducer(envelope, {
303
+ "count" => {
304
+ "type" => "sum",
305
+ },
306
+ })
307
+ message << gatherer(envelope)
308
+ message << searcher(envelope, :output_offset => 0,
309
+ :output_limit => 0)
310
+ assert_planned(message, envelope)
311
+ end
312
+
313
+ def test_have_records
314
+ envelope = {
315
+ "type" => "search",
316
+ "dataset" => "Droonga",
317
+ "body" => {
318
+ "queries" => {
319
+ "have_records" => {
320
+ "source" => "User",
321
+ "output" => {
322
+ "format" => "complex",
323
+ "elements" => ["records"],
324
+ "attributes" => ["_key", "name", "age"],
325
+ "offset" => 0,
326
+ "limit" => 1,
327
+ },
328
+ },
329
+ },
330
+ },
331
+ }
332
+
333
+ message = []
334
+ message << reducer(envelope, {
335
+ "records" => {
336
+ "type" => "sort",
337
+ "operators" => [],
338
+ "limit" => 1,
339
+ },
340
+ })
341
+ message << gatherer(envelope, :elements => {
342
+ "records" => records_mapper(
343
+ :offset => 0,
344
+ :limit => 1,
345
+ :format => "complex",
346
+ :attributes => ["_key", "name", "age"],
347
+ ),
348
+ })
349
+ message << searcher(envelope, :output_offset => 0,
350
+ :output_limit => 1)
351
+ assert_planned(message, envelope)
352
+ end
353
+
354
+ def test_have_output_offset
355
+ envelope = {
356
+ "type" => "search",
357
+ "dataset" => "Droonga",
358
+ "body" => {
359
+ "queries" => {
360
+ "have_records" => {
361
+ "source" => "User",
362
+ "output" => {
363
+ "format" => "complex",
364
+ "elements" => ["records"],
365
+ "attributes" => ["_key", "name", "age"],
366
+ "offset" => 1,
367
+ "limit" => 1,
368
+ },
369
+ },
370
+ },
371
+ },
372
+ }
373
+
374
+ message = []
375
+ message << reducer(envelope, {
376
+ "records" => {
377
+ "type" => "sort",
378
+ "operators" => [],
379
+ "limit" => 2,
380
+ },
381
+ })
382
+ message << gatherer(envelope, :elements => {
383
+ "records" => records_mapper(
384
+ :offset => 1,
385
+ :limit => 1,
386
+ :format => "complex",
387
+ :attributes => ["_key", "name", "age"],
388
+ ),
389
+ })
390
+ message << searcher(envelope, :output_offset => 0,
391
+ :output_limit => 2)
392
+ assert_planned(message, envelope)
393
+ end
394
+
395
+ def test_have_simple_sortBy
396
+ envelope = {
397
+ "type" => "search",
398
+ "dataset" => "Droonga",
399
+ "body" => {
400
+ "queries" => {
401
+ "have_records" => {
402
+ "source" => "User",
403
+ "sortBy" => ["name"],
404
+ "output" => {
405
+ "format" => "complex",
406
+ "elements" => ["records"],
407
+ "attributes" => ["_key", "name", "age"],
408
+ "offset" => 0,
409
+ "limit" => 1,
410
+ },
411
+ },
412
+ },
413
+ },
414
+ }
415
+
416
+ message = []
417
+ message << reducer(envelope, {
418
+ "records" => {
419
+ "type" => "sort",
420
+ "operators" => [
421
+ { "column" => 1, "operator" => "<" },
422
+ ],
423
+ "limit" => 1,
424
+ },
425
+ })
426
+ message << gatherer(envelope, :elements => {
427
+ "records" => records_mapper(
428
+ :offset => 0,
429
+ :limit => 1,
430
+ :format => "complex",
431
+ :attributes => ["_key", "name", "age"],
432
+ ),
433
+ })
434
+ message << searcher(envelope, :output_offset => 0,
435
+ :output_limit => 1)
436
+ assert_planned(message, envelope)
437
+ end
438
+
439
+ def test_have_sortBy
440
+ envelope = {
441
+ "type" => "search",
442
+ "dataset" => "Droonga",
443
+ "body" => {
444
+ "queries" => {
445
+ "have_records" => {
446
+ "source" => "User",
447
+ "sortBy" => {
448
+ "keys" => ["name"],
449
+ },
450
+ "output" => {
451
+ "format" => "complex",
452
+ "elements" => ["records"],
453
+ "attributes" => ["_key", "name", "age"],
454
+ "offset" => 0,
455
+ "limit" => 1,
456
+ },
457
+ },
458
+ },
459
+ },
460
+ }
461
+
462
+ message = []
463
+ message << reducer(envelope, {
464
+ "records" => {
465
+ "type" => "sort",
466
+ "operators" => [
467
+ { "column" => 1, "operator" => "<" },
468
+ ],
469
+ "limit" => 1,
470
+ },
471
+ })
472
+ message << gatherer(envelope, :elements => {
473
+ "records" => records_mapper(
474
+ :offset => 0,
475
+ :limit => 1,
476
+ :format => "complex",
477
+ :attributes => ["_key", "name", "age"],
478
+ ),
479
+ })
480
+ message << searcher(envelope, :sort_offset => 0,
481
+ :sort_limit => 1,
482
+ :output_offset => 0,
483
+ :output_limit => 1)
484
+ assert_planned(message, envelope)
485
+ end
486
+
487
+ def test_have_sortBy_offset_limit
488
+ envelope = {
489
+ "type" => "search",
490
+ "dataset" => "Droonga",
491
+ "body" => {
492
+ "queries" => {
493
+ "have_records" => {
494
+ "source" => "User",
495
+ "sortBy" => {
496
+ "keys" => ["name"],
497
+ "offset" => 1,
498
+ "limit" => 2,
499
+ },
500
+ "output" => {
501
+ "format" => "complex",
502
+ "elements" => ["records"],
503
+ "attributes" => ["_key", "name", "age"],
504
+ "offset" => 4,
505
+ "limit" => 8,
506
+ },
507
+ },
508
+ },
509
+ },
510
+ }
511
+
512
+ sort_limit = 1 + 4 + [2, 8].max
513
+ output_limit = 1 + 4 + [2, 8].min
514
+ message = []
515
+ message << reducer(envelope, {
516
+ "records" => {
517
+ "type" => "sort",
518
+ "operators" => [
519
+ { "column" => 1, "operator" => "<" },
520
+ ],
521
+ "limit" => output_limit,
522
+ },
523
+ })
524
+ message << gatherer(envelope, :elements => {
525
+ "records" => records_mapper(
526
+ :offset => 5,
527
+ :limit => 2,
528
+ :format => "complex",
529
+ :attributes => ["_key", "name", "age"],
530
+ ),
531
+ })
532
+ message << searcher(envelope, :sort_offset => 0,
533
+ :sort_limit => sort_limit,
534
+ :output_offset => 0,
535
+ :output_limit => output_limit)
536
+ assert_planned(message, envelope)
537
+ end
538
+
539
+ def test_have_sortBy_with_infinity_output_limit
540
+ envelope = {
541
+ "type" => "search",
542
+ "dataset" => "Droonga",
543
+ "body" => {
544
+ "queries" => {
545
+ "have_records" => {
546
+ "source" => "User",
547
+ "sortBy" => {
548
+ "keys" => ["name"],
549
+ "offset" => 1,
550
+ "limit" => 2,
551
+ },
552
+ "output" => {
553
+ "format" => "complex",
554
+ "elements" => ["records"],
555
+ "attributes" => ["_key", "name", "age"],
556
+ "offset" => 4,
557
+ "limit" => -1,
558
+ },
559
+ },
560
+ },
561
+ },
562
+ }
563
+
564
+ limit = 1 + 4 + 2
565
+ message = []
566
+ message << reducer(envelope, {
567
+ "records" => {
568
+ "type" => "sort",
569
+ "operators" => [
570
+ { "column" => 1, "operator" => "<" },
571
+ ],
572
+ "limit" => limit,
573
+ },
574
+ })
575
+ message << gatherer(envelope, :elements => {
576
+ "records" => records_mapper(
577
+ :offset => 5,
578
+ :limit => 2,
579
+ :format => "complex",
580
+ :attributes => ["_key", "name", "age"],
581
+ ),
582
+ })
583
+ message << searcher(envelope, :sort_offset => 0,
584
+ :sort_limit => limit,
585
+ :output_offset => 0,
586
+ :output_limit => limit)
587
+ assert_planned(message, envelope)
588
+ end
589
+
590
+ def test_have_sortBy_with_infinity_sort_limit
591
+ envelope = {
592
+ "type" => "search",
593
+ "dataset" => "Droonga",
594
+ "body" => {
595
+ "queries" => {
596
+ "have_records" => {
597
+ "source" => "User",
598
+ "sortBy" => {
599
+ "keys" => ["name"],
600
+ "offset" => 1,
601
+ "limit" => -1,
602
+ },
603
+ "output" => {
604
+ "format" => "complex",
605
+ "elements" => ["records"],
606
+ "attributes" => ["_key", "name", "age"],
607
+ "offset" => 4,
608
+ "limit" => 8,
609
+ },
610
+ },
611
+ },
612
+ },
613
+ }
614
+
615
+ limit = 1 + 4 + 8
616
+ message = []
617
+ message << reducer(envelope, {
618
+ "records" => {
619
+ "type" => "sort",
620
+ "operators" => [
621
+ { "column" => 1, "operator" => "<" },
622
+ ],
623
+ "limit" => limit,
624
+ },
625
+ })
626
+ message << gatherer(envelope, :elements => {
627
+ "records" => records_mapper(
628
+ :offset => 5,
629
+ :limit => 8,
630
+ :format => "complex",
631
+ :attributes => ["_key", "name", "age"],
632
+ ),
633
+ })
634
+ message << searcher(envelope, :sort_offset => 0,
635
+ :sort_limit => limit,
636
+ :output_offset => 0,
637
+ :output_limit => limit)
638
+ assert_planned(message, envelope)
639
+ end
640
+
641
+ def test_have_sortBy_with_infinity_limit
642
+ envelope = {
643
+ "type" => "search",
644
+ "dataset" => "Droonga",
645
+ "body" => {
646
+ "queries" => {
647
+ "have_records" => {
648
+ "source" => "User",
649
+ "sortBy" => {
650
+ "keys" => ["name"],
651
+ "offset" => 1,
652
+ "limit" => -1,
653
+ },
654
+ "output" => {
655
+ "format" => "complex",
656
+ "elements" => ["records"],
657
+ "attributes" => ["_key", "name", "age"],
658
+ "offset" => 4,
659
+ "limit" => -1,
660
+ },
661
+ },
662
+ },
663
+ },
664
+ }
665
+
666
+ message = []
667
+ message << reducer(envelope, {
668
+ "records" => {
669
+ "type" => "sort",
670
+ "operators" => [
671
+ { "column" => 1, "operator" => "<" },
672
+ ],
673
+ "limit" => -1,
674
+ },
675
+ })
676
+ message << gatherer(envelope, :elements => {
677
+ "records" => records_mapper(
678
+ :offset => 5,
679
+ :limit => -1,
680
+ :format => "complex",
681
+ :attributes => ["_key", "name", "age"],
682
+ ),
683
+ })
684
+ message << searcher(envelope, :sort_offset => 0,
685
+ :sort_limit => -1,
686
+ :output_offset => 0,
687
+ :output_limit => -1)
688
+ assert_planned(message, envelope)
689
+ end
690
+
691
+ def test_have_sortBy_with_multiple_sort_keys
692
+ envelope = {
693
+ "type" => "search",
694
+ "dataset" => "Droonga",
695
+ "body" => {
696
+ "queries" => {
697
+ "have_records" => {
698
+ "source" => "User",
699
+ "sortBy" => {
700
+ "keys" => ["-age", "name"],
701
+ "limit" => -1,
702
+ },
703
+ "output" => {
704
+ "format" => "complex",
705
+ "elements" => ["records"],
706
+ "attributes" => ["_key", "name", "age"],
707
+ "limit" => -1,
708
+ },
709
+ },
710
+ },
711
+ },
712
+ }
713
+
714
+ message = []
715
+ message << reducer(envelope, {
716
+ "records" => {
717
+ "type" => "sort",
718
+ "operators" => [
719
+ { "column" => 2, "operator" => ">" },
720
+ { "column" => 1, "operator" => "<" },
721
+ ],
722
+ "limit" => -1,
723
+ },
724
+ })
725
+ message << gatherer(envelope, :elements => {
726
+ "records" => records_mapper(
727
+ :offset => 0,
728
+ :limit => -1,
729
+ :format => "complex",
730
+ :attributes => ["_key", "name", "age"],
731
+ ),
732
+ })
733
+ message << searcher(envelope, :sort_offset => 0,
734
+ :sort_limit => -1,
735
+ :output_offset => 0,
736
+ :output_limit => -1)
737
+ assert_planned(message, envelope)
738
+ end
739
+
740
+ def test_have_sortBy_with_missing_sort_attributes
741
+ envelope = {
742
+ "type" => "search",
743
+ "dataset" => "Droonga",
744
+ "body" => {
745
+ "queries" => {
746
+ "have_records" => {
747
+ "source" => "User",
748
+ "sortBy" => {
749
+ "keys" => ["-public_age", "public_name"],
750
+ "limit" => -1,
751
+ },
752
+ "output" => {
753
+ "format" => "complex",
754
+ "elements" => ["records"],
755
+ "attributes" => ["_key", "name", "age"],
756
+ "limit" => -1,
757
+ },
758
+ },
759
+ },
760
+ },
761
+ }
762
+
763
+ message = []
764
+ message << reducer(envelope, {
765
+ "records" => {
766
+ "type" => "sort",
767
+ "operators" => [
768
+ { "column" => 3, "operator" => ">" },
769
+ { "column" => 4, "operator" => "<" },
770
+ ],
771
+ "limit" => -1,
772
+ },
773
+ })
774
+ message << gatherer(envelope, :elements => {
775
+ "records" => records_mapper(
776
+ :offset => 0,
777
+ :limit => -1,
778
+ :format => "complex",
779
+ :attributes => ["_key", "name", "age"],
780
+ ),
781
+ })
782
+ message << searcher(envelope, :sort_offset => 0,
783
+ :sort_limit => -1,
784
+ :output_offset => 0,
785
+ :output_limit => -1,
786
+ :extra_attributes => ["public_age", "public_name"])
787
+ assert_planned(message, envelope)
788
+ end
789
+
790
+ def test_hash_attributes
791
+ envelope = {
792
+ "type" => "search",
793
+ "dataset" => "Droonga",
794
+ "body" => {
795
+ "queries" => {
796
+ "have_records" => {
797
+ "source" => "User",
798
+ "sortBy" => {
799
+ "keys" => ["-public_age", "public_name"],
800
+ "limit" => -1,
801
+ },
802
+ "output" => {
803
+ "format" => "complex",
804
+ "elements" => ["records"],
805
+ "attributes" => {
806
+ "id" => "_key",
807
+ "name" => { "source" => "name" },
808
+ "age" => { "source" => "age" },
809
+ },
810
+ "limit" => -1,
811
+ },
812
+ },
813
+ },
814
+ },
815
+ }
816
+
817
+ message = []
818
+ message << reducer(envelope, {
819
+ "records" => {
820
+ "type" => "sort",
821
+ "operators" => [
822
+ { "column" => 3, "operator" => ">" },
823
+ { "column" => 4, "operator" => "<" },
824
+ ],
825
+ "limit" => -1,
826
+ },
827
+ })
828
+ message << gatherer(envelope, :elements => {
829
+ "records" => records_mapper(
830
+ :offset => 0,
831
+ :limit => -1,
832
+ :format => "complex",
833
+ :attributes => ["id", "name", "age"],
834
+ ),
835
+ })
836
+ message << searcher(envelope, :sort_offset => 0,
837
+ :sort_limit => -1,
838
+ :output_offset => 0,
839
+ :output_limit => -1,
840
+ :extra_attributes => ["public_age", "public_name"])
841
+ assert_planned(message, envelope)
842
+ end
843
+
844
+ def test_groupBy
845
+ envelope = {
846
+ "type" => "search",
847
+ "dataset" => "Droonga",
848
+ "body" => {
849
+ "queries" => {
850
+ "grouped_records" => {
851
+ "source" => "User",
852
+ "groupBy" => "family_name",
853
+ "output" => {
854
+ "format" => "complex",
855
+ "elements" => ["records"],
856
+ "attributes" => ["_key", "_nsubrecs"],
857
+ "limit" => -1,
858
+ },
859
+ },
860
+ },
861
+ },
862
+ }
863
+
864
+ message = []
865
+ message << reducer(envelope, {
866
+ "records" => {
867
+ "type" => "sort",
868
+ "operators" => [],
869
+ "key_column" => 0,
870
+ "limit" => -1,
871
+ },
872
+ })
873
+ message << gatherer(envelope, :elements => {
874
+ "records" => records_mapper(
875
+ :offset => 0,
876
+ :limit => -1,
877
+ :format => "complex",
878
+ :attributes => ["_key", "_nsubrecs"],
879
+ ),
880
+ })
881
+ message << searcher(envelope, :output_offset => 0,
882
+ :output_limit => -1,
883
+ :unifiable => true)
884
+ assert_planned(message, envelope)
885
+ end
886
+
887
+ def test_groupBy_count
888
+ envelope = {
889
+ "type" => "search",
890
+ "dataset" => "Droonga",
891
+ "body" => {
892
+ "queries" => {
893
+ "grouped_records" => {
894
+ "source" => "User",
895
+ "groupBy" => "family_name",
896
+ "output" => {
897
+ "elements" => ["count"],
898
+ },
899
+ },
900
+ },
901
+ },
902
+ }
903
+
904
+ message = []
905
+ message << reducer(envelope, {
906
+ "count" => {
907
+ "type" => "sum",
908
+ },
909
+ "records" => {
910
+ "type" => "sort",
911
+ "operators" => [],
912
+ "key_column" => 0,
913
+ "limit" => -1,
914
+ },
915
+ })
916
+ message << gatherer(envelope, :elements => {
917
+ "count" => count_mapper,
918
+ "records" => records_mapper(
919
+ :limit => -1,
920
+ :attributes => ["_key"],
921
+ :no_output => true,
922
+ ),
923
+ })
924
+ message << searcher(envelope, :output_limit => -1,
925
+ :extra_attributes => ["_key"],
926
+ :extra_elements => ["records"],
927
+ :unifiable => true)
928
+ assert_planned(message, envelope)
929
+ end
930
+
931
+ def test_groupBy_hash
932
+ envelope = {
933
+ "type" => "search",
934
+ "dataset" => "Droonga",
935
+ "body" => {
936
+ "queries" => {
937
+ "grouped_records" => {
938
+ "source" => "User",
939
+ "groupBy" => {
940
+ "key" => "family_name",
941
+ "maxNSubRecords" => 3,
942
+ },
943
+ "output" => {
944
+ "format" => "complex",
945
+ "elements" => ["records"],
946
+ "attributes" => [
947
+ { "label" => "family_name", "source" => "_key" },
948
+ { "label" => "count", "source" => "_nsubrecs" },
949
+ { "label" => "users",
950
+ "source" => "_subrecs",
951
+ "attributes" => ["name", "age"] },
952
+ ],
953
+ "limit" => -1,
954
+ },
955
+ },
956
+ },
957
+ },
958
+ }
959
+
960
+ message = []
961
+ message << reducer(envelope, {
962
+ "records" => {
963
+ "type" => "sort",
964
+ "operators" => [],
965
+ "key_column" => 3, # 0=family_name, 1=_nsubrecs, 2=_subrecs, 3=_keys
966
+ "limit" => -1,
967
+ },
968
+ })
969
+ message << gatherer(envelope, :elements => {
970
+ "records" => records_mapper(
971
+ :offset => 0,
972
+ :limit => -1,
973
+ :format => "complex",
974
+ :attributes => ["family_name", "count", "users"],
975
+ ),
976
+ })
977
+ message << searcher(envelope, :output_offset => 0,
978
+ :output_limit => -1,
979
+ :extra_attributes => ["_key"],
980
+ :unifiable => true)
981
+ assert_planned(message, envelope)
982
+ end
983
+
984
+ private
985
+ def reducer(search_request_envelope, reducer_body)
986
+ queries = search_request_envelope["body"]["queries"]
987
+ query_name = queries.keys.first
988
+
989
+ reducer = {
990
+ "type" => "reduce",
991
+ "body" => {
992
+ query_name => {
993
+ "#{query_name}_reduced" => reducer_body,
994
+ },
995
+ },
996
+ "inputs" => [query_name],
997
+ "outputs" => ["#{query_name}_reduced"],
998
+ }
999
+
1000
+ reducer
1001
+ end
1002
+
1003
+ def count_mapper(options={})
1004
+ mapper = {
1005
+ "type" => "count",
1006
+ "target" => "records",
1007
+ }
1008
+ mapper
1009
+ end
1010
+
1011
+ def records_mapper(options={})
1012
+ mapper = {
1013
+ "type" => "sort",
1014
+ "offset" => options[:offset] || 0,
1015
+ "limit" => options[:limit] || 0,
1016
+ "format" => options[:format] || "simple",
1017
+ "attributes" => options[:attributes] || [],
1018
+ }
1019
+ unless options[:no_output].nil?
1020
+ mapper["no_output"] = options[:no_output]
1021
+ end
1022
+ mapper
1023
+ end
1024
+
1025
+ def gatherer(search_request_envelope, options={})
1026
+ queries = search_request_envelope["body"]["queries"]
1027
+ query_name = queries.keys.first
1028
+
1029
+ gatherer = {
1030
+ "type" => "gather",
1031
+ "body" => {
1032
+ },
1033
+ "inputs" => [
1034
+ ],
1035
+ "post" => true,
1036
+ }
1037
+
1038
+ unless options[:no_output]
1039
+ output = {
1040
+ "output" => query_name,
1041
+ "elements" => options[:elements] || {},
1042
+ }
1043
+ gatherer["body"]["#{query_name}_reduced"] = output
1044
+ gatherer["inputs"] << "#{query_name}_reduced"
1045
+ end
1046
+
1047
+ gatherer
1048
+ end
1049
+
1050
+ def searcher(search_request_envelope, options={})
1051
+ # dup and clone don't copy it deeply...
1052
+ searcher = Marshal.load(Marshal.dump(search_request_envelope))
1053
+
1054
+ queries = searcher["body"]["queries"]
1055
+ query_name = queries.keys.first
1056
+ query = queries.values.first
1057
+ if options[:extra_attributes]
1058
+ attributes = query["output"]["attributes"] || []
1059
+ if attributes.is_a?(Hash)
1060
+ array_attributes = attributes.collect do |label, attribute|
1061
+ case attribute
1062
+ when Hash
1063
+ attribute["label"] = label
1064
+ when String
1065
+ attribute = { "label" => label, "source" => attribute }
1066
+ end
1067
+ attribute
1068
+ end
1069
+ attributes = array_attributes
1070
+ end
1071
+ query["output"]["attributes"] = attributes + options[:extra_attributes]
1072
+ end
1073
+ if options[:extra_elements]
1074
+ query["output"]["elements"] += options[:extra_elements]
1075
+ end
1076
+ if options[:sort_offset]
1077
+ query["sortBy"]["offset"] = options[:sort_offset]
1078
+ end
1079
+ if options[:sort_limit]
1080
+ query["sortBy"]["limit"] = options[:sort_limit]
1081
+ end
1082
+ if options[:output_offset]
1083
+ query["output"]["offset"] = options[:output_offset]
1084
+ end
1085
+ if options[:output_limit]
1086
+ query["output"]["limit"] = options[:output_limit]
1087
+ end
1088
+
1089
+ query["output"]["format"] = "simple" if query["output"]
1090
+ query["output"]["unifiable"] = true if options[:unifiable]
1091
+
1092
+ outputs = []
1093
+ outputs << query_name unless options[:no_output]
1094
+
1095
+ searcher["type"] = "broadcast"
1096
+ searcher["command"] = "search"
1097
+ searcher["outputs"] = outputs
1098
+ searcher["replica"] = "random"
1099
+ searcher
1100
+ end
1101
+ end
1102
+ end