xapit 0.1.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 (180) hide show
  1. data/LICENSE +20 -0
  2. data/Manifest +178 -0
  3. data/README.rdoc +183 -0
  4. data/Rakefile +15 -0
  5. data/TODO +23 -0
  6. data/features/facets.feature +51 -0
  7. data/features/finding.feature +119 -0
  8. data/features/indexing.feature +41 -0
  9. data/features/step_definitions/common_steps.rb +7 -0
  10. data/features/step_definitions/xapit_steps.rb +117 -0
  11. data/features/support/env.rb +7 -0
  12. data/features/support/xapit_helpers.rb +27 -0
  13. data/init.rb +3 -0
  14. data/install.rb +9 -0
  15. data/lib/xapit.rb +39 -0
  16. data/lib/xapit/collection.rb +165 -0
  17. data/lib/xapit/config.rb +83 -0
  18. data/lib/xapit/facet.rb +59 -0
  19. data/lib/xapit/facet_blueprint.rb +59 -0
  20. data/lib/xapit/facet_option.rb +56 -0
  21. data/lib/xapit/index_blueprint.rb +117 -0
  22. data/lib/xapit/indexers/abstract_indexer.rb +101 -0
  23. data/lib/xapit/indexers/classic_indexer.rb +27 -0
  24. data/lib/xapit/indexers/simple_indexer.rb +31 -0
  25. data/lib/xapit/membership.rb +103 -0
  26. data/lib/xapit/query.rb +62 -0
  27. data/lib/xapit/query_parsers/abstract_query_parser.rb +115 -0
  28. data/lib/xapit/query_parsers/classic_query_parser.rb +19 -0
  29. data/lib/xapit/query_parsers/simple_query_parser.rb +75 -0
  30. data/spec/spec_helper.rb +15 -0
  31. data/spec/tmp/xapdb/flintlock +0 -0
  32. data/spec/tmp/xapdb/iamflint +0 -0
  33. data/spec/tmp/xapdb/postlist.DB +0 -0
  34. data/spec/tmp/xapdb/postlist.baseA +0 -0
  35. data/spec/tmp/xapdb/postlist.baseB +0 -0
  36. data/spec/tmp/xapdb/record.DB +0 -0
  37. data/spec/tmp/xapdb/record.baseA +0 -0
  38. data/spec/tmp/xapdb/record.baseB +0 -0
  39. data/spec/tmp/xapdb/spelling.DB +0 -0
  40. data/spec/tmp/xapdb/spelling.baseA +0 -0
  41. data/spec/tmp/xapdb/spelling.baseB +0 -0
  42. data/spec/tmp/xapdb/termlist.DB +0 -0
  43. data/spec/tmp/xapdb/termlist.baseA +0 -0
  44. data/spec/tmp/xapdb/termlist.baseB +0 -0
  45. data/spec/tmp/xapian_db/flintlock +0 -0
  46. data/spec/tmp/xapian_db/iamflint +0 -0
  47. data/spec/tmp/xapian_db/postlist.DB +0 -0
  48. data/spec/tmp/xapian_db/postlist.baseA +0 -0
  49. data/spec/tmp/xapian_db/record.DB +0 -0
  50. data/spec/tmp/xapian_db/record.baseA +0 -0
  51. data/spec/tmp/xapian_db/termlist.DB +0 -0
  52. data/spec/tmp/xapian_db/termlist.baseA +0 -0
  53. data/spec/tmp/xapiandab/flintlock +0 -0
  54. data/spec/tmp/xapiandab/iamflint +0 -0
  55. data/spec/tmp/xapiandab/postlist.DB +0 -0
  56. data/spec/tmp/xapiandab/postlist.baseA +0 -0
  57. data/spec/tmp/xapiandab/postlist.baseB +0 -0
  58. data/spec/tmp/xapiandab/record.DB +0 -0
  59. data/spec/tmp/xapiandab/record.baseA +0 -0
  60. data/spec/tmp/xapiandab/record.baseB +0 -0
  61. data/spec/tmp/xapiandab/spelling.DB +0 -0
  62. data/spec/tmp/xapiandab/spelling.baseA +0 -0
  63. data/spec/tmp/xapiandab/spelling.baseB +0 -0
  64. data/spec/tmp/xapiandab/termlist.DB +0 -0
  65. data/spec/tmp/xapiandab/termlist.baseA +0 -0
  66. data/spec/tmp/xapiandab/termlist.baseB +0 -0
  67. data/spec/tmp/xapiandatab/flintlock +0 -0
  68. data/spec/tmp/xapiandatab/iamflint +0 -0
  69. data/spec/tmp/xapiandatab/postlist.DB +0 -0
  70. data/spec/tmp/xapiandatab/postlist.baseA +0 -0
  71. data/spec/tmp/xapiandatab/postlist.baseB +0 -0
  72. data/spec/tmp/xapiandatab/record.DB +0 -0
  73. data/spec/tmp/xapiandatab/record.baseA +0 -0
  74. data/spec/tmp/xapiandatab/record.baseB +0 -0
  75. data/spec/tmp/xapiandatab/spelling.DB +0 -0
  76. data/spec/tmp/xapiandatab/spelling.baseA +0 -0
  77. data/spec/tmp/xapiandatab/spelling.baseB +0 -0
  78. data/spec/tmp/xapiandatab/termlist.DB +0 -0
  79. data/spec/tmp/xapiandatab/termlist.baseA +0 -0
  80. data/spec/tmp/xapiandatab/termlist.baseB +0 -0
  81. data/spec/tmp/xapiandataba/flintlock +0 -0
  82. data/spec/tmp/xapiandataba/iamflint +0 -0
  83. data/spec/tmp/xapiandataba/postlist.DB +0 -0
  84. data/spec/tmp/xapiandataba/postlist.baseA +0 -0
  85. data/spec/tmp/xapiandataba/postlist.baseB +0 -0
  86. data/spec/tmp/xapiandataba/record.DB +0 -0
  87. data/spec/tmp/xapiandataba/record.baseA +0 -0
  88. data/spec/tmp/xapiandataba/record.baseB +0 -0
  89. data/spec/tmp/xapiandataba/spelling.DB +0 -0
  90. data/spec/tmp/xapiandataba/spelling.baseA +0 -0
  91. data/spec/tmp/xapiandataba/spelling.baseB +0 -0
  92. data/spec/tmp/xapiandataba/termlist.DB +0 -0
  93. data/spec/tmp/xapiandataba/termlist.baseA +0 -0
  94. data/spec/tmp/xapiandataba/termlist.baseB +0 -0
  95. data/spec/tmp/xapiandatabas/flintlock +0 -0
  96. data/spec/tmp/xapiandatabas/iamflint +0 -0
  97. data/spec/tmp/xapiandatabas/postlist.DB +0 -0
  98. data/spec/tmp/xapiandatabas/postlist.baseA +0 -0
  99. data/spec/tmp/xapiandatabas/record.DB +0 -0
  100. data/spec/tmp/xapiandatabas/record.baseA +0 -0
  101. data/spec/tmp/xapiandatabas/termlist.DB +0 -0
  102. data/spec/tmp/xapiandatabas/termlist.baseA +0 -0
  103. data/spec/tmp/xapiandatb/flintlock +0 -0
  104. data/spec/tmp/xapiandatb/iamflint +0 -0
  105. data/spec/tmp/xapiandatb/postlist.DB +0 -0
  106. data/spec/tmp/xapiandatb/postlist.baseA +0 -0
  107. data/spec/tmp/xapiandatb/postlist.baseB +0 -0
  108. data/spec/tmp/xapiandatb/record.DB +0 -0
  109. data/spec/tmp/xapiandatb/record.baseA +0 -0
  110. data/spec/tmp/xapiandatb/record.baseB +0 -0
  111. data/spec/tmp/xapiandatb/spelling.DB +0 -0
  112. data/spec/tmp/xapiandatb/spelling.baseA +0 -0
  113. data/spec/tmp/xapiandatb/spelling.baseB +0 -0
  114. data/spec/tmp/xapiandatb/termlist.DB +0 -0
  115. data/spec/tmp/xapiandatb/termlist.baseA +0 -0
  116. data/spec/tmp/xapiandatb/termlist.baseB +0 -0
  117. data/spec/tmp/xapiandbase/flintlock +0 -0
  118. data/spec/tmp/xapiandbase/iamflint +0 -0
  119. data/spec/tmp/xapiandbase/postlist.DB +0 -0
  120. data/spec/tmp/xapiandbase/postlist.baseA +0 -0
  121. data/spec/tmp/xapiandbase/postlist.baseB +0 -0
  122. data/spec/tmp/xapiandbase/record.DB +0 -0
  123. data/spec/tmp/xapiandbase/record.baseA +0 -0
  124. data/spec/tmp/xapiandbase/record.baseB +0 -0
  125. data/spec/tmp/xapiandbase/spelling.DB +0 -0
  126. data/spec/tmp/xapiandbase/spelling.baseA +0 -0
  127. data/spec/tmp/xapiandbase/spelling.baseB +0 -0
  128. data/spec/tmp/xapiandbase/termlist.DB +0 -0
  129. data/spec/tmp/xapiandbase/termlist.baseA +0 -0
  130. data/spec/tmp/xapiandbase/termlist.baseB +0 -0
  131. data/spec/xapit/collection_spec.rb +153 -0
  132. data/spec/xapit/config_spec.rb +48 -0
  133. data/spec/xapit/facet_blueprint_spec.rb +29 -0
  134. data/spec/xapit/facet_option_spec.rb +80 -0
  135. data/spec/xapit/facet_spec.rb +73 -0
  136. data/spec/xapit/index_blueprint_spec.rb +60 -0
  137. data/spec/xapit/indexers/abstract_indexer_spec.rb +74 -0
  138. data/spec/xapit/indexers/classic_indexer_spec.rb +26 -0
  139. data/spec/xapit/indexers/simple_indexer_spec.rb +53 -0
  140. data/spec/xapit/membership_spec.rb +39 -0
  141. data/spec/xapit/query_parsers/abstract_query_parser_spec.rb +23 -0
  142. data/spec/xapit/query_parsers/classic_query_parser_spec.rb +15 -0
  143. data/spec/xapit/query_parsers/simple_query_parser_spec.rb +86 -0
  144. data/spec/xapit/query_spec.rb +41 -0
  145. data/spec/xapit_member.rb +32 -0
  146. data/tasks/spec.rb +9 -0
  147. data/tasks/xapit.rake +9 -0
  148. data/tmp/xapiandatabase/flintlock +0 -0
  149. data/tmp/xapiandatabase/iamflint +0 -0
  150. data/tmp/xapiandatabase/postlist.DB +0 -0
  151. data/tmp/xapiandatabase/postlist.baseA +0 -0
  152. data/tmp/xapiandatabase/postlist.baseB +0 -0
  153. data/tmp/xapiandatabase/record.DB +0 -0
  154. data/tmp/xapiandatabase/record.baseA +0 -0
  155. data/tmp/xapiandatabase/record.baseB +0 -0
  156. data/tmp/xapiandatabase/spelling.DB +0 -0
  157. data/tmp/xapiandatabase/spelling.baseA +0 -0
  158. data/tmp/xapiandatabase/spelling.baseB +0 -0
  159. data/tmp/xapiandatabase/termlist.DB +0 -0
  160. data/tmp/xapiandatabase/termlist.baseA +0 -0
  161. data/tmp/xapiandatabase/termlist.baseB +0 -0
  162. data/tmp/xapiandatabase/value.baseB +0 -0
  163. data/tmp/xapiandb/flintlock +0 -0
  164. data/tmp/xapiandb/iamflint +0 -0
  165. data/tmp/xapiandb/postlist.DB +0 -0
  166. data/tmp/xapiandb/postlist.baseA +0 -0
  167. data/tmp/xapiandb/postlist.baseB +0 -0
  168. data/tmp/xapiandb/record.DB +0 -0
  169. data/tmp/xapiandb/record.baseA +0 -0
  170. data/tmp/xapiandb/record.baseB +0 -0
  171. data/tmp/xapiandb/spelling.DB +0 -0
  172. data/tmp/xapiandb/spelling.baseA +0 -0
  173. data/tmp/xapiandb/spelling.baseB +0 -0
  174. data/tmp/xapiandb/termlist.DB +0 -0
  175. data/tmp/xapiandb/termlist.baseA +0 -0
  176. data/tmp/xapiandb/termlist.baseB +0 -0
  177. data/tmp/xapiandb/value.baseB +0 -0
  178. data/uninstall.rb +5 -0
  179. data/xapit.gemspec +30 -0
  180. metadata +257 -0
@@ -0,0 +1,83 @@
1
+ module Xapit
2
+ # Singleton class for storing Xapit configuration settings. Currently this only includes the database path.
3
+ class Config
4
+ class << self
5
+ attr_reader :options
6
+
7
+ # Setup configuration options. The following options are supported.
8
+ #
9
+ # <tt>:database_path</tt>: Where the database is stored.
10
+ # <tt>:stemming</tt>: The language to use for stemming, defaults to "english".
11
+ # <tt>:spelling</tt>: True or false to enable/disable spelling, defaults to true.
12
+ # <tt>:indexer</tt>: Class to handle the indexing, defaults to SimpleIndexer.
13
+ # <tt>:query_parser</tt>: Class to handle the parsing, defaults to ClassicQueryParser.
14
+ # <tt>:breadcrumb_facets</tt>: Use breadcrumb mode for applied facets. See Collection#applied_facet_options for details.
15
+ #
16
+ def setup(options = {})
17
+ if @options && options[:database_path] != @options[:database_path]
18
+ @database = nil
19
+ @writable_database = nil
20
+ end
21
+ @options = options.reverse_merge(default_options)
22
+ end
23
+
24
+ def default_options
25
+ {
26
+ :indexer => SimpleIndexer,
27
+ :query_parser => ClassicQueryParser,
28
+ :spelling => true,
29
+ :stemming => "english"
30
+ }
31
+ end
32
+
33
+ # See if setup options are already set.
34
+ def setup?
35
+ @options
36
+ end
37
+
38
+ # The configured path to the database.
39
+ def path
40
+ @options[:database_path]
41
+ end
42
+
43
+ def query_parser
44
+ @options[:query_parser]
45
+ end
46
+
47
+ def indexer
48
+ @options[:indexer]
49
+ end
50
+
51
+ def spelling?
52
+ @options[:spelling]
53
+ end
54
+
55
+ def stemming
56
+ @options[:stemming]
57
+ end
58
+
59
+ def breadcrumb_facets?
60
+ @options[:breadcrumb_facets]
61
+ end
62
+
63
+ # Fetch Xapian::Database object at configured path. Database is stored in memory.
64
+ def database
65
+ @writable_database || (@database ||= Xapian::Database.new(path))
66
+ end
67
+
68
+ # Fetch Xapian::WritableDatabase object at configured path. Database is stored in memory.
69
+ # Creates the database directory if needed.
70
+ def writable_database
71
+ FileUtils.mkdir_p(File.dirname(path)) unless File.exist?(File.dirname(path))
72
+ @writable_database ||= Xapian::WritableDatabase.new(path, Xapian::DB_CREATE_OR_OPEN)
73
+ end
74
+
75
+ # Removes the configured database file and clears the stored one in memory.
76
+ def remove_database # this can be a bit dangers, maybe do some checking here first?
77
+ FileUtils.rm_rf(path) if File.exist? path
78
+ @database = nil
79
+ @writable_database = nil
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,59 @@
1
+ module Xapit
2
+ # Facets allow users to further filter the result set based on certain attributes.
3
+ # You should fetch facets by calling "facets" on a Xapit::Collection search result.
4
+ #
5
+ # <% for facet in @articles.facets %>
6
+ # <%= facet.name %>
7
+ # <% for option in facet.options %>
8
+ # <%= link_to option.name, :overwrite_params => { :facets => option } %>
9
+ # (<%= option.count %>)
10
+ # <% end %>
11
+ # <% end %>
12
+ #
13
+ # See Xapit::FacetBlueprint for details on how to index a facet.
14
+ class Facet
15
+ attr_accessor :existing_facet_identifiers
16
+
17
+ def initialize(blueprint, query, existing_facet_identifiers)
18
+ @blueprint = blueprint
19
+ @query = query.dup
20
+ @existing_facet_identifiers = existing_facet_identifiers
21
+ end
22
+
23
+ # Xapit::FacetOption objects for this facet. This only lists the ones which match the current query.
24
+ def options
25
+ matching_identifiers.map do |identifier, count|
26
+ option = FacetOption.find(identifier)
27
+ option.count = count
28
+ option.existing_facet_identifiers = @existing_facet_identifiers
29
+ option
30
+ end.sort_by(&:name)
31
+ end
32
+
33
+ def matching_identifiers
34
+ result = {}
35
+ matches.each do |match|
36
+ class_name, id = match.document.data.split('-')
37
+ record = class_name.constantize.find(id)
38
+ @blueprint.identifiers_for(record).each do |identifier|
39
+ unless existing_facet_identifiers.include? identifier
40
+ result[identifier] ||= 0
41
+ result[identifier] += (match.collapse_count + 1)
42
+ end
43
+ end
44
+ end
45
+ result
46
+ end
47
+
48
+ # The name of the facet. See Xapit::FacetBlueprint for details.
49
+ def name
50
+ @blueprint.name
51
+ end
52
+
53
+ private
54
+
55
+ def matches
56
+ @query.matches(:offset => 0, :limit => 1000, :collapse_key => @blueprint.position)
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,59 @@
1
+ module Xapit
2
+ # A facet blueprint keeps track of the settings for indexing a given facet. You can specify a custom
3
+ # name for a given facet by providing a second argument when defining.
4
+ #
5
+ # xapit do |index|
6
+ # index.facet :category_name, "Category"
7
+ # end
8
+ #
9
+ # Multiple facet values are supported for a single record. All you need to do is return an array of
10
+ # values instead of a single string.
11
+ #
12
+ # def category_names
13
+ # categories.map(&:name) # => ["Toys", "Clothing"]
14
+ # end
15
+ #
16
+ class FacetBlueprint
17
+ attr_reader :member_class
18
+ attr_reader :position
19
+ attr_reader :attribute
20
+
21
+ def initialize(member_class, position, attribute, custom_name = nil)
22
+ @member_class = member_class
23
+ @position = position
24
+ @attribute = attribute
25
+ @custom_name = custom_name
26
+ end
27
+
28
+ def identifiers_for(member)
29
+ values_for(member).map do |value|
30
+ Digest::SHA1.hexdigest(@attribute.to_s + value)[0..6]
31
+ end
32
+ end
33
+
34
+ # The name of the facet. This will return the custom name if given while setting up the index,
35
+ # or default to humanizing the attribute name.
36
+ def name
37
+ @custom_name || @attribute.to_s.humanize
38
+ end
39
+
40
+ def save_facet_options_for(member)
41
+ values_for(member).map do |value|
42
+ option = FacetOption.new(member.class.name, @attribute.to_s, value)
43
+ option.save
44
+ end
45
+ end
46
+
47
+ private
48
+
49
+ def values_for(member)
50
+ value = member.send(@attribute)
51
+ if value.kind_of? Array
52
+ value.map(&:to_s).reject(&:empty?)
53
+ else
54
+ [value].map(&:to_s).reject(&:empty?)
55
+ end
56
+ end
57
+ end
58
+ end
59
+
@@ -0,0 +1,56 @@
1
+ module Xapit
2
+ # A facet option is a specific value or choice for a facet. See Xapit::Facet for details on how to use it.
3
+ class FacetOption
4
+ attr_accessor :facet, :name, :existing_facet_identifiers, :count
5
+
6
+ # Fetch a facet option given an id.
7
+ def self.find(id)
8
+ match = Query.new("Q#{name}-#{id}").matches(:offset => 0, :limit => 1).first
9
+ if match.nil?
10
+ raise "Unable to find facet option for #{id}."
11
+ else
12
+ class_name, facet_attribute, name = match.document.data.split('|||')
13
+ new(class_name.to_s, facet_attribute.to_s, name.to_s)
14
+ end
15
+ end
16
+
17
+ # See if the given facet option exists with this id.
18
+ def self.exist?(id)
19
+ Query.new("Q#{name}-#{id}").count >= 1
20
+ end
21
+
22
+ def initialize(class_name, facet_attribute, name)
23
+ @facet = class_name.constantize.xapit_facet_blueprint(facet_attribute) if class_name && facet_attribute
24
+ @name = name
25
+ end
26
+
27
+ def identifier
28
+ Digest::SHA1.hexdigest(facet.attribute.to_s + name)[0..6]
29
+ end
30
+
31
+ # Saves the given facet option to the database if it hasn't been already.
32
+ def save
33
+ unless self.class.exist?(identifier)
34
+ doc = Xapian::Document.new
35
+ doc.data = [facet.member_class.name, facet.attribute, name].join("|||")
36
+ doc.add_term("Q#{self.class.name}-#{identifier}")
37
+ Xapit::Config.writable_database.add_document(doc)
38
+ end
39
+ end
40
+
41
+ # Converts the facet to be used in a URL. It adds to the existing ones for convenience.
42
+ # If this facet option is currently selected, then this will return all selected facets except
43
+ # this one. This conveniently allows you to use this as both an "add this facet" and "remove this facet" link.
44
+ def to_param
45
+ if existing_facet_identifiers.include? identifier
46
+ if Xapit::Config.breadcrumb_facets?
47
+ existing_facet_identifiers[0..existing_facet_identifiers.index(identifier)].join('-')
48
+ else
49
+ (existing_facet_identifiers - [identifier]).join('-')
50
+ end
51
+ else
52
+ (existing_facet_identifiers + [identifier]).join('-')
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,117 @@
1
+ module Xapit
2
+ # This is the object used in the block of the xapit method in Xapit::Membership. It keeps track of the
3
+ # index settings for a given class. It also provides some indexing functionality.
4
+ class IndexBlueprint
5
+ attr_reader :text_attributes
6
+ attr_reader :field_attributes
7
+ attr_reader :sortable_attributes
8
+ attr_reader :facets
9
+
10
+ # Indexes all classes known to have an index blueprint defined.
11
+ def self.index_all
12
+ load_models
13
+ @@instances.each do |member_class, blueprint|
14
+ yield(member_class) if block_given?
15
+ blueprint.index_all
16
+ end
17
+ end
18
+
19
+ def initialize(member_class, *args)
20
+ @member_class = member_class
21
+ @args = args
22
+ @text_attributes = {}
23
+ @field_attributes = []
24
+ @sortable_attributes = []
25
+ @facets = []
26
+ @@instances ||= {}
27
+ @@instances[member_class] = self # TODO make this thread safe
28
+ @indexer = SimpleIndexer.new(self)
29
+ end
30
+
31
+ # Adds a text attribute. Each word in the text will be indexed as a separate term allowing full text searching.
32
+ # Text terms are what is searched by the primary string in a search query.
33
+ #
34
+ # Article.search("kite")
35
+ #
36
+ # You can specify a :weight option to give a text attribute more importance. This will cause search terms matching
37
+ # that attribute to have a higher rank. The default weight is 1. Decimal (0.5) weight values are not supported.
38
+ #
39
+ # index.text :name, :weight => 10
40
+ #
41
+ def text(*attributes, &proc)
42
+ options = attributes.extract_options!
43
+ options[:proc] ||= proc
44
+ attributes.each do |attribute|
45
+ @text_attributes[attribute] = options
46
+ end
47
+ end
48
+
49
+ # Adds a field attribute. Field terms are not split by word so it is not designed for full text search.
50
+ # Instead you can filter through a field using the :conditions hash in a search query.
51
+ #
52
+ # Article.search("", :conditions => { :priority => 5 })
53
+ #
54
+ # Multiple field values are supported if the given attribute is an array.
55
+ #
56
+ # def priority
57
+ # [3, 5] # will match priority search for 3 or 5
58
+ # end
59
+ #
60
+ def field(*attributes)
61
+ @field_attributes += attributes
62
+ end
63
+
64
+ # Adds a facet attribute. See Xapit::FacetBlueprint and Xapit::Facet for details.
65
+ def facet(*args, &block)
66
+ @facets << FacetBlueprint.new(@member_class, @facets.size, *args, &block)
67
+ end
68
+
69
+ # Adds a sortable attribute for use with the :order option in a search call.
70
+ def sortable(*attributes)
71
+ @sortable_attributes += attributes
72
+ end
73
+
74
+ # Indexes all records of this blueprint class. It does this using the ".find_each" method on the member class.
75
+ # You will likely want to call Xapit::Config.remove_database before this.
76
+ def index_all
77
+ @member_class.find_each(*@args) do |member|
78
+ @indexer.add_member(member)
79
+ end
80
+ end
81
+
82
+ def sortable_position_for(sortable_attribute)
83
+ index = sortable_attributes.map(&:to_s).index(sortable_attribute.to_s)
84
+ raise "Unable to find indexed sortable attribute \"#{sortable_attribute}\" in #{@member_class} sortable attributes: #{sortable_attributes.inspect}" if index.nil?
85
+ index + facets.size
86
+ end
87
+
88
+ private
89
+
90
+ # Make sure all models are loaded - without reloading any that
91
+ # ActiveRecord::Base is already aware of (otherwise we start to hit some
92
+ # messy dependencies issues).
93
+ #
94
+ # Taken from thinking-sphinx
95
+ def self.load_models
96
+ if defined? Rails
97
+ base = "#{Rails.root}/app/models/"
98
+ Dir["#{base}**/*.rb"].each do |file|
99
+ model_name = file.gsub(/^#{base}([\w_\/\\]+)\.rb/, '\1')
100
+
101
+ next if model_name.nil?
102
+ next if ::ActiveRecord::Base.send(:subclasses).detect { |model|
103
+ model.name == model_name
104
+ }
105
+
106
+ begin
107
+ model_name.camelize.constantize
108
+ rescue LoadError
109
+ model_name.gsub!(/.*[\/\\]/, '').nil? ? next : retry
110
+ rescue NameError
111
+ next
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,101 @@
1
+ module Xapit
2
+ class AbstractIndexer
3
+ def initialize(blueprint)
4
+ @blueprint = blueprint
5
+ end
6
+
7
+ def add_member(member)
8
+ database.add_document(document_for(member))
9
+ end
10
+
11
+ def document_for(member)
12
+ document = Xapian::Document.new
13
+ document.data = "#{member.class}-#{member.id}"
14
+ index_text_attributes(member, document)
15
+ index_terms(other_terms(member), document)
16
+ values(member).each_with_index do |value, index|
17
+ document.add_value(index, value)
18
+ end
19
+ save_facet_options_for(member)
20
+ document
21
+ end
22
+
23
+ def index_terms(terms, document)
24
+ terms.each do |term|
25
+ document.add_term(term)
26
+ database.add_spelling(term) if Config.spelling?
27
+ end
28
+ end
29
+
30
+ def index_text_attributes(member, document)
31
+ # to be overridden by subclass
32
+ end
33
+
34
+ def other_terms(member)
35
+ base_terms(member) + field_terms(member) + facet_terms(member)
36
+ end
37
+
38
+ def base_terms(member)
39
+ ["C#{member.class}", "Q#{member.class}-#{member.id}"]
40
+ end
41
+
42
+ def field_terms(member)
43
+ @blueprint.field_attributes.map do |name|
44
+ [member.send(name)].flatten.map do |value|
45
+ if value.kind_of? Time
46
+ value = value.to_i
47
+ elsif value.kind_of? Date
48
+ value = value.to_time.to_i
49
+ end
50
+ "X#{name}-#{value.to_s.downcase}"
51
+ end
52
+ end.flatten
53
+ end
54
+
55
+ def facet_terms(member)
56
+ @blueprint.facets.map do |facet|
57
+ facet.identifiers_for(member).map { |id| "F#{id}" }
58
+ end.flatten
59
+ end
60
+
61
+ # used primarily by search similar functionality
62
+ def text_terms(member) # REFACTORME some duplicaiton with simple indexer
63
+ @blueprint.text_attributes.map do |name, options|
64
+ content = member.send(name).to_s
65
+ if options[:proc]
66
+ options[:proc].call(content).reject(&:blank?).map(&:to_s).map(&:downcase)
67
+ else
68
+ content.scan(/\w+/u).map(&:downcase)
69
+ end
70
+ end.flatten
71
+ end
72
+
73
+ def values(member)
74
+ facet_values(member) + sortable_values(member)
75
+ end
76
+
77
+ def sortable_values(member)
78
+ @blueprint.sortable_attributes.map do |sortable|
79
+ member.send(sortable).to_s.downcase
80
+ end
81
+ end
82
+
83
+ def facet_values(member)
84
+ @blueprint.facets.map do |facet|
85
+ facet.identifiers_for(member).join("-")
86
+ end
87
+ end
88
+
89
+ def save_facet_options_for(member)
90
+ @blueprint.facets.each do |facet|
91
+ facet.save_facet_options_for(member)
92
+ end
93
+ end
94
+
95
+ private
96
+
97
+ def database
98
+ Config.writable_database
99
+ end
100
+ end
101
+ end