supernova 0.2.2 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/TODO ADDED
@@ -0,0 +1,3 @@
1
+ - mapping von dynamic zu static fields in load_document
2
+ - setzen von solr_row in load document
3
+ - speichern von :type in :type_s
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.2.2
1
+ 0.3.0
@@ -24,6 +24,8 @@ module Supernova
24
24
  end
25
25
 
26
26
  require "supernova/numeric_extensions"
27
+ require "supernova/symbol_extensions"
28
+ require "supernova/condition"
27
29
  require "supernova/collection"
28
30
  require "supernova/criteria"
29
31
  require "supernova/thinking_sphinx"
@@ -0,0 +1,27 @@
1
+ class Supernova::Condition
2
+ attr_accessor :key, :type
3
+
4
+ def initialize(key, type)
5
+ self.key = key
6
+ self.type = type
7
+ end
8
+
9
+ def solr_filter_for(value)
10
+ case type
11
+ when :not, :ne
12
+ if value.nil?
13
+ "#{self.key}:[* TO *]"
14
+ else
15
+ "!#{self.key}:#{value}"
16
+ end
17
+ when :gt
18
+ "#{self.key}:{#{value} TO *}"
19
+ when :gte
20
+ "#{self.key}:[#{value} TO *]"
21
+ when :lt
22
+ "#{self.key}:{* TO #{value}}"
23
+ when :lte
24
+ "#{self.key}:[* TO #{value}]"
25
+ end
26
+ end
27
+ end
@@ -28,6 +28,10 @@ class Supernova::Criteria
28
28
  def for_classes(clazzes)
29
29
  merge_filters :classes, [clazzes].flatten
30
30
  end
31
+
32
+ def attribute_mapping(mapping)
33
+ merge_search_options :attribute_mapping, mapping
34
+ end
31
35
 
32
36
  def order(order_option)
33
37
  merge_search_options :order, order_option
@@ -41,14 +45,18 @@ class Supernova::Criteria
41
45
  merge_search_options :group_by, group_option
42
46
  end
43
47
 
44
- def search(query)
45
- merge_filters :search, query
48
+ def search(*terms)
49
+ merge_filters_array :search, terms
46
50
  end
47
51
 
48
52
  def with(filters)
49
53
  merge_filters :with, filters
50
54
  end
51
55
 
56
+ def where(*args)
57
+ with(*args)
58
+ end
59
+
52
60
  def without(filters)
53
61
  self.filters[:without] ||= Hash.new
54
62
  filters.each do |key, value|
@@ -59,11 +67,7 @@ class Supernova::Criteria
59
67
  end
60
68
 
61
69
  def select(*fields)
62
- self.search_options[:select] ||= Array.new
63
- fields.flatten.each do |field|
64
- self.search_options[:select] << field if !self.search_options[:select].include?(field)
65
- end
66
- self
70
+ merge_filters_array :select, fields
67
71
  end
68
72
 
69
73
  def conditions(filters)
@@ -105,6 +109,14 @@ class Supernova::Criteria
105
109
  def merge_filters(key, value)
106
110
  merge_filters_or_search_options(self.filters, key, value)
107
111
  end
112
+
113
+ def merge_filters_array(key, fields)
114
+ self.search_options[key] ||= Array.new
115
+ fields.flatten.each do |field|
116
+ self.search_options[key] << field if !self.search_options[key].include?(field)
117
+ end
118
+ self
119
+ end
108
120
 
109
121
  def merge_search_options(key, value)
110
122
  merge_filters_or_search_options(self.search_options, key, value)
@@ -1,26 +1,29 @@
1
1
  require "rsolr"
2
2
 
3
3
  class Supernova::SolrCriteria < Supernova::Criteria
4
+ # move this into separate methods (test each separatly)
4
5
  def to_params
5
6
  solr_options = { :fq => [], :q => "*:*" }
6
- solr_options[:fq] += self.filters[:with].map { |key, value| "#{key}:#{value}" } if self.filters[:with]
7
+ solr_options[:fq] += fq_from_with(self.filters[:with])
7
8
  if self.filters[:without]
8
- self.filters[:without].each do |key, values|
9
- solr_options[:fq] += values.map { |value| "!#{key}:#{value}" }
9
+ self.filters[:without].each do |field, values|
10
+ solr_options[:fq] += values.map { |value| "!#{solr_field_from_field(field)}:#{value}" }
10
11
  end
11
12
  end
12
- solr_options[:sort] = self.search_options[:order] if self.search_options[:order]
13
- solr_options[:q] = self.filters[:search] if self.filters[:search]
13
+ solr_options[:sort] = convert_search_order(self.search_options[:order]) if self.search_options[:order]
14
+ if self.search_options[:search].is_a?(Array)
15
+ solr_options[:q] = self.search_options[:search].map { |query| "(#{query})" }.join(" AND ")
16
+ end
14
17
 
15
18
  if self.search_options[:geo_center] && self.search_options[:geo_distance]
16
19
  solr_options[:pt] = "#{self.search_options[:geo_center][:lat]},#{self.search_options[:geo_center][:lng]}"
17
20
  solr_options[:d] = self.search_options[:geo_distance].to_f / Supernova::KM_TO_METER
18
- solr_options[:sfield] = :location
21
+ solr_options[:sfield] = solr_field_from_field(:location)
19
22
  solr_options[:fq] << "{!geofilt}"
20
23
  end
21
24
  if self.search_options[:select]
22
25
  self.search_options[:select] << :id
23
- solr_options[:fl] = self.search_options[:select].compact.join(",")
26
+ solr_options[:fl] = self.search_options[:select].compact.map { |field| solr_field_from_field(field) }.join(",")
24
27
  end
25
28
  solr_options[:fq] << "type:#{self.clazz}" if self.clazz
26
29
 
@@ -31,6 +34,39 @@ class Supernova::SolrCriteria < Supernova::Criteria
31
34
  solr_options
32
35
  end
33
36
 
37
+ def convert_search_order(order)
38
+ asc_or_desc = nil
39
+ field = solr_field_from_field(order)
40
+ if order.match(/^(.*?) (asc|desc)/i)
41
+ field = solr_field_from_field($1)
42
+ asc_or_desc = $2
43
+ end
44
+ [field, asc_or_desc].compact.join(" ")
45
+ end
46
+
47
+ def solr_field_from_field(field)
48
+ Supernova::SolrIndexer.solr_field_for_field_name_and_mapping(field, search_options[:attribute_mapping])
49
+ end
50
+
51
+ def fq_from_with(with)
52
+ if with.blank?
53
+ []
54
+ else
55
+ with.map do |key_or_condition, value|
56
+ if key_or_condition.respond_to?(:solr_filter_for)
57
+ key_or_condition.key = solr_field_from_field(key_or_condition.key)
58
+ key_or_condition.solr_filter_for(value)
59
+ else
60
+ fq_filter_for_key_and_value(solr_field_from_field(key_or_condition), value)
61
+ end
62
+ end
63
+ end
64
+ end
65
+
66
+ def fq_filter_for_key_and_value(key, value)
67
+ "#{key}:#{value.is_a?(Range) ? "[#{value.first} TO #{value.last}]" : value}"
68
+ end
69
+
34
70
  def build_docs(docs)
35
71
  docs.map do |hash|
36
72
  self.search_options[:build_doc_method] ? self.search_options[:build_doc_method].call(hash) : build_doc(hash)
@@ -42,8 +78,9 @@ class Supernova::SolrCriteria < Supernova::Criteria
42
78
  end
43
79
 
44
80
  def build_doc(hash)
45
- return hash if hash["type"].nil?
81
+ return hash if !hash["type"].respond_to?(:constantize)
46
82
  doc = hash["type"].constantize.new
83
+ doc.instance_variable_set("@solr_doc", hash)
47
84
  hash.each do |key, value|
48
85
  if key == "id"
49
86
  doc.id = value.to_s.split("/").last if doc.respond_to?(:id=)
@@ -1,14 +1,129 @@
1
1
  require "json"
2
2
 
3
3
  class Supernova::SolrIndexer
4
- attr_accessor :options, :db
4
+ attr_accessor :options, :db, :ids
5
5
  attr_writer :index_file_path
6
6
 
7
+ class << self
8
+ def field_definitions
9
+ @field_definitions ||= {}
10
+ end
11
+
12
+ def has(key, attributes)
13
+ field_definitions[key] = attributes
14
+ end
15
+
16
+ def clazz(class_name =:only_return)
17
+ @clazz = class_name if class_name != :only_return
18
+ @clazz
19
+ end
20
+
21
+ def table_name(name = :only_return)
22
+ @table_name = name if name != :only_return
23
+ @table_name
24
+ end
25
+
26
+ def method_missing(*args)
27
+ criteria = Supernova::SolrCriteria.new(self.clazz).attribute_mapping(self.field_definitions)
28
+ if criteria.respond_to?(args.first)
29
+ criteria.send(*args)
30
+ else
31
+ super
32
+ end
33
+ end
34
+ end
35
+
36
+ FIELD_SUFFIX_MAPPING = {
37
+ :raw => nil,
38
+ :string => :s,
39
+ :text => :t,
40
+ :int => :i,
41
+ :integer => :i,
42
+ :sint => :si,
43
+ :float => :f,
44
+ :date => :dt,
45
+ :boolean => :b,
46
+ :location => :p
47
+ }
48
+
7
49
  def initialize(options = {})
8
50
  options.each do |key, value|
9
51
  self.send(:"#{key}=", value) if self.respond_to?(:"#{key}=")
10
52
  end
11
53
  self.options = options
54
+ self.ids ||= :all
55
+ end
56
+
57
+ def index!
58
+ index_query(query_to_index) do |row|
59
+ row_to_solr(row)
60
+ end
61
+ end
62
+
63
+ def row_to_solr(row)
64
+ row
65
+ end
66
+
67
+ def table_name
68
+ self.class.table_name || (self.class.clazz && self.class.clazz.respond_to?(:table_name) ? self.class.clazz.table_name : nil)
69
+ end
70
+
71
+ def query_to_index
72
+ raise "no table_name defined" if self.table_name.nil?
73
+ query = "SELECT #{select_fields.join(", ")} FROM #{self.table_name}"
74
+ query << " WHERE id IN (#{ids.join(", ")})" if ids_given?
75
+ query
76
+ end
77
+
78
+ def default_fields
79
+ fields = ["id"]
80
+ fields << %("#{self.class.clazz}" AS type_s) if self.class.clazz
81
+ fields
82
+ end
83
+
84
+ def defined_fields
85
+ self.class.field_definitions.map do |field, options|
86
+ sql_column_from_field_and_type(field, options[:type]) if options[:virtual] != true
87
+ end.compact
88
+ end
89
+
90
+ def select_fields
91
+ default_fields + defined_fields
92
+ end
93
+
94
+ def validate_lat(lat)
95
+ float_or_nil_when_abs_bigger_than(lat, 90)
96
+ end
97
+
98
+ def validate_lng(lng)
99
+ float_or_nil_when_abs_bigger_than(lng, 180)
100
+ end
101
+
102
+ def float_or_nil_when_abs_bigger_than(value, border)
103
+ return nil if value.to_s.strip.length == 0
104
+ value_f = value.to_f
105
+ value_f.abs > border ? nil : value_f
106
+ end
107
+
108
+ def sql_column_from_field_and_type(field, type)
109
+ return sql_date_column_from_field(field) if type == :date
110
+ if suffix = self.class.suffix_from_type(type)
111
+ "#{field} AS #{field}_#{suffix}"
112
+ else
113
+ raise "no suffix for #{type} defined"
114
+ end
115
+ end
116
+
117
+ def self.suffix_from_type(type)
118
+ FIELD_SUFFIX_MAPPING[type.to_sym]
119
+ end
120
+
121
+ def self.solr_field_for_field_name_and_mapping(field, mapping)
122
+ [field, mapping && mapping[field.to_sym] ? suffix_from_type(mapping[field.to_sym][:type]) : nil].compact.join("_")
123
+ end
124
+
125
+ def sql_date_column_from_field(field)
126
+ %(IF(#{field} IS NULL, NULL, CONCAT(REPLACE(#{field}, " ", "T"), "Z")) AS #{field}_dt)
12
127
  end
13
128
 
14
129
  def query_db(query)
@@ -23,6 +138,10 @@ class Supernova::SolrIndexer
23
138
  finish
24
139
  end
25
140
 
141
+ def ids_given?
142
+ self.ids.is_a?(Array)
143
+ end
144
+
26
145
  def index_file_path
27
146
  @index_file_path ||= File.expand_path("/tmp/index_file_#{Time.now.to_i}.json")
28
147
  end
@@ -0,0 +1,7 @@
1
+ Symbol.class_eval do
2
+ [:not, :gt, :gte, :lt, :lte, :ne].each do |method|
3
+ define_method(method) do
4
+ Supernova::Condition.new(self, method)
5
+ end
6
+ end
7
+ end
@@ -31,7 +31,7 @@ class Supernova::ThinkingSphinxCriteria < Supernova::Criteria
31
31
  sphinx_options[:geo] = [self.search_options[:geo_center][:lat].to_radians, self.search_options[:geo_center][:lng].to_radians]
32
32
  sphinx_options[:with]["@geodist"] = self.search_options[:geo_distance].is_a?(Range) ? self.search_options[:geo_distance] : Range.new(0.0, self.search_options[:geo_distance])
33
33
  end
34
- [self.filters[:search], sphinx_options]
34
+ [(self.search_options[:search] || Array.new).join(" "), sphinx_options]
35
35
  end
36
36
 
37
37
  def to_a
@@ -7,12 +7,14 @@ describe "Solr" do
7
7
  Supernova::Solr.truncate!
8
8
  Offer.criteria_class = Supernova::SolrCriteria
9
9
  root = Geokit::LatLng.new(47, 11)
10
- endpoint = root.endpoint(90, 50, :units => :kms)
10
+ # endpoint = root.endpoint(90, 50, :units => :kms)
11
+ e_lat = 46.9981112912042
12
+ e_lng = 11.6587158814378
11
13
  Supernova::Solr.connection.add(:id => "offers/1", :type => "Offer", :user_id => 1, :enabled => false, :text => "Hans Meyer", :popularity => 10,
12
14
  :location => "#{root.lat},#{root.lng}", :type => "Offer"
13
15
  )
14
16
  Supernova::Solr.connection.add(:id => "offers/2", :user_id => 2, :enabled => true, :text => "Marek Mintal", :popularity => 1,
15
- :location => "#{endpoint.lat},#{endpoint.lng}", :type => "Offer"
17
+ :location => "#{e_lat},#{e_lng}", :type => "Offer"
16
18
  )
17
19
  Supernova::Solr.connection.commit
18
20
  end
@@ -44,6 +46,25 @@ describe "Solr" do
44
46
  new_criteria.to_a.per_page.should == 25
45
47
  end
46
48
 
49
+ describe "plain text search" do
50
+ it "returns the correct entries for 1 term" do
51
+ new_criteria.search("Hans").to_a.map { |h| h["id"] }.should == [1]
52
+ new_criteria.search("Hans").search("Meyer").to_a.map { |h| h["id"] }.should == [1]
53
+ new_criteria.search("Marek").to_a.map { |h| h["id"] }.should == [2]
54
+ end
55
+
56
+ it "returns the correct options for a combined search" do
57
+ new_criteria.search("Hans", "Marek").to_a.map.should == []
58
+ end
59
+ end
60
+
61
+ it "includes the returned solr_doc" do
62
+ new_criteria.search("Hans").to_a.first.instance_variable_get("@solr_doc").should == {
63
+ "id" => "offers/1", "type" => "Offer", "user_id" => 1, "enabled" => [false], "text" => "Hans Meyer", "popularity" => 10,
64
+ "location" => "47,11", "type" => "Offer"
65
+ }
66
+ end
67
+
47
68
  describe "nearby search" do
48
69
  { 49.kms => 1, 51.kms => 2 }.each do |distance, total_entries|
49
70
  it "returns #{total_entries} for distance #{distance}" do
@@ -52,6 +73,37 @@ describe "Solr" do
52
73
  end
53
74
  end
54
75
 
76
+ describe "range search" do
77
+ { Range.new(2, 3) => [2], Range.new(3, 10) => [], Range.new(1, 2) => [1, 2] }.each do |range, ids|
78
+ it "returns #{ids.inspect} for range #{range.inspect}" do
79
+ new_criteria.with(:user_id => range).map { |doc| doc["id"] }.sort.should == ids
80
+ end
81
+ end
82
+ end
83
+
84
+ describe "not searches" do
85
+ it "finds the correct documents for not nil" do
86
+ Supernova::Solr.connection.add(:id => "offers/3", :enabled => true, :text => "Marek Mintal", :popularity => 1,
87
+ :type => "Offer"
88
+ )
89
+ Supernova::Solr.connection.commit
90
+ raise "There should be 3 docs" if new_criteria.to_a.total_entries != 3
91
+ new_criteria.with(:user_id.not => nil).to_a.map { |h| h["id"] }.should == [1, 2]
92
+ end
93
+
94
+ it "finds the correct values for not specific value" do
95
+ new_criteria.with(:user_id.not => 1).to_a.map { |h| h["id"] }.should == [2]
96
+ end
97
+ end
98
+
99
+ describe "gt and lt searches" do
100
+ { :gt => [2], :gte => [1, 2], :lt => [], :lte => [1] }.each do |type, ids|
101
+ it "finds ids #{ids.inspect} for #{type}" do
102
+ new_criteria.with(:user_id.send(type) => 1).to_a.map { |row| row["id"] }.sort.should == ids
103
+ end
104
+ end
105
+ end
106
+
55
107
  it "returns the correct objects" do
56
108
  new_criteria.with(:user_id => 1).to_a.first.should be_an_instance_of(Offer)
57
109
  end
@@ -23,7 +23,7 @@ describe "ThinkingSphinx" do
23
23
  @offer1 = Offer.create!(:id => 1, :user_id => 1, :enabled => false, :text => "Hans Meyer", :popularity => 10, :lat => root.lat, :lng => root.lng)
24
24
  @offer2 = Offer.create!(:id => 2, :user_id => 2, :enabled => true, :text => "Marek Mintal", :popularity => 1, :lat => endpoint.lat, :lng => endpoint.lng)
25
25
  ts.controller.index
26
- sleep 0.1
26
+ sleep 0.2
27
27
  end
28
28
 
29
29
  it "finds the correct objects" do
@@ -0,0 +1,41 @@
1
+ require 'spec_helper'
2
+
3
+ describe "Supernova::Condition" do
4
+ it "can be initialize" do
5
+ cond = Supernova::Condition.new(:user_id, :not)
6
+ cond.key.should == :user_id
7
+ cond.type.should == :not
8
+ end
9
+
10
+ describe "solr_filter_for" do
11
+ it "returns the correct filter for numbers" do
12
+ :user_id.not.solr_filter_for(7).should == "!user_id:7"
13
+ end
14
+
15
+ it "returns the correct filter for numbers" do
16
+ :user_id.ne.solr_filter_for(7).should == "!user_id:7"
17
+ end
18
+
19
+ it "returns the correct filter for not nil" do
20
+ :user_id.not.solr_filter_for(nil).should == "user_id:[* TO *]"
21
+ end
22
+
23
+ it "returns the correct filter for gt" do
24
+ :user_id.gt.solr_filter_for(1).should == "user_id:{1 TO *}"
25
+ end
26
+
27
+ it "returns the correct filter for gte" do
28
+ :user_id.gte.solr_filter_for(1).should == "user_id:[1 TO *]"
29
+ end
30
+
31
+ it "returns the correct filter for lt" do
32
+ :user_id.lt.solr_filter_for(1).should == "user_id:{* TO 1}"
33
+ end
34
+
35
+ it "returns the correct filter for lt" do
36
+ :user_id.lte.solr_filter_for(1).should == "user_id:[* TO 1]"
37
+ end
38
+ end
39
+
40
+
41
+ end
@@ -59,7 +59,7 @@ describe "Supernova::Criteria" do
59
59
 
60
60
  describe "#search" do
61
61
  it "sets the query" do
62
- scope.search("title").filters[:search].should == "title"
62
+ scope.search("title").search_options[:search].should == ["title"]
63
63
  end
64
64
  end
65
65
 
@@ -180,6 +180,21 @@ describe "Supernova::Criteria" do
180
180
  end
181
181
  end
182
182
 
183
+ describe "#where" do
184
+ it "delegates to with" do
185
+ ret = double("ret")
186
+ scope.should_receive(:with).with(:a => 9).and_return ret
187
+ scope.where(:a => 9).should == ret
188
+ end
189
+ end
190
+
191
+ describe "#attribute_mapping" do
192
+ it "sets the attribute_mapping option" do
193
+ mapping = { :title => { :type => :integer } }
194
+ scope.attribute_mapping(mapping).search_options[:attribute_mapping].should == mapping
195
+ end
196
+ end
197
+
183
198
  describe "#merge" do
184
199
  let(:criteria) { Supernova::Criteria.new.order("popularity asc").with(:a => 1).conditions(:b => 2).search("New Search") }
185
200
  let(:new_crit) { Supernova::Criteria.new.order("popularity desc").with(:c => 8).conditions(:e => 9).search("Search") }
@@ -201,7 +216,7 @@ describe "Supernova::Criteria" do
201
216
  end
202
217
 
203
218
  it "merges search search" do
204
- new_crit.merge(criteria).filters[:search].should == "New Search"
219
+ new_crit.merge(criteria).search_options[:search].should == ["New Search"]
205
220
  end
206
221
 
207
222
  it "calls merge on options" do
@@ -19,6 +19,16 @@ describe Supernova::SolrCriteria do
19
19
  Supernova::Solr.stub!(:connection).and_return rsolr
20
20
  end
21
21
 
22
+ describe "#fq_from_with" do
23
+ it "returns the correct filter for with ranges" do
24
+ criteria.fq_from_with(:user_id => Range.new(10, 12)).should == ["user_id:[10 TO 12]"]
25
+ end
26
+
27
+ it "returns the correct filter for not queries" do
28
+ criteria.fq_from_with(:user_id.not => nil).should == ["user_id:[* TO *]"]
29
+ end
30
+ end
31
+
22
32
  describe "#to_params" do
23
33
  it "returns a Hash" do
24
34
  criteria.to_params.should be_an_instance_of(Hash)
@@ -36,10 +46,26 @@ describe Supernova::SolrCriteria do
36
46
  criteria.order("title").to_params[:sort].should == "title"
37
47
  end
38
48
 
49
+ it "uses a mapped field for order" do
50
+ criteria.attribute_mapping(:title => { :type => :string }).order("title").to_params[:sort].should == "title_s"
51
+ end
52
+
53
+ %w(asc desc).each do |order|
54
+ it "uses a mapped field for order even when #{order} is present" do
55
+ criteria.attribute_mapping(:title => { :type => :string }).order("title #{order}").to_params[:sort].should == "title_s #{order}"
56
+ end
57
+ end
58
+
59
+
39
60
  it "sets search correct search query" do
40
- criteria.search("some query").to_params[:q].should == "some query"
61
+ criteria.search("some query").to_params[:q].should == "(some query)"
41
62
  end
42
63
 
64
+ it "joins the search terms with AND" do
65
+ criteria.search("some", "query").to_params[:q].should == "(some) AND (query)"
66
+ end
67
+
68
+ # fix me: use type_s
43
69
  it "adds a filter on type when clazz set" do
44
70
  Supernova::SolrCriteria.new(Offer).to_params[:fq].should == ["type:#{Offer}"]
45
71
  end
@@ -52,11 +78,23 @@ describe Supernova::SolrCriteria do
52
78
  criteria.select(:user_id).select(:user_id).select(:enabled).to_params[:fl].should == "user_id,enabled,id"
53
79
  end
54
80
 
81
+ it "uses mapped fields for select" do
82
+ mapping = {
83
+ :user_id => { :type => :integer },
84
+ :enabled => { :type => :boolean }
85
+ }
86
+ criteria.attribute_mapping(mapping).select(:user_id, :enabled).to_params[:fl].should == "user_id_i,enabled_b,id"
87
+ end
88
+
55
89
  it "adds all without filters" do
56
90
  criteria.without(:user_id => 1).to_params[:fq].should == ["!user_id:1"]
57
91
  criteria.without(:user_id => 1).without(:user_id => 1).without(:user_id => 2).to_params[:fq].sort.should == ["!user_id:1", "!user_id:2"]
58
92
  end
59
93
 
94
+ it "uses mapped fields for without" do
95
+ criteria.attribute_mapping(:user_id => { :type => :integer }).without(:user_id => 1).to_params[:fq].should == ["!user_id_i:1"]
96
+ end
97
+
60
98
  describe "with a nearby search" do
61
99
  let(:nearby_criteria) { Supernova::SolrCriteria.new.near(47, 11).within(10.kms) }
62
100
 
@@ -69,7 +107,11 @@ describe Supernova::SolrCriteria do
69
107
  end
70
108
 
71
109
  it "sets the sfield to location" do
72
- nearby_criteria.to_params[:sfield].should == :location
110
+ nearby_criteria.to_params[:sfield].should == "location"
111
+ end
112
+
113
+ it "uses the mapped field when mapping defined" do
114
+ nearby_criteria.attribute_mapping(:location => { :type => :location }).to_params[:sfield].should == "location_p"
73
115
  end
74
116
 
75
117
  it "sets the fq field to {!geofilt}" do
@@ -94,6 +136,30 @@ describe Supernova::SolrCriteria do
94
136
  criteria.paginate(:per_page => 10, :page => 2).to_params[:start].should == 10
95
137
  end
96
138
  end
139
+
140
+ describe "with attribute mapping" do
141
+ it "uses the mapped fields" do
142
+ criteria.attribute_mapping(:artist_name => { :type => :string }).where(:artist_name => "test").to_params[:fq].should == ["artist_name_s:test"]
143
+ end
144
+
145
+ it "uses the mapped fields for all criteria queries" do
146
+ criteria.attribute_mapping(:artist_name => { :type => :string }).where(:artist_name.ne => nil).to_params[:fq].should == ["artist_name_s:[* TO *]"]
147
+ end
148
+
149
+ it "uses the column when no mapping defined" do
150
+ criteria.where(:artist_name => "test").to_params[:fq].should == ["artist_name:test"]
151
+ end
152
+ end
153
+ end
154
+
155
+ describe "#solr_field_from_field" do
156
+ it "returns the field when no mappings defined" do
157
+ criteria.solr_field_from_field(:artist_name).should == "artist_name"
158
+ end
159
+
160
+ it "returns the mapped field when mapping found" do
161
+ criteria.attribute_mapping(:artist_name => { :type => :string }).solr_field_from_field(:artist_name).should == "artist_name_s"
162
+ end
97
163
  end
98
164
 
99
165
  describe "#to_a" do
@@ -201,6 +267,17 @@ describe Supernova::SolrCriteria do
201
267
  end
202
268
  end
203
269
 
270
+ it "returns a Hash when type does not response to " do
271
+ type = double("type")
272
+ type.should_receive(:respond_to?).with(:constantize).and_return false
273
+ criteria.build_doc("type" => type).should be_an_instance_of(Hash)
274
+ end
275
+
276
+ it "sets the original solr_doc" do
277
+ solr_doc = { "type" => "Offer", "id" => "offers/id" }
278
+ criteria.build_doc(solr_doc).instance_variable_get("@solr_doc").should == solr_doc
279
+ end
280
+
204
281
  it "should be readonly" do
205
282
  criteria.build_doc(docs.first).should be_readonly
206
283
  end
@@ -1,18 +1,27 @@
1
1
  require "spec_helper"
2
2
 
3
3
  describe Supernova::SolrIndexer do
4
-
4
+ let(:indexer_clazz) { Class.new(Supernova::SolrIndexer) }
5
5
  let(:db) { double("db", :query => [to_index]) }
6
6
  let(:to_index) { { :id => 1, :title => "Some Title"} }
7
7
  let(:file_stub) { double("file").as_null_object }
8
8
 
9
- let(:indexer) {
9
+ let(:indexer) do
10
10
  indexer = Supernova::SolrIndexer.new
11
11
  indexer.db = db
12
12
  Supernova::Solr.url = "http://solr.xx:9333/solr"
13
13
  indexer.stub!(:system).and_return true
14
14
  indexer
15
- }
15
+ end
16
+
17
+ let(:custom_indexer) { indexer_clazz.new }
18
+
19
+ before(:each) do
20
+ indexer_clazz.has(:title, :type => :text)
21
+ indexer_clazz.has(:artist_id, :type => :integer)
22
+ indexer_clazz.has(:description, :type => :text)
23
+ indexer_clazz.has(:created_at, :type => :date)
24
+ end
16
25
 
17
26
  before(:each) do
18
27
  File.stub!(:open).and_return file_stub
@@ -29,6 +38,82 @@ describe Supernova::SolrIndexer do
29
38
  indexer = Supernova::SolrIndexer.new(:db => db)
30
39
  indexer.db.should == db
31
40
  end
41
+
42
+ it "can be initialized with ids" do
43
+ Supernova::SolrIndexer.new(:ids => [1, 2]).ids.should == [1, 2]
44
+ end
45
+
46
+ it "sets ids to all when nil" do
47
+ Supernova::SolrIndexer.new.ids.should == :all
48
+ end
49
+ end
50
+
51
+ describe "index!" do
52
+ it "calls query_to_index" do
53
+ indexer.should_receive(:query_to_index).and_return "some query"
54
+ indexer.index!
55
+ end
56
+
57
+ it "calls index_query on query_to_index" do
58
+ query = "some query"
59
+ indexer.stub!(:query_to_index).and_return query
60
+ indexer.should_receive(:index_query).with(query)
61
+ indexer.index!
62
+ end
63
+
64
+ it "calls row_to_solr with all returned rows from sql" do
65
+ row1 = double("row1")
66
+ row2 = double("row2")
67
+ indexer.stub!(:query).and_return [row1, row2]
68
+ indexer.stub!(:query_to_index).and_return "some query"
69
+ indexer.should_receive(:row_to_solr).with(row1)
70
+ indexer.stub!(:index_query).and_yield(row1)
71
+ indexer.index!
72
+ end
73
+ end
74
+
75
+ describe "validate_lat" do
76
+ { nil => nil, 10 => 10.0, 90.1 => nil, 90 => 90, -90.1 => nil, -90 => -90 }.each do |from, to|
77
+ it "converts #{from} to #{to}" do
78
+ indexer.validate_lat(from).should == to
79
+ end
80
+ end
81
+ end
82
+
83
+ describe "validate_lng" do
84
+ { nil => nil, 10 => 10.0, 180.1 => nil, 180 => 180, -180.1 => nil, -180 => -180 }.each do |from, to|
85
+ it "converts #{from} to #{to}" do
86
+ indexer.validate_lng(from).should == to
87
+ end
88
+ end
89
+ end
90
+
91
+ describe "#sql_column_from_field_and_type" do
92
+ {
93
+ [:title, :string] => "title AS title_s",
94
+ [:count, :int] => "count AS count_i",
95
+ [:test, :sint] => "test AS test_si",
96
+ [:lat, :float] => "lat AS lat_f",
97
+ [:text, :boolean] => "text AS text_b",
98
+ [:loc, :location] => "loc AS loc_p",
99
+ [:deleted_at, :date] => %(IF(deleted_at IS NULL, NULL, CONCAT(REPLACE(deleted_at, " ", "T"), "Z")) AS deleted_at_dt),
100
+ }.each do |(field, type), name|
101
+ it "maps #{field} with #{type} to #{name}" do
102
+ indexer.sql_column_from_field_and_type(field, type).should == name
103
+ end
104
+ end
105
+
106
+ it "raises an error when no mapping defined" do
107
+ lambda {
108
+ indexer.sql_column_from_field_and_type(:text, :rgne)
109
+ }.should raise_error
110
+ end
111
+ end
112
+
113
+ describe "#row_to_solr" do
114
+ it "returns the db row by default" do
115
+ indexer.row_to_solr("id" => 1).should == { "id" => 1 }
116
+ end
32
117
  end
33
118
 
34
119
  describe "#query_db" do
@@ -164,4 +249,193 @@ describe Supernova::SolrIndexer do
164
249
  indexer.do_index_file
165
250
  end
166
251
  end
252
+
253
+ describe "define mappings" do
254
+ let(:blank_indexer_clazz) { Class.new(Supernova::SolrIndexer) }
255
+
256
+ it "has an empty array of field_definitions by default" do
257
+ blank_indexer_clazz.field_definitions.should == {}
258
+ end
259
+
260
+ it "has adds filters to the field_definitions" do
261
+ blank_indexer_clazz.has(:artist_id, :type => :integer, :sortable => true)
262
+ blank_indexer_clazz.field_definitions.should == { :artist_id => { :type => :integer, :sortable => true } }
263
+ end
264
+
265
+ it "clazz sets indexed class" do
266
+ blank_indexer_clazz.clazz(Integer)
267
+ blank_indexer_clazz.instance_variable_get("@clazz").should == Integer
268
+ end
269
+
270
+ it "does not change but return the clazz when nil" do
271
+ blank_indexer_clazz.clazz(Integer)
272
+ blank_indexer_clazz.clazz.should == Integer
273
+ end
274
+
275
+ it "allows setting the clazz to nil" do
276
+ blank_indexer_clazz.clazz(Integer)
277
+ blank_indexer_clazz.clazz(nil)
278
+ blank_indexer_clazz.clazz.should be_nil
279
+ end
280
+
281
+ it "table_name sets the table name" do
282
+ blank_indexer_clazz.table_name(:people)
283
+ blank_indexer_clazz.instance_variable_get("@table_name").should == :people
284
+ end
285
+
286
+ it "table_name does not overwrite but return table_name when nil given" do
287
+ blank_indexer_clazz.table_name(:people)
288
+ blank_indexer_clazz.table_name.should == :people
289
+ end
290
+
291
+ it "allows setting the table_name to nil" do
292
+ blank_indexer_clazz.table_name(:people)
293
+ blank_indexer_clazz.table_name(nil).should be_nil
294
+ end
295
+ end
296
+
297
+ describe "#default_mappings" do
298
+ it "returns id when no class defined" do
299
+ indexer_clazz.new.default_fields.should == ["id"]
300
+ end
301
+
302
+ it "adds type when class defined" do
303
+ indexer_clazz.clazz Integer
304
+ indexer_clazz.new.default_fields.should == ["id", %("Integer" AS type_s)]
305
+ end
306
+ end
307
+
308
+ describe "#defined_fields" do
309
+ let(:field_definitions) { { :title => { :type => :string } } }
310
+
311
+ it "calls field_definitions" do
312
+ indexer_clazz.should_receive(:field_definitions).and_return field_definitions
313
+ custom_indexer.defined_fields
314
+ end
315
+
316
+ ["title AS title_t", "artist_id AS artist_id_i", "description AS description_t",
317
+ %(IF(created_at IS NULL, NULL, CONCAT(REPLACE(created_at, " ", "T"), "Z")) AS created_at_dt)
318
+ ].each do |field|
319
+ it "includes field #{field.inspect}" do
320
+ custom_indexer.defined_fields.should include(field)
321
+ end
322
+ end
323
+
324
+ it "does not include virtual fields" do
325
+ clazz = Class.new(Supernova::SolrIndexer)
326
+ clazz.has :location, :type => :location, :virtual => true
327
+ clazz.has :title, :type => :string
328
+ clazz.new.defined_fields.should == ["title AS title_s"]
329
+ end
330
+ end
331
+
332
+ describe "#table_name" do
333
+ it "returns nil when no table_name defined on indexer class and no class defined" do
334
+ Class.new(Supernova::SolrIndexer).new.table_name.should be_nil
335
+ end
336
+
337
+ it "returns nil when no table_name defined on indexer class and class does not respond to table name" do
338
+ clazz = Class.new(Supernova::SolrIndexer)
339
+ clazz.clazz(Integer)
340
+ clazz.new.table_name.should be_nil
341
+ end
342
+
343
+ it "returns the table name defined in indexer class" do
344
+ clazz = Class.new(Supernova::SolrIndexer)
345
+ clazz.table_name(:some_table)
346
+ clazz.new.table_name.should == :some_table
347
+ end
348
+
349
+ it "returns the table name ob class when responding to table_name" do
350
+ model_clazz = double("clazz", :table_name => "model_table")
351
+ clazz = Class.new(Supernova::SolrIndexer)
352
+ clazz.clazz(model_clazz)
353
+ clazz.new.table_name.should == "model_table"
354
+ end
355
+ end
356
+
357
+ describe "#query_to_index" do
358
+ before(:each) do
359
+ @indexer_clazz = Class.new(Supernova::SolrIndexer)
360
+ @indexer_clazz.clazz Integer
361
+ @indexer_clazz.table_name "integers"
362
+ @indexer = @indexer_clazz.new
363
+ end
364
+
365
+ it "raises an error when table_name returns nil" do
366
+ @indexer_clazz.clazz(nil)
367
+ @indexer_clazz.table_name(nil)
368
+ @indexer.should_receive(:table_name).and_return nil
369
+ lambda {
370
+ @indexer.query_to_index
371
+ }.should raise_error("no table_name defined")
372
+ end
373
+
374
+ it "returns a string" do
375
+ @indexer.query_to_index.should be_an_instance_of(String)
376
+ end
377
+
378
+ it "does not include a where when ids is nil" do
379
+ @indexer.query_to_index.should_not include("WHERE")
380
+ end
381
+
382
+ it "does include a where when ids are present" do
383
+ @indexer_clazz.new(:ids => %w(1 2)).query_to_index.should include("WHERE id IN (1, 2)")
384
+ end
385
+
386
+ it "calls and includes select_fields" do
387
+ @indexer.should_receive(:select_fields).and_return %w(a c)
388
+ @indexer.query_to_index.should include("SELECT a, c FROM integers")
389
+ end
390
+ end
391
+
392
+ describe "#select_fields" do
393
+ it "joins default_fields with defined_fields" do
394
+ default = double("default fields")
395
+ defined = double("defined fields")
396
+ indexer.should_receive(:default_fields).and_return [default]
397
+ indexer.should_receive(:defined_fields).and_return [defined]
398
+ indexer.select_fields.should == [default, defined]
399
+ end
400
+ end
401
+
402
+ describe "#method_missing" do
403
+ it "returns a new supernova criteria" do
404
+ indexer_clazz.where(:a => 1).should be_an_instance_of(Supernova::SolrCriteria)
405
+ end
406
+
407
+ it "sets the correct clazz" do
408
+ indexer_clazz = Class.new(Supernova::SolrIndexer)
409
+ indexer_clazz.clazz(String)
410
+ indexer_clazz.where(:a => 1).clazz.should == String
411
+ end
412
+
413
+ it "adds the attribute_mapping" do
414
+ indexer_clazz.where(:a => 1).search_options[:attribute_mapping].should == {
415
+ :artist_id=>{:type=>:integer}, :title=>{:type=>:text}, :created_at=>{:type=>:date}, :description=>{:type=>:text}
416
+ }
417
+ end
418
+ end
419
+
420
+ describe "#solr_field_for_field_name_and_mapping" do
421
+ let(:mapping) do
422
+ {
423
+ :artist_name => { :type => :string },
424
+ :artist_id => { :type => :integer },
425
+ }
426
+ end
427
+
428
+ {
429
+ :artist_name => "artist_name_s", "artist_name" => "artist_name_s",
430
+ :artist_id => "artist_id_i", :popularity => "popularity"
431
+ }.each do |from, to|
432
+ it "maps #{from} to #{to}" do
433
+ Supernova::SolrIndexer.solr_field_for_field_name_and_mapping(from, mapping).should == to
434
+ end
435
+ end
436
+
437
+ it "returns the original field when mapping is nil" do
438
+ Supernova::SolrIndexer.solr_field_for_field_name_and_mapping(:artist, nil).should == "artist"
439
+ end
440
+ end
167
441
  end
@@ -0,0 +1,15 @@
1
+ require 'spec_helper'
2
+
3
+ describe "Supernova::SymbolExtensions" do
4
+ [:not, :gt, :lt, :gte, :lte, :ne].each do |type|
5
+ it "returns the correct condition for #{type}" do
6
+ cond = :user_id.send(type)
7
+ cond.key.should == :user_id
8
+ cond.type.should == type
9
+ end
10
+ end
11
+
12
+ it "sets the correct key" do
13
+ :other_id.not.key.should == :other_id
14
+ end
15
+ end
@@ -5,16 +5,17 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{supernova}
8
- s.version = "0.2.2"
8
+ s.version = "0.3.0"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Tobias Schwab"]
12
- s.date = %q{2011-06-11}
12
+ s.date = %q{2011-06-12}
13
13
  s.description = %q{Unified search scopes}
14
14
  s.email = %q{tobias.schwab@dynport.de}
15
15
  s.extra_rdoc_files = [
16
16
  "LICENSE.txt",
17
- "README.rdoc"
17
+ "README.rdoc",
18
+ "TODO"
18
19
  ]
19
20
  s.files = [
20
21
  ".autotest",
@@ -29,11 +30,13 @@ Gem::Specification.new do |s|
29
30
  "autotest/discover.rb",
30
31
  "lib/supernova.rb",
31
32
  "lib/supernova/collection.rb",
33
+ "lib/supernova/condition.rb",
32
34
  "lib/supernova/criteria.rb",
33
35
  "lib/supernova/numeric_extensions.rb",
34
36
  "lib/supernova/solr.rb",
35
37
  "lib/supernova/solr_criteria.rb",
36
38
  "lib/supernova/solr_indexer.rb",
39
+ "lib/supernova/symbol_extensions.rb",
37
40
  "lib/supernova/thinking_sphinx.rb",
38
41
  "lib/supernova/thinking_sphinx_criteria.rb",
39
42
  "solr/conf/admin-extra.html",
@@ -77,11 +80,13 @@ Gem::Specification.new do |s|
77
80
  "spec/integration/solr_spec.rb",
78
81
  "spec/integration/thinking_sphinx_spec.rb",
79
82
  "spec/spec_helper.rb",
83
+ "spec/supernova/condition_spec.rb",
80
84
  "spec/supernova/criteria_spec.rb",
81
85
  "spec/supernova/numeric_extensions_spec.rb",
82
86
  "spec/supernova/solr_criteria_spec.rb",
83
87
  "spec/supernova/solr_indexer_spec.rb",
84
88
  "spec/supernova/solr_spec.rb",
89
+ "spec/supernova/symbol_extensions_spec.rb",
85
90
  "spec/supernova/thinking_sphinx_criteria_spec.rb",
86
91
  "spec/supernova_spec.rb",
87
92
  "supernova.gemspec"
metadata CHANGED
@@ -5,9 +5,9 @@ version: !ruby/object:Gem::Version
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
- - 2
9
- - 2
10
- version: 0.2.2
8
+ - 3
9
+ - 0
10
+ version: 0.3.0
11
11
  platform: ruby
12
12
  authors:
13
13
  - Tobias Schwab
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2011-06-11 00:00:00 +02:00
18
+ date: 2011-06-12 00:00:00 +02:00
19
19
  default_executable:
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency
@@ -219,6 +219,7 @@ extensions: []
219
219
  extra_rdoc_files:
220
220
  - LICENSE.txt
221
221
  - README.rdoc
222
+ - TODO
222
223
  files:
223
224
  - .autotest
224
225
  - .document
@@ -232,11 +233,13 @@ files:
232
233
  - autotest/discover.rb
233
234
  - lib/supernova.rb
234
235
  - lib/supernova/collection.rb
236
+ - lib/supernova/condition.rb
235
237
  - lib/supernova/criteria.rb
236
238
  - lib/supernova/numeric_extensions.rb
237
239
  - lib/supernova/solr.rb
238
240
  - lib/supernova/solr_criteria.rb
239
241
  - lib/supernova/solr_indexer.rb
242
+ - lib/supernova/symbol_extensions.rb
240
243
  - lib/supernova/thinking_sphinx.rb
241
244
  - lib/supernova/thinking_sphinx_criteria.rb
242
245
  - solr/conf/admin-extra.html
@@ -280,14 +283,17 @@ files:
280
283
  - spec/integration/solr_spec.rb
281
284
  - spec/integration/thinking_sphinx_spec.rb
282
285
  - spec/spec_helper.rb
286
+ - spec/supernova/condition_spec.rb
283
287
  - spec/supernova/criteria_spec.rb
284
288
  - spec/supernova/numeric_extensions_spec.rb
285
289
  - spec/supernova/solr_criteria_spec.rb
286
290
  - spec/supernova/solr_indexer_spec.rb
287
291
  - spec/supernova/solr_spec.rb
292
+ - spec/supernova/symbol_extensions_spec.rb
288
293
  - spec/supernova/thinking_sphinx_criteria_spec.rb
289
294
  - spec/supernova_spec.rb
290
295
  - supernova.gemspec
296
+ - TODO
291
297
  has_rdoc: true
292
298
  homepage: http://github.com/dynport/supernova
293
299
  licenses: