dbldots_oedipus 0.0.16

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 (54) hide show
  1. data/.gitignore +10 -0
  2. data/.rspec +1 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE +20 -0
  5. data/README.md +435 -0
  6. data/Rakefile +26 -0
  7. data/ext/oedipus/extconf.rb +72 -0
  8. data/ext/oedipus/lexing.c +96 -0
  9. data/ext/oedipus/lexing.h +20 -0
  10. data/ext/oedipus/oedipus.c +339 -0
  11. data/ext/oedipus/oedipus.h +58 -0
  12. data/lib/oedipus.rb +40 -0
  13. data/lib/oedipus/comparison.rb +88 -0
  14. data/lib/oedipus/comparison/between.rb +21 -0
  15. data/lib/oedipus/comparison/equal.rb +21 -0
  16. data/lib/oedipus/comparison/gt.rb +21 -0
  17. data/lib/oedipus/comparison/gte.rb +21 -0
  18. data/lib/oedipus/comparison/in.rb +21 -0
  19. data/lib/oedipus/comparison/lt.rb +21 -0
  20. data/lib/oedipus/comparison/lte.rb +21 -0
  21. data/lib/oedipus/comparison/not.rb +25 -0
  22. data/lib/oedipus/comparison/not_equal.rb +21 -0
  23. data/lib/oedipus/comparison/not_in.rb +21 -0
  24. data/lib/oedipus/comparison/outside.rb +21 -0
  25. data/lib/oedipus/comparison/shortcuts.rb +144 -0
  26. data/lib/oedipus/connection.rb +124 -0
  27. data/lib/oedipus/connection/pool.rb +133 -0
  28. data/lib/oedipus/connection/registry.rb +56 -0
  29. data/lib/oedipus/connection_error.rb +14 -0
  30. data/lib/oedipus/index.rb +320 -0
  31. data/lib/oedipus/query_builder.rb +185 -0
  32. data/lib/oedipus/rspec/test_rig.rb +132 -0
  33. data/lib/oedipus/version.rb +12 -0
  34. data/oedipus.gemspec +42 -0
  35. data/spec/data/.gitkeep +0 -0
  36. data/spec/integration/connection/registry_spec.rb +50 -0
  37. data/spec/integration/connection_spec.rb +156 -0
  38. data/spec/integration/index_spec.rb +442 -0
  39. data/spec/spec_helper.rb +16 -0
  40. data/spec/unit/comparison/between_spec.rb +36 -0
  41. data/spec/unit/comparison/equal_spec.rb +22 -0
  42. data/spec/unit/comparison/gt_spec.rb +22 -0
  43. data/spec/unit/comparison/gte_spec.rb +22 -0
  44. data/spec/unit/comparison/in_spec.rb +22 -0
  45. data/spec/unit/comparison/lt_spec.rb +22 -0
  46. data/spec/unit/comparison/lte_spec.rb +22 -0
  47. data/spec/unit/comparison/not_equal_spec.rb +22 -0
  48. data/spec/unit/comparison/not_in_spec.rb +22 -0
  49. data/spec/unit/comparison/not_spec.rb +37 -0
  50. data/spec/unit/comparison/outside_spec.rb +36 -0
  51. data/spec/unit/comparison/shortcuts_spec.rb +125 -0
  52. data/spec/unit/comparison_spec.rb +109 -0
  53. data/spec/unit/query_builder_spec.rb +205 -0
  54. metadata +164 -0
