supernova 0.2.2 → 0.3.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.
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: