xapit 0.2.7 → 0.3.0

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