outoftime-sunspot 0.0.2 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,43 +1,110 @@
1
1
  module Sunspot
2
+ #
3
+ # A Sunspot session encapsulates a connection to Solr and a set of
4
+ # configuration choices. Though users of Sunspot may manually instantiate
5
+ # Session objects, in the general case it's easier to use the singleton
6
+ # stored in the Sunspot module. Since the Sunspot module provides all of
7
+ # the instance methods of Session as class methods, they are not documented
8
+ # again here.
9
+ #
2
10
  class Session
3
11
  attr_reader :config
4
12
 
5
- def initialize(config = Sunspot::Configuration.build, connection = nil)
13
+ #
14
+ # Sessions are initialized with a Sunspot configuration and a Solr
15
+ # connection. Usually you will want to stick with the default arguments
16
+ # when instantiating your own sessions.
17
+ #
18
+ def initialize(config = Configuration.build, connection = nil)
6
19
  @config = config
7
20
  yield(@config) if block_given?
8
21
  @connection = connection
9
22
  end
10
23
 
24
+ #
25
+ # See Sunspot.search
26
+ #
11
27
  def search(*types, &block)
12
- ::Sunspot::Search.new(connection, @config, *types, &block).execute!
28
+ types.flatten!
29
+ Search.new(connection, @config, *types, &block).execute!
13
30
  end
14
31
 
32
+ #
33
+ # See Sunspot.index
34
+ #
15
35
  def index(*objects)
36
+ objects.flatten!
16
37
  for object in objects
17
- ::Sunspot::Indexer.add(connection, object)
38
+ setup_for(object).indexer(connection).add(object)
18
39
  end
19
40
  end
20
41
 
42
+ #
43
+ # See Sunspot.index!
44
+ #
45
+ def index!(*objects)
46
+ index(*objects)
47
+ commit
48
+ end
49
+
50
+ #
51
+ # See Sunspot.commit!
52
+ #
53
+ def commit
54
+ connection.commit
55
+ end
56
+
57
+ #
58
+ # See Sunspot.remove
59
+ #
21
60
  def remove(*objects)
61
+ objects.flatten!
22
62
  for object in objects
23
- ::Sunspot::Indexer.remove(connection, object)
63
+ setup_for(object).indexer(connection).remove(object)
24
64
  end
25
65
  end
26
66
 
67
+ #
68
+ # See Sunspot.remove_all
69
+ #
27
70
  def remove_all(*classes)
71
+ classes.flatten!
28
72
  if classes.empty?
29
- ::Sunspot::Indexer.remove_all(connection)
73
+ Indexer.remove_all(connection)
30
74
  else
31
- classes.each do |clazz|
32
- ::Sunspot::Indexer.remove_all(connection, clazz)
75
+ for clazz in classes
76
+ Setup.for(clazz).indexer(connection).remove_all
33
77
  end
34
78
  end
35
79
  end
36
80
 
37
81
  private
38
82
 
83
+ #
84
+ # Get the Setup object for the given object's class.
85
+ #
86
+ # ==== Parameters
87
+ #
88
+ # object<Object>:: The object whose setup is to be retrieved
89
+ #
90
+ # ==== Returns
91
+ #
92
+ # Sunspot::Setup:: The setup for the object's class
93
+ #
94
+ def setup_for(object)
95
+ Setup.for(object.class) || raise(ArgumentError, "Sunspot is not configured for #{object.class.inspect}")
96
+ end
97
+
98
+ #
99
+ # Retrieve the Solr connection for this session, creating one if it does not
100
+ # already exist.
101
+ #
102
+ # ==== Returns
103
+ #
104
+ # Solr::Connection:: The connection for this session
105
+ #
39
106
  def connection
40
- @connection ||= Solr::Connection.new(config.solr.url, :autocommit => :on)
107
+ @connection ||= Solr::Connection.new(config.solr.url)
41
108
  end
42
109
  end
43
110
  end
@@ -0,0 +1,171 @@
1
+ module Sunspot
2
+ #
3
+ # This class encapsulates the search/indexing setup for a given class. Its
4
+ # contents are built using the Sunspot.setup method.
5
+ #
6
+ class Setup #:nodoc:
7
+ def initialize(clazz)
8
+ @class_name = clazz.name
9
+ @fields, @text_fields = [], []
10
+ @dsl = DSL::Fields.new(self)
11
+ end
12
+
13
+ #
14
+ # Add fields for scope/ordering
15
+ #
16
+ # ==== Parameters
17
+ #
18
+ # fields<Array>:: Array of Sunspot::Field objects
19
+ #
20
+ def add_fields(fields)
21
+ @fields.concat(Array(fields))
22
+ end
23
+
24
+ #
25
+ # Add fields for fulltext search
26
+ #
27
+ # ==== Parameters
28
+ #
29
+ # fields<Array>:: Array of Sunspot::Field objects
30
+ #
31
+ def add_text_fields(fields)
32
+ @text_fields.concat(Array(fields))
33
+ end
34
+
35
+ #
36
+ # Builder method for evaluating the setup DSL
37
+ #
38
+ def setup(&block)
39
+ @dsl.instance_eval(&block)
40
+ end
41
+
42
+ #
43
+ # Get the fields associated with this setup as well as all inherited fields
44
+ #
45
+ # ==== Returns
46
+ #
47
+ # Array:: Collection of all fields associated with this setup
48
+ #
49
+ def fields
50
+ fields = @fields.dup
51
+ fields.concat(parent.fields) if parent
52
+ fields
53
+ end
54
+
55
+ #
56
+ # Get the text fields associated with this setup as well as all inherited
57
+ # text fields
58
+ #
59
+ # ==== Returns
60
+ #
61
+ # Array:: Collection of all text fields associated with this setup
62
+ #
63
+ def text_fields
64
+ text_fields = @text_fields.dup
65
+ text_fields.concat(parent.text_fields) if parent
66
+ text_fields
67
+ end
68
+
69
+ #
70
+ # Get all scope and text fields associated with this setup as well as all
71
+ # inherited fields
72
+ #
73
+ # ==== Returns
74
+ #
75
+ # Array:: Collection of all text and scope fields associated with this setup
76
+ #
77
+ def all_fields
78
+ fields + text_fields
79
+ end
80
+
81
+ #
82
+ # Factory method for an Indexer object configured to use this setup
83
+ #
84
+ # ==== Returns
85
+ #
86
+ # Sunspot::Indexer:: Indexer configured with this setup
87
+ def indexer(connection)
88
+ Indexer.new(connection, self)
89
+ end
90
+
91
+ #
92
+ # Return the class associated with this setup.
93
+ #
94
+ # ==== Returns
95
+ #
96
+ # clazz<Class>:: Class setup is configured for
97
+ #
98
+ def clazz
99
+ Util.full_const_get(@class_name)
100
+ end
101
+
102
+ protected
103
+
104
+ #
105
+ # Get the nearest inherited setup, if any
106
+ #
107
+ # ==== Returns
108
+ #
109
+ # Sunspot::Setup:: Setup for the nearest ancestor of this setup's class
110
+ #
111
+ def parent
112
+ Setup.for(clazz.superclass)
113
+ end
114
+
115
+ class <<self
116
+ #
117
+ # Retrieve or create the Setup instance for the given class, evaluating
118
+ # the given block to add to the setup's configuration
119
+ #
120
+ def setup(clazz, &block) #:nodoc:
121
+ self.for!(clazz).setup(&block)
122
+ end
123
+
124
+ #
125
+ # Retrieve the setup instance for the given class, or for the nearest
126
+ # ancestor that has a setup, if any.
127
+ #
128
+ # ==== Parameters
129
+ #
130
+ # clazz<Class>:: Class for which to retrieve a setup
131
+ #
132
+ # ==== Returns
133
+ #
134
+ # Sunspot::Setup::
135
+ # Setup instance associated with the given class or its nearest ancestor
136
+ #
137
+ def for(clazz) #:nodoc:
138
+ setups[clazz.name.to_sym] || self.for(clazz.superclass) if clazz
139
+ end
140
+
141
+ protected
142
+
143
+ #
144
+ # Retrieve or create a Setup instance for this class
145
+ #
146
+ # ==== Parameters
147
+ #
148
+ # clazz<Class>:: Class for which to retrieve a setup
149
+ #
150
+ # ==== Returns
151
+ #
152
+ # Sunspot::Setup:: New or existing setup for this class
153
+ #
154
+ def for!(clazz) #:nodoc:
155
+ setups[clazz.name.to_sym] ||= new(clazz)
156
+ end
157
+
158
+ private
159
+
160
+ # Singleton hash of class names to Setup instances
161
+ #
162
+ # ==== Returns
163
+ #
164
+ # Hash:: Class names keyed to Setup instances
165
+ #
166
+ def setups
167
+ @setups ||= {}
168
+ end
169
+ end
170
+ end
171
+ end
data/lib/sunspot/type.rb CHANGED
@@ -1,60 +1,144 @@
1
1
  module Sunspot
2
+ #
3
+ # This module contains singleton objects that represent the types that can be
4
+ # indexed and searched using Sunspot. Plugin developers should be able to
5
+ # add new constants to the Type module; as long as they implement the
6
+ # appropriate methods, Sunspot should be able to integrate them (note that
7
+ # this capability is untested at the moment). The required methods are:
8
+ #
9
+ # +indexed_name+::
10
+ # Convert a given field name into its form as stored in Solr. This
11
+ # generally means adding a suffix to match a Solr dynamicField definition.
12
+ # +to_indexed+::
13
+ # Convert a value of this type into the appropriate Solr string
14
+ # representation.
15
+ # +cast+::
16
+ # Convert a Solr string representation of a value into the appropriate
17
+ # Ruby type.
18
+ #
2
19
  module Type
3
- TextType = Module.new
4
- StringType = Module.new
5
- IntegerType = Module.new
6
- FloatType = Module.new
7
- TimeType = Module.new
8
-
9
- class <<TextType
10
- def indexed_name(name)
20
+ #
21
+ # Text is a special type that stores data for fulltext search. Unlike other
22
+ # types, Text fields are tokenized and are made available to the keyword
23
+ # search phrase. Text fields cannot be faceted, ordered upon, or used in
24
+ # restrictions. Similarly, text fields are the only fields that are made
25
+ # available to keyword search.
26
+ #
27
+ module TextType
28
+ class <<self
29
+ def indexed_name(name) #:nodoc:
11
30
  "#{name}_text"
12
- end
31
+ end
13
32
 
14
- def to_indexed(value)
15
- value.to_s if value
33
+ def to_indexed(value) #:nodoc:
34
+ value.to_s if value
35
+ end
16
36
  end
17
37
  end
18
38
 
19
- class <<StringType
20
- def indexed_name(name)
39
+ #
40
+ # The String type represents string data.
41
+ #
42
+ module StringType
43
+ class <<self
44
+ def indexed_name(name) #:nodoc:
21
45
  "#{name}_s"
22
- end
46
+ end
47
+
48
+ def to_indexed(value) #:nodoc:
49
+ value.to_s if value
50
+ end
23
51
 
24
- def to_indexed(value)
25
- value.to_s if value
52
+ def cast(string) #:nodoc:
53
+ string
54
+ end
26
55
  end
27
56
  end
28
57
 
29
- class <<IntegerType
30
- def indexed_name(name)
58
+ #
59
+ # The Integer type represents integers.
60
+ #
61
+ module IntegerType
62
+ class <<self
63
+ def indexed_name(name) #:nodoc:
31
64
  "#{name}_i"
32
- end
65
+ end
33
66
 
34
- def to_indexed(value)
35
- value.to_i.to_s if value
67
+ def to_indexed(value) #:nodoc:
68
+ value.to_i.to_s if value
69
+ end
70
+
71
+ def cast(string) #:nodoc:
72
+ string.to_i
73
+ end
36
74
  end
37
75
  end
38
76
 
39
- class <<FloatType
40
- def indexed_name(name)
77
+ #
78
+ # The Float type represents floating-point numbers.
79
+ #
80
+ module FloatType
81
+ class <<self
82
+ def indexed_name(name) #:nodoc:
41
83
  "#{name}_f"
42
- end
84
+ end
85
+
86
+ def to_indexed(value) #:nodoc:
87
+ value.to_f.to_s if value
88
+ end
43
89
 
44
- def to_indexed(value)
45
- value.to_f.to_s if value
90
+ def cast(string) #:nodoc:
91
+ string.to_f
92
+ end
46
93
  end
47
94
  end
48
95
 
49
- class <<TimeType
50
- def indexed_name(name)
96
+ #
97
+ # The time type represents times. Note that times are always converted to
98
+ # UTC before indexing, and facets of Time fields always return times in UTC.
99
+ #
100
+ module TimeType
101
+ class <<self
102
+ def indexed_name(name)
51
103
  "#{name}_d"
104
+ end
105
+
106
+ def to_indexed(value)
107
+ if value
108
+ time = value.respond_to?(:to_time) ? value.to_time : Time.parse(value.to_s)
109
+ time.utc.xmlschema
110
+ end
111
+ end
112
+
113
+ def cast(string)
114
+ Time.xmlschema(string)
115
+ end
52
116
  end
117
+ end
118
+
119
+ #
120
+ # The boolean type represents true/false values. Note that +nil+ will not be
121
+ # indexed at all; only +false+ will be indexed with a false value.
122
+ #
123
+ module BooleanType
124
+ class <<self
125
+ def indexed_name(name)
126
+ "#{name}_b"
127
+ end
128
+
129
+ def to_indexed(value)
130
+ unless value.nil?
131
+ value ? 'true' : 'false'
132
+ end
133
+ end
53
134
 
54
- def to_indexed(value)
55
- if value
56
- time = value.respond_to?(:to_time) ? value.to_time : Time.parse(value.to_s)
57
- time.utc.strftime('%FT%TZ')
135
+ def cast(string)
136
+ case string
137
+ when 'true'
138
+ true
139
+ when 'false'
140
+ false
141
+ end
58
142
  end
59
143
  end
60
144
  end
data/lib/sunspot/util.rb CHANGED
@@ -1,12 +1,146 @@
1
1
  module Sunspot
