oedipus 0.0.5 → 0.0.6

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Oedipus: Sphinx 2 Search Client for Ruby
2
2
 
3
3
  Oedipus is a client for the Sphinx search engine (>= 2.0.2), with support for
4
- real-time indexes and multi and/or faceted searches.
4
+ real-time indexes and multi and/or multi-dimensional faceted searches.
5
5
 
6
6
  It is not a clone of the PHP API, rather it is written from the ground up,
7
7
  wrapping the SphinxQL API offered by searchd. Nor is it a plugin for
@@ -14,6 +14,11 @@ search may be implemented, while remaining light and simple.
14
14
  Data structures are managed using core ruby data types (Array and Hash), ensuring
15
15
  simplicity and flexibilty.
16
16
 
17
+ The current development focus is on supporting realtime indexes, where data is
18
+ indexed from your application, rather than by running the indexer tool that comes
19
+ with Sphinx. You may use indexes that are indexed with the indexer tool, but
20
+ Oedipus does not (yet) provide wrappers for indexing that data via ruby [1].
21
+
17
22
  ## Dependencies
18
23
 
19
24
  * ruby (>= 1.9)
@@ -242,13 +247,16 @@ from the base query.
242
247
 
243
248
  Oedipus allows you to replace '%{query}' in your facets with whatever was in the
244
249
  original query. This can be useful if you want to provide facets that only
245
- perform the search in the title of the document (`"@title (%{query})"`) for example.
250
+ perform the search in the title of the document (`"@title (%{query})"`) for
251
+ example.
246
252
 
247
- Each facet is given a name, which is used to reference them in the results.
253
+ Each facet is given a key, which is used to reference it in the results. This
254
+ key is any arbitrary object that can be used as a key in a ruby Hash. You may,
255
+ for example, use domain-specific objects as keys.
248
256
 
249
257
  Sphinx optimizes the queries by figuring out what the common parts are. Currently
250
258
  it does two optimizations, though in future this will likely improve further, so
251
- using this technique to do your faceted searches is a good idea.
259
+ using this technique to do your faceted searches is the correct approach.
252
260
 
253
261
  ``` ruby
254
262
  results = sphinx[:articles].search(
@@ -283,12 +291,57 @@ results = sphinx[:articles].search(
283
291
  # }
284
292
  ```
285
293
 
294
+ #### Multi-dimensional faceted search
295
+
296
+ If you can add facets to the root query, how about adding facets to the facets
297
+ themselves? Easy:
298
+
299
+ ``` ruby
300
+ results = sphinx[:articles].search(
301
+ "badgers",
302
+ facets: {
303
+ popular: {
304
+ views: 100..10000,
305
+ facets: {
306
+ in_title: "@title (%{query})"
307
+ }
308
+ }
309
+ }
310
+ )
311
+ # => {
312
+ # total_found: 987,
313
+ # time: 0.000,
314
+ # records: [ ... ],
315
+ # facets: {
316
+ # popular: {
317
+ # total_found: 25,
318
+ # time: 0.000,
319
+ # records: [ ... ],
320
+ # facets: {
321
+ # in_title: {
322
+ # total_found: 24,
323
+ # time: 0.000,
324
+ # records: [ ... ]
325
+ # }
326
+ # }
327
+ # }
328
+ # }
329
+ # }
330
+ ```
331
+
332
+ In the above example, the nested facet `:in_title` inherits the default
333
+ parameters from the facet `:popular`, which inherits its parameters from
334
+ the root query. The result is a search for "badgers" limited only to the
335
+ title, with views between 100 and 10000.
336
+
337
+ There is no limit imposed in Oedipus for how deeply facets can be nested.
338
+
286
339
  ### General purpose multi-search
287
340
 
288
341
  If you want to execute multiple queries in a batch that are not related to each
289
342
  other (which would be a faceted search), then you can use `#multi_search`.
290
343
 
291
- You pass a Hash of named queries and get a Hash of named resultsets.
344
+ You pass a Hash of keyed-queries and get a Hash of keyed-resultsets.
292
345
 
