jbasdf-muck-solr 0.4.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.
Files changed (156) hide show
  1. data/CHANGE_LOG +239 -0
  2. data/LICENSE +19 -0
  3. data/README.markdown +118 -0
  4. data/README.rdoc +107 -0
  5. data/Rakefile +99 -0
  6. data/TESTING_THE_PLUGIN +25 -0
  7. data/VERSION.yml +4 -0
  8. data/config/solr.yml +15 -0
  9. data/config/solr_environment.rb +32 -0
  10. data/lib/acts_as_solr/acts_methods.rb +352 -0
  11. data/lib/acts_as_solr/class_methods.rb +236 -0
  12. data/lib/acts_as_solr/common_methods.rb +89 -0
  13. data/lib/acts_as_solr/deprecation.rb +61 -0
  14. data/lib/acts_as_solr/instance_methods.rb +165 -0
  15. data/lib/acts_as_solr/lazy_document.rb +18 -0
  16. data/lib/acts_as_solr/parser_methods.rb +203 -0
  17. data/lib/acts_as_solr/search_results.rb +68 -0
  18. data/lib/acts_as_solr/solr_fixtures.rb +13 -0
  19. data/lib/acts_as_solr/tasks/database.rake +16 -0
  20. data/lib/acts_as_solr/tasks/solr.rake +135 -0
  21. data/lib/acts_as_solr/tasks/test.rake +5 -0
  22. data/lib/acts_as_solr/tasks.rb +10 -0
  23. data/lib/acts_as_solr.rb +65 -0
  24. data/lib/solr/connection.rb +177 -0
  25. data/lib/solr/document.rb +75 -0
  26. data/lib/solr/exception.rb +13 -0
  27. data/lib/solr/field.rb +36 -0
  28. data/lib/solr/importer/array_mapper.rb +26 -0
  29. data/lib/solr/importer/delimited_file_source.rb +38 -0
  30. data/lib/solr/importer/hpricot_mapper.rb +27 -0
  31. data/lib/solr/importer/mapper.rb +51 -0
  32. data/lib/solr/importer/solr_source.rb +41 -0
  33. data/lib/solr/importer/xpath_mapper.rb +35 -0
  34. data/lib/solr/importer.rb +19 -0
  35. data/lib/solr/indexer.rb +52 -0
  36. data/lib/solr/request/add_document.rb +58 -0
  37. data/lib/solr/request/base.rb +36 -0
  38. data/lib/solr/request/commit.rb +29 -0
  39. data/lib/solr/request/delete.rb +48 -0
  40. data/lib/solr/request/dismax.rb +46 -0
  41. data/lib/solr/request/index_info.rb +22 -0
  42. data/lib/solr/request/modify_document.rb +46 -0
  43. data/lib/solr/request/optimize.rb +19 -0
  44. data/lib/solr/request/ping.rb +36 -0
  45. data/lib/solr/request/select.rb +54 -0
  46. data/lib/solr/request/spellcheck.rb +30 -0
  47. data/lib/solr/request/standard.rb +402 -0
  48. data/lib/solr/request/update.rb +23 -0
  49. data/lib/solr/request.rb +26 -0
  50. data/lib/solr/response/add_document.rb +17 -0
  51. data/lib/solr/response/base.rb +42 -0
  52. data/lib/solr/response/commit.rb +15 -0
  53. data/lib/solr/response/delete.rb +13 -0
  54. data/lib/solr/response/dismax.rb +8 -0
  55. data/lib/solr/response/index_info.rb +26 -0
  56. data/lib/solr/response/modify_document.rb +17 -0
  57. data/lib/solr/response/optimize.rb +14 -0
  58. data/lib/solr/response/ping.rb +26 -0
  59. data/lib/solr/response/ruby.rb +42 -0
  60. data/lib/solr/response/select.rb +17 -0
  61. data/lib/solr/response/spellcheck.rb +20 -0
  62. data/lib/solr/response/standard.rb +60 -0
  63. data/lib/solr/response/xml.rb +39 -0
  64. data/lib/solr/response.rb +27 -0
  65. data/lib/solr/solrtasks.rb +27 -0
  66. data/lib/solr/util.rb +32 -0
  67. data/lib/solr/xml.rb +44 -0
  68. data/lib/solr.rb +26 -0
  69. data/solr/CHANGES.txt +1207 -0
  70. data/solr/LICENSE.txt +712 -0
  71. data/solr/NOTICE.txt +90 -0
  72. data/solr/etc/jetty.xml +205 -0
  73. data/solr/etc/webdefault.xml +379 -0
  74. data/solr/lib/easymock.jar +0 -0
  75. data/solr/lib/jetty-6.1.3.jar +0 -0
  76. data/solr/lib/jetty-util-6.1.3.jar +0 -0
  77. data/solr/lib/jsp-2.1/ant-1.6.5.jar +0 -0
  78. data/solr/lib/jsp-2.1/core-3.1.1.jar +0 -0
  79. data/solr/lib/jsp-2.1/jsp-2.1.jar +0 -0
  80. data/solr/lib/jsp-2.1/jsp-api-2.1.jar +0 -0
  81. data/solr/lib/servlet-api-2.4.jar +0 -0
  82. data/solr/lib/servlet-api-2.5-6.1.3.jar +0 -0
  83. data/solr/lib/xpp3-1.1.3.4.O.jar +0 -0
  84. data/solr/solr/README.txt +52 -0
  85. data/solr/solr/bin/abc +176 -0
  86. data/solr/solr/bin/abo +176 -0
  87. data/solr/solr/bin/backup +108 -0
  88. data/solr/solr/bin/backupcleaner +142 -0
  89. data/solr/solr/bin/commit +128 -0
  90. data/solr/solr/bin/optimize +129 -0
  91. data/solr/solr/bin/readercycle +129 -0
  92. data/solr/solr/bin/rsyncd-disable +77 -0
  93. data/solr/solr/bin/rsyncd-enable +76 -0
  94. data/solr/solr/bin/rsyncd-start +145 -0
  95. data/solr/solr/bin/rsyncd-stop +105 -0
  96. data/solr/solr/bin/scripts-util +83 -0
  97. data/solr/solr/bin/snapcleaner +148 -0
  98. data/solr/solr/bin/snapinstaller +168 -0
  99. data/solr/solr/bin/snappuller +248 -0
  100. data/solr/solr/bin/snappuller-disable +77 -0
  101. data/solr/solr/bin/snappuller-enable +77 -0
  102. data/solr/solr/bin/snapshooter +109 -0
  103. data/solr/solr/conf/admin-extra.html +31 -0
  104. data/solr/solr/conf/protwords.txt +21 -0
  105. data/solr/solr/conf/schema.xml +126 -0
  106. data/solr/solr/conf/scripts.conf +24 -0
  107. data/solr/solr/conf/solrconfig.xml +458 -0
  108. data/solr/solr/conf/stopwords.txt +57 -0
  109. data/solr/solr/conf/synonyms.txt +31 -0
  110. data/solr/solr/conf/xslt/example.xsl +132 -0
  111. data/solr/solr/conf/xslt/example_atom.xsl +63 -0
  112. data/solr/solr/conf/xslt/example_rss.xsl +62 -0
  113. data/solr/start.jar +0 -0
  114. data/solr/webapps/solr.war +0 -0
  115. data/test/config/solr.yml +2 -0
  116. data/test/db/connections/mysql/connection.rb +10 -0
  117. data/test/db/connections/sqlite/connection.rb +8 -0
  118. data/test/db/migrate/001_create_books.rb +15 -0
  119. data/test/db/migrate/002_create_movies.rb +12 -0
  120. data/test/db/migrate/003_create_categories.rb +11 -0
  121. data/test/db/migrate/004_create_electronics.rb +16 -0
  122. data/test/db/migrate/005_create_authors.rb +12 -0
  123. data/test/db/migrate/006_create_postings.rb +9 -0
  124. data/test/db/migrate/007_create_posts.rb +13 -0
  125. data/test/db/migrate/008_create_gadgets.rb +11 -0
  126. data/test/fixtures/authors.yml +9 -0
  127. data/test/fixtures/books.yml +13 -0
  128. data/test/fixtures/categories.yml +7 -0
  129. data/test/fixtures/db_definitions/mysql.sql +41 -0
  130. data/test/fixtures/electronics.yml +49 -0
  131. data/test/fixtures/movies.yml +9 -0
  132. data/test/fixtures/postings.yml +10 -0
  133. data/test/functional/acts_as_solr_test.rb +413 -0
  134. data/test/functional/association_indexing_test.rb +37 -0
  135. data/test/functional/faceted_search_test.rb +163 -0
  136. data/test/functional/multi_solr_search_test.rb +57 -0
  137. data/test/models/author.rb +10 -0
  138. data/test/models/book.rb +10 -0
  139. data/test/models/category.rb +8 -0
  140. data/test/models/electronic.rb +25 -0
  141. data/test/models/gadget.rb +9 -0
  142. data/test/models/movie.rb +17 -0
  143. data/test/models/novel.rb +2 -0
  144. data/test/models/post.rb +3 -0
  145. data/test/models/posting.rb +11 -0
  146. data/test/test_helper.rb +54 -0
  147. data/test/unit/acts_methods_shoulda.rb +68 -0
  148. data/test/unit/class_methods_shoulda.rb +85 -0
  149. data/test/unit/common_methods_shoulda.rb +111 -0
  150. data/test/unit/instance_methods_shoulda.rb +318 -0
  151. data/test/unit/lazy_document_shoulda.rb +34 -0
  152. data/test/unit/parser_instance.rb +19 -0
  153. data/test/unit/parser_methods_shoulda.rb +268 -0
  154. data/test/unit/solr_instance.rb +49 -0
  155. data/test/unit/test_helper.rb +24 -0
  156. metadata +241 -0
@@ -0,0 +1,61 @@
1
+ module ActsAsSolr #:nodoc:
2
+
3
+ class Post
4
+ def initialize(body, mode = :search)
5
+ @body = body
6
+ @mode = mode
7
+ puts "The method ActsAsSolr::Post.new(body, mode).execute_post is depracated. " +
8
+ "Use ActsAsSolr::Post.execute(body, mode) instead!"
9
+ end
10
+
11
+ def execute_post
12
+ ActsAsSolr::Post.execute(@body, @mode)
13
+ end
14
+ end
15
+
16
+ module ClassMethods
17
+ def find_with_facet(query, options={})
18
+ Deprecation.plog "The method find_with_facet is deprecated. Use find_by_solr instead, passing the " +
19
+ "arguments the same way you used to do with find_with_facet."
20
+ find_by_solr(query, options)
21
+ end
22
+ end
23
+
24
+ class Deprecation
25
+ # Validates the options passed during query
26
+ def self.validate_query options={}
27
+ if options[:field_types]
28
+ plog "The option :field_types for searching is deprecated. " +
29
+ "The field types are automatically traced back when you specify a field type in your model."
30
+ end
31
+ if options[:sort_by]
32
+ plog "The option :sort_by is deprecated, use :order instead!"
33
+ options[:order] ||= options[:sort_by]
34
+ end
35
+ if options[:start]
36
+ plog "The option :start is deprecated, use :offset instead!"
37
+ options[:offset] ||= options[:start]
38
+ end
39
+ if options[:rows]
40
+ plog "The option :rows is deprecated, use :limit instead!"
41
+ options[:limit] ||= options[:rows]
42
+ end
43
+ end
44
+
45
+ # Validates the options passed during indexing
46
+ def self.validate_index options={}
47
+ if options[:background]
48
+ plog "The :background option is being deprecated. There are better and more efficient " +
49
+ "ways to handle delayed saving of your records."
50
+ end
51
+ end
52
+
53
+ # This will print the text to stdout and log the text
54
+ # if rails logger is available
55
+ def self.plog text
56
+ puts text
57
+ RAILS_DEFAULT_LOGGER.warn text if defined? RAILS_DEFAULT_LOGGER
58
+ end
59
+ end
60
+
61
+ end
@@ -0,0 +1,165 @@
1
+ module ActsAsSolr #:nodoc:
2
+
3
+ module InstanceMethods
4
+
5
+ # Solr id is <class.name>:<id> to be unique across all models
6
+ def solr_id
7
+ "#{self.class.name}:#{record_id(self)}"
8
+ end
9
+
10
+ # saves to the Solr index
11
+ def solr_save
12
+ return true if indexing_disabled?
13
+ if evaluate_condition(:if, self)
14
+ logger.debug "solr_save: #{self.class.name} : #{record_id(self)}"
15
+ solr_add to_solr_doc
16
+ solr_commit if configuration[:auto_commit]
17
+ true
18
+ else
19
+ solr_destroy
20
+ end
21
+ end
22
+
23
+ def indexing_disabled?
24
+ evaluate_condition(:offline, self) || !configuration[:if]
25
+ end
26
+
27
+ # remove from index
28
+ def solr_destroy
29
+ return true if indexing_disabled?
30
+ logger.debug "solr_destroy: #{self.class.name} : #{record_id(self)}"
31
+ solr_delete solr_id
32
+ solr_commit if configuration[:auto_commit]
33
+ true
34
+ end
35
+
36
+ # convert instance to Solr document
37
+ def to_solr_doc
38
+ logger.debug "to_solr_doc: creating doc for class: #{self.class.name}, id: #{record_id(self)}"
39
+ doc = Solr::Document.new
40
+ doc.boost = validate_boost(configuration[:boost]) if configuration[:boost]
41
+
42
+ doc << {:id => solr_id,
43
+ solr_configuration[:type_field] => self.class.name,
44
+ solr_configuration[:primary_key_field] => record_id(self).to_s}
45
+
46
+ # iterate through the fields and add them to the document,
47
+ configuration[:solr_fields].each do |field_name, options|
48
+ #field_type = configuration[:facets] && configuration[:facets].include?(field) ? :facet : :text
49
+
50
+ field_boost = options[:boost] || solr_configuration[:default_boost]
51
+ field_type = get_solr_field_type(options[:type])
52
+ solr_name = options[:as] || field_name
53
+
54
+ value = self.send("#{field_name}_for_solr")
55
+ value = set_value_if_nil(field_type) if value.to_s == ""
56
+
57
+ # add the field to the document, but only if it's not the id field
58
+ # or the type field (from single table inheritance), since these
59
+ # fields have already been added above.
60
+ if field_name.to_s != self.class.primary_key and field_name.to_s != "type"
61
+ suffix = get_solr_field_type(field_type)
62
+ # This next line ensures that e.g. nil dates are excluded from the
63
+ # document, since they choke Solr. Also ignores e.g. empty strings,
64
+ # but these can't be searched for anyway:
65
+ # http://www.mail-archive.com/solr-dev@lucene.apache.org/msg05423.html
66
+ next if value.nil? || value.to_s.strip.empty?
67
+ [value].flatten.each do |v|
68
+ v = set_value_if_nil(suffix) if value.to_s == ""
69
+ field = Solr::Field.new("#{solr_name}_#{suffix}" => ERB::Util.html_escape(v.to_s))
70
+ field.boost = validate_boost(field_boost)
71
+ doc << field
72
+ end
73
+ end
74
+ end
75
+
76
+ add_includes(doc)
77
+ logger.debug doc.to_xml
78
+ doc
79
+ end
80
+
81
+ private
82
+ def add_includes(doc)
83
+ if configuration[:solr_includes].respond_to?(:each)
84
+ configuration[:solr_includes].each do |association, options|
85
+ data = options[:multivalued] ? [] : ""
86
+ field_name = options[:as] || association.to_s.singularize
87
+ field_type = get_solr_field_type(options[:type])
88
+ field_boost = options[:boost] || solr_configuration[:default_boost]
89
+ suffix = get_solr_field_type(field_type)
90
+ case self.class.reflect_on_association(association).macro
91
+ when :has_many, :has_and_belongs_to_many
92
+ records = self.send(association).to_a
93
+ unless records.empty?
94
+ records.each {|r| data << include_value(r, options)}
95
+ [data].flatten.each do |value|
96
+ field = Solr::Field.new("#{field_name}_#{suffix}" => value)
97
+ field.boost = validate_boost(field_boost)
98
+ doc << field
99
+ end
100
+ end
101
+ when :has_one, :belongs_to
102
+ record = self.send(association)
103
+ unless record.nil?
104
+ doc["#{field_name}_#{suffix}"] = include_value(record, options)
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
110
+
111
+ def include_value(record, options)
112
+ if options[:using].is_a? Proc
113
+ options[:using].call(record)
114
+ elsif options[:using].is_a? Symbol
115
+ record.send(options[:using])
116
+ else
117
+ record.attributes.inject([]){|k,v| k << "#{v.first}=#{ERB::Util.html_escape(v.last)}"}.join(" ")
118
+ end
119
+ end
120
+
121
+ def validate_boost(boost)
122
+ boost_value = case boost
123
+ when Float
124
+ return solr_configuration[:default_boost] if boost < 0
125
+ boost
126
+ when Proc
127
+ boost.call(self)
128
+ when Symbol
129
+ if self.respond_to?(boost)
130
+ self.send(boost)
131
+ end
132
+ end
133
+
134
+ boost_value || solr_configuration[:default_boost]
135
+ end
136
+
137
+ def condition_block?(condition)
138
+ condition.respond_to?("call") && (condition.arity == 1 || condition.arity == -1)
139
+ end
140
+
141
+ def evaluate_condition(which_condition, field)
142
+ condition = configuration[which_condition]
143
+ case condition
144
+ when Symbol
145
+ field.send(condition)
146
+ when String
147
+ eval(condition, binding)
148
+ when FalseClass, NilClass
149
+ false
150
+ when TrueClass
151
+ true
152
+ else
153
+ if condition_block?(condition)
154
+ condition.call(field)
155
+ else
156
+ raise(
157
+ ArgumentError,
158
+ "The :#{which_condition} option has to be either a symbol, string (to be eval'ed), proc/method, true/false, or " +
159
+ "class implementing a static validation method"
160
+ )
161
+ end
162
+ end
163
+ end
164
+ end
165
+ end
@@ -0,0 +1,18 @@
1
+ module ActsAsSolr
2
+ class LazyDocument
3
+ attr_reader :id, :clazz
4
+
5
+ def initialize(id, clazz)
6
+ @id = id
7
+ @clazz = clazz
8
+ end
9
+
10
+ def method_missing(name, *args)
11
+ unless @__instance
12
+ @__instance = @clazz.find(@id)
13
+ end
14
+
15
+ @__instance.send(name, *args)
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,203 @@
1
+ module ActsAsSolr #:nodoc:
2
+ module ParserMethods
3
+ protected
4
+
5
+ # Method used by mostly all the ClassMethods when doing a search
6
+ def parse_query(query=nil, options={}, models=nil)
7
+ valid_options = [:offset, :limit, :facets, :models, :results_format, :order, :scores, :operator, :include, :lazy, :joins, :select, :core]
8
+ query_options = {}
9
+
10
+ return nil if (query.nil? || query.strip == '')
11
+
12
+ raise "Invalid parameters: #{(options.keys - valid_options).join(',')}" unless (options.keys - valid_options).empty?
13
+ begin
14
+ Deprecation.validate_query(options)
15
+ query_options[:start] = options[:offset]
16
+ query_options[:rows] = options[:limit]
17
+ query_options[:operator] = options[:operator]
18
+
19
+ # first steps on the facet parameter processing
20
+ if options[:facets]
21
+ query_options[:facets] = {}
22
+ query_options[:facets][:limit] = -1 # TODO: make this configurable
23
+ query_options[:facets][:sort] = :count if options[:facets][:sort]
24
+ query_options[:facets][:mincount] = 0
25
+ query_options[:facets][:mincount] = 1 if options[:facets][:zeros] == false
26
+ # override the :zeros (it's deprecated anyway) if :mincount exists
27
+ query_options[:facets][:mincount] = options[:facets][:mincount] if options[:facets][:mincount]
28
+ query_options[:facets][:fields] = options[:facets][:fields].collect{|k| "#{k}_facet"} if options[:facets][:fields]
29
+ query_options[:filter_queries] = replace_types([*options[:facets][:browse]].collect{|k| "#{k.sub!(/ *: */,"_facet:")}"}) if options[:facets][:browse]
30
+ query_options[:facets][:queries] = replace_types(options[:facets][:query].collect{|k| "#{k.sub!(/ *: */,"_t:")}"}) if options[:facets][:query]
31
+
32
+
33
+ if options[:facets][:dates]
34
+ query_options[:date_facets] = {}
35
+ # if options[:facets][:dates][:fields] exists then :start, :end, and :gap must be there
36
+ if options[:facets][:dates][:fields]
37
+ [:start, :end, :gap].each { |k| raise "#{k} must be present in faceted date query" unless options[:facets][:dates].include?(k) }
38
+ query_options[:date_facets][:fields] = []
39
+ options[:facets][:dates][:fields].each { |f|
40
+ if f.kind_of? Hash
41
+ key = f.keys[0]
42
+ query_options[:date_facets][:fields] << {"#{key}_d" => f[key]}
43
+ validate_date_facet_other_options(f[key][:other]) if f[key][:other]
44
+ else
45
+ query_options[:date_facets][:fields] << "#{f}_d"
46
+ end
47
+ }
48
+ end
49
+
50
+ query_options[:date_facets][:start] = options[:facets][:dates][:start] if options[:facets][:dates][:start]
51
+ query_options[:date_facets][:end] = options[:facets][:dates][:end] if options[:facets][:dates][:end]
52
+ query_options[:date_facets][:gap] = options[:facets][:dates][:gap] if options[:facets][:dates][:gap]
53
+ query_options[:date_facets][:hardend] = options[:facets][:dates][:hardend] if options[:facets][:dates][:hardend]
54
+ query_options[:date_facets][:filter] = replace_types([*options[:facets][:dates][:filter]].collect{|k| "#{k.sub!(/ *:(?!\d) */,"_d:")}"}) if options[:facets][:dates][:filter]
55
+
56
+ if options[:facets][:dates][:other]
57
+ validate_date_facet_other_options(options[:facets][:dates][:other])
58
+ query_options[:date_facets][:other] = options[:facets][:dates][:other]
59
+ end
60
+
61
+ end
62
+ end
63
+
64
+ if models.nil?
65
+ # TODO: use a filter query for type, allowing Solr to cache it individually
66
+ models = "AND #{solr_type_condition}"
67
+ field_list = solr_configuration[:primary_key_field]
68
+ else
69
+ field_list = "id"
70
+ end
71
+
72
+ query_options[:field_list] = [field_list, 'score']
73
+ query = "(#{query.gsub(/ *: */,"_t:")}) #{models}"
74
+ order = options[:order].split(/\s*,\s*/).collect{|e| e.gsub(/\s+/,'_t ').gsub(/\bscore_t\b/, 'score') }.join(',') if options[:order]
75
+ query_options[:query] = replace_types([query])[0] # TODO adjust replace_types to work with String or Array
76
+
77
+ if options[:order]
78
+ # TODO: set the sort parameter instead of the old ;order. style.
79
+ query_options[:query] << ';' << replace_types([order], false)[0]
80
+ end
81
+
82
+ ActsAsSolr::Post.execute(Solr::Request::Standard.new(query_options), options[:core])
83
+ rescue
84
+ raise "There was a problem executing your search\n#{query_options.inspect}\n: #{$!} in #{$!.backtrace.first}"
85
+ end
86
+ end
87
+
88
+ def solr_type_condition
89
+ subclasses.inject("(#{solr_configuration[:type_field]}:#{self.name}") do |condition, subclass|
90
+ condition << " OR #{solr_configuration[:type_field]}:#{subclass.name}"
91
+ end << ')'
92
+ end
93
+
94
+ # Parses the data returned from Solr
95
+ def parse_results(solr_data, options = {})
96
+ results = {
97
+ :docs => [],
98
+ :total => 0
99
+ }
100
+
101
+ configuration = {
102
+ :format => :objects
103
+ }
104
+ results.update(:facets => {'facet_fields' => []}) if options[:facets]
105
+ return SearchResults.new(results) if (solr_data.nil? || solr_data.total_hits == 0)
106
+
107
+ configuration.update(options) if options.is_a?(Hash)
108
+
109
+ ids = solr_data.hits.collect {|doc| doc["#{solr_configuration[:primary_key_field]}"]}.flatten
110
+
111
+ result = find_objects(ids, options, configuration)
112
+
113
+ add_scores(result, solr_data) if configuration[:format] == :objects && options[:scores]
114
+
115
+ results.update(:facets => solr_data.data['facet_counts']) if options[:facets]
116
+ results.update({:docs => result, :total => solr_data.total_hits, :max_score => solr_data.max_score, :query_time => solr_data.data['responseHeader']['QTime']})
117
+ SearchResults.new(results)
118
+ end
119
+
120
+
121
+ def find_objects(ids, options, configuration)
122
+ result = if configuration[:lazy] && configuration[:format] != :ids
123
+ ids.collect {|id| ActsAsSolr::LazyDocument.new(id, self)}
124
+ elsif configuration[:format] == :objects
125
+ conditions = [ "#{self.table_name}.#{primary_key} in (?)", ids ]
126
+ find_options = {:conditions => conditions}
127
+ find_options[:include] = options[:include] if options[:include]
128
+ find_options[:select] = options[:select] if options[:select]
129
+ find_options[:joins] = options[:joins] if options[:joins]
130
+ result = reorder(self.find(:all, find_options), ids)
131
+ else
132
+ ids
133
+ end
134
+
135
+ result
136
+ end
137
+
138
+ # Reorders the instances keeping the order returned from Solr
139
+ def reorder(things, ids)
140
+ ordered_things = Array.new(things.size)
141
+ raise "Out of sync! Found #{ids.size} items in index, but only #{things.size} were found in database!" unless things.size == ids.size
142
+ things.each do |thing|
143
+ position = ids.index(thing.id)
144
+ ordered_things[position] = thing
145
+ end
146
+ ordered_things
147
+ end
148
+
149
+ # Replaces the field types based on the types (if any) specified
150
+ # on the acts_as_solr call
151
+ def replace_types(strings, include_colon=true)
152
+ suffix = include_colon ? ":" : ""
153
+ if configuration[:solr_fields]
154
+ configuration[:solr_fields].each do |name, options|
155
+ solr_name = options[:as] || name.to_s
156
+ solr_type = get_solr_field_type(options[:type])
157
+ field = "#{solr_name}_#{solr_type}#{suffix}"
158
+ strings.each_with_index {|s,i| strings[i] = s.gsub(/#{solr_name.to_s}_t#{suffix}/,field) }
159
+ end
160
+ end
161
+ if configuration[:solr_includes]
162
+ configuration[:solr_includes].each do |association, options|
163
+ solr_name = options[:as] || association.to_s.singularize
164
+ solr_type = get_solr_field_type(options[:type])
165
+ field = "#{solr_name}_#{solr_type}#{suffix}"
166
+ strings.each_with_index {|s,i| strings[i] = s.gsub(/#{solr_name.to_s}_t#{suffix}/,field) }
167
+ end
168
+ end
169
+ strings
170
+ end
171
+
172
+ # Adds the score to each one of the instances found
173
+ def add_scores(results, solr_data)
174
+ with_score = []
175
+ solr_data.hits.each do |doc|
176
+ with_score.push([doc["score"],
177
+ results.find {|record| scorable_record?(record, doc) }])
178
+ end
179
+ with_score.each do |score, object|
180
+ class << object; attr_accessor :solr_score; end
181
+ object.solr_score = score
182
+ end
183
+ end
184
+
185
+ def scorable_record?(record, doc)
186
+ doc_id = doc["#{solr_configuration[:primary_key_field]}"]
187
+ if doc_id.nil?
188
+ doc_id = doc["id"]
189
+ "#{record.class.name}:#{record_id(record)}" == doc_id.first.to_s
190
+ else
191
+ record_id(record).to_s == doc_id.to_s
192
+ end
193
+ end
194
+
195
+ def validate_date_facet_other_options(options)
196
+ valid_other_options = [:after, :all, :before, :between, :none]
197
+ options = [options] unless options.kind_of? Array
198
+ bad_options = options.map {|x| x.to_sym} - valid_other_options
199
+ raise "Invalid option#{'s' if bad_options.size > 1} for faceted date's other param: #{bad_options.join(', ')}. May only be one of :after, :all, :before, :between, :none" if bad_options.size > 0
200
+ end
201
+
202
+ end
203
+ end
@@ -0,0 +1,68 @@
1
+ module ActsAsSolr #:nodoc:
2
+
3
+ # TODO: Possibly looking into hooking it up with Solr::Response::Standard
4
+ #
5
+ # Class that returns the search results with four methods.
6
+ #
7
+ # books = Book.find_by_solr 'ruby'
8
+ #
9
+ # the above will return a SearchResults class with 4 methods:
10
+ #
11
+ # docs|results|records: will return an array of records found
12
+ #
13
+ # books.records.empty?
14
+ # => false
15
+ #
16
+ # total|num_found|total_hits: will return the total number of records found
17
+ #
18
+ # books.total
19
+ # => 2
20
+ #
21
+ # facets: will return the facets when doing a faceted search
22
+ #
23
+ # max_score|highest_score: returns the highest score found
24
+ #
25
+ # books.max_score
26
+ # => 1.3213213
27
+ #
28
+ #
29
+ class SearchResults
30
+ def initialize(solr_data={})
31
+ @solr_data = solr_data
32
+ end
33
+
34
+ # Returns an array with the instances. This method
35
+ # is also aliased as docs and records
36
+ def results
37
+ @solr_data[:docs]
38
+ end
39
+
40
+ # Returns the total records found. This method is
41
+ # also aliased as num_found and total_hits
42
+ def total
43
+ @solr_data[:total]
44
+ end
45
+
46
+ # Returns the facets when doing a faceted search
47
+ def facets
48
+ @solr_data[:facets]
49
+ end
50
+
51
+ # Returns the highest score found. This method is
52
+ # also aliased as highest_score
53
+ def max_score
54
+ @solr_data[:max_score]
55
+ end
56
+
57
+ def query_time
58
+ @solr_data[:query_time]
59
+ end
60
+
61
+ alias docs results
62
+ alias records results
63
+ alias num_found total
64
+ alias total_hits total
65
+ alias highest_score max_score
66
+ end
67
+
68
+ end
@@ -0,0 +1,13 @@
1
+ module ActsAsSolr
2
+
3
+ class SolrFixtures
4
+ def self.load(table_names)
5
+ [table_names].flatten.map { |n| n.to_s }.each do |table_name|
6
+ klass = instance_eval(File.split(table_name.to_s).last.to_s.gsub('_',' ').split(" ").collect{|w| w.capitalize}.to_s.singularize)
7
+ klass.rebuild_solr_index if klass.respond_to?(:rebuild_solr_index)
8
+ end
9
+ ActsAsSolr::Post.execute(Solr::Request::Commit.new)
10
+ end
11
+ end
12
+
13
+ end
@@ -0,0 +1,16 @@
1
+ namespace :db do
2
+ namespace :fixtures do
3
+ desc "Load fixtures into the current environment's database. Load specific fixtures using FIXTURES=x,y"
4
+ task :load => :environment do
5
+ begin
6
+ ActsAsSolr::Post.execute(Solr::Request::Delete.new(:query => "*:*"))
7
+ ActsAsSolr::Post.execute(Solr::Request::Commit.new)
8
+ (ENV['FIXTURES'] ? ENV['FIXTURES'].split(/,/) : Dir.glob(File.join(RAILS_ROOT, 'test', 'fixtures', '*.{yml,csv}'))).each do |fixture_file|
9
+ ActsAsSolr::SolrFixtures.load(File.basename(fixture_file, '.*'))
10
+ end
11
+ puts "The fixtures loaded have been added to Solr"
12
+ rescue
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,135 @@
1
+ namespace :solr do
2
+
3
+ desc 'Starts Solr. Options accepted: RAILS_ENV=your_env, PORT=XX. Defaults to development if none.'
4
+ task :start do
5
+ require File.expand_path("#{File.dirname(__FILE__)}/../../../config/solr_environment")
6
+ FileUtils.mkdir_p(SOLR_LOGS_PATH)
7
+ FileUtils.mkdir_p(SOLR_DATA_PATH)
8
+ FileUtils.mkdir_p(SOLR_PIDS_PATH)
9
+ begin
10
+ n = Net::HTTP.new('127.0.0.1', SOLR_PORT)
11
+ n.request_head('/').value
12
+
13
+ rescue Net::HTTPServerException #responding
14
+ puts "Port #{SOLR_PORT} in use" and return
15
+
16
+ rescue Errno::ECONNREFUSED #not responding
17
+ Dir.chdir(SOLR_PATH) do
18
+ pid = fork do
19
+ #STDERR.close
20
+ exec "java #{SOLR_JVM_OPTIONS} -Dsolr.data.dir=#{SOLR_DATA_PATH} -Djetty.logs=#{SOLR_LOGS_PATH} -Djetty.port=#{SOLR_PORT} -jar start.jar"
21
+ end
22
+ sleep(5)
23
+ File.open("#{SOLR_PIDS_PATH}/#{ENV['RAILS_ENV']}_pid", "w"){ |f| f << pid}
24
+ puts "#{ENV['RAILS_ENV']} Solr started successfully on #{SOLR_PORT}, pid: #{pid}."
25
+ end
26
+ end
27
+ end
28
+
29
+ desc 'Stops Solr. Specify the environment by using: RAILS_ENV=your_env. Defaults to development if none.'
30
+ task :stop do
31
+ require File.expand_path("#{File.dirname(__FILE__)}/../../../config/solr_environment")
32
+ fork do
33
+ file_path = "#{SOLR_PIDS_PATH}/#{ENV['RAILS_ENV']}_pid"
34
+ if File.exists?(file_path)
35
+ File.open(file_path, "r") do |f|
36
+ pid = f.readline
37
+ Process.kill('TERM', pid.to_i)
38
+ end
39
+ File.unlink(file_path)
40
+ Rake::Task["solr:destroy_index"].invoke if ENV['RAILS_ENV'] == 'test'
41
+ puts "Solr shutdown successfully."
42
+ else
43
+ puts "PID file not found at #{file_path}. Either Solr is not running or no PID file was written."
44
+ end
45
+ end
46
+ end
47
+
48
+ desc 'Remove Solr index'
49
+ task :destroy_index do
50
+ require File.expand_path("#{File.dirname(__FILE__)}/../../../config/solr_environment")
51
+ raise "In production mode. I'm not going to delete the index, sorry." if ENV['RAILS_ENV'] == "production"
52
+ if File.exists?("#{SOLR_DATA_PATH}")
53
+ Dir["#{SOLR_DATA_PATH}/index/*"].each{|f| File.unlink(f) if File.exists?(f)}
54
+ Dir.rmdir("#{SOLR_DATA_PATH}/index")
55
+ puts "Index files removed under " + ENV['RAILS_ENV'] + " environment"
56
+ end
57
+ end
58
+
59
+ # this task is by Henrik Nyh
60
+ # http://henrik.nyh.se/2007/06/rake-task-to-reindex-models-for-acts_as_solr
61
+ desc %{Reindexes data for all acts_as_solr models. Clears index first to get rid of orphaned records and optimizes index afterwards. RAILS_ENV=your_env to set environment. ONLY=book,person,magazine to only reindex those models; EXCEPT=book,magazine to exclude those models. START_SERVER=true to solr:start before and solr:stop after. BATCH=123 to post/commit in batches of that size: default is 300. CLEAR=false to not clear the index first; OPTIMIZE=false to not optimize the index afterwards.}
62
+ task :reindex => :environment do
63
+ require File.expand_path("#{File.dirname(__FILE__)}/../../../config/solr_environment")
64
+
65
+ includes = env_array_to_constants('ONLY')
66
+ if includes.empty?
67
+ includes = Dir.glob("#{RAILS_ROOT}/app/models/*.rb").map { |path| File.basename(path, ".rb").camelize.constantize }
68
+ end
69
+ excludes = env_array_to_constants('EXCEPT')
70
+ includes -= excludes
71
+
72
+ optimize = env_to_bool('OPTIMIZE', true)
73
+ start_server = env_to_bool('START_SERVER', false)
74
+ clear_first = env_to_bool('CLEAR', true)
75
+ batch_size = ENV['BATCH'].to_i.nonzero? || 300
76
+ debug_output = env_to_bool("DEBUG", false)
77
+
78
+ RAILS_DEFAULT_LOGGER.level = ActiveSupport::BufferedLogger::INFO unless debug_output
79
+
80
+ if start_server
81
+ puts "Starting Solr server..."
82
+ Rake::Task["solr:start"].invoke
83
+ end
84
+
85
+ # Disable solr_optimize
86
+ module ActsAsSolr::CommonMethods
87
+ def blank() end
88
+ alias_method :deferred_solr_optimize, :solr_optimize
89
+ alias_method :solr_optimize, :blank
90
+ end
91
+
92
+ models = includes.select { |m| m.respond_to?(:rebuild_solr_index) }
93
+ models.each do |model|
94
+
95
+ if clear_first
96
+ puts "Clearing index for #{model}..."
97
+ ActsAsSolr::Post.execute(Solr::Request::Delete.new(:query => "#{model.solr_configuration[:type_field]}:#{model}"))
98
+ ActsAsSolr::Post.execute(Solr::Request::Commit.new)
99
+ end
100
+
101
+ puts "Rebuilding index for #{model}..."
102
+ model.rebuild_solr_index(batch_size)
103
+
104
+ end
105
+
106
+ if models.empty?
107
+ puts "There were no models to reindex."
108
+ elsif optimize
109
+ puts "Optimizing..."
110
+ models.last.deferred_solr_optimize
111
+ end
112
+
113
+ if start_server
114
+ puts "Shutting down Solr server..."
115
+ Rake::Task["solr:stop"].invoke
116
+ end
117
+
118
+ end
119
+
120
+ def env_array_to_constants(env)
121
+ env = ENV[env] || ''
122
+ env.split(/\s*,\s*/).map { |m| m.singularize.camelize.constantize }.uniq
123
+ end
124
+
125
+ def env_to_bool(env, default)
126
+ env = ENV[env] || ''
127
+ case env
128
+ when /^true$/i then true
129
+ when /^false$/i then false
130
+ else default
131
+ end
132
+ end
133
+
134
+ end
135
+
@@ -0,0 +1,5 @@
1
+ namespace :test do
2
+ task :migrate do
3
+ ActiveRecord::Migrator.migrate("test/db/migrate/", ENV["VERSION"] ? ENV["VERSION"].to_i : nil)
4
+ end
5
+ end