2
- module Util
3
- class ClosedStruct
4
- def initialize(data)
5
- (class <<self; self; end).module_eval do
6
- data.each_pair do |attr_name, value|
7
- define_method(attr_name.to_s) { value }
8
- end
2
+ #
3
+ # The Sunspot::Util module provides utility methods used elsewhere in the
4
+ # library.
5
+ #
6
+ module Util #:nodoc:
7
+ class <<self
8
+ #
9
+ # Get all of the superclasses for a given class, including the class
10
+ # itself.
11
+ #
12
+ # ==== Parameters
13
+ #
14
+ # clazz<Class>:: class for which to get superclasses
15
+ #
16
+ # ==== Returns
17
+ #
18
+ # Array:: Collection containing class and its superclasses
19
+ #
20
+ def superclasses_for(clazz)
21
+ superclasses = [clazz]
22
+ superclasses << (clazz = clazz.superclass) while clazz.superclass != Object
23
+ superclasses
24
+ end
25
+
26
+ #
27
+ # Convert a string to snake case
28
+ #
29
+ # ==== Parameters
30
+ #
31
+ # string<String>:: String to convert to snake case
32
+ #
33
+ # ==== Returns
34
+ #
35
+ # String:: String in snake case
36
+ #
37
+ def snake_case(string)
38
+ string.scan(/(^|[A-Z])([^A-Z]+)/).map! { |word| word.join.downcase }.join('_')
39
+ end
40
+
41
+ #
42
+ # Convert a string to camel case
43
+ #
44
+ # ==== Parameters
45
+ #
46
+ # string<String>:: String to convert to camel case
47
+ #
48
+ # ==== Returns
49
+ #
50
+ # String:: String in camel case
51
+ #
52
+ def camel_case(string)
53
+ string.split('_').map! { |word| word.capitalize }.join
54
+ end
55
+
56
+ #
57
+ # Get a constant from a fully qualified name
58
+ #
59
+ # ==== Parameters
60
+ #
61
+ # string<String>:: The fully qualified name of a constant
62
+ #
63
+ # ==== Returns
64
+ #
65
+ # Object:: Value of constant named
66
+ #
67
+ def full_const_get(string)
68
+ string.split('::').inject(Object) do |context, const_name|
69
+ context.const_get(const_name)
70
+ end
71
+ end
72
+
73
+ #
74
+ # Perform a deep merge of hashes, returning the result as a new hash.
75
+ # See #deep_merge_into for rules used to merge the hashes
76
+ #
77
+ # ==== Parameters
78
+ #
79
+ # left<Hash>:: Hash to merge
80
+ # right<Hash>:: The other hash to merge
81
+ #
82
+ # ==== Returns
83
+ #
84
+ # Hash:: New hash containing the given hashes deep-merged.
85
+ #
86
+ def deep_merge(left, right)
87
+ deep_merge_into({}, left, right)
88
+ end
89
+
90
+ #
91
+ # Perform a deep merge of the right hash into the left hash
92
+ #
93
+ # ==== Parameters
94
+ #
95
+ # left:: Hash to receive merge
96
+ # right:: Hash to merge into left
97
+ #
98
+ # ==== Returns
99
+ #
100
+ # Hash:: left
101
+ #
102
+ def deep_merge!(left, right)
103
+ deep_merge_into(left, left, right)
104
+ end
105
+
106
+ private
107
+
108
+ #
109
+ # Deep merge two hashes into a third hash, using rules that produce nice
110
+ # merged parameter hashes. The rules are as follows, for a given key:
111
+ #
112
+ # * If only one hash has a value, or if both hashes have the same value,
113
+ # just use the value.
114
+ # * If either of the values is not a hash, create arrays out of both
115
+ # values and concatenate them.
116
+ # * Otherwise, deep merge the two values (which are both hashes)
117
+ #
118
+ # ==== Parameters
119
+ #
120
+ # destination<Hash>:: Hash into which to perform the merge
121
+ # left<Hash>:: One hash to merge
122
+ # right<Hash>:: The other hash to merge
123
+ #
124
+ # ==== Returns
125
+ #
126
+ # Hash:: destination
127
+ #
128
+ def deep_merge_into(destination, left, right)
129
+ left.each_pair do |name, left_value|
130
+ right_value = right[name]
131
+ destination[name] =
132
+ if right_value.nil? || left_value == right_value
133
+ left_value
134
+ elsif !left_value.respond_to?(:each_pair) || !right_value.respond_to?(:each_pair)
135
+ Array(left_value) + Array(right_value)
136
+ else
137
+ merged_value = {}
138
+ deep_merge_into(merged_value, left_value, right_value)
139
+ end
9
140
  end
141
+ left_keys = Set.new(left.keys)
142
+ destination.merge!(right.reject { |k, v| left_keys.include?(k) })
143
+ destination
10
144
  end
11
145
  end
12
146
  end