sunspot_rbg 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (222) hide show
  1. data/.gitignore +12 -0
  2. data/Gemfile +4 -0
  3. data/History.txt +222 -0
  4. data/LICENSE +18 -0
  5. data/Rakefile +17 -0
  6. data/TODO +13 -0
  7. data/VERSION.yml +4 -0
  8. data/bin/sunspot-installer +19 -0
  9. data/bin/sunspot-solr +74 -0
  10. data/installer/config/schema.yml +95 -0
  11. data/lib/light_config.rb +40 -0
  12. data/lib/sunspot/adapters.rb +265 -0
  13. data/lib/sunspot/composite_setup.rb +202 -0
  14. data/lib/sunspot/configuration.rb +46 -0
  15. data/lib/sunspot/data_extractor.rb +50 -0
  16. data/lib/sunspot/dsl/adjustable.rb +47 -0
  17. data/lib/sunspot/dsl/field_query.rb +279 -0
  18. data/lib/sunspot/dsl/fields.rb +103 -0
  19. data/lib/sunspot/dsl/fulltext.rb +243 -0
  20. data/lib/sunspot/dsl/function.rb +14 -0
  21. data/lib/sunspot/dsl/functional.rb +44 -0
  22. data/lib/sunspot/dsl/more_like_this_query.rb +56 -0
  23. data/lib/sunspot/dsl/paginatable.rb +28 -0
  24. data/lib/sunspot/dsl/query_facet.rb +36 -0
  25. data/lib/sunspot/dsl/restriction.rb +25 -0
  26. data/lib/sunspot/dsl/restriction_with_near.rb +121 -0
  27. data/lib/sunspot/dsl/scope.rb +217 -0
  28. data/lib/sunspot/dsl/search.rb +30 -0
  29. data/lib/sunspot/dsl/standard_query.rb +121 -0
  30. data/lib/sunspot/dsl.rb +5 -0
  31. data/lib/sunspot/field.rb +193 -0
  32. data/lib/sunspot/field_factory.rb +129 -0
  33. data/lib/sunspot/indexer.rb +131 -0
  34. data/lib/sunspot/installer/library_installer.rb +45 -0
  35. data/lib/sunspot/installer/schema_builder.rb +219 -0
  36. data/lib/sunspot/installer/solrconfig_updater.rb +76 -0
  37. data/lib/sunspot/installer/task_helper.rb +18 -0
  38. data/lib/sunspot/installer.rb +31 -0
  39. data/lib/sunspot/query/abstract_field_facet.rb +52 -0
  40. data/lib/sunspot/query/boost_query.rb +24 -0
  41. data/lib/sunspot/query/common_query.rb +85 -0
  42. data/lib/sunspot/query/composite_fulltext.rb +36 -0
  43. data/lib/sunspot/query/connective.rb +206 -0
  44. data/lib/sunspot/query/date_field_facet.rb +14 -0
  45. data/lib/sunspot/query/dismax.rb +128 -0
  46. data/lib/sunspot/query/field_facet.rb +41 -0
  47. data/lib/sunspot/query/filter.rb +38 -0
  48. data/lib/sunspot/query/function_query.rb +52 -0
  49. data/lib/sunspot/query/geo.rb +53 -0
  50. data/lib/sunspot/query/highlighting.rb +55 -0
  51. data/lib/sunspot/query/more_like_this.rb +61 -0
  52. data/lib/sunspot/query/more_like_this_query.rb +12 -0
  53. data/lib/sunspot/query/pagination.rb +38 -0
  54. data/lib/sunspot/query/query_facet.rb +16 -0
  55. data/lib/sunspot/query/restriction.rb +262 -0
  56. data/lib/sunspot/query/scope.rb +9 -0
  57. data/lib/sunspot/query/sort.rb +95 -0
  58. data/lib/sunspot/query/sort_composite.rb +33 -0
  59. data/lib/sunspot/query/standard_query.rb +16 -0
  60. data/lib/sunspot/query/text_field_boost.rb +17 -0
  61. data/lib/sunspot/query.rb +11 -0
  62. data/lib/sunspot/schema.rb +151 -0
  63. data/lib/sunspot/search/abstract_search.rb +293 -0
  64. data/lib/sunspot/search/date_facet.rb +35 -0
  65. data/lib/sunspot/search/facet_row.rb +27 -0
  66. data/lib/sunspot/search/field_facet.rb +88 -0
  67. data/lib/sunspot/search/highlight.rb +38 -0
  68. data/lib/sunspot/search/hit.rb +136 -0
  69. data/lib/sunspot/search/more_like_this_search.rb +31 -0
  70. data/lib/sunspot/search/paginated_collection.rb +55 -0
  71. data/lib/sunspot/search/query_facet.rb +67 -0
  72. data/lib/sunspot/search/standard_search.rb +21 -0
  73. data/lib/sunspot/search.rb +9 -0
  74. data/lib/sunspot/server.rb +152 -0
  75. data/lib/sunspot/session.rb +260 -0
  76. data/lib/sunspot/session_proxy/abstract_session_proxy.rb +29 -0
  77. data/lib/sunspot/session_proxy/class_sharding_session_proxy.rb +66 -0
  78. data/lib/sunspot/session_proxy/id_sharding_session_proxy.rb +89 -0
  79. data/lib/sunspot/session_proxy/master_slave_session_proxy.rb +43 -0
  80. data/lib/sunspot/session_proxy/sharding_session_proxy.rb +222 -0
  81. data/lib/sunspot/session_proxy/silent_fail_session_proxy.rb +42 -0
  82. data/lib/sunspot/session_proxy/thread_local_session_proxy.rb +37 -0
  83. data/lib/sunspot/session_proxy.rb +87 -0
  84. data/lib/sunspot/setup.rb +350 -0
  85. data/lib/sunspot/text_field_setup.rb +29 -0
  86. data/lib/sunspot/type.rb +372 -0
  87. data/lib/sunspot/util.rb +243 -0
  88. data/lib/sunspot/version.rb +3 -0
  89. data/lib/sunspot.rb +569 -0
  90. data/lib/sunspot_rbg.rb +7 -0
  91. data/log/.gitignore +1 -0
  92. data/pkg/.gitignore +1 -0
  93. data/script/console +10 -0
  94. data/solr/README.txt +42 -0
  95. data/solr/etc/jetty.xml +218 -0
  96. data/solr/etc/webdefault.xml +379 -0
  97. data/solr/lib/jetty-6.1.3.jar +0 -0
  98. data/solr/lib/jetty-util-6.1.3.jar +0 -0
  99. data/solr/lib/jsp-2.1/ant-1.6.5.jar +0 -0
  100. data/solr/lib/jsp-2.1/core-3.1.1.jar +0 -0
  101. data/solr/lib/jsp-2.1/jsp-2.1.jar +0 -0
  102. data/solr/lib/jsp-2.1/jsp-api-2.1.jar +0 -0
  103. data/solr/lib/servlet-api-2.5-6.1.3.jar +0 -0
  104. data/solr/logs/.gitignore +1 -0
  105. data/solr/solr/.gitignore +1 -0
  106. data/solr/solr/README.txt +54 -0
  107. data/solr/solr/conf/admin-extra.html +31 -0
  108. data/solr/solr/conf/elevate.xml +36 -0
  109. data/solr/solr/conf/mapping-ISOLatin1Accent.txt +246 -0
  110. data/solr/solr/conf/protwords.txt +21 -0
  111. data/solr/solr/conf/schema.xml +238 -0
  112. data/solr/solr/conf/scripts.conf +24 -0
  113. data/solr/solr/conf/solrconfig.xml +934 -0
  114. data/solr/solr/conf/spellings.txt +2 -0
  115. data/solr/solr/conf/stopwords.txt +58 -0
  116. data/solr/solr/conf/synonyms.txt +31 -0
  117. data/solr/solr/conf/xslt/example.xsl +132 -0
  118. data/solr/solr/conf/xslt/example_atom.xsl +67 -0
  119. data/solr/solr/conf/xslt/example_rss.xsl +66 -0
  120. data/solr/solr/conf/xslt/luke.xsl +337 -0
  121. data/solr/start.jar +0 -0
  122. data/solr/webapps/solr.war +0 -0
  123. data/solr-1.3/etc/jetty.xml +212 -0
  124. data/solr-1.3/etc/webdefault.xml +379 -0
  125. data/solr-1.3/lib/jetty-6.1.3.jar +0 -0
  126. data/solr-1.3/lib/jetty-util-6.1.3.jar +0 -0
  127. data/solr-1.3/lib/jsp-2.1/ant-1.6.5.jar +0 -0
  128. data/solr-1.3/lib/jsp-2.1/core-3.1.1.jar +0 -0
  129. data/solr-1.3/lib/jsp-2.1/jsp-2.1.jar +0 -0
  130. data/solr-1.3/lib/jsp-2.1/jsp-api-2.1.jar +0 -0
  131. data/solr-1.3/lib/servlet-api-2.5-6.1.3.jar +0 -0
  132. data/solr-1.3/solr/conf/elevate.xml +36 -0
  133. data/solr-1.3/solr/conf/protwords.txt +21 -0
  134. data/solr-1.3/solr/conf/schema.xml +64 -0
  135. data/solr-1.3/solr/conf/solrconfig.xml +725 -0
  136. data/solr-1.3/solr/conf/stopwords.txt +57 -0
  137. data/solr-1.3/solr/conf/synonyms.txt +31 -0
  138. data/solr-1.3/solr/lib/geoapi-nogenerics-2.1-M2.jar +0 -0
  139. data/solr-1.3/solr/lib/gt2-referencing-2.3.1.jar +0 -0
  140. data/solr-1.3/solr/lib/jsr108-0.01.jar +0 -0
  141. data/solr-1.3/solr/lib/locallucene.jar +0 -0
  142. data/solr-1.3/solr/lib/localsolr.jar +0 -0
  143. data/solr-1.3/start.jar +0 -0
  144. data/solr-1.3/webapps/solr.war +0 -0
  145. data/spec/api/adapters_spec.rb +33 -0
  146. data/spec/api/binding_spec.rb +50 -0
  147. data/spec/api/indexer/attributes_spec.rb +149 -0
  148. data/spec/api/indexer/batch_spec.rb +46 -0
  149. data/spec/api/indexer/dynamic_fields_spec.rb +42 -0
  150. data/spec/api/indexer/fixed_fields_spec.rb +57 -0
  151. data/spec/api/indexer/fulltext_spec.rb +43 -0
  152. data/spec/api/indexer/removal_spec.rb +53 -0
  153. data/spec/api/indexer/spec_helper.rb +1 -0
  154. data/spec/api/indexer_spec.rb +14 -0
  155. data/spec/api/query/advanced_manipulation_examples.rb +35 -0
  156. data/spec/api/query/connectives_examples.rb +189 -0
  157. data/spec/api/query/dsl_spec.rb +18 -0
  158. data/spec/api/query/dynamic_fields_examples.rb +165 -0
  159. data/spec/api/query/faceting_examples.rb +397 -0
  160. data/spec/api/query/fulltext_examples.rb +313 -0
  161. data/spec/api/query/function_spec.rb +70 -0
  162. data/spec/api/query/geo_examples.rb +68 -0
  163. data/spec/api/query/highlighting_examples.rb +223 -0
  164. data/spec/api/query/more_like_this_spec.rb +140 -0
  165. data/spec/api/query/ordering_pagination_examples.rb +95 -0
  166. data/spec/api/query/scope_examples.rb +275 -0
  167. data/spec/api/query/spec_helper.rb +1 -0
  168. data/spec/api/query/standard_spec.rb +28 -0
  169. data/spec/api/query/text_field_scoping_examples.rb +30 -0
  170. data/spec/api/query/types_spec.rb +20 -0
  171. data/spec/api/search/dynamic_fields_spec.rb +33 -0
  172. data/spec/api/search/faceting_spec.rb +360 -0
  173. data/spec/api/search/highlighting_spec.rb +69 -0
  174. data/spec/api/search/hits_spec.rb +120 -0
  175. data/spec/api/search/paginated_collection_spec.rb +26 -0
  176. data/spec/api/search/results_spec.rb +66 -0
  177. data/spec/api/search/search_spec.rb +23 -0
  178. data/spec/api/search/spec_helper.rb +1 -0
  179. data/spec/api/server_spec.rb +91 -0
  180. data/spec/api/session_proxy/class_sharding_session_proxy_spec.rb +85 -0
  181. data/spec/api/session_proxy/id_sharding_session_proxy_spec.rb +30 -0
  182. data/spec/api/session_proxy/master_slave_session_proxy_spec.rb +41 -0
  183. data/spec/api/session_proxy/sharding_session_proxy_spec.rb +77 -0
  184. data/spec/api/session_proxy/silent_fail_session_proxy_spec.rb +24 -0
  185. data/spec/api/session_proxy/spec_helper.rb +9 -0
  186. data/spec/api/session_proxy/thread_local_session_proxy_spec.rb +50 -0
  187. data/spec/api/session_spec.rb +220 -0
  188. data/spec/api/spec_helper.rb +3 -0
  189. data/spec/api/sunspot_spec.rb +18 -0
  190. data/spec/ext.rb +11 -0
  191. data/spec/helpers/indexer_helper.rb +29 -0
  192. data/spec/helpers/query_helper.rb +38 -0
  193. data/spec/helpers/search_helper.rb +80 -0
  194. data/spec/integration/dynamic_fields_spec.rb +55 -0
  195. data/spec/integration/faceting_spec.rb +238 -0
  196. data/spec/integration/highlighting_spec.rb +22 -0
  197. data/spec/integration/indexing_spec.rb +33 -0
  198. data/spec/integration/keyword_search_spec.rb +317 -0
  199. data/spec/integration/local_search_spec.rb +64 -0
  200. data/spec/integration/more_like_this_spec.rb +43 -0
  201. data/spec/integration/scoped_search_spec.rb +354 -0
  202. data/spec/integration/spec_helper.rb +7 -0
  203. data/spec/integration/stored_fields_spec.rb +10 -0
  204. data/spec/integration/test_pagination.rb +32 -0
  205. data/spec/mocks/adapters.rb +32 -0
  206. data/spec/mocks/blog.rb +3 -0
  207. data/spec/mocks/comment.rb +21 -0
  208. data/spec/mocks/connection.rb +126 -0
  209. data/spec/mocks/mock_adapter.rb +30 -0
  210. data/spec/mocks/mock_class_sharding_session_proxy.rb +24 -0
  211. data/spec/mocks/mock_record.rb +52 -0
  212. data/spec/mocks/mock_sharding_session_proxy.rb +15 -0
  213. data/spec/mocks/photo.rb +11 -0
  214. data/spec/mocks/post.rb +85 -0
  215. data/spec/mocks/super_class.rb +2 -0
  216. data/spec/mocks/user.rb +13 -0
  217. data/spec/spec_helper.rb +30 -0
  218. data/sunspot.gemspec +40 -0
  219. data/tasks/rdoc.rake +27 -0
  220. data/tasks/schema.rake +19 -0
  221. data/tasks/todo.rake +4 -0
  222. metadata +457 -0
@@ -0,0 +1,76 @@
1
+ require 'rubygems'
2
+ require 'fileutils'
3
+ require 'nokogiri'
4
+
5
+ module Sunspot
6
+ class Installer
7
+ class SolrconfigUpdater
8
+ include TaskHelper
9
+
10
+ CONFIG_FILES = %w(solrconfig.xml elevate.xml spellings.txt stopwords.txt synonyms.txt)
11
+
12
+ class <<self
13
+ def execute(solrconfig_path, options = {})
14
+ new(solrconfig_path, options).execute
15
+ end
16
+ end
17
+
18
+ def initialize(solrconfig_path, options)
19
+ @force = !!options[:force]
20
+ unless @force || File.exist?(solrconfig_path)
21
+ abort("#{File.expand_path(@solrconfig_path)} doesn't exist." +
22
+ " Are you sure you're pointing to a SOLR_HOME?")
23
+ end
24
+ @solrconfig_path = solrconfig_path
25
+ @verbose = !!options[:verbose]
26
+ end
27
+
28
+ def execute
29
+ if @force
30
+ config_dir = File.dirname(@solrconfig_path)
31
+ FileUtils.mkdir_p(config_dir)
32
+ CONFIG_FILES.each do |file|
33
+ source_path =
34
+ File.join(File.dirname(__FILE__), '..', '..', '..', 'solr', 'solr', 'conf', file)
35
+ FileUtils.cp(source_path, config_dir)
36
+ say("Copied default #{file} to #{config_dir}")
37
+ end
38
+ else
39
+ @document = File.open(@solrconfig_path) do |f|
40
+ Nokogiri::XML(
41
+ f, nil, nil,
42
+ Nokogiri::XML::ParseOptions::DEFAULT_XML |
43
+ Nokogiri::XML::ParseOptions::NOBLANKS
44
+ )
45
+ end
46
+ @root = @document.root
47
+ maybe_add_more_like_this_handler
48
+ original_path = "#{@solrconfig_path}.orig"
49
+ FileUtils.cp(@solrconfig_path, original_path)
50
+ say("Saved backup of original to #{original_path}")
51
+ File.open(@solrconfig_path, 'w') do |file|
52
+ @document.write_to(
53
+ file,
54
+ :indent => 2
55
+ )
56
+ end
57
+ say("Wrote solrconfig to #{@solrconfig_path}")
58
+ end
59
+ end
60
+
61
+ private
62
+
63
+ def maybe_add_more_like_this_handler
64
+ unless @root.xpath('requestHandler[@name="/mlt"]').first
65
+ mlt_node = add_element(
66
+ @root, 'requestHandler',
67
+ :name => '/mlt', :class => 'solr.MoreLikeThisHandler'
68
+ )
69
+ defaults_node = add_element(mlt_node, 'lst', :name => 'defaults')
70
+ add_element(defaults_node, 'str', :name => 'mlt.mintf').content = '1'
71
+ add_element(defaults_node, 'str', :name => 'mlt.mindf').content = '2'
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,18 @@
1
+ module Sunspot
2
+ class Installer
3
+ module TaskHelper
4
+ def say(message)
5
+ if @verbose
6
+ STDOUT.puts(message)
7
+ end
8
+ end
9
+
10
+ def add_element(node, name, attributes = {})
11
+ new_node = Nokogiri::XML::Node.new(name, @document)
12
+ attributes.each_pair { |name, value| new_node[name.to_s] = value }
13
+ node << new_node
14
+ new_node
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,31 @@
1
+ %w(task_helper library_installer schema_builder solrconfig_updater).each do |file|
2
+ require File.join(File.dirname(__FILE__), 'installer', file)
3
+ end
4
+
5
+ module Sunspot
6
+ class Installer
7
+ class <<self
8
+ def execute(solr_home, options = {})
9
+ new(solr_home, options).execute
10
+ end
11
+
12
+ private :new
13
+ end
14
+
15
+ def initialize(solr_home, options)
16
+ @solr_home, @options = solr_home, options
17
+ end
18
+
19
+ def execute
20
+ SchemaBuilder.execute(
21
+ File.join(@solr_home, 'conf', 'schema.xml'),
22
+ @options
23
+ )
24
+ SolrconfigUpdater.execute(
25
+ File.join(@solr_home, 'conf', 'solrconfig.xml'),
26
+ @options
27
+ )
28
+ LibraryInstaller.execute(File.join(@solr_home, 'lib'), @options)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,52 @@
1
+ module Sunspot
2
+ module Query
3
+ class AbstractFieldFacet
4
+ include RSolr::Char
5
+
6
+ def initialize(field, options)
7
+ @field, @options = field, options
8
+ end
9
+
10
+ def to_params
11
+ params = {
12
+ :facet => 'true',
13
+ }
14
+ case @options[:sort]
15
+ when :count
16
+ params[qualified_param('sort')] = 'true'
17
+ when :index
18
+ params[qualified_param('sort')] = 'false'
19
+ when nil
20
+ else
21
+ raise(
22
+ ArgumentError,
23
+ "#{@options[:sort].inspect} is not an allowed value for :sort. Allowed options are :count and :index"
24
+ )
25
+ end
26
+ if @options[:limit]
27
+ params[qualified_param('limit')] = @options[:limit].to_i
28
+ end
29
+ if @options[:prefix]
30
+ params[qualified_param('prefix')] = escape(@options[:prefix].to_s)
31
+ end
32
+ params[qualified_param('mincount')] =
33
+ case
34
+ when @options[:minimum_count] then @options[:minimum_count].to_i
35
+ when @options[:zeros] then 0
36
+ else 1
37
+ end
38
+ params
39
+ end
40
+
41
+ private
42
+
43
+ def qualified_param(param)
44
+ :"f.#{key}.facet.#{param}"
45
+ end
46
+
47
+ def key
48
+ @key ||= @options[:name] || @field.indexed_name
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,24 @@
1
+ module Sunspot
2
+ module Query
3
+ #
4
+ # Representation of a BoostQuery, which allows the searcher to specify a
5
+ # scope for which matching documents should have an extra boost. This is
6
+ # essentially a conjunction, with an extra instance variable containing
7
+ # the boost that should be applied.
8
+ #
9
+ class BoostQuery < Connective::Conjunction #:nodoc:
10
+ def initialize(boost)
11
+ super(false)
12
+ @boost = boost
13
+ end
14
+
15
+ def to_boolean_phrase
16
+ if @boost.is_a?(FunctionQuery)
17
+ "#{@boost}"
18
+ else
19
+ "#{super}^#{@boost}"
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,85 @@
1
+ module Sunspot
2
+ module Query #:nodoc:
3
+ class CommonQuery
4
+ def initialize(types)
5
+ @scope = Scope.new
6
+ @sort = SortComposite.new
7
+ @components = [@scope, @sort]
8
+ if types.length == 1
9
+ @scope.add_positive_restriction(TypeField.instance, Restriction::EqualTo, types.first)
10
+ else
11
+ @scope.add_positive_restriction(TypeField.instance, Restriction::AnyOf, types)
12
+ end
13
+ end
14
+
15
+ def solr_parameter_adjustment=(block)
16
+ @parameter_adjustment = block
17
+ end
18
+
19
+ def add_sort(sort)
20
+ @sort << sort
21
+ end
22
+
23
+ def add_field_facet(facet)
24
+ @components << facet
25
+ facet
26
+ end
27
+
28
+ def add_query_facet(facet)
29
+ @components << facet
30
+ facet
31
+ end
32
+
33
+ def add_function(function)
34
+ @components << function
35
+ function
36
+ end
37
+
38
+ def paginate(page, per_page)
39
+ if @pagination
40
+ @pagination.page = page
41
+ @pagination.per_page = per_page
42
+ else
43
+ @components << @pagination = Pagination.new(page, per_page)
44
+ end
45
+ end
46
+
47
+ def to_params
48
+ params = {}
49
+ @components.each do |component|
50
+ Sunspot::Util.deep_merge!(params, component.to_params)
51
+ end
52
+ @parameter_adjustment.call(params) if @parameter_adjustment
53
+ params[:q] ||= '*:*'
54
+ params
55
+ end
56
+
57
+ def [](key)
58
+ to_params[key.to_sym]
59
+ end
60
+
61
+ def page
62
+ @pagination.page if @pagination
63
+ end
64
+
65
+ def per_page
66
+ @pagination.per_page if @pagination
67
+ end
68
+
69
+
70
+ private
71
+
72
+ #
73
+ # If we have a single fulltext query, merge is normally. If there are
74
+ # multiple nested queries, serialize them as `_query_` subqueries.
75
+ #
76
+ def merge_fulltext(params)
77
+ return nil if @fulltexts.empty?
78
+ return Sunspot::Util.deep_merge!(params, @fulltexts.first.to_params) if @fulltexts.length == 1
79
+ subqueries = @fulltexts.map {|fulltext| fulltext.to_subquery }.join(' ')
80
+ Sunspot::Util.deep_merge!(params, {:q => subqueries})
81
+ end
82
+
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,36 @@
1
+ module Sunspot
2
+ module Query
3
+ class CompositeFulltext
4
+ def initialize
5
+ @components = []
6
+ end
7
+
8
+ def add(keywords)
9
+ @components << dismax = Dismax.new(keywords)
10
+ dismax
11
+ end
12
+
13
+ def add_location(field, lat, lng, options)
14
+ @components << location = Geo.new(field, lat, lng, options)
15
+ location
16
+ end
17
+
18
+ def to_params
19
+ case @components.length
20
+ when 0
21
+ {}
22
+ when 1
23
+ @components.first.to_params.merge(:fl => '* score')
24
+ else
25
+ to_subqueries.merge(:fl => '* score')
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def to_subqueries
32
+ { :q => @components.map { |dismax| dismax.to_subquery }.join(' ') }
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,206 @@
1
+ module Sunspot
2
+ module Query
3
+ module Connective #:nodoc:all
4
+ #
5
+ # Base class for connectives (conjunctions and disjunctions).
6
+ #
7
+ class Abstract
8
+ include Filter
9
+
10
+ def initialize(negated = false) #:nodoc:
11
+ @negated = negated
12
+ @components = []
13
+ end
14
+
15
+ #
16
+ # Add a restriction to the connective.
17
+ #
18
+ def add_restriction(negated, field, restriction_type, *value)
19
+ add_component(restriction_type.new(negated, field, *value))
20
+ end
21
+
22
+ #
23
+ # Add a shorthand restriction; the restriction type is determined by
24
+ # the value.
25
+ #
26
+ def add_shorthand_restriction(negated, field, value)
27
+ restriction_type =
28
+ case value
29
+ when Array then Restriction::AnyOf
30
+ when Range then Restriction::Between
31
+ else Restriction::EqualTo
32
+ end
33
+ add_restriction(negated, field, restriction_type, value)
34
+ end
35
+
36
+ #
37
+ # Add a positive restriction. The restriction will match all
38
+ # documents who match the terms fo the restriction.
39
+ #
40
+ def add_positive_restriction(field, restriction_type, value)
41
+ add_restriction(false, field, restriction_type, value)
42
+ end
43
+
44
+ #
45
+ # Add a positive shorthand restriction (see add_shorthand_restriction)
46
+ #
47
+ def add_positive_shorthand_restriction(field, value)
48
+ add_shorthand_restriction(false, field, value)
49
+ end
50
+
51
+ #
52
+ # Add a negated restriction. The added restriction will match all
53
+ # documents who do not match the terms of the restriction.
54
+ #
55
+ def add_negated_restriction(field, restriction_type, value)
56
+ add_restriction(true, field, restriction_type, value)
57
+ end
58
+
59
+ #
60
+ # Add a negated shorthand restriction (see add_shorthand_restriction)
61
+ #
62
+ def add_negated_shorthand_restriction(field, value)
63
+ add_shorthand_restriction(true, field, value)
64
+ end
65
+
66
+ #
67
+ # Add a new conjunction and return it.
68
+ #
69
+ def add_conjunction
70
+ add_component(Conjunction.new)
71
+ end
72
+
73
+ #
74
+ # Add a new disjunction and return it.
75
+ #
76
+ def add_disjunction
77
+ add_component(Disjunction.new)
78
+ end
79
+
80
+ #
81
+ # Add an arbitrary component to the conjunction, and return it.
82
+ # The component must respond to #to_boolean_phrase
83
+ #
84
+ def add_component(component)
85
+ @components << component
86
+ component
87
+ end
88
+
89
+ #
90
+ # Express the connective as a Lucene boolean phrase.
91
+ #
92
+ def to_boolean_phrase #:nodoc:
93
+ unless @components.empty?
94
+ phrase =
95
+ if @components.length == 1
96
+ @components.first.to_boolean_phrase
97
+ else
98
+ component_phrases = @components.map do |component|
99
+ component.to_boolean_phrase
100
+ end
101
+ "(#{component_phrases.join(" #{connector} ")})"
102
+ end
103
+ if negated?
104
+ "-#{phrase}"
105
+ else
106
+ phrase
107
+ end
108
+ end
109
+ end
110
+
111
+ #
112
+ # Connectives can be negated during the process of denormalization that
113
+ # is performed when a disjunction contains a negated component. This
114
+ # method conforms to the duck type for all boolean query components.
115
+ #
116
+ def negated?
117
+ @negated
118
+ end
119
+
120
+ #
121
+ # Returns a new connective that's a negated version of this one.
122
+ #
123
+ def negate
124
+ negated = self.class.new(!negated?)
125
+ @components.each do |component|
126
+ negated.add_component(component)
127
+ end
128
+ negated
129
+ end
130
+ end
131
+
132
+ #
133
+ # Disjunctions combine their components with an OR operator.
134
+ #
135
+ class Disjunction < Abstract
136
+ class <<self
137
+ def inverse
138
+ Conjunction
139
+ end
140
+ end
141
+
142
+ #
143
+ # Express this disjunction as a Lucene boolean phrase
144
+ #
145
+ def to_boolean_phrase
146
+ if @components.any? { |component| component.negated? }
147
+ denormalize.to_boolean_phrase
148
+ else
149
+ super
150
+ end
151
+ end
152
+
153
+ #
154
+ # No-op - this is already a disjunction
155
+ #
156
+ def add_disjunction
157
+ self
158
+ end
159
+
160
+ private
161
+
162
+ def connector
163
+ 'OR'
164
+ end
165
+
166
+ #
167
+ # If a disjunction contains negated components, it must be
168
+ # "denormalized", because the Lucene parser interprets any negated
169
+ # boolean phrase using AND semantics (this isn't a bug, it's just a
170
+ # subtlety of how Lucene parses queries). So, per DeMorgan's law we
171
+ # create a negated conjunction and add to it all of our components,
172
+ # negated themselves, which creates a query whose Lucene semantics are
173
+ # in line with our intentions.
174
+ #
175
+ def denormalize
176
+ denormalized = self.class.inverse.new(!negated?)
177
+ @components.each do |component|
178
+ denormalized.add_component(component.negate)
179
+ end
180
+ denormalized
181
+ end
182
+ end
183
+
184
+ #
185
+ # Conjunctions combine their components with an AND operator.
186
+ #
187
+ class Conjunction < Abstract
188
+ class <<self
189
+ def inverse
190
+ Disjunction
191
+ end
192
+ end
193
+
194
+ def add_conjunction
195
+ self
196
+ end
197
+
198
+ private
199
+
200
+ def connector
201
+ 'AND'
202
+ end
203
+ end
204
+ end
205
+ end
206
+ end
@@ -0,0 +1,14 @@
1
+ module Sunspot
2
+ module Query
3
+ class DateFieldFacet < AbstractFieldFacet
4
+ def to_params
5
+ params = super
6
+ params[:"facet.date"] = [@field.indexed_name]
7
+ params[qualified_param('date.start')] = @field.to_indexed(@options[:time_range].first)
8
+ params[qualified_param('date.end')] = @field.to_indexed(@options[:time_range].last)
9
+ params[qualified_param('date.gap')] = "+#{@options[:time_interval] || 86400}SECONDS"
10
+ params
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,128 @@
1
+ module Sunspot
2
+ module Query
3
+
4
+ #
5
+ # Solr full-text queries use Solr's DisMaxRequestHandler, a search handler
6
+ # designed to process user-entered phrases, and search for individual
7
+ # words across a union of several fields.
8
+ #
9
+ class Dismax
10
+ attr_writer :minimum_match, :phrase_slop, :query_phrase_slop, :tie
11
+
12
+ def initialize(keywords)
13
+ @keywords = keywords
14
+ @fulltext_fields = {}
15
+ @boost_queries = []
16
+ @boost_functions = []
17
+ @highlights = []
18
+ end
19
+
20
+ #
21
+ # The query as Solr parameters
22
+ #
23
+ def to_params
24
+ params = { :q => @keywords }
25
+ params[:fl] = '* score'
26
+ params[:qf] = @fulltext_fields.values.map { |field| field.to_boosted_field }.join(' ')
27
+ params[:defType] = 'dismax'
28
+ if @phrase_fields
29
+ params[:pf] = @phrase_fields.map { |field| field.to_boosted_field }.join(' ')
30
+ end
31
+ unless @boost_queries.empty?
32
+ params[:bq] = @boost_queries.map do |boost_query|
33
+ boost_query.to_boolean_phrase
34
+ end
35
+ end
36
+ unless @boost_functions.empty?
37
+ params[:bf] = @boost_functions.map do |boost_function|
38
+ boost_function.to_s
39
+ end
40
+ end
41
+ if @minimum_match
42
+ params[:mm] = @minimum_match
43
+ end
44
+ if @phrase_slop
45
+ params[:ps] = @phrase_slop
46
+ end
47
+ if @query_phrase_slop
48
+ params[:qs] = @query_phrase_slop
49
+ end
50
+ if @tie
51
+ params[:tie] = @tie
52
+ end
53
+ @highlights.each do |highlight|
54
+ Sunspot::Util.deep_merge!(params, highlight.to_params)
55
+ end
56
+ params
57
+ end
58
+
59
+ #
60
+ # Serialize the query as a Solr nested subquery.
61
+ #
62
+ def to_subquery
63
+ params = self.to_params
64
+ params.delete :defType
65
+ params.delete :fl
66
+ keywords = params.delete(:q)
67
+ options = params.map { |key, value| "#{key}='#{escape_quotes(value)}'"}.join(' ')
68
+ "_query_:\"{!dismax #{options}}#{escape_quotes(keywords)}\""
69
+ end
70
+
71
+ #
72
+ # Assign a new boost query and return it.
73
+ #
74
+ def create_boost_query(factor)
75
+ @boost_queries << boost_query = BoostQuery.new(factor)
76
+ boost_query
77
+ end
78
+
79
+ #
80
+ # Add a boost function
81
+ #
82
+ def add_boost_function(function_query)
83
+ @boost_functions << function_query
84
+ end
85
+
86
+ #
87
+ # Add a fulltext field to be searched, with optional boost.
88
+ #
89
+ def add_fulltext_field(field, boost = nil)
90
+ @fulltext_fields[field.indexed_name] = TextFieldBoost.new(field, boost)
91
+ end
92
+
93
+ #
94
+ # Add a phrase field for extra boost.
95
+ #
96
+ def add_phrase_field(field, boost = nil)
97
+ @phrase_fields ||= []
98
+ @phrase_fields << TextFieldBoost.new(field, boost)
99
+ end
100
+
101
+ #
102
+ # Set highlighting options for the query. If fields is empty, the
103
+ # Highlighting object won't pass field names at all, which means
104
+ # the dismax's :qf parameter will be used by Solr.
105
+ #
106
+ def add_highlight(fields=[], options={})
107
+ @highlights << Highlighting.new(fields, options)
108
+ end
109
+
110
+ #
111
+ # Determine if a given field is being searched. Used by DSL to avoid
112
+ # overwriting boost parameters when injecting defaults.
113
+ #
114
+ def has_fulltext_field?(field)
115
+ @fulltext_fields.has_key?(field.indexed_name)
116
+ end
117
+
118
+
119
+ private
120
+
121
+ def escape_quotes(value)
122
+ return value unless value.is_a? String
123
+ value.gsub(/(['"])/, '\\\\\1')
124
+ end
125
+
126
+ end
127
+ end
128
+ end