xapit 0.2.7 → 0.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 (105) hide show
  1. data/{CHANGELOG → CHANGELOG.rdoc} +7 -2
  2. data/Gemfile +19 -0
  3. data/LICENSE +4 -4
  4. data/README.rdoc +61 -108
  5. data/Rakefile +11 -10
  6. data/features/facets.feature +93 -82
  7. data/features/finding.feature +196 -138
  8. data/features/indexing.feature +35 -37
  9. data/features/remote_server.feature +10 -0
  10. data/features/step_definitions/xapit_steps.rb +53 -25
  11. data/features/suggestions.feature +20 -14
  12. data/features/support/env.rb +13 -6
  13. data/features/support/xapit_helpers.rb +8 -9
  14. data/lib/generators/xapit/install_generator.rb +14 -0
  15. data/lib/generators/xapit/templates/xapit.ru +6 -0
  16. data/lib/generators/xapit/templates/xapit.yml +11 -0
  17. data/lib/xapit.rb +106 -64
  18. data/lib/xapit/client/collection.rb +150 -0
  19. data/lib/xapit/client/facet.rb +11 -0
  20. data/lib/xapit/client/facet_option.rb +29 -0
  21. data/lib/xapit/client/index_builder.rb +67 -0
  22. data/lib/xapit/client/membership.rb +46 -0
  23. data/lib/xapit/client/model_adapters/abstract_model_adapter.rb +30 -0
  24. data/lib/xapit/client/model_adapters/active_record_adapter.rb +27 -0
  25. data/lib/xapit/client/model_adapters/default_model_adapter.rb +7 -0
  26. data/lib/xapit/client/railtie.rb +18 -0
  27. data/lib/xapit/client/remote_database.rb +21 -0
  28. data/lib/xapit/client/tasks.rb +18 -0
  29. data/lib/xapit/server/app.rb +27 -0
  30. data/lib/xapit/server/database.rb +47 -0
  31. data/lib/xapit/server/indexer.rb +138 -0
  32. data/lib/xapit/server/query.rb +240 -0
  33. data/spec/fixtures/blankdb/flintlock +0 -0
  34. data/spec/fixtures/blankdb/iamchert +1 -0
  35. data/spec/fixtures/blankdb/postlist.DB +0 -0
  36. data/spec/fixtures/blankdb/postlist.baseA +0 -0
  37. data/spec/fixtures/blankdb/record.DB +0 -0
  38. data/spec/fixtures/blankdb/record.baseA +0 -0
  39. data/spec/fixtures/blankdb/termlist.DB +0 -0
  40. data/spec/fixtures/blankdb/termlist.baseA +0 -0
  41. data/spec/fixtures/xapit.ru +13 -0
  42. data/spec/fixtures/xapit.yml +4 -0
  43. data/spec/spec_helper.rb +8 -9
  44. data/spec/support/spec_macros.rb +6 -0
  45. data/spec/{xapit_member.rb → support/xapit_member.rb} +14 -16
  46. data/spec/xapit/client/collection_spec.rb +63 -0
  47. data/spec/xapit/client/facet_option_spec.rb +26 -0
  48. data/spec/xapit/client/facet_spec.rb +13 -0
  49. data/spec/xapit/client/index_builder_spec.rb +66 -0
  50. data/spec/xapit/client/membership_spec.rb +43 -0
  51. data/spec/xapit/client/model_adapters/active_record_adapter_spec.rb +62 -0
  52. data/spec/xapit/client/model_adapters/default_model_adapter_spec.rb +7 -0
  53. data/spec/xapit/client/remote_database_spec.rb +19 -0
  54. data/spec/xapit/server/app_spec.rb +22 -0
  55. data/spec/xapit/server/database_spec.rb +37 -0
  56. data/spec/xapit/server/indexer_spec.rb +82 -0
  57. data/spec/xapit/server/query_spec.rb +43 -0
  58. data/spec/xapit/xapit_spec.rb +28 -0
  59. metadata +124 -93
  60. data/Manifest +0 -60
  61. data/features/sorting.feature +0 -29
  62. data/init.rb +0 -1
  63. data/install.rb +0 -8
  64. data/lib/xapit/adapters/abstract_adapter.rb +0 -47
  65. data/lib/xapit/adapters/active_record_adapter.rb +0 -20
  66. data/lib/xapit/adapters/data_mapper_adapter.rb +0 -10
  67. data/lib/xapit/collection.rb +0 -187
  68. data/lib/xapit/config.rb +0 -84
  69. data/lib/xapit/facet.rb +0 -67
  70. data/lib/xapit/facet_blueprint.rb +0 -59
  71. data/lib/xapit/facet_option.rb +0 -56
  72. data/lib/xapit/index_blueprint.rb +0 -147
  73. data/lib/xapit/indexers/abstract_indexer.rb +0 -116
  74. data/lib/xapit/indexers/classic_indexer.rb +0 -29
  75. data/lib/xapit/indexers/simple_indexer.rb +0 -38
  76. data/lib/xapit/membership.rb +0 -137
  77. data/lib/xapit/query.rb +0 -89
  78. data/lib/xapit/query_parsers/abstract_query_parser.rb +0 -174
  79. data/lib/xapit/query_parsers/classic_query_parser.rb +0 -29
  80. data/lib/xapit/query_parsers/simple_query_parser.rb +0 -75
  81. data/lib/xapit/rake_tasks.rb +0 -13
  82. data/rails_generators/xapit/USAGE +0 -13
  83. data/rails_generators/xapit/templates/setup_xapit.rb +0 -1
  84. data/rails_generators/xapit/templates/xapit.rake +0 -4
  85. data/rails_generators/xapit/xapit_generator.rb +0 -20
  86. data/spec/xapit/adapters/active_record_adapter_spec.rb +0 -31
  87. data/spec/xapit/adapters/data_mapper_adapter_spec.rb +0 -10
  88. data/spec/xapit/collection_spec.rb +0 -176
  89. data/spec/xapit/config_spec.rb +0 -62
  90. data/spec/xapit/facet_blueprint_spec.rb +0 -29
  91. data/spec/xapit/facet_option_spec.rb +0 -80
  92. data/spec/xapit/facet_spec.rb +0 -73
  93. data/spec/xapit/index_blueprint_spec.rb +0 -112
  94. data/spec/xapit/indexers/abstract_indexer_spec.rb +0 -111
  95. data/spec/xapit/indexers/classic_indexer_spec.rb +0 -35
  96. data/spec/xapit/indexers/simple_indexer_spec.rb +0 -69
  97. data/spec/xapit/membership_spec.rb +0 -55
  98. data/spec/xapit/query_parsers/abstract_query_parser_spec.rb +0 -60
  99. data/spec/xapit/query_parsers/classic_query_parser_spec.rb +0 -20
  100. data/spec/xapit/query_parsers/simple_query_parser_spec.rb +0 -86
  101. data/spec/xapit/query_spec.rb +0 -60
  102. data/tasks/spec.rb +0 -9
  103. data/tasks/xapit.rake +0 -1
  104. data/uninstall.rb +0 -5
  105. data/xapit.gemspec +0 -30
@@ -1,67 +0,0 @@
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
20
- @existing_facet_identifiers = existing_facet_identifiers
21
- end
22
-
23
- # Xapit::FacetOption objects for this facet which match the current query.
24
- # Hides options which don't narrow down results.
25
- def options
26
- unfiltered_options.select { |o| o.count < @query.count }
27
- end
28
-
29
- # Xapit::FacetOption objects for this facet which match the current query.
30
- # Includes options which may not narrow down result set.
31
- def unfiltered_options
32
- matching_identifiers.map do |identifier, count|
33
- option = FacetOption.find(identifier)
34
- if option.facet.attribute == @blueprint.attribute
35
- option.count = count
36
- option.existing_facet_identifiers = @existing_facet_identifiers
37
- option
38
- end
39
- end.compact.sort_by(&:name)
40
- end
41
-
42
- def matching_identifiers
43
- result = {}
44
- matches.each do |match|
45
- identifiers = match.document.terms.map(&:term).grep(/^F/).map { |t| t[1..-1] }
46
- identifiers.each do |identifier|
47
- unless existing_facet_identifiers.include? identifier
48
- result[identifier] ||= 0
49
- result[identifier] += (match.collapse_count + 1)
50
- end
51
- end
52
- end
53
- result
54
- end
55
-
56
- # The name of the facet. See Xapit::FacetBlueprint for details.
57
- def name
58
- @blueprint.name
59
- end
60
-
61
- private
62
-
63
- def matches
64
- @query.matches(:offset => 0, :limit => 1000, :collapse_key => @blueprint.position)
65
- end
66
- end
67
- end
@@ -1,59 +0,0 @@
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
-
@@ -1,56 +0,0 @@
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
@@ -1,147 +0,0 @@
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.remove_database before this.
76
- def index_all
77
- @member_class.xapit_adapter.find_each(*@args) do |member|
78
- @indexer.add_member(member)
79
- end
80
- end
81
-
82
- # The Xapian value index position of a sortable attribute
83
- def position_of_sortable(sortable_attribute)
84
- index = sortable_attributes.map(&:to_s).index(sortable_attribute.to_s)
85
- raise "Unable to find indexed sortable attribute \"#{sortable_attribute}\" in #{@member_class} sortable attributes: #{sortable_attributes.inspect}" if index.nil?
86
- index + facets.size
87
- end
88
-
89
- # The Xapian value index position of a field attribute
90
- def position_of_field(field_attribute)
91
- index = field_attributes.map(&:to_s).index(field_attribute.to_s)
92
- raise "Unable to find indexed field attribute \"#{field_attribute}\" in #{@member_class} field attributes: #{field_attributes.inspect}" if index.nil?
93
- index + facets.size + sortable_attributes.size
94
- end
95
-
96
- # Add a single record to the index if it matches the xapit options.
97
- def create_record(member_id)
98
- member = @member_class.xapit_adapter.find_single(member_id, *@args)
99
- @indexer.add_member(member) if member
100
- end
101
-
102
- # Update a single record in the index. If the record does not match the xapit
103
- # conditions then it is removed from the index instead.
104
- def update_record(member_id)
105
- member = @member_class.xapit_adapter.find_single(member_id, *@args)
106
- if member
107
- @indexer.update_member(member)
108
- else
109
- destroy_record(member_id)
110
- end
111
- end
112
-
113
- # Remove a single record from the index.
114
- def destroy_record(member_id)
115
- Xapit::Config.writable_database.delete_document("Q#{@member_class}-#{member_id}")
116
- end
117
-
118
- private
119
-
120
- # Make sure all models are loaded - without reloading any that
121
- # ActiveRecord::Base is already aware of (otherwise we start to hit some
122
- # messy dependencies issues).
123
- #
124
- # Taken from thinking-sphinx
125
- def self.load_models
126
- if defined? Rails
127
- base = "#{Rails.root}/app/models/"
128
- Dir["#{base}**/*.rb"].each do |file|
129
- model_name = file.gsub(/^#{base}([\w_\/\\]+)\.rb/, '\1')
130
-
131
- next if model_name.nil?
132
- next if ::ActiveRecord::Base.send(:subclasses).detect { |model|
133
- model.name == model_name
134
- }
135
-
136
- begin
137
- model_name.camelize.constantize
138
- rescue LoadError
139
- model_name.gsub!(/.*[\/\\]/, '').nil? ? next : retry
140
- rescue NameError
141
- next
142
- end
143
- end
144
- end
145
- end
146
- end
147
- end
@@ -1,116 +0,0 @@
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 update_member(member)
12
- database.replace_document("Q#{member.class}-#{member.id}", document_for(member))
13
- end
14
-
15
- def document_for(member)
16
- document = Xapian::Document.new
17
- document.data = "#{member.class}-#{member.id}"
18
- index_text_attributes(member, document)
19
- index_terms(other_terms(member), document)
20
- values(member).each_with_index do |value, index|
21
- document.add_value(index, value)
22
- end
23
- save_facet_options_for(member)
24
- document
25
- end
26
-
27
- def index_terms(terms, document)
28
- terms.each do |term|
29
- document.add_term(term)
30
- database.add_spelling(term) if Config.spelling?
31
- end
32
- end
33
-
34
- def index_text_attributes(member, document)
35
- # to be overridden by subclass
36
- end
37
-
38
- def other_terms(member)
39
- base_terms(member) + field_terms(member) + facet_terms(member)
40
- end
41
-
42
- def base_terms(member)
43
- ["C#{member.class}", "Q#{member.class}-#{member.id}"]
44
- end
45
-
46
- def field_terms(member)
47
- @blueprint.field_attributes.map do |name|
48
- [member.send(name)].flatten.map do |value|
49
- if value.kind_of? Time
50
- value = value.to_i
51
- elsif value.kind_of? Date
52
- value = value.to_time.to_i
53
- end
54
- "X#{name}-#{value.to_s.downcase}"
55
- end
56
- end.flatten
57
- end
58
-
59
- def facet_terms(member)
60
- @blueprint.facets.map do |facet|
61
- facet.identifiers_for(member).map { |id| "F#{id}" }
62
- end.flatten
63
- end
64
-
65
- # used primarily by search similar functionality
66
- def text_terms(member) # REFACTORME some duplicaiton with simple indexer
67
- @blueprint.text_attributes.map do |name, options|
68
- content = member.send(name).to_s
69
- if options[:proc]
70
- options[:proc].call(content).reject(&:blank?).map(&:to_s).map(&:downcase)
71
- else
72
- content.scan(/\w+/u).map(&:downcase)
73
- end
74
- end.flatten
75
- end
76
-
77
- def values(member)
78
- facet_values(member) + sortable_values(member) + field_values(member)
79
- end
80
-
81
- def sortable_values(member)
82
- @blueprint.sortable_attributes.map do |sortable|
83
- value = member.send(sortable)
84
- value = value.first if value.kind_of? Array
85
- Xapit.serialize_value(value)
86
- end
87
- end
88
-
89
- # TODO remove duplication with sortable_values
90
- def field_values(member)
91
- @blueprint.field_attributes.map do |sortable|
92
- value = member.send(sortable)
93
- value = value.first if value.kind_of? Array
94
- Xapit.serialize_value(value)
95
- end
96
- end
97
-
98
- def facet_values(member)
99
- @blueprint.facets.map do |facet|
100
- facet.identifiers_for(member).join("-")
101
- end
102
- end
103
-
104
- def save_facet_options_for(member)
105
- @blueprint.facets.each do |facet|
106
- facet.save_facet_options_for(member)
107
- end
108
- end
109
-
110
- private
111
-
112
- def database
113
- Config.writable_database
114
- end
115
- end
116
- end