293
346
  ``` ruby
294
347
  results = sphinx[:articles].multi_search(
@@ -313,7 +366,7 @@ results = sphinx[:articles].multi_search(
313
366
 
314
367
  There are both unit tests and integration tests in the specs/ directory. By default they
315
368
  will both run, but in order for the integration specs to work, you need a locally
316
- installed copy of [Sphinx] [1]. You then execute the specs as follows:
369
+ installed copy of [Sphinx] [2]. You then execute the specs as follows:
317
370
 
318
371
  SEARCHD=/path/to/bin/searchd bundle exec rake spec
319
372
 
@@ -334,7 +387,9 @@ You may also compile the C extension and run the specs separately, if you prefer
334
387
 
335
388
  ### Footnotes
336
389
 
337
- [1]: You can build a local copy of sphinx without installing it on the system:
390
+ [1]: In practice I find such an abstraction not to be very useful, as it assumes a single-server setup
391
+
392
+ [2]: You can build a local copy of sphinx without installing it on the system:
338
393
 
339
394
  cd sphinx-2.0.4/
340
395
  ./configure
@@ -344,11 +399,10 @@ You may also compile the C extension and run the specs separately, if you prefer
344
399
 
345
400
  ## Future Plans
346
401
 
347
- * Integration with DataMapper and ActiveRecord (DataMapper first)
402
+ * Integration ActiveRecord (DataMapper support has already been added)
348
403
  * Support for re-indexing non-realtime indexes from ruby code
349
404
  * Distributed index support (sharding writes between indexes)
350
405
  * Make C extension optional and provide an implementation in pure-ruby
351
- * N-dimensional faceted search (facets inside of facets)
352
406
  * Query translation layer for Lucene-style AND/OR/NOT and attribute:value interpretation
353
407
  * Fulltext query sanitization for unsafe user input (e.g. @@missing field)
354
408
 
data/lib/oedipus/index.rb CHANGED
@@ -123,7 +123,7 @@ module Oedipus
123
123
  # The results returned include a :facets key, containing the results for each facet.
124
124
  #
125
125
  # @example Performing a faceted search
126
- # index.faceted_search(
126
+ # index.search(
127
127
  # "cats | dogs",
128
128
  # category_id: 7,
129
129
  # facets: {
@@ -132,6 +132,26 @@ module Oedipus
132
132
  # }
133
133
  # )
134
134
  #
135
+ # To perform an n-dimensional faceted search, add a :facets option to each
136
+ # facet. Each facet will inherit from its immediate parent, which inerits
137
+ # from its parent, up to the root query.
138
+ #
139
+ # @example Performing a n-dimensional faceted search
140
+ # index.search(
141
+ # "cats | dogs",
142
+ # facets: {
143
+ # popular: {
144
+ # views: Oedipus.gte(1000),
145
+ # facets: {
146
+ # in_title: "@title (%{query})"
147
+ # }
148
+ # }
149
+ # }
150
+ # )
151
+ #
152
+ # The results in a n-dimensional faceted search are returned with each set
153
+ # of facet results in turn containing a :facets element.
154
+ #
135
155
  # @param [String] query
136
156
  # a fulltext query
137
157
  #
@@ -160,15 +180,7 @@ module Oedipus
160
180
  # a Hash containing meta data, with the records in :records, and if any
161
181
  # facets were included, the facets inside the :facets Hash
162
182
  def search(*args)
163
- query, options = extract_query_data(args)
164
- main_query = [query, options.reject { |k, _| k == :facets }]
165
- facets = merge_queries(main_query, options.fetch(:facets, {}))
166
-
167
- { facets: {} }.tap do |results|
168
- multi_search({ _main_: main_query }.merge(facets)).each do |k, v|
169
- k == :_main_ ? results.merge!(v) : results[:facets].merge!(k => v)
170
- end
171
- end
183
+ expand_facet_tree(multi_search(deep_merge_facets(args)))
172
184
  end
173
185
 
174
186
  # Perform a faceted search on the index, using a base query and one or more facets.
@@ -257,19 +269,51 @@ module Oedipus
257
269
  end
258
270
 
259
271
  case args[0]
260
- when String then [args[0], args.fetch(1, {})]
261
- when Hash then [default_query, args[0] ]
272
+ when String then [args[0], args.fetch(1, {}).dup]
273
+ when Hash then [default_query, args[0].dup ]
262
274
  else raise ArgumentError, "Invalid query argument type #{args.first.class}"
263
275
  end
264
276
  end
265
277
 
266
- def merge_queries(base, others)
267
- base_query, base_filters = base
278
+ def expand_facet_tree(result)
279
+ Hash[].tap do |tree|
280
+ result.each do |k, v|
281
+ t = tree
282
+
283
+ k.each do |name|
284
+ f = t[:facets] ||= {}
285
+ t = f[name] ||= {}
286
+ end
287
+
288
+ t.merge!(v)
289
+ end
290
+ end
291
+ end
292
+
293
+ def deep_merge_facets(base_args, list = {}, path = [])
294
+ # FIXME: Try and make this shorter and more functional in style
295
+ base_query, base_options = extract_query_data(base_args)
296
+
297
+ facets = base_options.delete(:facets)
298
+
299
+ list.merge!(path => [base_query, base_options])
300
+
301
+ unless facets.nil?
302
+ facets.each do |k, q|
303
+ facet_query, facet_options = extract_query_data(q, base_query)
304
+
305
+ deep_merge_facets(
306
+ [
307
+ facet_query.gsub("%{query}", base_query),
308
+ base_options.merge(facet_options)
309
+ ],
310
+ list,
311
+ path.dup << k
312
+ )
313
+ end
314
+ end
268
315
 
269
- Hash[others.map { |k, q|
270
- query, filters = extract_query_data(q, base_query)
271
- [k, [query.gsub("%{query}", base_query), base_filters.merge(filters)]]
272
- }]
316
+ list
273
317
  end
274
318
  end
275
319
  end
@@ -70,7 +70,7 @@ module Oedipus
70
70
 
71
71
  rt_attr_uint = user_id
72
72
  rt_attr_uint = views
73
- rt_attr_string = status
73
+ rt_attr_string = state
74
74
  }
75
75
 
76
76
  searchd
@@ -8,5 +8,5 @@
8
8
  ##
9
9
 
10
10
  module Oedipus
11
- VERSION = "0.0.5"
11
+ VERSION = "0.0.6"
12
12
  end
@@ -63,7 +63,7 @@ describe Oedipus::Index do
63
63
 
64
64
  context "with a valid document ID" do
65
65
  it "returns the matched document" do
66
- index.fetch(2).should == { id: 2, views: 73, user_id: 0, status: "" }
66
+ index.fetch(2).should == { id: 2, views: 73, user_id: 0, state: "" }
67
67
  end
68
68
  end
69
69
 
@@ -86,7 +86,7 @@ describe Oedipus::Index do
86
86
 
87
87
  it "modifies the data" do
88
88
  index.update(1, views: 721)
89
- index.fetch(1).should == { id: 1, views: 721, user_id: 7, status: "" }
89
+ index.fetch(1).should == { id: 1, views: 721, user_id: 7, state: "" }
90
90
  end
91
91
  end
92
92
 
@@ -123,7 +123,7 @@ describe Oedipus::Index do
123
123
 
124
124
  it "entirely replaces the record" do
125
125
  index.replace(1, title: "Badgers and foxes", views: 150)
126
- index.fetch(1).should == { id: 1, views: 150, user_id: 0, status: "" }
126
+ index.fetch(1).should == { id: 1, views: 150, user_id: 0, state: "" }
127
127
  end
128
128
  end
129
129
 
@@ -134,7 +134,7 @@ describe Oedipus::Index do
134
134
 
135
135
  it "entirely replaces the record" do
136
136
  index.replace(2, title: "Beer and wine", views: 15)
137
- index.fetch(2).should == { id: 2, views: 15, user_id: 0, status: "" }
137
+ index.fetch(2).should == { id: 2, views: 15, user_id: 0, state: "" }
138
138
  end
139
139
  end
140
140
 
@@ -181,9 +181,9 @@ describe Oedipus::Index do
181
181
 
182
182
  it "includes the matches records" do
183
183
  index.search("badgers")[:records].should == [
184
- { id: 1, views: 150, user_id: 0, status: "" },
185
- { id: 3, views: 41, user_id: 0, status: "" },
186
- { id: 4, views: 3003, user_id: 0, status: "" }
184
+ { id: 1, views: 150, user_id: 0, state: "" },
185
+ { id: 3, views: 41, user_id: 0, state: "" },
186
+ { id: 4, views: 3003, user_id: 0, state: "" }
187
187
  ]
188
188
  end
189
189
  end
@@ -195,19 +195,19 @@ describe Oedipus::Index do
195
195
 
196
196
  it "includes the matches records" do
197
197
  index.search(views: 40..90)[:records].should == [
198
- { id: 2, views: 87, user_id: 0, status: "" },
199
- { id: 3, views: 41, user_id: 0, status: "" }
198
+ { id: 2, views: 87, user_id: 0, state: "" },
199
+ { id: 3, views: 41, user_id: 0, state: "" }
200
200
  ]
201
201
  end
202
202
 
203
203
  pending "the sphinxql grammar does not currently support this, though I'm patching it" do
204
204
  context "with string attributes" do
205
205
  before(:each) do
206
- index.insert(5, title: "No more badgers, please", views: 0, status: "new")
206
+ index.insert(5, title: "No more badgers, please", views: 0, state: "new")
207
207
  end
208
208
 
209
209
  it "filters by the string attribute" do
210
- index.search(status: "new")[:records].should == [{ id: 5, views: 0, user_id: 0, status: "new" }]
210
+ index.search(state: "new")[:records].should == [{ id: 5, views: 0, user_id: 0, state: "new" }]
211
211
  end
212
212
  end
213
213
  end
@@ -220,8 +220,8 @@ describe Oedipus::Index do
220
220
 
221
221
  it "includes the matches records" do
222
222
  index.search("badgers", views: Oedipus.gt(100))[:records].should == [
223
- { id: 1, views: 150, user_id: 0, status: "" },
224
- { id: 4, views: 3003, user_id: 0, status: "" }
223
+ { id: 1, views: 150, user_id: 0, state: "" },
224
+ { id: 4, views: 3003, user_id: 0, state: "" }
225
225
  ]
226
226
  end
227
227
  end
@@ -233,14 +233,14 @@ describe Oedipus::Index do
233
233
 
234
234
  it "returns the limited subset of the results" do
235
235
  index.search("badgers", limit: 2)[:records].should == [
236
- { id: 1, views: 150, user_id: 0, status: "" },
237
- { id: 3, views: 41, user_id: 0, status: "" }
236
+ { id: 1, views: 150, user_id: 0, state: "" },
237
+ { id: 3, views: 41, user_id: 0, state: "" }
238
238
  ]
239
239
  end
240
240
 
241
241
  it "can use an offset" do
242
242
  index.search("badgers", limit: 1, offset: 1)[:records].should == [
243
- { id: 3, views: 41, user_id: 0, status: "" }
243
+ { id: 3, views: 41, user_id: 0, state: "" }
244
244
  ]
245
245
  end
246
246
  end
@@ -248,9 +248,9 @@ describe Oedipus::Index do
248
248
  context "with ordering" do
249
249
  it "returns the results ordered accordingly" do
250
250
  index.search("badgers", order: {views: :desc})[:records].should == [
251
- { id: 4, views: 3003, user_id: 0, status: "" },
252
- { id: 1, views: 150, user_id: 0, status: "" },
253
- { id: 3, views: 41, user_id: 0, status: "" },
251
+ { id: 4, views: 3003, user_id: 0, state: "" },
252
+ { id: 1, views: 150, user_id: 0, state: "" },
253
+ { id: 3, views: 41, user_id: 0, state: "" },
254
254
  ]
255
255
  end
256
256
 
@@ -265,9 +265,9 @@ describe Oedipus::Index do
265
265
  context "with attribute additions" do
266
266
  it "fetches the additional attributes" do
267
267
  index.search("badgers", attrs: [:*, "7 AS x"])[:records].should == [
268
- { id: 1, views: 150, user_id: 0, status: "", x: 7 },
269
- { id: 3, views: 41, user_id: 0, status: "", x: 7 },
270
- { id: 4, views: 3003, user_id: 0, status: "", x: 7 },
268
+ { id: 1, views: 150, user_id: 0, state: "", x: 7 },
269
+ { id: 3, views: 41, user_id: 0, state: "", x: 7 },
270
+ { id: 4, views: 3003, user_id: 0, state: "", x: 7 },
271
271
  ]
272
272
  end
273
273
  end
@@ -304,19 +304,19 @@ describe Oedipus::Index do
304
304
 
305
305
  it "returns the main results in the top-level" do
306
306
  results[:records].should == [
307
- { id: 1, views: 150, user_id: 1, status: "" },
308
- { id: 3, views: 41, user_id: 2, status: "" },
309
- { id: 4, views: 3003, user_id: 1, status: "" }
307
+ { id: 1, views: 150, user_id: 1, state: "" },
308
+ { id: 3, views: 41, user_id: 2, state: "" },
309
+ { id: 4, views: 3003, user_id: 1, state: "" }
310
310
  ]
311
311
  end
312
312
 
313
313
  it "applies the filters on top of the base query" do
314
314
  results[:facets][:popular][:records].should == [
315
- { id: 1, views: 150, user_id: 1, status: "" },
316
- { id: 4, views: 3003, user_id: 1, status: "" }
315
+ { id: 1, views: 150, user_id: 1, state: "" },
316
+ { id: 4, views: 3003, user_id: 1, state: "" }
317
317
  ]
318
318
  results[:facets][:di_carla][:records].should == [
319
- { id: 3, views: 41, user_id: 2, status: "" }
319
+ { id: 3, views: 41, user_id: 2, state: "" }
320
320
  ]
321
321
  end
322
322
  end
@@ -334,7 +334,7 @@ describe Oedipus::Index do
334
334
 
335
335
  it "applies the filters on top of the base query" do
336
336
  results[:facets][:di_carla][:records].should == [
337
- { id: 3, views: 41, user_id: 2, status: "" }
337
+ { id: 3, views: 41, user_id: 2, state: "" }
338
338
  ]
339
339
  end
340
340
  end
@@ -351,7 +351,7 @@ describe Oedipus::Index do
351
351
 
352
352
  it "entirely replaces the base query" do
353
353
  results[:facets][:rabbits][:records].should == [
354
- { id: 2, views: 87, user_id: 1, status: "" }
354
+ { id: 2, views: 87, user_id: 1, state: "" }
355
355
  ]
356
356
  end
357
357
  end
@@ -368,7 +368,36 @@ describe Oedipus::Index do
368
368
 
369
369
  it "merges the queries" do
370
370
  results[:facets][:in_body][:records].should == [
371
- { id: 1, views: 150, user_id: 1, status: "" },
371
+ { id: 1, views: 150, user_id: 1, state: "" },
372
+ ]
373
+ end
374
+ end
375
+
376
+ context "with multi-dimensional facets" do
377
+ let(:results) do
378
+ index.search(
379
+ "badgers",
380
+ facets: {
381
+ popular: {
382
+ views: Oedipus.gte(50),
383
+ facets: {
384
+ with_foxes: "%{query} & foxes"
385
+ }
386
+ },
387
+ }
388
+ )
389
+ end
390
+
391
+ it "merges the results in the outer facets" do
392
+ results[:facets][:popular][:records].should == [
393
+ { id: 1, views: 150, user_id: 1, state: "" },
394
+ { id: 4, views: 3003, user_id: 1, state: "" }
395
+ ]
396
+ end
397
+
398
+ it "merges the results in the inner facets" do
399
+ results[:facets][:popular][:facets][:with_foxes][:records].should == [
400
+ { id: 1, views: 150, user_id: 1, state: "" }
372
401
  ]
373
402
  end
374
403
  end
@@ -398,12 +427,12 @@ describe Oedipus::Index do
398
427
  rabbits: "rabbits"
399
428
  )
400
429
  results[:badgers][:records].should == [
401
- { id: 1, views: 150, user_id: 1, status: "" },
402
- { id: 3, views: 41, user_id: 2, status: "" },
403
- { id: 4, views: 3003, user_id: 1, status: "" }
430
+ { id: 1, views: 150, user_id: 1, state: "" },
431
+ { id: 3, views: 41, user_id: 2, state: "" },
432
+ { id: 4, views: 3003, user_id: 1, state: "" }
404
433
  ]
405
434
  results[:rabbits][:records].should == [
406
- { id: 2, views: 87, user_id: 1, status: "" }
435
+ { id: 2, views: 87, user_id: 1, state: "" }
407
436
  ]
408
437
  end
409
438
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: oedipus
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.5
4
+ version: 0.0.6
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-04-29 00:00:00.000000000 Z
12
+ date: 2012-05-02 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rspec
16
- requirement: &14417400 !ruby/object:Gem::Requirement
16
+ requirement: &20757600 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ! '>='
@@ -21,10 +21,10 @@ dependencies:
21
21
  version: '0'
22
22
  type: :development
23
23
  prerelease: false
24
- version_requirements: *14417400
24
+ version_requirements: *20757600
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: rake-compiler
27
- requirement: &14717600 !ruby/object:Gem::Requirement
27
+ requirement: &20756700 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
30
  - - ! '>='
@@ -32,7 +32,7 @@ dependencies:
32
32
  version: '0'
33
33
  type: :development
34
34
  prerelease: false
35
- version_requirements: *14717600
35
+ version_requirements: *20756700
36
36
  description: ! "== Sphinx 2 Comes to Ruby\n\nOedipus brings full support for Sphinx
37
37
  2 to Ruby:\n\n - real-time indexes (insert, replace, update, delete)\n - faceted
38
38
  search (variations on a base query)\n - multi-queries (multiple queries executed
@@ -109,7 +109,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
109
109
  version: '0'
110
110
  segments:
111
111
  - 0
112
- hash: 4166348847258820467
112
+ hash: -1569263709563515069
113
113
  required_rubygems_version: !ruby/object:Gem::Requirement
114
114
  none: false
115
115
  requirements:
@@ -118,7 +118,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
118
118
  version: '0'
119
119
  segments:
120
120
  - 0
121
- hash: 4166348847258820467
121
+ hash: -1569263709563515069
122
122
  requirements: []
123
123
  rubyforge_project: oedipus
124
124
  rubygems_version: 1.8.11