dbldots_oedipus 0.0.16

Sign up to get free protection for your applications and to get access to all the features.
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