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