xapit 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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