UnderpantsGnome-sunspot 0.9.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/History.txt +39 -0
- data/LICENSE +18 -0
- data/README.rdoc +154 -0
- data/Rakefile +9 -0
- data/TODO +4 -0
- data/VERSION.yml +4 -0
- data/bin/sunspot-configure-solr +46 -0
- data/bin/sunspot-solr +62 -0
- data/lib/light_config.rb +40 -0
- data/lib/sunspot.rb +470 -0
- data/lib/sunspot/adapters.rb +265 -0
- data/lib/sunspot/composite_setup.rb +186 -0
- data/lib/sunspot/configuration.rb +38 -0
- data/lib/sunspot/data_extractor.rb +47 -0
- data/lib/sunspot/date_facet.rb +36 -0
- data/lib/sunspot/date_facet_row.rb +17 -0
- data/lib/sunspot/dsl.rb +3 -0
- data/lib/sunspot/dsl/field_query.rb +72 -0
- data/lib/sunspot/dsl/fields.rb +86 -0
- data/lib/sunspot/dsl/query.rb +59 -0
- data/lib/sunspot/dsl/query_facet.rb +31 -0
- data/lib/sunspot/dsl/restriction.rb +25 -0
- data/lib/sunspot/dsl/scope.rb +193 -0
- data/lib/sunspot/dsl/search.rb +30 -0
- data/lib/sunspot/facet.rb +51 -0
- data/lib/sunspot/facet_row.rb +34 -0
- data/lib/sunspot/field.rb +157 -0
- data/lib/sunspot/field_factory.rb +126 -0
- data/lib/sunspot/indexer.rb +127 -0
- data/lib/sunspot/instantiated_facet.rb +38 -0
- data/lib/sunspot/instantiated_facet_row.rb +12 -0
- data/lib/sunspot/query.rb +190 -0
- data/lib/sunspot/query/base_query.rb +90 -0
- data/lib/sunspot/query/connective.rb +77 -0
- data/lib/sunspot/query/dynamic_query.rb +69 -0
- data/lib/sunspot/query/field_facet.rb +149 -0
- data/lib/sunspot/query/field_query.rb +57 -0
- data/lib/sunspot/query/pagination.rb +39 -0
- data/lib/sunspot/query/query_facet.rb +72 -0
- data/lib/sunspot/query/query_facet_row.rb +19 -0
- data/lib/sunspot/query/restriction.rb +225 -0
- data/lib/sunspot/query/scope.rb +165 -0
- data/lib/sunspot/query/sort.rb +36 -0
- data/lib/sunspot/query/sort_composite.rb +33 -0
- data/lib/sunspot/query_facet.rb +33 -0
- data/lib/sunspot/query_facet_row.rb +21 -0
- data/lib/sunspot/schema.rb +165 -0
- data/lib/sunspot/search.rb +222 -0
- data/lib/sunspot/search/hit.rb +62 -0
- data/lib/sunspot/session.rb +201 -0
- data/lib/sunspot/setup.rb +271 -0
- data/lib/sunspot/type.rb +200 -0
- data/lib/sunspot/util.rb +164 -0
- data/solr/etc/jetty.xml +212 -0
- data/solr/etc/webdefault.xml +379 -0
- data/solr/lib/jetty-6.1.3.jar +0 -0
- data/solr/lib/jetty-util-6.1.3.jar +0 -0
- data/solr/lib/jsp-2.1/ant-1.6.5.jar +0 -0
- data/solr/lib/jsp-2.1/core-3.1.1.jar +0 -0
- data/solr/lib/jsp-2.1/jsp-2.1.jar +0 -0
- data/solr/lib/jsp-2.1/jsp-api-2.1.jar +0 -0
- data/solr/lib/servlet-api-2.5-6.1.3.jar +0 -0
- data/solr/solr/conf/elevate.xml +36 -0
- data/solr/solr/conf/protwords.txt +21 -0
- data/solr/solr/conf/schema.xml +50 -0
- data/solr/solr/conf/solrconfig.xml +696 -0
- data/solr/solr/conf/stopwords.txt +57 -0
- data/solr/solr/conf/synonyms.txt +31 -0
- data/solr/start.jar +0 -0
- data/solr/webapps/solr.war +0 -0
- data/spec/api/adapters_spec.rb +33 -0
- data/spec/api/build_search_spec.rb +918 -0
- data/spec/api/indexer_spec.rb +311 -0
- data/spec/api/query_spec.rb +153 -0
- data/spec/api/search_retrieval_spec.rb +325 -0
- data/spec/api/session_spec.rb +157 -0
- data/spec/api/spec_helper.rb +1 -0
- data/spec/api/sunspot_spec.rb +18 -0
- data/spec/integration/dynamic_fields_spec.rb +55 -0
- data/spec/integration/faceting_spec.rb +169 -0
- data/spec/integration/keyword_search_spec.rb +83 -0
- data/spec/integration/scoped_search_spec.rb +188 -0
- data/spec/integration/spec_helper.rb +1 -0
- data/spec/integration/stored_fields_spec.rb +10 -0
- data/spec/integration/test_pagination.rb +32 -0
- data/spec/mocks/adapters.rb +32 -0
- data/spec/mocks/blog.rb +3 -0
- data/spec/mocks/comment.rb +19 -0
- data/spec/mocks/connection.rb +84 -0
- data/spec/mocks/mock_adapter.rb +30 -0
- data/spec/mocks/mock_record.rb +41 -0
- data/spec/mocks/photo.rb +8 -0
- data/spec/mocks/post.rb +70 -0
- data/spec/mocks/user.rb +8 -0
- data/spec/spec_helper.rb +47 -0
- data/tasks/gemspec.rake +25 -0
- data/tasks/rcov.rake +28 -0
- data/tasks/rdoc.rake +21 -0
- data/tasks/schema.rake +19 -0
- data/tasks/spec.rake +24 -0
- data/tasks/todo.rake +4 -0
- data/templates/schema.xml.haml +24 -0
- metadata +245 -0
data/History.txt
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
== 0.8.0 2009-05-22
|
2
|
+
* Access query API directly; instantiate search without running it
|
3
|
+
* Dynamic fields
|
4
|
+
* Search blocks can be evaluated in calling context
|
5
|
+
|
6
|
+
== 0.7.3 2009-05-06
|
7
|
+
* Better exception handling when class doesn't have adapter/setup
|
8
|
+
|
9
|
+
== 0.7.2 2009-04-29
|
10
|
+
* Dirty sessions
|
11
|
+
|
12
|
+
== 0.7.1 2009-04-29
|
13
|
+
* Removed extlib dependency from gemspec
|
14
|
+
|
15
|
+
== 0.7.0 2009-04-28
|
16
|
+
* Less magic in the DSL
|
17
|
+
* Restrict by empty values
|
18
|
+
* Negative scoping using without() method
|
19
|
+
* Exclusion by object identity using without(instance)
|
20
|
+
* Support for faceting
|
21
|
+
* Explicit commits
|
22
|
+
* Boolean field type
|
23
|
+
* Attribute field flexibility
|
24
|
+
* Virtual field blocks can be evaluated in calling context
|
25
|
+
* Order available by multiple fields
|
26
|
+
* New adapter API
|
27
|
+
* Got rid of builder API
|
28
|
+
* Full documentation
|
29
|
+
|
30
|
+
== 0.0.2 2009-02-14
|
31
|
+
* Run sunspot's built-in Solr instance using
|
32
|
+
sunspot-solr executable
|
33
|
+
* Search hash interpretation delegated to
|
34
|
+
Builder object
|
35
|
+
|
36
|
+
== 0.0.1 2008-12-11
|
37
|
+
* Initial release
|
38
|
+
* Define indexing for any class using DSL
|
39
|
+
* 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
data/TODO
ADDED
data/VERSION.yml
ADDED
@@ -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
|
data/lib/light_config.rb
ADDED
@@ -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,470 @@
|
|
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 date_facet date_facet_row query_facet query_facet_row
|
15
|
+
session type dsl).each do |filename|
|
16
|
+
require File.join(File.dirname(__FILE__), 'sunspot', filename)
|
17
|
+
end
|
18
|
+
|
19
|
+
#
|
20
|
+
# The Sunspot module provides class-method entry points to most of the
|
21
|
+
# functionality provided by the Sunspot library. Internally, the Sunspot
|
22
|
+
# singleton class contains a (non-thread-safe!) instance of Sunspot::Session,
|
23
|
+
# to which it delegates most of the class methods it exposes. In the method
|
24
|
+
# documentation below, this instance is referred to as the "singleton session".
|
25
|
+
#
|
26
|
+
# Though the singleton session provides a convenient entry point to Sunspot,
|
27
|
+
# it is by no means required to use the Sunspot class methods. Multiple sessions
|
28
|
+
# may be instantiated and used (if you need to connect to multiple Solr
|
29
|
+
# instances, for example.)
|
30
|
+
#
|
31
|
+
# Note that the configuration of classes for index/search (the +setup+
|
32
|
+
# method) is _not_ session-specific, but rather global.
|
33
|
+
#
|
34
|
+
module Sunspot
|
35
|
+
UnrecognizedFieldError = Class.new(Exception)
|
36
|
+
UnrecognizedRestrictionError = Class.new(Exception)
|
37
|
+
NoAdapterError = Class.new(Exception)
|
38
|
+
NoSetupError = Class.new(Exception)
|
39
|
+
|
40
|
+
class <<self
|
41
|
+
# Configures indexing and search for a given class.
|
42
|
+
#
|
43
|
+
# ==== Parameters
|
44
|
+
#
|
45
|
+
# clazz<Class>:: class to configure
|
46
|
+
#
|
47
|
+
# ==== Example
|
48
|
+
#
|
49
|
+
# Sunspot.setup(Post) do
|
50
|
+
# text :title, :body
|
51
|
+
# string :author_name
|
52
|
+
# integer :blog_id
|
53
|
+
# integer :category_ids
|
54
|
+
# float :average_rating, :using => :ratings_average
|
55
|
+
# time :published_at
|
56
|
+
# string :sort_title do
|
57
|
+
# title.downcase.sub(/^(an?|the)\W+/, ''/) if title = self.title
|
58
|
+
# end
|
59
|
+
# end
|
60
|
+
#
|
61
|
+
# ====== Attribute Fields vs. Virtual Fields
|
62
|
+
#
|
63
|
+
# Attribute fields call a method on the indexed object and index the
|
64
|
+
# return value. All of the fields defined above except for the last one are
|
65
|
+
# attribute fields. By default, the field name will also be the attribute
|
66
|
+
# used; this can be overriden with the +:using+ option, as in
|
67
|
+
# +:average_rating+ above. In that case, the attribute +:ratings_average+
|
68
|
+
# will be indexed with the field name +:average_rating+.
|
69
|
+
#
|
70
|
+
# +:sort_title+ is a virtual field, which evaluates the block inside the
|
71
|
+
# context of the instance being indexed, and indexes the value returned
|
72
|
+
# by the block. If the block you pass takes an argument, it will be passed
|
73
|
+
# the instance rather than being evaluated inside of it; so, the following
|
74
|
+
# example is equivalent to the one above (assuming #title is public):
|
75
|
+
#
|
76
|
+
# Sunspot.setup(Post) do
|
77
|
+
# string :sort_title do |post|
|
78
|
+
# post.title.downcase.sub(/^(an?|the)\W+/, ''/) if title = self.title
|
79
|
+
# end
|
80
|
+
# end
|
81
|
+
#
|
82
|
+
# ===== Field Types
|
83
|
+
#
|
84
|
+
# The available types are:
|
85
|
+
#
|
86
|
+
# * +text+
|
87
|
+
# * +string+
|
88
|
+
# * +integer+
|
89
|
+
# * +float+
|
90
|
+
# * +time+
|
91
|
+
# * +boolean+
|
92
|
+
#
|
93
|
+
# Note that the +text+ type behaves quite differently from the others -
|
94
|
+
# this is the type that is indexed as fulltext, and is searched using the
|
95
|
+
# +keywords+ method inside the search DSL. Text fields cannot have
|
96
|
+
# restrictions set on them, nor can they be used in order statements or
|
97
|
+
# for facets. All other types are indexed literally, and thus can be used
|
98
|
+
# for all of those operations. They will not, however, be searched in
|
99
|
+
# fulltext. In this way, Sunspot provides a complete barrier between
|
100
|
+
# fulltext fields and value fields.
|
101
|
+
#
|
102
|
+
# It is fine to specify a field both as a text field and a string field;
|
103
|
+
# internally, the fields will have different names so there is no danger
|
104
|
+
# of conflict.
|
105
|
+
#
|
106
|
+
# ===== Dynamic Fields
|
107
|
+
#
|
108
|
+
# For use cases which have highly dynamic data models (for instance, an
|
109
|
+
# open set of key-value pairs attached to a model), it may be useful to
|
110
|
+
# defer definition of fields until indexing time. Sunspot exposes dynamic
|
111
|
+
# fields, which define a data accessor (either attribute or virtual, see
|
112
|
+
# above), which accepts a hash of field names to values. Note that the field
|
113
|
+
# names in the hash are internally scoped to the base name of the dynamic
|
114
|
+
# field, so any time they are referred to, they are referred to using both
|
115
|
+
# the base name and the dynamic (runtime-specified) name.
|
116
|
+
#
|
117
|
+
# Dynamic fields are speficied in the setup block using the type name
|
118
|
+
# prefixed by +dynamic_+. For example:
|
119
|
+
#
|
120
|
+
# Sunspot.setup(Post) do
|
121
|
+
# dynamic_string :custom_values do
|
122
|
+
# key_value_pairs.inject({}) do |hash, key_value_pair|
|
123
|
+
# hash[key_value_pair.key.to_sym] = key_value_pair.value
|
124
|
+
# end
|
125
|
+
# end
|
126
|
+
# end
|
127
|
+
#
|
128
|
+
# If you later wanted to facet all of the values for the key "cuisine",
|
129
|
+
# you could issue:
|
130
|
+
#
|
131
|
+
# Sunspot.search(Post) do
|
132
|
+
# dynamic :custom_values do
|
133
|
+
# facet :cuisine
|
134
|
+
# end
|
135
|
+
# end
|
136
|
+
#
|
137
|
+
# In the documentation, +:custom_values+ is referred to as the "base name" -
|
138
|
+
# that is, the one specified statically - and +:cuisine+ is referred to as
|
139
|
+
# the dynamic name, which is the part that is specified at indexing time.
|
140
|
+
#
|
141
|
+
def setup(clazz, &block)
|
142
|
+
Setup.setup(clazz, &block)
|
143
|
+
end
|
144
|
+
|
145
|
+
# Indexes objects on the singleton session.
|
146
|
+
#
|
147
|
+
# ==== Parameters
|
148
|
+
#
|
149
|
+
# objects...<Object>:: objects to index (may pass an array or varargs)
|
150
|
+
#
|
151
|
+
# ==== Example
|
152
|
+
#
|
153
|
+
# post1, post2 = Array(2) { Post.create }
|
154
|
+
# Sunspot.index(post1, post2)
|
155
|
+
#
|
156
|
+
# Note that indexed objects won't be reflected in search until a commit is
|
157
|
+
# sent - see Sunspot.index! and Sunspot.commit
|
158
|
+
#
|
159
|
+
def index(*objects)
|
160
|
+
session.index(*objects)
|
161
|
+
end
|
162
|
+
|
163
|
+
# Indexes objects on the singleton session and commits immediately.
|
164
|
+
#
|
165
|
+
# See: Sunspot.index and Sunspot.commit
|
166
|
+
#
|
167
|
+
# ==== Parameters
|
168
|
+
#
|
169
|
+
# objects...<Object>:: objects to index (may pass an array or varargs)
|
170
|
+
#
|
171
|
+
def index!(*objects)
|
172
|
+
session.index!(*objects)
|
173
|
+
end
|
174
|
+
|
175
|
+
# Commits the singleton session
|
176
|
+
#
|
177
|
+
# When documents are added to or removed from Solr, the changes are
|
178
|
+
# initially stored in memory, and are not reflected in Solr's existing
|
179
|
+
# searcher instance. When a commit message is sent, the changes are written
|
180
|
+
# to disk, and a new searcher is spawned. Commits are thus fairly
|
181
|
+
# expensive, so if your application needs to index several documents as part
|
182
|
+
# of a single operation, it is advisable to index them all and then call
|
183
|
+
# commit at the end of the operation.
|
184
|
+
#
|
185
|
+
# Note that Solr can also be configured to automatically perform a commit
|
186
|
+
# after either a specified interval after the last change, or after a
|
187
|
+
# specified number of documents are added. See
|
188
|
+
# http://wiki.apache.org/solr/SolrConfigXml
|
189
|
+
#
|
190
|
+
def commit
|
191
|
+
session.commit
|
192
|
+
end
|
193
|
+
|
194
|
+
#
|
195
|
+
# Create a new Search instance, but do not execute it immediately. Generally
|
196
|
+
# you will want to use the #search method to execute searches using the
|
197
|
+
# DSL; however, if you are building searches dynamically (using the Builder
|
198
|
+
# pattern, for instance), it may be easier to access the Query API directly.
|
199
|
+
#
|
200
|
+
# ==== Parameters
|
201
|
+
#
|
202
|
+
# types<Class>...::
|
203
|
+
# Zero, one, or more types to search for. If no types are passed, all
|
204
|
+
# configured types will be searched for.
|
205
|
+
#
|
206
|
+
# ==== Returns
|
207
|
+
#
|
208
|
+
# Sunspot::Search::
|
209
|
+
# Search object, not yet executed. Query parameters can be added manually;
|
210
|
+
# then #execute! should be called.
|
211
|
+
#
|
212
|
+
def new_search(*types)
|
213
|
+
session.new_search(*types)
|
214
|
+
end
|
215
|
+
|
216
|
+
|
217
|
+
# Search for objects in the index.
|
218
|
+
#
|
219
|
+
# ==== Parameters
|
220
|
+
#
|
221
|
+
# types<Class>...::
|
222
|
+
# Zero, one, or more types to search for. If no types are passed, all
|
223
|
+
# configured types will be searched.
|
224
|
+
#
|
225
|
+
# ==== Options (last argument, optional)
|
226
|
+
#
|
227
|
+
# :keywords<String>:: Fulltext search string
|
228
|
+
# :conditions<Hash>::
|
229
|
+
# Hash of key-value pairs to be used as restrictions. Keys are field
|
230
|
+
# names. Scalar values are used as equality restrictions; arrays are used
|
231
|
+
# as "any of" restrictions; and Ranges are used as range restrictions.
|
232
|
+
# :order<String>:: order field and direction (e.g., 'updated_at desc')
|
233
|
+
# :page<Integer>:: Page to start on for pagination
|
234
|
+
# :per_page<Integer>::
|
235
|
+
# Number of results to use per page. Ignored if :page is not specified.
|
236
|
+
#
|
237
|
+
# ==== Returns
|
238
|
+
#
|
239
|
+
# Sunspot::Search:: Object containing results, facets, count, etc.
|
240
|
+
#
|
241
|
+
# The fields available for restriction, ordering, etc. are those that meet
|
242
|
+
# the following criteria:
|
243
|
+
#
|
244
|
+
# * They are not of type +text+.
|
245
|
+
# * They are defined for all of the classes being searched
|
246
|
+
# * They have the same data type for all of the classes being searched
|
247
|
+
# * They have the same multiple flag for all of the classes being searched.
|
248
|
+
#
|
249
|
+
# The restrictions available are the constants defined in the
|
250
|
+
# Sunspot::Restriction class. The standard restrictions are:
|
251
|
+
#
|
252
|
+
# with(:field_name).equal_to(value)
|
253
|
+
# with(:field_name, value) # shorthand for above
|
254
|
+
# with(:field_name).less_than(value)
|
255
|
+
# with(:field_name).greater_than(value)
|
256
|
+
# with(:field_name).between(value1..value2)
|
257
|
+
# with(:field_name).any_of([value1, value2, value3])
|
258
|
+
# with(:field_name).all_of([value1, value2, value3])
|
259
|
+
# without(some_instance) # exclude that particular instance
|
260
|
+
#
|
261
|
+
# +without+ can be substituted for +with+, causing the restriction to be
|
262
|
+
# negated. In the last example above, only +without+ works, as it does not
|
263
|
+
# make sense to search only for an instance you already have.
|
264
|
+
#
|
265
|
+
# Equality restrictions can take +nil+ as a value, which restricts the
|
266
|
+
# results to documents that have no value for the given field. Passing +nil+
|
267
|
+
# as a value to other restriction types is illegal. Thus:
|
268
|
+
#
|
269
|
+
# with(:field_name, nil) # ok
|
270
|
+
# with(:field_name).equal_to(nil) # ok
|
271
|
+
# with(:field_name).less_than(nil) # bad
|
272
|
+
#
|
273
|
+
# ==== Example
|
274
|
+
#
|
275
|
+
# Sunspot.search(Post) do
|
276
|
+
# keywords 'great pizza'
|
277
|
+
# with(:published_at).less_than Time.now
|
278
|
+
# with :blog_id, 1
|
279
|
+
# without current_post
|
280
|
+
# facet :category_ids
|
281
|
+
# order_by :published_at, :desc
|
282
|
+
# paginate 2, 15
|
283
|
+
# end
|
284
|
+
#
|
285
|
+
# If the block passed to #search takes an argument, that argument will
|
286
|
+
# present the DSL, and the block will be evaluated in the calling context.
|
287
|
+
# This will come in handy for building searches using instance data or
|
288
|
+
# methods, e.g.:
|
289
|
+
#
|
290
|
+
# Sunspot.search(Post) do |query|
|
291
|
+
# query.with(:blog_id, @current_blog.id)
|
292
|
+
# end
|
293
|
+
#
|
294
|
+
# See Sunspot::DSL::Search, Sunspot::DSL::Scope, Sunspot::DSL::FieldQuery
|
295
|
+
# and Sunspot::DSL::Query for the full API presented inside the block.
|
296
|
+
#
|
297
|
+
def search(*types, &block)
|
298
|
+
session.search(*types, &block)
|
299
|
+
end
|
300
|
+
|
301
|
+
# Remove objects from the index. Any time an object is destroyed, it must
|
302
|
+
# be removed from the index; otherwise, the index will contain broken
|
303
|
+
# references to objects that do not exist, which will cause errors when
|
304
|
+
# those objects are matched in search results.
|
305
|
+
#
|
306
|
+
# ==== Parameters
|
307
|
+
#
|
308
|
+
# objects...<Object>::
|
309
|
+
# Objects to remove from the index (may pass an array or varargs)
|
310
|
+
#
|
311
|
+
# ==== Example
|
312
|
+
#
|
313
|
+
# post.destroy
|
314
|
+
# Sunspot.remove(post)
|
315
|
+
#
|
316
|
+
def remove(*objects)
|
317
|
+
session.remove(*objects)
|
318
|
+
end
|
319
|
+
|
320
|
+
#
|
321
|
+
# Remove objects from the index and immediately commit. See Sunspot.remove
|
322
|
+
#
|
323
|
+
# ==== Parameters
|
324
|
+
#
|
325
|
+
# objects...<Object>:: Objects to remove from the index
|
326
|
+
#
|
327
|
+
def remove!
|
328
|
+
session.remove!(*objects)
|
329
|
+
end
|
330
|
+
|
331
|
+
#
|
332
|
+
# Remove an object from the index using its class name and primary key.
|
333
|
+
# Useful if you know this information and want to remove an object without
|
334
|
+
# instantiating it from persistent storage
|
335
|
+
#
|
336
|
+
# ==== Parameters
|
337
|
+
#
|
338
|
+
# clazz<Class>:: Class of the object, or class name as a string or symbol
|
339
|
+
# id::
|
340
|
+
# Primary key of the object. This should be the same id that would be
|
341
|
+
# returned by the class's instance adapter.
|
342
|
+
#
|
343
|
+
def remove_by_id(clazz, id)
|
344
|
+
session.remove_by_id(clazz, id)
|
345
|
+
end
|
346
|
+
|
347
|
+
#
|
348
|
+
# Remove an object by class name and primary key, and immediately commit.
|
349
|
+
# See #remove_by_id and #commit
|
350
|
+
#
|
351
|
+
def remove_by_id!(clazz, id)
|
352
|
+
session.remove_by_id!(clazz, id)
|
353
|
+
end
|
354
|
+
|
355
|
+
# Remove all objects of the given classes from the index. There isn't much
|
356
|
+
# use for this in general operations but it can be useful for maintenance,
|
357
|
+
# testing, etc. If no arguments are passed, remove everything from the
|
358
|
+
# index.
|
359
|
+
#
|
360
|
+
# ==== Parameters
|
361
|
+
#
|
362
|
+
# classes...<Class>::
|
363
|
+
# classes for which to remove all instances from the index (may pass an
|
364
|
+
# array or varargs)
|
365
|
+
#
|
366
|
+
# ==== Example
|
367
|
+
#
|
368
|
+
# Sunspot.remove_all(Post, Blog)
|
369
|
+
#
|
370
|
+
def remove_all(*classes)
|
371
|
+
session.remove_all(*classes)
|
372
|
+
end
|
373
|
+
|
374
|
+
#
|
375
|
+
# Remove all objects of the given classes from the index and immediately
|
376
|
+
# commit. See Sunspot.remove_all
|
377
|
+
#
|
378
|
+
# ==== Parameters
|
379
|
+
#
|
380
|
+
# classes...<Class>::
|
381
|
+
# classes for which to remove all instances from the index
|
382
|
+
def remove_all!(*classes)
|
383
|
+
session.remove_all!(*classes)
|
384
|
+
end
|
385
|
+
|
386
|
+
#
|
387
|
+
# Process all adds in a batch. Any Sunspot adds initiated inside the block
|
388
|
+
# will be sent in bulk when the block finishes. Useful if your application
|
389
|
+
# initiates index adds from various places in code as part of a single
|
390
|
+
# operation; doing a batch add will give better performance.
|
391
|
+
#
|
392
|
+
# ==== Example
|
393
|
+
#
|
394
|
+
# Sunspot.batch do
|
395
|
+
# post = Post.new
|
396
|
+
# Sunspot.add(post)
|
397
|
+
# comment = Comment.new
|
398
|
+
# Sunspot.add(comment)
|
399
|
+
# end
|
400
|
+
#
|
401
|
+
# Sunspot will send both the post and the comment in a single request.
|
402
|
+
#
|
403
|
+
def batch(&block)
|
404
|
+
session.batch(&block)
|
405
|
+
end
|
406
|
+
|
407
|
+
#
|
408
|
+
# True if documents have been added, updated, or removed since the last
|
409
|
+
# commit.
|
410
|
+
#
|
411
|
+
# ==== Returns
|
412
|
+
#
|
413
|
+
# Boolean:: Whether there have been any updates since the last commit
|
414
|
+
#
|
415
|
+
def dirty?
|
416
|
+
session.dirty?
|
417
|
+
end
|
418
|
+
|
419
|
+
#
|
420
|
+
# Sends a commit if the session is dirty (see #dirty?).
|
421
|
+
#
|
422
|
+
def commit_if_dirty
|
423
|
+
session.commit_if_dirty
|
424
|
+
end
|
425
|
+
|
426
|
+
# Returns the configuration associated with the singleton session. See
|
427
|
+
# Sunspot::Configuration for details.
|
428
|
+
#
|
429
|
+
# ==== Returns
|
430
|
+
#
|
431
|
+
# LightConfig::Configuration:: configuration for singleton session
|
432
|
+
#
|
433
|
+
def config
|
434
|
+
session.config
|
435
|
+
end
|
436
|
+
|
437
|
+
#
|
438
|
+
# Resets the singleton session. This is useful for clearing out all
|
439
|
+
# static data between tests, but probably nowhere else.
|
440
|
+
#
|
441
|
+
# ==== Parameters
|
442
|
+
#
|
443
|
+
# keep_config<Boolean>::
|
444
|
+
# Whether to retain the configuration used by the current singleton
|
445
|
+
# session. Default false.
|
446
|
+
#
|
447
|
+
def reset!(keep_config = false)
|
448
|
+
config =
|
449
|
+
if keep_config
|
450
|
+
session.config
|
451
|
+
else
|
452
|
+
Configuration.build
|
453
|
+
end
|
454
|
+
@session = Session.new(config)
|
455
|
+
end
|
456
|
+
|
457
|
+
private
|
458
|
+
|
459
|
+
#
|
460
|
+
# Get the singleton session, creating it if none yet exists.
|
461
|
+
#
|
462
|
+
# ==== Returns
|
463
|
+
#
|
464
|
+
# Sunspot::Session:: the singleton session
|
465
|
+
#
|
466
|
+
def session #:nodoc:
|
467
|
+
@session ||= Session.new
|
468
|
+
end
|
469
|
+
end
|
470
|
+
end
|