sunspot 0.9.7

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 (101) hide show
  1. data/History.txt +83 -0
  2. data/LICENSE +18 -0
  3. data/README.rdoc +154 -0
  4. data/Rakefile +9 -0
  5. data/TODO +9 -0
  6. data/VERSION.yml +4 -0
  7. data/bin/sunspot-configure-solr +46 -0
  8. data/bin/sunspot-solr +62 -0
  9. data/lib/light_config.rb +40 -0
  10. data/lib/sunspot.rb +469 -0
  11. data/lib/sunspot/adapters.rb +265 -0
  12. data/lib/sunspot/composite_setup.rb +186 -0
  13. data/lib/sunspot/configuration.rb +38 -0
  14. data/lib/sunspot/data_extractor.rb +47 -0
  15. data/lib/sunspot/dsl.rb +3 -0
  16. data/lib/sunspot/dsl/field_query.rb +72 -0
  17. data/lib/sunspot/dsl/fields.rb +86 -0
  18. data/lib/sunspot/dsl/query.rb +59 -0
  19. data/lib/sunspot/dsl/query_facet.rb +31 -0
  20. data/lib/sunspot/dsl/restriction.rb +25 -0
  21. data/lib/sunspot/dsl/scope.rb +193 -0
  22. data/lib/sunspot/dsl/search.rb +30 -0
  23. data/lib/sunspot/facet.rb +16 -0
  24. data/lib/sunspot/facet_data.rb +120 -0
  25. data/lib/sunspot/facet_row.rb +10 -0
  26. data/lib/sunspot/field.rb +157 -0
  27. data/lib/sunspot/field_factory.rb +126 -0
  28. data/lib/sunspot/indexer.rb +123 -0
  29. data/lib/sunspot/instantiated_facet.rb +42 -0
  30. data/lib/sunspot/instantiated_facet_row.rb +22 -0
  31. data/lib/sunspot/query.rb +191 -0
  32. data/lib/sunspot/query/base_query.rb +90 -0
  33. data/lib/sunspot/query/connective.rb +126 -0
  34. data/lib/sunspot/query/dynamic_query.rb +69 -0
  35. data/lib/sunspot/query/field_facet.rb +151 -0
  36. data/lib/sunspot/query/field_query.rb +63 -0
  37. data/lib/sunspot/query/pagination.rb +39 -0
  38. data/lib/sunspot/query/query_facet.rb +73 -0
  39. data/lib/sunspot/query/query_facet_row.rb +19 -0
  40. data/lib/sunspot/query/query_field_facet.rb +13 -0
  41. data/lib/sunspot/query/restriction.rb +233 -0
  42. data/lib/sunspot/query/scope.rb +165 -0
  43. data/lib/sunspot/query/sort.rb +36 -0
  44. data/lib/sunspot/query/sort_composite.rb +33 -0
  45. data/lib/sunspot/schema.rb +165 -0
  46. data/lib/sunspot/search.rb +219 -0
  47. data/lib/sunspot/search/hit.rb +66 -0
  48. data/lib/sunspot/session.rb +201 -0
  49. data/lib/sunspot/setup.rb +271 -0
  50. data/lib/sunspot/type.rb +200 -0
  51. data/lib/sunspot/util.rb +164 -0
  52. data/solr/etc/jetty.xml +212 -0
  53. data/solr/etc/webdefault.xml +379 -0
  54. data/solr/lib/jetty-6.1.3.jar +0 -0
  55. data/solr/lib/jetty-util-6.1.3.jar +0 -0
  56. data/solr/lib/jsp-2.1/ant-1.6.5.jar +0 -0
  57. data/solr/lib/jsp-2.1/core-3.1.1.jar +0 -0
  58. data/solr/lib/jsp-2.1/jsp-2.1.jar +0 -0
  59. data/solr/lib/jsp-2.1/jsp-api-2.1.jar +0 -0
  60. data/solr/lib/servlet-api-2.5-6.1.3.jar +0 -0
  61. data/solr/solr/conf/elevate.xml +36 -0
  62. data/solr/solr/conf/protwords.txt +21 -0
  63. data/solr/solr/conf/schema.xml +50 -0
  64. data/solr/solr/conf/solrconfig.xml +696 -0
  65. data/solr/solr/conf/stopwords.txt +57 -0
  66. data/solr/solr/conf/synonyms.txt +31 -0
  67. data/solr/start.jar +0 -0
  68. data/solr/webapps/solr.war +0 -0
  69. data/spec/api/adapters_spec.rb +33 -0
  70. data/spec/api/build_search_spec.rb +1039 -0
  71. data/spec/api/indexer_spec.rb +311 -0
  72. data/spec/api/query_spec.rb +153 -0
  73. data/spec/api/search_retrieval_spec.rb +362 -0
  74. data/spec/api/session_spec.rb +157 -0
  75. data/spec/api/spec_helper.rb +1 -0
  76. data/spec/api/sunspot_spec.rb +18 -0
  77. data/spec/integration/dynamic_fields_spec.rb +55 -0
  78. data/spec/integration/faceting_spec.rb +169 -0
  79. data/spec/integration/keyword_search_spec.rb +83 -0
  80. data/spec/integration/scoped_search_spec.rb +289 -0
  81. data/spec/integration/spec_helper.rb +1 -0
  82. data/spec/integration/stored_fields_spec.rb +10 -0
  83. data/spec/integration/test_pagination.rb +32 -0
  84. data/spec/mocks/adapters.rb +32 -0
  85. data/spec/mocks/blog.rb +3 -0
  86. data/spec/mocks/comment.rb +19 -0
  87. data/spec/mocks/connection.rb +84 -0
  88. data/spec/mocks/mock_adapter.rb +30 -0
  89. data/spec/mocks/mock_record.rb +48 -0
  90. data/spec/mocks/photo.rb +8 -0
  91. data/spec/mocks/post.rb +73 -0
  92. data/spec/mocks/user.rb +8 -0
  93. data/spec/spec_helper.rb +47 -0
  94. data/tasks/gemspec.rake +25 -0
  95. data/tasks/rcov.rake +28 -0
  96. data/tasks/rdoc.rake +22 -0
  97. data/tasks/schema.rake +19 -0
  98. data/tasks/spec.rake +24 -0
  99. data/tasks/todo.rake +4 -0
  100. data/templates/schema.xml.haml +24 -0
  101. metadata +246 -0
data/History.txt ADDED
@@ -0,0 +1,83 @@
1
+ == 0.9.0 2009-07-21
2
+ * Use Dismax parser for keyword search
3
+ * Field and document boosting
4
+ * Specify which fields to search in keyword search
5
+ * Allow indexing of multiple values in text fields
6
+ * Access keyword relevance score in Hit objects
7
+ * Allow stored fields, retrieve stored values from Hit objects
8
+ * Support more values in shorthand restrictions
9
+ * Disjunctions and conjunctions
10
+ * Random ordering
11
+ * Control all options for field facets
12
+ * Time range facets
13
+ * Get referenced objects from facets on foreign keys
14
+ * Facet by class
15
+ * Batch indexing
16
+ * New Date field type
17
+ * Direct access to data accessors
18
+ * Executable to configure production Solr instances
19
+ * Replace solr-ruby with RSolr
20
+ * Remove accidental ActiveSupport dependency
21
+
22
+ == 0.8.9 2009-06-23
23
+ * Fix OrderedHash bug in older versions of ActiveSupport
24
+
25
+ == 0.8.8 2009-06-15
26
+ * Escape type names to support namespaced classes
27
+ * Fix bug with anonymous modules in Ruby 1.9
28
+
29
+ == 0.8.7 2009-06-10
30
+ * Add --pid-dir option for sunspot-solr executable
31
+
32
+ == 0.8.5 2009-06-09
33
+ * Added dependencies for sunspot-solr executable to gem dependencies
34
+ * Search for adapters using class ancestors rather than superclasses
35
+
36
+ == 0.8.3 2009-06-03
37
+ * Index objects passed as a collection in a single HTTP request
38
+
39
+ == 0.8.2 2009-05-27
40
+ * Allow specification of Solr home when using sunspot-solr
41
+
42
+ == 0.8.1 2009-05-26
43
+ * Add Search#execute! to public API
44
+
45
+ == 0.8.0 2009-05-22
46
+ * Access query API directly; instantiate search without running it
47
+ * Dynamic fields
48
+ * Search blocks can be evaluated in calling context
49
+
50
+ == 0.7.3 2009-05-06
51
+ * Better exception handling when class doesn't have adapter/setup
52
+
53
+ == 0.7.2 2009-04-29
54
+ * Dirty sessions
55
+
56
+ == 0.7.1 2009-04-29
57
+ * Removed extlib dependency from gemspec
58
+
59
+ == 0.7.0 2009-04-28
60
+ * Less magic in the DSL
61
+ * Restrict by empty values
62
+ * Negative scoping using without() method
63
+ * Exclusion by object identity using without(instance)
64
+ * Support for faceting
65
+ * Explicit commits
66
+ * Boolean field type
67
+ * Attribute field flexibility
68
+ * Virtual field blocks can be evaluated in calling context
69
+ * Order available by multiple fields
70
+ * New adapter API
71
+ * Got rid of builder API
72
+ * Full documentation
73
+
74
+ == 0.0.2 2009-02-14
75
+ * Run sunspot's built-in Solr instance using
76
+ sunspot-solr executable
77
+ * Search hash interpretation delegated to
78
+ Builder object
79
+
80
+ == 0.0.1 2008-12-11
81
+ * Initial release
82
+ * Define indexing for any class using DSL
83
+ * Search indexed classes using DSL
data/LICENSE ADDED
@@ -0,0 +1,18 @@
1
+ Permission is hereby granted, free of charge, to any person obtaining
2
+ a copy of this software and associated documentation files (the
3
+ 'Software'), to deal in the Software without restriction, including
4
+ without limitation the rights to use, copy, modify, merge, publish,
5
+ distribute, sublicense, and/or sell copies of the Software, and to
6
+ permit persons to whom the Software is furnished to do so, subject to
7
+ the following conditions:
8
+
9
+ The above copyright notice and this permission notice shall be
10
+ included in all copies or substantial portions of the Software.
11
+
12
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
13
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
14
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
15
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
16
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
17
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
18
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,154 @@
1
+ = Sunspot
2
+
3
+ http://outoftime.github.com/sunspot
4
+
5
+ Sunspot is a Ruby library for expressive, powerful interaction with the Solr search engine.
6
+ Sunspot is built on top of the RSolr gem, which provides a low-level interface for Solr
7
+ interaction; Sunspot provides a simple, intuitive, expressive DSL backed by powerful
8
+ features for indexing objects and searching for them.
9
+
10
+ Sunspot is designed to be easily plugged in to any ORM, or even non-database-backed
11
+ objects such as the filesystem.
12
+
13
+ === Features:
14
+
15
+ * Define indexing strategy for each searchable class using intuitive block-based API
16
+ * Clean separation between keyword-searchable fields and fields for scoping/ordering
17
+ * Define fields based on existing attributes or "virtual fields" for custom indexing
18
+ * Indexes each object's entire superclass hierarchy, for easy searching for all objects inheriting from a parent class
19
+ * Intuitive DSL for scoping searches, with all the usual boolean operators available
20
+ * Intuitive interface for requesting facets on indexed fields
21
+ * Extensible adapter architecture for easy integration of other ORMs or non-model classes
22
+ * Refine search using field facets, date range facets, or ultra-powerful query facets
23
+ * Full compatibility with will_paginate
24
+ * Ordering
25
+
26
+ == Installation
27
+
28
+ gem sources -a http://gems.github.com
29
+ gem install outoftime-sunspot
30
+
31
+ In order to start the packaged Solr installation, run:
32
+
33
+ sunspot-solr start -- [-d /path/to/data/directory] [-p port] [-s path/to/solr/home] [--pid-dir=path/to/pid/dir]
34
+
35
+ If you don't specify a data directory, your Solr index will be stored in your operating system's temporary directory.
36
+
37
+ If you specify a solr home, the directory must contain a <code>conf</code>
38
+ directory, which should contain at least <code>schema.xml</code> and
39
+ <code>solrconfig.xml</code>. Be sure to copy the <code>schema.xml</code> out of
40
+ the Sunspot gem's <code>solr/solr/conf</code> directory. Sunspot relies on the
41
+ field name patterns defined in the packaged <code>schema.xml</code>, so those
42
+ cannot be modified.
43
+
44
+ You can also run your own instance of Solr wherever you'd like; just copy the solr/config/schema.xml file out of the gem's solr into your installation.
45
+ You can change the URL at which Sunspot accesses Solr with:
46
+
47
+ Sunspot.config.solr.url = 'http://solr.my.host:9818/solr'
48
+
49
+ == Rails Integration
50
+
51
+ The {Sunspot::Rails}[http://github.com/outoftime/sunspot_rails] plugin makes
52
+ integrating Sunspot into Rails drop-in easy.
53
+
54
+ == Using Sunspot
55
+
56
+ === Define an index:
57
+
58
+ class Post
59
+ #...
60
+ end
61
+
62
+ Sunspot.setup(Post) do
63
+ text :title, :body
64
+ string :author_name
65
+ integer :blog_id
66
+ integer :category_ids
67
+ float :average_rating, :using => :ratings_average
68
+ time :published_at
69
+ string :sort_title do
70
+ title.downcase.sub(/^(an?|the)\W+/, ''/) if title = self.title
71
+ end
72
+ end
73
+
74
+ See Sunspot.setup for more information.
75
+
76
+ Note that in order for a class to be searchable, it must have an adapter
77
+ registered for itself or one of its subclasses. Adapters allow Sunspot to load
78
+ objects out of persistent storage, and to determine their primary key for
79
+ indexing. {Sunspot::Rails}[http://github.com/outoftime/sunspot_rails] comes with
80
+ an adapter for ActiveRecord objects, but for other types of models you will need
81
+ to define your own. See Sunspot::Adapters for more information.
82
+
83
+ === Search for objects:
84
+
85
+ search = Sunspot.search Post do
86
+ keywords 'great pizza'
87
+ with :author_name, 'Mark Twain'
88
+ with(:blog_id).any_of [2, 14]
89
+ with(:category_ids).all_of [4, 10]
90
+ with(:published_at).less_than Time.now
91
+ any_of do
92
+ with(:expired_at).greater_than(Time.now)
93
+ with(:expired_at, nil)
94
+ end
95
+ without :title, 'Bad Title'
96
+ without bad_instance # specifically exclude this instance from results
97
+
98
+ paginate :page => 3, :per_page => 15
99
+ order_by :average_rating, :desc
100
+
101
+ facet :blog_id
102
+ end
103
+
104
+ See Sunspot.search for more information.
105
+
106
+ === Get data from search:
107
+
108
+ search.results
109
+ search.total
110
+ search.page
111
+ search.per_page
112
+ search.facet(:blog_id)
113
+
114
+ == About the API documentation
115
+
116
+ All of the methods documented in the RDoc are considered part of Sunspot's
117
+ public API. Methods that are not part of the public API are documented in the
118
+ code, but excluded from the RDoc. If you find yourself needing to access methods
119
+ that are not part of the public API in order to do what you need, please contact
120
+ me so I can rectify the situation!
121
+
122
+ == Dependencies
123
+
124
+ 1. RSolr
125
+ 2. Daemons
126
+ 3. OptiFlag
127
+ 4. Haml
128
+ 5. Java
129
+
130
+ Sunspot has been tested with MRI 1.8.6 and 1.8.7, REE 1.8.6, YARV 1.9.1, and
131
+ JRuby 1.2.0
132
+
133
+ == Bugs
134
+
135
+ Please submit bug reports to
136
+ http://outoftime.lighthouseapp.com/projects/20339-sunspot
137
+
138
+ == Further Reading
139
+
140
+ * Sunspot Discussion: http://groups.google.com/group/ruby-sunspot
141
+ * IRC: #sunspot-ruby @ Freenode
142
+ * Posts about Sunspot from my tumblog: http://outofti.me/tagged/sunspot
143
+ * Read about it on Linux Magazine: http://www.linux-mag.com/id/7341
144
+
145
+ == Contributors
146
+
147
+ * Mat Brown (mat@patch.com)
148
+ * Peer Allan (peer.allan@gmail.com)
149
+ * Dmitriy Dzema (dima@dzema.name)
150
+ * Benjamin Krause (bk@benjaminkrause.com)
151
+
152
+ == License
153
+
154
+ Sunspot is distributed under the MIT License, copyright (c) 2008-2009 Mat Brown
data/Rakefile ADDED
@@ -0,0 +1,9 @@
1
+ ENV['RUBYOPT'] = '-W1'
2
+
3
+ task :environment do
4
+ require File.dirname(__FILE__) + '/lib/sunspot'
5
+ end
6
+
7
+ Dir['tasks/**/*.rake'].each { |t| load t }
8
+
9
+ task :default => 'spec:api'
data/TODO ADDED
@@ -0,0 +1,9 @@
1
+ === 0.9.X ===
2
+ * Deal with empty facet queries
3
+ * Passing an integer into the second argument of dynamic_facet() when multiple facets are requested gives the wrong value
4
+ === 0.10 ===
5
+ * Highlighting
6
+ * LocalSolr
7
+ * Text field restrictions
8
+ * Prefixes
9
+ * Intelligently decide whether to instantiate all facet rows at once
data/VERSION.yml ADDED
@@ -0,0 +1,4 @@
1
+ ---
2
+ :minor: 9
3
+ :patch: 7
4
+ :major: 0
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ using_gems = false
4
+ begin
5
+ require 'fileutils'
6
+ require 'optiflag'
7
+ require File.join(File.dirname(__FILE__), '..', 'lib', 'sunspot', 'schema')
8
+ rescue LoadError => e
9
+ if using_gems
10
+ raise(e)
11
+ else
12
+ using_gems = true
13
+ require 'rubygems'
14
+ retry
15
+ end
16
+ end
17
+
18
+ module ConfigureSolrFlags extend OptiFlagSet
19
+ optional_flag 'tokenizer'
20
+ optional_flag 'extra_filters'
21
+ optional_flag 'dir'
22
+ and_process!
23
+ end
24
+
25
+ solr_directory = ARGV.flags.dir || FileUtils.pwd
26
+ conf_directory = File.join(solr_directory, 'conf')
27
+ schema_file = File.join(conf_directory, 'schema.xml')
28
+ FileUtils.mkdir_p(conf_directory)
29
+
30
+ schema = Sunspot::Schema.new
31
+ schema.tokenizer = ARGV.flags.tokenizer if ARGV.flags.tokenizer
32
+ if ARGV.flags.extra_filters
33
+ for filter in ARGV.flags.extra_filters.split(',')
34
+ schema.add_filter(filter)
35
+ end
36
+ end
37
+
38
+ if File.exist?(schema_file)
39
+ backup_file = File.join(conf_directory, "schema-#{File.mtime(schema_file).strftime('%Y%m%d%H%M%S')}.xml")
40
+ STDERR.puts("Backing up current schema file to #{File.expand_path(backup_file)}")
41
+ FileUtils.mv(schema_file, backup_file)
42
+ end
43
+
44
+ File.open(File.join(conf_directory, 'schema.xml'), 'w') do |file|
45
+ file << schema.to_xml
46
+ end
data/bin/sunspot-solr ADDED
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env ruby
2
+ using_gems = false
3
+ begin
4
+ require 'fileutils'
5
+ require 'tmpdir'
6
+ require 'daemons'
7
+ require 'optiflag'
8
+ rescue LoadError => e
9
+ if using_gems
10
+ raise(e)
11
+ else
12
+ using_gems = true
13
+ require 'rubygems'
14
+ retry
15
+ end
16
+ end
17
+
18
+ working_directory = FileUtils.pwd
19
+ solr_home = File.join(File.dirname(__FILE__), '..', 'solr')
20
+
21
+ module SolrFlags extend OptiFlagSet
22
+ optional_flag 'p' do
23
+ description 'Port on which to run Solr (default 8983)'
24
+ long_form 'port'
25
+ end
26
+
27
+ optional_flag 'd' do
28
+ description 'Solr data directory'
29
+ end
30
+
31
+ optional_flag 's' do
32
+ description 'Solr home (should contain conf/ directory)'
33
+ end
34
+
35
+ optional_flag 'pd' do
36
+ long_form 'pid-dir'
37
+ description 'Directory for pid files'
38
+ end
39
+
40
+ and_process!
41
+ end
42
+
43
+ port = ARGV.flags.p || '8983'
44
+ data_dir = File.expand_path(ARGV.flags.d || File.join(Dir.tmpdir, 'solr_data'))
45
+ home = File.expand_path(ARGV.flags.s) if ARGV.flags.s
46
+ pid_dir = File.expand_path(ARGV.flags.pd || working_directory)
47
+
48
+ options = { :dir_mode => :normal, :dir => pid_dir }
49
+
50
+ Daemons.run_proc('sunspot-solr', options) do
51
+ FileUtils.cd(working_directory) do
52
+ FileUtils.cd(solr_home) do
53
+ args = ['java']
54
+ args << "-Djetty.port=#{port}" if port
55
+ args << "-Dsolr.data.dir=#{data_dir}" if data_dir
56
+ args << "-Dsolr.solr.home=#{home}" if home
57
+ args << '-jar' << 'start.jar'
58
+ STDERR.puts(args * ' ')
59
+ Kernel.exec(*args)
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,40 @@
1
+ module LightConfig
2
+ class Configuration
3
+ def initialize(&block)
4
+ @properties = {}
5
+ ::LightConfig::Builder.new(self).instance_eval(&block)
6
+ singleton = (class <<self; self; end)
7
+ @properties.keys.each do |property|
8
+ singleton.module_eval do
9
+ define_method property do
10
+ @properties[property]
11
+ end
12
+
13
+ define_method "#{property}=" do |value|
14
+ @properties[property] = value
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+
21
+ class Builder
22
+ def initialize(configuration)
23
+ @configuration = configuration
24
+ end
25
+
26
+ def method_missing(method, *args, &block)
27
+ raise ArgumentError("wrong number of arguments(#{args.length} for 1)") unless args.length < 2
28
+ value = if block then ::LightConfig::Configuration.new(&block)
29
+ else args.first
30
+ end
31
+ @configuration.instance_variable_get(:@properties)[method] = value
32
+ end
33
+ end
34
+
35
+ class <<self
36
+ def build(&block)
37
+ LightConfig::Configuration.new(&block)
38
+ end
39
+ end
40
+ end
data/lib/sunspot.rb ADDED
@@ -0,0 +1,469 @@
1
+ begin
2
+ require 'time'
3
+ require 'date'
4
+ require 'rsolr'
5
+ rescue LoadError
6
+ require 'rubygems'
7
+ require 'rsolr'
8
+ end
9
+
10
+ require File.join(File.dirname(__FILE__), 'light_config')
11
+
12
+ %w(util adapters configuration setup composite_setup field field_factory
13
+ data_extractor indexer query search facet facet_row instantiated_facet
14
+ instantiated_facet_row facet_data session type dsl).each do |filename|
15
+ require File.join(File.dirname(__FILE__), 'sunspot', filename)
16
+ end
17
+
18
+ #
19
+ # The Sunspot module provides class-method entry points to most of the
20
+ # functionality provided by the Sunspot library. Internally, the Sunspot
21
+ # singleton class contains a (non-thread-safe!) instance of Sunspot::Session,
22
+ # to which it delegates most of the class methods it exposes. In the method
23
+ # documentation below, this instance is referred to as the "singleton session".
24
+ #
25
+ # Though the singleton session provides a convenient entry point to Sunspot,
26
+ # it is by no means required to use the Sunspot class methods. Multiple sessions
27
+ # may be instantiated and used (if you need to connect to multiple Solr
28
+ # instances, for example.)
29
+ #
30
+ # Note that the configuration of classes for index/search (the +setup+
31
+ # method) is _not_ session-specific, but rather global.
32
+ #
33
+ module Sunspot
34
+ UnrecognizedFieldError = Class.new(Exception)
35
+ UnrecognizedRestrictionError = Class.new(Exception)
36
+ NoAdapterError = Class.new(Exception)
37
+ NoSetupError = Class.new(Exception)
38
+
39
+ class <<self
40
+ # Configures indexing and search for a given class.
41
+ #
42
+ # ==== Parameters
43
+ #
44
+ # clazz<Class>:: class to configure
45
+ #
46
+ # ==== Example
47
+ #
48
+ # Sunspot.setup(Post) do
49
+ # text :title, :body
50
+ # string :author_name
51
+ # integer :blog_id
52
+ # integer :category_ids
53
+ # float :average_rating, :using => :ratings_average
54
+ # time :published_at
55
+ # string :sort_title do
56
+ # title.downcase.sub(/^(an?|the)\W+/, ''/) if title = self.title
57
+ # end
58
+ # end
59
+ #
60
+ # ====== Attribute Fields vs. Virtual Fields
61
+ #
62
+ # Attribute fields call a method on the indexed object and index the
63
+ # return value. All of the fields defined above except for the last one are
64
+ # attribute fields. By default, the field name will also be the attribute
65
+ # used; this can be overriden with the +:using+ option, as in
66
+ # +:average_rating+ above. In that case, the attribute +:ratings_average+
67
+ # will be indexed with the field name +:average_rating+.
68
+ #
69
+ # +:sort_title+ is a virtual field, which evaluates the block inside the
70
+ # context of the instance being indexed, and indexes the value returned
71
+ # by the block. If the block you pass takes an argument, it will be passed
72
+ # the instance rather than being evaluated inside of it; so, the following
73
+ # example is equivalent to the one above (assuming #title is public):
74
+ #
75
+ # Sunspot.setup(Post) do
76
+ # string :sort_title do |post|
77
+ # post.title.downcase.sub(/^(an?|the)\W+/, ''/) if title = self.title
78
+ # end
79
+ # end
80
+ #
81
+ # ===== Field Types
82
+ #
83
+ # The available types are:
84
+ #
85
+ # * +text+
86
+ # * +string+
87
+ # * +integer+
88
+ # * +float+
89
+ # * +time+
90
+ # * +boolean+
91
+ #
92
+ # Note that the +text+ type behaves quite differently from the others -
93
+ # this is the type that is indexed as fulltext, and is searched using the
94
+ # +keywords+ method inside the search DSL. Text fields cannot have
95
+ # restrictions set on them, nor can they be used in order statements or
96
+ # for facets. All other types are indexed literally, and thus can be used
97
+ # for all of those operations. They will not, however, be searched in
98
+ # fulltext. In this way, Sunspot provides a complete barrier between
99
+ # fulltext fields and value fields.
100
+ #
101
+ # It is fine to specify a field both as a text field and a string field;
102
+ # internally, the fields will have different names so there is no danger
103
+ # of conflict.
104
+ #
105
+ # ===== Dynamic Fields
106
+ #
107
+ # For use cases which have highly dynamic data models (for instance, an
108
+ # open set of key-value pairs attached to a model), it may be useful to
109
+ # defer definition of fields until indexing time. Sunspot exposes dynamic
110
+ # fields, which define a data accessor (either attribute or virtual, see
111
+ # above), which accepts a hash of field names to values. Note that the field
112
+ # names in the hash are internally scoped to the base name of the dynamic
113
+ # field, so any time they are referred to, they are referred to using both
114
+ # the base name and the dynamic (runtime-specified) name.
115
+ #
116
+ # Dynamic fields are speficied in the setup block using the type name
117
+ # prefixed by +dynamic_+. For example:
118
+ #
119
+ # Sunspot.setup(Post) do
120
+ # dynamic_string :custom_values do
121
+ # key_value_pairs.inject({}) do |hash, key_value_pair|
122
+ # hash[key_value_pair.key.to_sym] = key_value_pair.value
123
+ # end
124
+ # end
125
+ # end
126
+ #
127
+ # If you later wanted to facet all of the values for the key "cuisine",
128
+ # you could issue:
129
+ #
130
+ # Sunspot.search(Post) do
131
+ # dynamic :custom_values do
132
+ # facet :cuisine
133
+ # end
134
+ # end
135
+ #
136
+ # In the documentation, +:custom_values+ is referred to as the "base name" -
137
+ # that is, the one specified statically - and +:cuisine+ is referred to as
138
+ # the dynamic name, which is the part that is specified at indexing time.
139
+ #
140
+ def setup(clazz, &block)
141
+ Setup.setup(clazz, &block)
142
+ end
143
+
144
+ # Indexes objects on the singleton session.
145
+ #
146
+ # ==== Parameters
147
+ #
148
+ # objects...<Object>:: objects to index (may pass an array or varargs)
149
+ #
150
+ # ==== Example
151
+ #
152
+ # post1, post2 = Array(2) { Post.create }
153
+ # Sunspot.index(post1, post2)
154
+ #
155
+ # Note that indexed objects won't be reflected in search until a commit is
156
+ # sent - see Sunspot.index! and Sunspot.commit
157
+ #
158
+ def index(*objects)
159
+ session.index(*objects)
160
+ end
161
+
162
+ # Indexes objects on the singleton session and commits immediately.
163
+ #
164
+ # See: Sunspot.index and Sunspot.commit
165
+ #
166
+ # ==== Parameters
167
+ #
168
+ # objects...<Object>:: objects to index (may pass an array or varargs)
169
+ #
170
+ def index!(*objects)
171
+ session.index!(*objects)
172
+ end
173
+
174
+ # Commits the singleton session
175
+ #
176
+ # When documents are added to or removed from Solr, the changes are
177
+ # initially stored in memory, and are not reflected in Solr's existing
178
+ # searcher instance. When a commit message is sent, the changes are written
179
+ # to disk, and a new searcher is spawned. Commits are thus fairly
180
+ # expensive, so if your application needs to index several documents as part
181
+ # of a single operation, it is advisable to index them all and then call
182
+ # commit at the end of the operation.
183
+ #
184
+ # Note that Solr can also be configured to automatically perform a commit
185
+ # after either a specified interval after the last change, or after a
186
+ # specified number of documents are added. See
187
+ # http://wiki.apache.org/solr/SolrConfigXml
188
+ #
189
+ def commit
190
+ session.commit
191
+ end
192
+
193
+ #
194
+ # Create a new Search instance, but do not execute it immediately. Generally
195
+ # you will want to use the #search method to execute searches using the
196
+ # DSL; however, if you are building searches dynamically (using the Builder
197
+ # pattern, for instance), it may be easier to access the Query API directly.
198
+ #
199
+ # ==== Parameters
200
+ #
201
+ # types<Class>...::
202
+ # Zero, one, or more types to search for. If no types are passed, all
203
+ # configured types will be searched for.
204
+ #
205
+ # ==== Returns
206
+ #
207
+ # Sunspot::Search::
208
+ # Search object, not yet executed. Query parameters can be added manually;
209
+ # then #execute! should be called.
210
+ #
211
+ def new_search(*types)
212
+ session.new_search(*types)
213
+ end
214
+
215
+
216
+ # Search for objects in the index.
217
+ #
218
+ # ==== Parameters
219
+ #
220
+ # types<Class>...::
221
+ # Zero, one, or more types to search for. If no types are passed, all
222
+ # configured types will be searched.
223
+ #
224
+ # ==== Options (last argument, optional)
225
+ #
226
+ # :keywords<String>:: Fulltext search string
227
+ # :conditions<Hash>::
228
+ # Hash of key-value pairs to be used as restrictions. Keys are field
229
+ # names. Scalar values are used as equality restrictions; arrays are used
230
+ # as "any of" restrictions; and Ranges are used as range restrictions.
231
+ # :order<String>:: order field and direction (e.g., 'updated_at desc')
232
+ # :page<Integer>:: Page to start on for pagination
233
+ # :per_page<Integer>::
234
+ # Number of results to use per page. Ignored if :page is not specified.
235
+ #
236
+ # ==== Returns
237
+ #
238
+ # Sunspot::Search:: Object containing results, facets, count, etc.
239
+ #
240
+ # The fields available for restriction, ordering, etc. are those that meet
241
+ # the following criteria:
242
+ #
243
+ # * They are not of type +text+.
244
+ # * They are defined for all of the classes being searched
245
+ # * They have the same data type for all of the classes being searched
246
+ # * They have the same multiple flag for all of the classes being searched.
247
+ #
248
+ # The restrictions available are the constants defined in the
249
+ # Sunspot::Restriction class. The standard restrictions are:
250
+ #
251
+ # with(:field_name).equal_to(value)
252
+ # with(:field_name, value) # shorthand for above
253
+ # with(:field_name).less_than(value)
254
+ # with(:field_name).greater_than(value)
255
+ # with(:field_name).between(value1..value2)
256
+ # with(:field_name).any_of([value1, value2, value3])
257
+ # with(:field_name).all_of([value1, value2, value3])
258
+ # without(some_instance) # exclude that particular instance
259
+ #
260
+ # +without+ can be substituted for +with+, causing the restriction to be
261
+ # negated. In the last example above, only +without+ works, as it does not
262
+ # make sense to search only for an instance you already have.
263
+ #
264
+ # Equality restrictions can take +nil+ as a value, which restricts the
265
+ # results to documents that have no value for the given field. Passing +nil+
266
+ # as a value to other restriction types is illegal. Thus:
267
+ #
268
+ # with(:field_name, nil) # ok
269
+ # with(:field_name).equal_to(nil) # ok
270
+ # with(:field_name).less_than(nil) # bad
271
+ #
272
+ # ==== Example
273
+ #
274
+ # Sunspot.search(Post) do
275
+ # keywords 'great pizza'
276
+ # with(:published_at).less_than Time.now
277
+ # with :blog_id, 1
278
+ # without current_post
279
+ # facet :category_ids
280
+ # order_by :published_at, :desc
281
+ # paginate 2, 15
282
+ # end
283
+ #
284
+ # If the block passed to #search takes an argument, that argument will
285
+ # present the DSL, and the block will be evaluated in the calling context.
286
+ # This will come in handy for building searches using instance data or
287
+ # methods, e.g.:
288
+ #
289
+ # Sunspot.search(Post) do |query|
290
+ # query.with(:blog_id, @current_blog.id)
291
+ # end
292
+ #
293
+ # See Sunspot::DSL::Search, Sunspot::DSL::Scope, Sunspot::DSL::FieldQuery
294
+ # and Sunspot::DSL::Query for the full API presented inside the block.
295
+ #
296
+ def search(*types, &block)
297
+ session.search(*types, &block)
298
+ end
299
+
300
+ # Remove objects from the index. Any time an object is destroyed, it must
301
+ # be removed from the index; otherwise, the index will contain broken
302
+ # references to objects that do not exist, which will cause errors when
303
+ # those objects are matched in search results.
304
+ #
305
+ # ==== Parameters
306
+ #
307
+ # objects...<Object>::
308
+ # Objects to remove from the index (may pass an array or varargs)
309
+ #
310
+ # ==== Example
311
+ #
312
+ # post.destroy
313
+ # Sunspot.remove(post)
314
+ #
315
+ def remove(*objects)
316
+ session.remove(*objects)
317
+ end
318
+
319
+ #
320
+ # Remove objects from the index and immediately commit. See Sunspot.remove
321
+ #
322
+ # ==== Parameters
323
+ #
324
+ # objects...<Object>:: Objects to remove from the index
325
+ #
326
+ def remove!
327
+ session.remove!(*objects)
328
+ end
329
+
330
+ #
331
+ # Remove an object from the index using its class name and primary key.
332
+ # Useful if you know this information and want to remove an object without
333
+ # instantiating it from persistent storage
334
+ #
335
+ # ==== Parameters
336
+ #
337
+ # clazz<Class>:: Class of the object, or class name as a string or symbol
338
+ # id::
339
+ # Primary key of the object. This should be the same id that would be
340
+ # returned by the class's instance adapter.
341
+ #
342
+ def remove_by_id(clazz, id)
343
+ session.remove_by_id(clazz, id)
344
+ end
345
+
346
+ #
347
+ # Remove an object by class name and primary key, and immediately commit.
348
+ # See #remove_by_id and #commit
349
+ #
350
+ def remove_by_id!(clazz, id)
351
+ session.remove_by_id!(clazz, id)
352
+ end
353
+
354
+ # Remove all objects of the given classes from the index. There isn't much
355
+ # use for this in general operations but it can be useful for maintenance,
356
+ # testing, etc. If no arguments are passed, remove everything from the
357
+ # index.
358
+ #
359
+ # ==== Parameters
360
+ #
361
+ # classes...<Class>::
362
+ # classes for which to remove all instances from the index (may pass an
363
+ # array or varargs)
364
+ #
365
+ # ==== Example
366
+ #
367
+ # Sunspot.remove_all(Post, Blog)
368
+ #
369
+ def remove_all(*classes)
370
+ session.remove_all(*classes)
371
+ end
372
+
373
+ #
374
+ # Remove all objects of the given classes from the index and immediately
375
+ # commit. See Sunspot.remove_all
376
+ #
377
+ # ==== Parameters
378
+ #
379
+ # classes...<Class>::
380
+ # classes for which to remove all instances from the index
381
+ def remove_all!(*classes)
382
+ session.remove_all!(*classes)
383
+ end
384
+
385
+ #
386
+ # Process all adds in a batch. Any Sunspot adds initiated inside the block
387
+ # will be sent in bulk when the block finishes. Useful if your application
388
+ # initiates index adds from various places in code as part of a single
389
+ # operation; doing a batch add will give better performance.
390
+ #
391
+ # ==== Example
392
+ #
393
+ # Sunspot.batch do
394
+ # post = Post.new
395
+ # Sunspot.add(post)
396
+ # comment = Comment.new
397
+ # Sunspot.add(comment)
398
+ # end
399
+ #
400
+ # Sunspot will send both the post and the comment in a single request.
401
+ #
402
+ def batch(&block)
403
+ session.batch(&block)
404
+ end
405
+
406
+ #
407
+ # True if documents have been added, updated, or removed since the last
408
+ # commit.
409
+ #
410
+ # ==== Returns
411
+ #
412
+ # Boolean:: Whether there have been any updates since the last commit
413
+ #
414
+ def dirty?
415
+ session.dirty?
416
+ end
417
+
418
+ #
419
+ # Sends a commit if the session is dirty (see #dirty?).
420
+ #
421
+ def commit_if_dirty
422
+ session.commit_if_dirty
423
+ end
424
+
425
+ # Returns the configuration associated with the singleton session. See
426
+ # Sunspot::Configuration for details.
427
+ #
428
+ # ==== Returns
429
+ #
430
+ # LightConfig::Configuration:: configuration for singleton session
431
+ #
432
+ def config
433
+ session.config
434
+ end
435
+
436
+ #
437
+ # Resets the singleton session. This is useful for clearing out all
438
+ # static data between tests, but probably nowhere else.
439
+ #
440
+ # ==== Parameters
441
+ #
442
+ # keep_config<Boolean>::
443
+ # Whether to retain the configuration used by the current singleton
444
+ # session. Default false.
445
+ #
446
+ def reset!(keep_config = false)
447
+ config =
448
+ if keep_config
449
+ session.config
450
+ else
451
+ Configuration.build
452
+ end
453
+ @session = Session.new(config)
454
+ end
455
+
456
+ private
457
+
458
+ #
459
+ # Get the singleton session, creating it if none yet exists.
460
+ #
461
+ # ==== Returns
462
+ #
463
+ # Sunspot::Session:: the singleton session
464
+ #
465
+ def session #:nodoc:
466
+ @session ||= Session.new
467
+ end
468
+ end
469
+ end