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.rb +65 -0
  11. data/lib/acts_as_solr/acts_methods.rb +352 -0
  12. data/lib/acts_as_solr/class_methods.rb +236 -0
  13. data/lib/acts_as_solr/common_methods.rb +89 -0
  14. data/lib/acts_as_solr/deprecation.rb +61 -0
  15. data/lib/acts_as_solr/instance_methods.rb +165 -0
  16. data/lib/acts_as_solr/lazy_document.rb +18 -0
  17. data/lib/acts_as_solr/parser_methods.rb +203 -0
  18. data/lib/acts_as_solr/search_results.rb +68 -0
  19. data/lib/acts_as_solr/solr_fixtures.rb +13 -0
  20. data/lib/acts_as_solr/tasks.rb +10 -0
  21. data/lib/acts_as_solr/tasks/database.rake +16 -0
  22. data/lib/acts_as_solr/tasks/solr.rake +135 -0
  23. data/lib/acts_as_solr/tasks/test.rake +5 -0
  24. data/lib/solr.rb +26 -0
  25. data/lib/solr/connection.rb +177 -0
  26. data/lib/solr/document.rb +75 -0
  27. data/lib/solr/exception.rb +13 -0
  28. data/lib/solr/field.rb +36 -0
  29. data/lib/solr/importer.rb +19 -0
  30. data/lib/solr/importer/array_mapper.rb +26 -0
  31. data/lib/solr/importer/delimited_file_source.rb +38 -0
  32. data/lib/solr/importer/hpricot_mapper.rb +27 -0
  33. data/lib/solr/importer/mapper.rb +51 -0
  34. data/lib/solr/importer/solr_source.rb +41 -0
  35. data/lib/solr/importer/xpath_mapper.rb +35 -0
  36. data/lib/solr/indexer.rb +52 -0
  37. data/lib/solr/request.rb +26 -0
  38. data/lib/solr/request/add_document.rb +58 -0
  39. data/lib/solr/request/base.rb +36 -0
  40. data/lib/solr/request/commit.rb +29 -0
  41. data/lib/solr/request/delete.rb +48 -0
  42. data/lib/solr/request/dismax.rb +46 -0
  43. data/lib/solr/request/index_info.rb +22 -0
  44. data/lib/solr/request/modify_document.rb +46 -0
  45. data/lib/solr/request/optimize.rb +19 -0
  46. data/lib/solr/request/ping.rb +36 -0
  47. data/lib/solr/request/select.rb +54 -0
  48. data/lib/solr/request/spellcheck.rb +30 -0
  49. data/lib/solr/request/standard.rb +402 -0
  50. data/lib/solr/request/update.rb +23 -0
  51. data/lib/solr/response.rb +27 -0
  52. data/lib/solr/response/add_document.rb +17 -0
  53. data/lib/solr/response/base.rb +42 -0
  54. data/lib/solr/response/commit.rb +15 -0
  55. data/lib/solr/response/delete.rb +13 -0
  56. data/lib/solr/response/dismax.rb +8 -0
  57. data/lib/solr/response/index_info.rb +26 -0
  58. data/lib/solr/response/modify_document.rb +17 -0
  59. data/lib/solr/response/optimize.rb +14 -0
  60. data/lib/solr/response/ping.rb +26 -0
  61. data/lib/solr/response/ruby.rb +42 -0
  62. data/lib/solr/response/select.rb +17 -0
  63. data/lib/solr/response/spellcheck.rb +20 -0
  64. data/lib/solr/response/standard.rb +60 -0
  65. data/lib/solr/response/xml.rb +39 -0
  66. data/lib/solr/solrtasks.rb +27 -0
  67. data/lib/solr/util.rb +32 -0
  68. data/lib/solr/xml.rb +44 -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,99 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'rake/testtask'
4
+ require 'rake/rdoctask'
5
+
6
+ Dir["#{File.dirname(__FILE__)}/lib/acts_as_solr/tasks/**/*.rake"].sort.each { |ext| load ext }
7
+
8
+ desc "Default Task"
9
+ task :default => [:test]
10
+
11
+ desc "Runs the unit tests"
12
+ task :test => "test:unit"
13
+
14
+ namespace :test do
15
+ task :setup do
16
+ RAILS_ROOT = File.expand_path("#{File.dirname(__FILE__)}/test") unless defined? RAILS_ROOT
17
+ ENV['RAILS_ENV'] = "test"
18
+ ENV["ACTS_AS_SOLR_TEST"] = "true"
19
+ require File.expand_path("#{File.dirname(__FILE__)}/config/solr_environment")
20
+ puts "Using " + DB
21
+ %x(mysql -u#{MYSQL_USER} < #{File.dirname(__FILE__) + "/test/fixtures/db_definitions/mysql.sql"}) if DB == 'mysql'
22
+
23
+ Rake::Task["test:migrate"].invoke
24
+ end
25
+
26
+ desc 'Measures test coverage using rcov'
27
+ task :rcov => :setup do
28
+ rm_f "coverage"
29
+ rm_f "coverage.data"
30
+ rcov = "rcov --rails --aggregate coverage.data --text-summary -Ilib"
31
+
32
+ system("#{rcov} --html #{Dir.glob('test/**/*_test.rb').join(' ')}")
33
+ system("open coverage/index.html") if PLATFORM['darwin']
34
+ end
35
+
36
+ desc 'Runs the functional tests, testing integration with Solr'
37
+ Rake::TestTask.new('functional' => :setup) do |t|
38
+ t.pattern = "test/functional/*_test.rb"
39
+ t.verbose = true
40
+ end
41
+
42
+ desc "Unit tests"
43
+ Rake::TestTask.new(:unit) do |t|
44
+ t.libs << 'test/unit'
45
+ t.pattern = "test/unit/*_shoulda.rb"
46
+ t.verbose = true
47
+ end
48
+ end
49
+
50
+ Rake::RDocTask.new do |rd|
51
+ rd.main = "README.rdoc"
52
+ rd.rdoc_dir = "rdoc"
53
+ rd.rdoc_files.exclude("lib/solr/**/*.rb", "lib/solr.rb")
54
+ rd.rdoc_files.include("README.rdoc", "lib/**/*.rb")
55
+ end
56
+
57
+ begin
58
+ require 'jeweler'
59
+ Jeweler::Tasks.new do |s|
60
+ s.name = "muck-solr"
61
+ s.summary = "This gem adds full text search capabilities and many other nifty features from Apache�s Solr to any Rails model. I'm currently rearranging the test suite to include a real unit test suite, and adding a few features I need myself."
62
+ s.email = "meyer@paperplanes.de"
63
+ s.homepage = "http://github.com/mattmatt/acts_as_solr"
64
+ s.description = "This gem adds full text search capabilities and many other nifty features from Apache�s Solr to any Rails model. I'm currently rearranging the test suite to include a real unit test suite, and adding a few features I need myself."
65
+ s.authors = ["Mathias Meyer, Joel Duffin, Justin Ball"]
66
+ s.rubyforge_project = 'muck-solr'
67
+ s.files = FileList["[A-Z]*", "{bin,generators,config,lib,solr}/**/*"] +
68
+ FileList["test/**/*"].reject {|f| f.include?("test/log")}.reject {|f| f.include?("test/tmp")}
69
+ end
70
+ rescue LoadError
71
+ puts "Jeweler, or one of its dependencies, is not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
72
+ end
73
+
74
+ # rubyforge tasks
75
+ begin
76
+ require 'rake/contrib/sshpublisher'
77
+ namespace :rubyforge do
78
+
79
+ desc "Release gem and RDoc documentation to RubyForge"
80
+ task :release => ["rubyforge:release:gem", "rubyforge:release:docs"]
81
+
82
+ namespace :release do
83
+ desc "Publish RDoc to RubyForge."
84
+ task :docs => [:rdoc] do
85
+ config = YAML.load(
86
+ File.read(File.expand_path('~/.rubyforge/user-config.yml'))
87
+ )
88
+
89
+ host = "#{config['username']}@rubyforge.org"
90
+ remote_dir = "/var/www/gforge-projects/muck-solr/"
91
+ local_dir = 'rdoc'
92
+
93
+ Rake::SshDirPublisher.new(host, remote_dir, local_dir).upload
94
+ end
95
+ end
96
+ end
97
+ rescue LoadError
98
+ puts "Rake SshDirPublisher is unavailable or your rubyforge environment is not configured."
99
+ end
@@ -0,0 +1,25 @@
1
+ acts_as_solr comes with a quick and fast unit test suite, and with a longer-running
2
+ functional test suite, the latter testing the actual integration with Solr.
3
+
4
+ The unit test suite is written using Shoulda, so make sure you have a recent version
5
+ installed.
6
+
7
+ Running `rake test` or just `rake` will run both test suites. Use `rake test:unit` to
8
+ just run the unit test suite.
9
+
10
+ == How to run functional tests for this plugin:
11
+ To run the acts_as_solr's plugin tests run the following steps:
12
+
13
+ - create a MySQL database called "actsassolr_test" (if you want to use MySQL)
14
+
15
+ - create a new Rails project, if needed (the plugin can only be tested from within a Rails project); move/checkout acts_as_solr into its vendor/plugins/, as usual
16
+
17
+ - copy vendor/plugins/acts_as_solr/config/solr.yml to config/ (the Rails config folder)
18
+
19
+ - rake solr:start RAILS_ENV=test
20
+
21
+ - rake test:functional (Accepts the following arguments: DB=sqlite|mysql and MYSQL_USER=user)
22
+
23
+ == Troubleshooting:
24
+ If for some reason the tests don't run and you get MySQL errors, make sure you edit the MYSQL_USER entry under
25
+ config/environment.rb. It's recommended to create or use a MySQL user with no password.
@@ -0,0 +1,4 @@
1
+ ---
2
+ :patch: 0
3
+ :major: 0
4
+ :minor: 4
@@ -0,0 +1,15 @@
1
+ # Config file for the acts_as_solr plugin.
2
+ #
3
+ # If you change the host or port number here, make sure you update
4
+ # them in your Solr config file
5
+
6
+ development:
7
+ url: http://127.0.0.1:8982/solr
8
+
9
+ production:
10
+ url: http://127.0.0.1:8983/solr
11
+ jvm_options: -server -d64 -Xmx1024M -Xms64M
12
+
13
+ test:
14
+ url: http://127.0.0.1:8981/solr
15
+
@@ -0,0 +1,32 @@
1
+ ENV['RAILS_ENV'] = (ENV['RAILS_ENV'] || 'development').dup
2
+ # RAILS_ROOT isn't defined yet, so figure it out.
3
+ require "uri"
4
+ require "fileutils"
5
+ dir = File.dirname(__FILE__)
6
+ SOLR_PATH = File.expand_path("#{dir}/../solr") unless defined? SOLR_PATH
7
+
8
+ RAILS_ROOT = File.expand_path("#{File.dirname(__FILE__)}/../test") unless defined? RAILS_ROOT
9
+ unless defined? SOLR_LOGS_PATH
10
+ SOLR_LOGS_PATH = ENV["SOLR_LOGS_PATH"] || "#{RAILS_ROOT}/log"
11
+ end
12
+ unless defined? SOLR_PIDS_PATH
13
+ SOLR_PIDS_PATH = ENV["SOLR_PIDS_PATH"] || "#{RAILS_ROOT}/tmp/pids"
14
+ end
15
+ unless defined? SOLR_DATA_PATH
16
+ SOLR_DATA_PATH = ENV["SOLR_DATA_PATH"] || "#{RAILS_ROOT}/solr/#{ENV['RAILS_ENV']}"
17
+ end
18
+
19
+ unless defined? SOLR_PORT
20
+ config = YAML::load_file(RAILS_ROOT+'/config/solr.yml')
21
+ raise("No solr environment defined for RAILS_ENV the #{ENV['RAILS_ENV'].inspect}") unless config[ENV['RAILS_ENV']]
22
+ SOLR_PORT = ENV['PORT'] || URI.parse(config[ENV['RAILS_ENV']]['url']).port
23
+ end
24
+
25
+ SOLR_JVM_OPTIONS = config[ENV['RAILS_ENV']]['jvm_options'] unless defined? SOLR_JVM_OPTIONS
26
+
27
+ if ENV["ACTS_AS_SOLR_TEST"]
28
+ require "activerecord"
29
+ DB = (ENV['DB'] ? ENV['DB'] : 'sqlite') unless defined?(DB)
30
+ MYSQL_USER = (ENV['MYSQL_USER'].nil? ? 'root' : ENV['MYSQL_USER']) unless defined? MYSQL_USER
31
+ require File.join(File.dirname(File.expand_path(__FILE__)), '..', 'test', 'db', 'connections', DB, 'connection.rb')
32
+ end
@@ -0,0 +1,65 @@
1
+ # Copyright (c) 2006 Erik Hatcher, Thiago Jackiw
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in all
11
+ # copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ # SOFTWARE.
20
+
21
+ require 'active_record'
22
+ require 'rexml/document'
23
+ require 'net/http'
24
+ require 'yaml'
25
+ require 'time'
26
+ require 'erb'
27
+ require 'rexml/xpath'
28
+
29
+ require File.dirname(__FILE__) + '/solr'
30
+ require File.dirname(__FILE__) + '/acts_as_solr/acts_methods'
31
+ require File.dirname(__FILE__) + '/acts_as_solr/common_methods'
32
+ require File.dirname(__FILE__) + '/acts_as_solr/parser_methods'
33
+ require File.dirname(__FILE__) + '/acts_as_solr/class_methods'
34
+ require File.dirname(__FILE__) + '/acts_as_solr/instance_methods'
35
+ require File.dirname(__FILE__) + '/acts_as_solr/common_methods'
36
+ require File.dirname(__FILE__) + '/acts_as_solr/deprecation'
37
+ require File.dirname(__FILE__) + '/acts_as_solr/search_results'
38
+ require File.dirname(__FILE__) + '/acts_as_solr/lazy_document'
39
+ module ActsAsSolr
40
+
41
+ class Post
42
+ def self.execute(request, core = nil)
43
+ begin
44
+ if File.exists?(RAILS_ROOT+'/config/solr.yml')
45
+ config = YAML::load_file(RAILS_ROOT+'/config/solr.yml')
46
+ url = config[ENV['RAILS_ENV']]['url']
47
+ # for backwards compatibility
48
+ url ||= "http://#{config[ENV['RAILS_ENV']]['host']}:#{config[ENV['RAILS_ENV']]['port']}/#{config[ENV['RAILS_ENV']]['servlet_path']}"
49
+ else
50
+ url = 'http://localhost:8982/solr'
51
+ end
52
+ url += "/" + core if !core.nil?
53
+ connection = Solr::Connection.new(url)
54
+ return connection.send(request)
55
+ rescue
56
+ raise "Couldn't connect to the Solr server at #{url}. #{$!}"
57
+ false
58
+ end
59
+ end
60
+ end
61
+
62
+ end
63
+
64
+ # reopen ActiveRecord and include the acts_as_solr method
65
+ ActiveRecord::Base.extend ActsAsSolr::ActsMethods
@@ -0,0 +1,352 @@
1
+ module ActsAsSolr #:nodoc:
2
+
3
+ module ActsMethods
4
+
5
+ # declares a class as solr-searchable
6
+ #
7
+ # ==== options:
8
+ # fields:: This option can be used to specify only the fields you'd
9
+ # like to index. If not given, all the attributes from the
10
+ # class will be indexed. You can also use this option to
11
+ # include methods that should be indexed as fields
12
+ #
13
+ # class Movie < ActiveRecord::Base
14
+ # acts_as_solr :fields => [:name, :description, :current_time]
15
+ # def current_time
16
+ # Time.now.to_s
17
+ # end
18
+ # end
19
+ #
20
+ # Each field passed can also be a hash with the value being a field type
21
+ #
22
+ # class Electronic < ActiveRecord::Base
23
+ # acts_as_solr :fields => [{:price => :range_float}]
24
+ # def current_time
25
+ # Time.now
26
+ # end
27
+ # end
28
+ #
29
+ # The field types accepted are:
30
+ #
31
+ # :float:: Index the field value as a float (ie.: 12.87)
32
+ # :integer:: Index the field value as an integer (ie.: 31)
33
+ # :boolean:: Index the field value as a boolean (ie.: true/false)
34
+ # :date:: Index the field value as a date (ie.: Wed Nov 15 23:13:03 PST 2006)
35
+ # :string:: Index the field value as a text string, not applying the same indexing
36
+ # filters as a regular text field
37
+ # :range_integer:: Index the field value for integer range queries (ie.:[5 TO 20])
38
+ # :range_float:: Index the field value for float range queries (ie.:[14.56 TO 19.99])
39
+ #
40
+ # Setting the field type preserves its original type when indexed
41
+ #
42
+ # The field may also be passed with a hash value containing options
43
+ #
44
+ # class Author < ActiveRecord::Base
45
+ # acts_as_solr :fields => [{:full_name => {:type => :text, :as => :name}}]
46
+ # def full_name
47
+ # self.first_name + ' ' + self.last_name
48
+ # end
49
+ # end
50
+ #
51
+ # The options accepted are:
52
+ #
53
+ # :type:: Index the field using the specified type
54
+ # :as:: Index the field using the specified field name
55
+ #
56
+ # additional_fields:: This option takes fields to be include in the index
57
+ # in addition to those derived from the database. You
58
+ # can also use this option to include custom fields
59
+ # derived from methods you define. This option will be
60
+ # ignored if the :fields option is given. It also accepts
61
+ # the same field types as the option above
62
+ #
63
+ # class Movie < ActiveRecord::Base
64
+ # acts_as_solr :additional_fields => [:current_time]
65
+ # def current_time
66
+ # Time.now.to_s
67
+ # end
68
+ # end
69
+ #
70
+ # exclude_fields:: This option taks an array of fields that should be ignored from indexing:
71
+ #
72
+ # class User < ActiveRecord::Base
73
+ # acts_as_solr :exclude_fields => [:password, :login, :credit_card_number]
74
+ # end
75
+ #
76
+ # include:: This option can be used for association indexing, which
77
+ # means you can include any :has_one, :has_many, :belongs_to
78
+ # and :has_and_belongs_to_many association to be indexed:
79
+ #
80
+ # class Category < ActiveRecord::Base
81
+ # has_many :books
82
+ # acts_as_solr :include => [:books]
83
+ # end
84
+ #
85
+ # Each association may also be specified as a hash with an option hash as a value
86
+ #
87
+ # class Book < ActiveRecord::Base
88
+ # belongs_to :author
89
+ # has_many :distribution_companies
90
+ # has_many :copyright_dates
91
+ # has_many :media_types
92
+ # acts_as_solr(
93
+ # :fields => [:name, :description],
94
+ # :include => [
95
+ # {:author => {:using => :fullname, :as => :name}},
96
+ # {:media_types => {:using => lambda{|media| type_lookup(media.id)}}}
97
+ # {:distribution_companies => {:as => :distributor, :multivalued => true}},
98
+ # {:copyright_dates => {:as => :copyright, :type => :date}}
99
+ # ]
100
+ # ]
101
+ #
102
+ # The options accepted are:
103
+ #
104
+ # :type:: Index the associated objects using the specified type
105
+ # :as:: Index the associated objects using the specified field name
106
+ # :using:: Index the associated objects using the value returned by the specified method or proc. If a method
107
+ # symbol is supplied, it will be sent to each object to look up the value to index; if a proc is
108
+ # supplied, it will be called once for each object with the object as the only argument
109
+ # :multivalued:: Index the associated objects using one field for each object rather than joining them
110
+ # all into a single field
111
+ #
112
+ # facets:: This option can be used to specify the fields you'd like to
113
+ # index as facet fields
114
+ #
115
+ # class Electronic < ActiveRecord::Base
116
+ # acts_as_solr :facets => [:category, :manufacturer]
117
+ # end
118
+ #
119
+ # boost:: You can pass a boost (float) value that will be used to boost the document and/or a field. To specify a more
120
+ # boost for the document, you can either pass a block or a symbol. The block will be called with the record
121
+ # as an argument, a symbol will result in the according method being called:
122
+ #
123
+ # class Electronic < ActiveRecord::Base
124
+ # acts_as_solr :fields => [{:price => {:boost => 5.0}}], :boost => 10.0
125
+ # end
126
+ #
127
+ # class Electronic < ActiveRecord::Base
128
+ # acts_as_solr :fields => [{:price => {:boost => 5.0}}], :boost => proc {|record| record.id + 120*37}
129
+ # end
130
+ #
131
+ # class Electronic < ActiveRecord::Base
132
+ # acts_as_solr :fields => [{:price => {:boost => :price_rating}}], :boost => 10.0
133
+ # end
134
+ #
135
+ # if:: Only indexes the record if the condition evaluated is true. The argument has to be
136
+ # either a symbol, string (to be eval'ed), proc/method, or class implementing a static
137
+ # validation method. It behaves the same way as ActiveRecord's :if option.
138
+ #
139
+ # class Electronic < ActiveRecord::Base
140
+ # acts_as_solr :if => proc{|record| record.is_active?}
141
+ # end
142
+ #
143
+ # offline:: Assumes that your using an outside mechanism to explicitly trigger indexing records, e.g. you only
144
+ # want to update your index through some asynchronous mechanism. Will accept either a boolean or a block
145
+ # that will be evaluated before actually contacting the index for saving or destroying a document. Defaults
146
+ # to false. It doesn't refer to the mechanism of an offline index in general, but just to get a centralized point
147
+ # where you can control indexing. Note: This is only enabled for saving records. acts_as_solr doesn't always like
148
+ # it, if you have a different number of results coming from the database and the index. This might be rectified in
149
+ # another patch to support lazy loading.
150
+ #
151
+ # class Electronic < ActiveRecord::Base
152
+ # acts_as_solr :offline => proc {|record| record.automatic_indexing_disabled?}
153
+ # end
154
+ #
155
+ # auto_commit:: The commit command will be sent to Solr only if its value is set to true:
156
+ #
157
+ # class Author < ActiveRecord::Base
158
+ # acts_as_solr :auto_commit => false
159
+ # end
160
+ #
161
+ def acts_as_solr(options={}, solr_options={}, &deferred_solr_configuration)
162
+
163
+ extend ClassMethods
164
+ include InstanceMethods
165
+ include CommonMethods
166
+ include ParserMethods
167
+
168
+ define_solr_configuration_methods
169
+
170
+ after_save :solr_save
171
+ after_destroy :solr_destroy
172
+
173
+ if deferred_solr_configuration
174
+ self.deferred_solr_configuration = deferred_solr_configuration
175
+ else
176
+ process_acts_as_solr(options, solr_options)
177
+ end
178
+ end
179
+
180
+ def process_acts_as_solr(options, solr_options)
181
+ process_solr_options(options, solr_options)
182
+ end
183
+
184
+ def define_solr_configuration_methods
185
+ # I'd like to use cattr_accessor, but it does not support lazy loaders and delegation to the class in the instance methods.
186
+ # TODO: Reconcile with cattr_accessor, or a more appropriate method.
187
+ class_eval(<<-EOS, __FILE__, __LINE__)
188
+ @@configuration = nil unless defined?(@@configuration)
189
+ @@solr_configuration = nil unless defined?(@@solr_configuration)
190
+ @@deferred_solr_configuration = nil unless defined?(@@deferred_solr_configuration)
191
+
192
+ def self.configuration
193
+ return @@configuration if @@configuration
194
+ process_deferred_solr_configuration
195
+ @@configuration
196
+ end
197
+ def configuration
198
+ self.class.configuration
199
+ end
200
+ def self.configuration=(value)
201
+ @@configuration = value
202
+ end
203
+ def configuration=(value)
204
+ self.class.configuration = value
205
+ end
206
+
207
+ def self.solr_configuration
208
+ return @@solr_configuration if @@solr_configuration
209
+ process_deferred_solr_configuration
210
+ @@solr_configuration
211
+ end
212
+ def solr_configuration
213
+ self.class.solr_configuration
214
+ end
215
+ def self.solr_configuration=(value)
216
+ @@solr_configuration = value
217
+ end
218
+ def solr_configuration=(value)
219
+ self.class.solr_configuration = value
220
+ end
221
+
222
+ def self.deferred_solr_configuration
223
+ return @@deferred_solr_configuration if @@deferred_solr_configuration
224
+ @@deferred_solr_configuration
225
+ end
226
+ def deferred_solr_configuration
227
+ self.class.deferred_solr_configuration
228
+ end
229
+ def self.deferred_solr_configuration=(value)
230
+ @@deferred_solr_configuration = value
231
+ end
232
+ def deferred_solr_configuration=(value)
233
+ self.class.deferred_solr_configuration = value
234
+ end
235
+ EOS
236
+ end
237
+
238
+ def process_deferred_solr_configuration
239
+ return unless deferred_solr_configuration
240
+ options, solr_options = deferred_solr_configuration.call
241
+ self.deferred_solr_configuration = nil
242
+ self.process_solr_options(options, solr_options)
243
+ end
244
+
245
+ def process_solr_options(options={}, solr_options={})
246
+ self.configuration = {
247
+ :fields => nil,
248
+ :additional_fields => nil,
249
+ :exclude_fields => [],
250
+ :auto_commit => true,
251
+ :include => nil,
252
+ :facets => nil,
253
+ :boost => nil,
254
+ :if => "true",
255
+ :offline => false
256
+ }
257
+ self.solr_configuration = {
258
+ :type_field => "type_s",
259
+ :primary_key_field => "pk_i",
260
+ :default_boost => 1.0
261
+ }
262
+
263
+ configuration.update(options) if options.is_a?(Hash)
264
+ solr_configuration.update(solr_options) if solr_options.is_a?(Hash)
265
+ Deprecation.validate_index(configuration)
266
+
267
+ configuration[:solr_fields] = {}
268
+ configuration[:solr_includes] = {}
269
+
270
+ if configuration[:fields].respond_to?(:each)
271
+ process_fields(configuration[:fields])
272
+ else
273
+ process_fields(self.new.attributes.keys.map { |k| k.to_sym })
274
+ process_fields(configuration[:additional_fields])
275
+ end
276
+
277
+ if configuration[:include].respond_to?(:each)
278
+ process_includes(configuration[:include])
279
+ end
280
+ end
281
+
282
+ private
283
+
284
+ def get_field_value(field)
285
+ field_name, options = determine_field_name_and_options(field)
286
+ configuration[:solr_fields][field_name] = options
287
+
288
+ define_method("#{field_name}_for_solr".to_sym) do
289
+ begin
290
+ value = self[field_name] || self.instance_variable_get("@#{field_name.to_s}".to_sym) || self.send(field_name.to_sym)
291
+ case options[:type]
292
+ # format dates properly; return nil for nil dates
293
+ when :date
294
+ value ? (value.respond_to?(:utc) ? value.utc : value).strftime("%Y-%m-%dT%H:%M:%SZ") : nil
295
+ else value
296
+ end
297
+ rescue
298
+ puts $!
299
+ logger.debug "There was a problem getting the value for the field '#{field_name}': #{$!}"
300
+ value = ''
301
+ end
302
+ end
303
+ end
304
+
305
+ def process_fields(raw_field)
306
+ if raw_field.respond_to?(:each)
307
+ raw_field.each do |field|
308
+ next if configuration[:exclude_fields].include?(field)
309
+ get_field_value(field)
310
+ end
311
+ end
312
+ end
313
+
314
+ def process_includes(includes)
315
+ if includes.respond_to?(:each)
316
+ includes.each do |assoc|
317
+ field_name, options = determine_field_name_and_options(assoc)
318
+ configuration[:solr_includes][field_name] = options
319
+ end
320
+ end
321
+ end
322
+
323
+ def determine_field_name_and_options(field)
324
+ if field.is_a?(Hash)
325
+ name = field.keys.first
326
+ options = field.values.first
327
+ if options.is_a?(Hash)
328
+ [name, {:type => type_for_field(field)}.merge(options)]
329
+ else
330
+ [name, {:type => options}]
331
+ end
332
+ else
333
+ [field, {:type => type_for_field(field)}]
334
+ end
335
+ end
336
+
337
+ def type_for_field(field)
338
+ if configuration[:facets] && configuration[:facets].include?(field)
339
+ :facet
340
+ elsif column = columns_hash[field.to_s]
341
+ case column.type
342
+ when :string then :text
343
+ when :datetime then :date
344
+ when :time then :date
345
+ else column.type
346
+ end
347
+ else
348
+ :text
349
+ end
350
+ end
351
+ end
352
+ end