@@ -0,0 +1,442 @@
1
+ # encoding: utf-8
2
+
3
+ ##
4
+ # Oedipus Sphinx 2 Search.
5
+ # Copyright © 2012 Chris Corbyn.
6
+ #
7
+ # See LICENSE file for details.
8
+ ##
9
+
10
+ require "spec_helper"
11
+ require "oedipus/rspec/test_rig"
12
+
13
+ describe Oedipus::Index do
14
+ include_context "oedipus test rig"
15
+ include_context "oedipus posts_rt"
16
+
17
+ let(:conn) { connection }
18
+ let(:index) { Oedipus::Index.new(:posts_rt, conn) }
19
+
20
+ describe "#insert" do
21
+ context "with valid data" do
22
+ it "returns the number of rows inserted" do
23
+ index.insert(
24
+ 10,
25
+ title: "Badgers",
26
+ body: "They live in setts, do badgers.",
27
+ views: 721,
28
+ user_id: 7
29
+ ).should == 1
30
+ end
31
+ end
32
+
33
+ context "with invalid data" do
34
+ it "raises an error" do
35
+ expect {
36
+ index.insert(
37
+ 10,
38
+ bad_field: "Invalid",
39
+ body: "They live in setts, do badgers.",
40
+ views: 721,
41
+ user_id: 7
42
+ )
43
+ }.to raise_error(Oedipus::ConnectionError)
44
+ end
45
+ end
46
+ end
47
+
48
+ describe "#fetch" do
49
+ before(:each) do
50
+ index.insert(1, title: "Badgers and foxes", views: 150)
51
+ index.insert(2, title: "Rabbits and hares", views: 73)
52
+ index.insert(3, title: "Clowns and cannon girls", views: 1)
53
+ end
54
+
55
+ context "with a valid document ID" do
56
+ it "returns the matched document" do
57
+ index.fetch(2).should == { id: 2, views: 73, user_id: 0, state: "" }
58
+ end
59
+ end
60
+
61
+ context "with a bad document ID" do
62
+ it "returns nil" do
63
+ index.fetch(7).should be_nil
64
+ end
65
+ end
66
+ end
67
+
68
+ describe "#update" do
69
+ before(:each) do
70
+ index.insert(1, title: "Badgers and foxes", views: 150, user_id: 7)
71
+ end
72
+
73
+ context "with valid data" do
74
+ it "returns the number of rows modified" do
75
+ index.update(1, views: 721).should == 1
76
+ end
77
+
78
+ it "modifies the data" do
79
+ index.update(1, views: 721)
80
+ index.fetch(1).should == { id: 1, views: 721, user_id: 7, state: "" }
81
+ end
82
+ end
83
+
84
+ context "with unmatched data" do
85
+ it "returns 0" do
86
+ index.update(3, views: 721).should == 0
87
+ end
88
+ end
89
+
90
+ context "with invalid data" do
91
+ it "raises an error" do
92
+ expect {
93
+ index.update(1, bad_field: "Invalid", views: 721)
94
+ }.to raise_error(Oedipus::ConnectionError)
95
+ end
96
+ end
97
+ end
98
+
99
+ describe "#replace" do
100
+ before(:each) do
101
+ index.insert(
102
+ 1,
103
+ title: "Badgers",
104
+ body: "They live in setts, do badgers.",
105
+ views: 721,
106
+ user_id: 7
107
+ )
108
+ end
109
+
110
+ context "with valid existing data" do
111
+ it "returns the number of rows inserted" do
112
+ index.replace(1, title: "Badgers and foxes", views: 150).should == 1
113
+ end
114
+
115
+ it "entirely replaces the record" do
116
+ index.replace(1, title: "Badgers and foxes", views: 150)
117
+ index.fetch(1).should == { id: 1, views: 150, user_id: 0, state: "" }
118
+ end
119
+ end
120
+
121
+ context "with valid new data" do
122
+ it "returns the number of rows inserted" do
123
+ index.replace(2, title: "Beer and wine", views: 15).should == 1
124
+ end
125
+
126
+ it "entirely replaces the record" do
127
+ index.replace(2, title: "Beer and wine", views: 15)
128
+ index.fetch(2).should == { id: 2, views: 15, user_id: 0, state: "" }
129
+ end
130
+ end
131
+
132
+ context "with invalid data" do
133
+ it "raises an error" do
134
+ expect {
135
+ index.replace(1, bad_field: "Badgers and foxes", views: 150)
136
+ }.to raise_error(Oedipus::ConnectionError)
137
+ end
138
+ end
139
+ end
140
+
141
+ describe "#delete" do
142
+ before(:each) do
143
+ index.insert(
144
+ 1,
145
+ title: "Badgers",
146
+ body: "They live in setts, do badgers.",
147
+ views: 721,
148
+ user_id: 7
149
+ )
150
+ end
151
+
152
+ context "with valid existing data" do
153
+ it "entirely deletes the record" do
154
+ index.delete(1)
155
+ index.fetch(1).should be_nil
156
+ end
157
+ end
158
+ end
159
+
160
+ describe "#search" do
161
+ before(:each) do
162
+ index.insert(1, title: "Badgers and foxes", views: 150)
163
+ index.insert(2, title: "Rabbits and hares", views: 87)
164
+ index.insert(3, title: "Badgers in the wild", views: 41)
165
+ index.insert(4, title: "Badgers for all, badgers!", views: 3003)
166
+ end
167
+
168
+ context "by fulltext matching" do
169
+ it "indicates the number of records found" do
170
+ index.search("badgers")[:total_found].should == 3
171
+ end
172
+
173
+ it "includes the matches records" do
174
+ index.search("badgers")[:records].should == [
175
+ { id: 1, views: 150, user_id: 0, state: "" },
176
+ { id: 3, views: 41, user_id: 0, state: "" },
177
+ { id: 4, views: 3003, user_id: 0, state: "" }
178
+ ]
179
+ end
180
+ end
181
+
182
+ context "by attribute filtering" do
183
+ it "indicates the number of records found" do
184
+ index.search(views: 40..90)[:total_found].should == 2
185
+ end
186
+
187
+ it "includes the matches records" do
188
+ index.search(views: 40..90)[:records].should == [
189
+ { id: 2, views: 87, user_id: 0, state: "" },
190
+ { id: 3, views: 41, user_id: 0, state: "" }
191
+ ]
192
+ end
193
+
194
+ pending "the sphinxql grammar does not currently support this, though I'm patching it" do
195
+ context "with string attributes" do
196
+ before(:each) do
197
+ index.insert(5, title: "No more badgers, please", views: 0, state: "new")
198
+ end
199
+
200
+ it "filters by the string attribute" do
201
+ index.search(state: "new")[:records].should == [{ id: 5, views: 0, user_id: 0, state: "new" }]
202
+ end
203
+ end
204
+ end
205
+ end
206
+
207
+ context "by fulltext with attribute filtering" do
208
+ it "indicates the number of records found" do
209
+ index.search("badgers", views: Oedipus.gt(100))[:total_found].should == 2
210
+ end
211
+
212
+ it "includes the matches records" do
213
+ index.search("badgers", views: Oedipus.gt(100))[:records].should == [
214
+ { id: 1, views: 150, user_id: 0, state: "" },
215
+ { id: 4, views: 3003, user_id: 0, state: "" }
216
+ ]
217
+ end
218
+ end
219
+
220
+ context "with limits" do
221
+ it "still indicates the number of records found" do
222
+ index.search("badgers", limit: 2)[:total_found].should == 3
223
+ end
224
+
225
+ it "returns the limited subset of the results" do
226
+ index.search("badgers", limit: 2)[:records].should == [
227
+ { id: 1, views: 150, user_id: 0, state: "" },
228
+ { id: 3, views: 41, user_id: 0, state: "" }
229
+ ]
230
+ end
231
+
232
+ it "can use an offset" do
233
+ index.search("badgers", limit: 1, offset: 1)[:records].should == [
234
+ { id: 3, views: 41, user_id: 0, state: "" }
235
+ ]
236
+ end
237
+ end
238
+
239
+ context "with ordering" do
240
+ it "returns the results ordered accordingly" do
241
+ index.search("badgers", order: {views: :desc})[:records].should == [
242
+ { id: 4, views: 3003, user_id: 0, state: "" },
243
+ { id: 1, views: 150, user_id: 0, state: "" },
244
+ { id: 3, views: 41, user_id: 0, state: "" },
245
+ ]
246
+ end
247
+
248
+ context "by relevance" do
249
+ it "returns the results ordered by most relevant" do
250
+ records = index.search("badgers", order: {relevance: :desc})[:records]
251
+ records.first[:relevance].should > records.last[:relevance]
252
+ end
253
+ end
254
+ end
255
+
256
+ context "with attribute additions" do
257
+ it "fetches the additional attributes" do
258
+ index.search("badgers", attrs: [:*, "7 AS x"])[:records].should == [
259
+ { id: 1, views: 150, user_id: 0, state: "", x: 7 },
260
+ { id: 3, views: 41, user_id: 0, state: "", x: 7 },
261
+ { id: 4, views: 3003, user_id: 0, state: "", x: 7 },
262
+ ]
263
+ end
264
+ end
265
+
266
+ context "with attribute restrictions" do
267
+ it "fetches the restricted attributes" do
268
+ index.search("badgers", attrs: [:id, :views])[:records].should == [
269
+ { id: 1, views: 150 },
270
+ { id: 3, views: 41 },
271
+ { id: 4, views: 3003 },
272
+ ]
273
+ end
274
+ end
275
+ end
276
+
277
+ describe "#search", "with :facets" do
278
+ before(:each) do
279
+ index.insert(1, title: "Badgers and foxes", body: "Badgers", views: 150, user_id: 1)
280
+ index.insert(2, title: "Rabbits and hares", body: "Rabbits", views: 87, user_id: 1)
281
+ index.insert(3, title: "Badgers in the wild", body: "Test", views: 41, user_id: 2)
282
+ index.insert(4, title: "Badgers for all!", body: "For all", views: 3003, user_id: 1)
283
+ end
284
+
285
+ context "with additional attribute filters" do
286
+ let(:results) do
287
+ index.search(
288
+ "badgers",
289
+ facets: {
290
+ popular: {views: Oedipus.gte(50)},
291
+ di_carla: {user_id: 2}
292
+ }
293
+ )
294
+ end
295
+
296
+ it "returns the main results in the top-level" do
297
+ results[:records].should == [
298
+ { id: 1, views: 150, user_id: 1, state: "" },
299
+ { id: 3, views: 41, user_id: 2, state: "" },
300
+ { id: 4, views: 3003, user_id: 1, state: "" }
301
+ ]
302
+ end
303
+
304
+ it "applies the filters on top of the base query" do
305
+ results[:facets][:popular][:records].should == [
306
+ { id: 1, views: 150, user_id: 1, state: "" },
307
+ { id: 4, views: 3003, user_id: 1, state: "" }
308
+ ]
309
+ results[:facets][:di_carla][:records].should == [
310
+ { id: 3, views: 41, user_id: 2, state: "" }
311
+ ]
312
+ end
313
+ end
314
+
315
+ context "with overriding attribute filters" do
316
+ let(:results) do
317
+ index.search(
318
+ "badgers",
319
+ user_id: 1,
320
+ facets: {
321
+ di_carla: {user_id: 2}
322
+ }
323
+ )
324
+ end
325
+
326
+ it "applies the filters on top of the base query" do
327
+ results[:facets][:di_carla][:records].should == [
328
+ { id: 3, views: 41, user_id: 2, state: "" }
329
+ ]
330
+ end
331
+ end
332
+
333
+ context "with overriding overriding fulltext queries" do
334
+ let(:results) do # FIXME: Weird RSpec bug is not clearing the previous result, hence the ridiculous naming
335
+ index.search(
336
+ "badgers",
337
+ facets: {
338
+ rabbits: "rabbits"
339
+ }
340
+ )
341
+ end
342
+
343
+ it "entirely replaces the base query" do
344
+ results[:facets][:rabbits][:records].should == [
345
+ { id: 2, views: 87, user_id: 1, state: "" }
346
+ ]
347
+ end
348
+ end
349
+
350
+ context "with overriding refined fulltext queries" do
351
+ let(:results) do
352
+ index.search(
353
+ "badgers",
354
+ facets: {
355
+ in_body: "@body (%{query})"
356
+ }
357
+ )
358
+ end
359
+
360
+ it "merges the queries" do
361
+ results[:facets][:in_body][:records].should == [
362
+ { id: 1, views: 150, user_id: 1, state: "" },
363
+ ]
364
+ end
365
+ end
366
+
367
+ context "with multi-dimensional facets" do
368
+ let(:results) do
369
+ index.search(
370
+ "badgers",
371
+ facets: {
372
+ popular: {
373
+ views: Oedipus.gte(50),
374
+ facets: {
375
+ with_foxes: "%{query} & foxes"
376
+ }
377
+ },
378
+ }
379
+ )
380
+ end
381
+
382
+ it "merges the results in the outer facets" do
383
+ results[:facets][:popular][:records].should == [
384
+ { id: 1, views: 150, user_id: 1, state: "" },
385
+ { id: 4, views: 3003, user_id: 1, state: "" }
386
+ ]
387
+ end
388
+
389
+ it "merges the results in the inner facets" do
390
+ results[:facets][:popular][:facets][:with_foxes][:records].should == [
391
+ { id: 1, views: 150, user_id: 1, state: "" }
392
+ ]
393
+ end
394
+ end
395
+ end
396
+
397
+ describe "#multi_search" do
398
+ before(:each) do
399
+ index.insert(1, title: "Badgers and foxes", views: 150, user_id: 1)
400
+ index.insert(2, title: "Rabbits and hares", views: 87, user_id: 1)
401
+ index.insert(3, title: "Badgers in the wild", views: 41, user_id: 2)
402
+ index.insert(4, title: "Badgers for all!", views: 3003, user_id: 1)
403
+ end
404
+
405
+ context "by fulltext querying" do
406
+ it "indicates the number of results for each query" do
407
+ results = index.multi_search(
408
+ badgers: "badgers",
409
+ rabbits: "rabbits"
410
+ )
411
+ results[:badgers][:total_found].should == 3
412
+ results[:rabbits][:total_found].should == 1
413
+ end
414
+
415
+ it "returns the records for each search" do
416
+ results = index.multi_search(
417
+ badgers: "badgers",
418
+ rabbits: "rabbits"
419
+ )
420
+ results[:badgers][:records].should == [
421
+ { id: 1, views: 150, user_id: 1, state: "" },
422
+ { id: 3, views: 41, user_id: 2, state: "" },
423
+ { id: 4, views: 3003, user_id: 1, state: "" }
424
+ ]
425
+ results[:rabbits][:records].should == [
426
+ { id: 2, views: 87, user_id: 1, state: "" }
427
+ ]
428
+ end
429
+ end
430
+
431
+ context "by attribute filtering" do
432
+ it "indicates the number of results for each query" do
433
+ results = index.multi_search(
434
+ shiela: {user_id: 1},
435
+ barry: {user_id: 2}
436
+ )
437
+ results[:shiela][:total_found].should == 3
438
+ results[:barry][:total_found].should == 1
439
+ end
440
+ end
441
+ end
442
+ end
@@ -0,0 +1,16 @@
1
+ # encoding: utf-8
2
+
3
+ ##
4
+ # Oedipus Sphinx 2 Search.
5
+ # Copyright © 2012 Chris Corbyn.
6
+ #
7
+ # See LICENSE file for details.
8
+ ##
9
+
10
+ require "bundler/setup"
11
+
12
+ require "rspec"
13
+ require "oedipus"
14
+
15
+ RSpec.configure do |config|
16
+ end