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
@@ -0,0 +1,240 @@
1
+ module Xapit
2
+ module Server
3
+ class Query
4
+ def initialize(clauses)
5
+ @clauses = clauses
6
+ @xapian_query = nil
7
+ end
8
+
9
+ def matches
10
+ enquire = Xapian::Enquire.new(Xapit.database.xapian_database)
11
+ enquire.query = xapian_query
12
+ enquire.set_sort_by_key_then_relevance(sorter, false) if sorter
13
+ enquire.mset((page.to_i-1)*per_page.to_i, per_page.to_i).matches
14
+ end
15
+
16
+ def records
17
+ matches.map do |match|
18
+ class_name, id = match.document.data.split('-')
19
+ {:class => class_name, :id => id, :relevance => match.percent}
20
+ end
21
+ end
22
+
23
+ def spelling_suggestion
24
+ text = @clauses.map { |clause| clause[:search] }.compact.join(" ")
25
+ if [text, *text.scan(/\w+/)].all? { |term| term_suggestion(term).nil? }
26
+ nil
27
+ else
28
+ return term_suggestion(text) unless term_suggestion(text).to_s.empty?
29
+ text.downcase.gsub(/\w+/) do |term|
30
+ term_suggestion(term) || term
31
+ end
32
+ end
33
+ end
34
+
35
+ def facets
36
+ facets = {}
37
+ enquire = Xapian::Enquire.new(Xapit.database.xapian_database)
38
+ enquire.query = xapian_query
39
+ spies = facet_spies
40
+ spies.values.each do |spy|
41
+ enquire.add_matchspy(spy)
42
+ end
43
+ enquire.mset(0, 200)
44
+ spies.each do |attribute, spy|
45
+ values = {}
46
+ spy.values.map do |spy_value|
47
+ spy_value.term.split("\3").each do |term| # used to support multiple facet values
48
+ values[term] ||= 0
49
+ values[term] += spy_value.termfreq.to_i
50
+ end
51
+ end
52
+ facets[attribute] = values.map { |value, count| {:value => value, :count => count} }
53
+ end
54
+ facets
55
+ end
56
+
57
+ def applied_facet_options
58
+ facet_options = []
59
+ @clauses.each do |clause|
60
+ if clause[:with_facets]
61
+ clause[:with_facets].each do |identifier|
62
+ facet_options << facet_option(identifier)
63
+ end
64
+ end
65
+ end
66
+ facet_options
67
+ end
68
+
69
+ def facet_option(identifier)
70
+ match = self.class.new([{:in_classes => ["FacetOption"]}, {:where => {:id => identifier}}]).matches.first
71
+ if match.nil?
72
+ raise "Unable to find facet option for #{identifier}."
73
+ else
74
+ name, value = match.document.data.split('|||')
75
+ {:id => identifier, :name => name, :value => value}
76
+ end
77
+ end
78
+
79
+ def total
80
+ enquire = Xapian::Enquire.new(Xapit.database.xapian_database)
81
+ enquire.query = xapian_query
82
+ enquire.mset(0, Xapit.database.xapian_database.doccount).matches_estimated
83
+ end
84
+
85
+ def data
86
+ {:records => records, :facets => facets, :applied_facet_options => applied_facet_options, :total => total}
87
+ end
88
+
89
+ private
90
+
91
+ def page
92
+ @clauses.map { |clause| clause[:page] }.compact.last || 1
93
+ end
94
+
95
+ def per_page
96
+ @clauses.map { |clause| clause[:per_page] }.compact.last || Xapit::Client::Collection::DEFAULT_PER_PAGE
97
+ end
98
+
99
+ def term_suggestion(term)
100
+ suggestion = Xapit.database.xapian_database.get_spelling_suggestion(term.downcase)
101
+ suggestion.to_s.empty? ? nil : suggestion
102
+ end
103
+
104
+ def sorter
105
+ if @clauses.any? { |c| c[:order] }
106
+ sorter = Xapian::MultiValueKeyMaker.new
107
+ @clauses.each do |clause|
108
+ if clause[:order]
109
+ attribute, direction = clause[:order]
110
+ sorter.add_value(Xapit.value_index(:sortable, attribute), direction.to_sym == :desc)
111
+ end
112
+ end
113
+ sorter
114
+ end
115
+ end
116
+
117
+ def facet_spies
118
+ spies = {}
119
+ @clauses.each do |clause|
120
+ if clause[:include_facets]
121
+ clause[:include_facets].each do |facet|
122
+ spies[facet] = Xapian::ValueCountMatchSpy.new(Xapit.value_index(:facet, facet))
123
+ end
124
+ end
125
+ end
126
+ spies
127
+ end
128
+
129
+ def xapian_query
130
+ build_xapian_query if @query.nil?
131
+ @xapian_query
132
+ end
133
+
134
+ def build_xapian_query
135
+ @xapian_query = Xapian::Query.new("")
136
+ @clauses.each do |clause|
137
+ clause.each do |type, options|
138
+ apply_clause(type, options)
139
+ end
140
+ end
141
+ end
142
+
143
+ def apply_clause(type, value)
144
+ case type
145
+ when :search
146
+ merge(:and, search_query(value))
147
+ when :where
148
+ merge(:and, where_query(value))
149
+ when :or_where
150
+ merge(:or, where_query(value))
151
+ when :not_where
152
+ merge(:not, where_query(value))
153
+ when :in_classes
154
+ merge(:and, value.map { |c| "C#{c}" })
155
+ when :not_in_classes
156
+ merge(:not, value.map { |c| "C#{c}" })
157
+ when :similar_to
158
+ similar_to(value)
159
+ when :with_facets
160
+ merge(:and, facet_terms(value))
161
+ end
162
+ end
163
+
164
+ def similar_to(data)
165
+ indexer = Indexer.new(data)
166
+ terms = (indexer.text_terms + indexer.field_terms).map { |a| a.first }
167
+ merge(:and, Xapian::Query.new(xapian_operator(:or), terms))
168
+ merge(:not, ["Q#{data[:class]}-#{data[:id]}"])
169
+ end
170
+
171
+ def where_query(conditions)
172
+ queries = []
173
+ terms = []
174
+ conditions.each do |name, value|
175
+ if value.kind_of?(Hash) && value[:from] && value[:to]
176
+ queries << Xapian::Query.new(xapian_operator(:range), Xapit.value_index(:field, name), Xapit.serialize_value(value[:from]), Xapit.serialize_value(value[:to]))
177
+ else
178
+ terms << "X#{name}-#{value.to_s.downcase}"
179
+ end
180
+ end
181
+ queries << Xapian::Query.new(xapian_operator(:and), terms) unless terms.empty?
182
+ queries.inject(queries.shift) do |merged_query, query|
183
+ Xapian::Query.new(xapian_operator(:and), merged_query, query)
184
+ end
185
+ end
186
+
187
+ def where_terms(conditions)
188
+ conditions.map do |name, value|
189
+ "X#{name}-#{value.to_s.downcase}"
190
+ end
191
+ end
192
+
193
+ def facet_terms(facets)
194
+ facets.map { |facet| "F#{facet}" }
195
+ end
196
+
197
+ def search_query(text)
198
+ clean_text = text.gsub(/\b([a-z])\*/i, "\\1").gsub(/[^\w\*\s:]/u, "")
199
+ xapian_parser.parse_query(clean_text, Xapian::QueryParser::FLAG_WILDCARD | Xapian::QueryParser::FLAG_BOOLEAN) # Xapian::QueryParser::FLAG_LOVEHATE
200
+ end
201
+
202
+ def merge(operator, query)
203
+ query = Xapian::Query.new(xapian_operator(:and), query) unless query.kind_of? Xapian::Query
204
+ @xapian_query = Xapian::Query.new(xapian_operator(operator), @xapian_query, query)
205
+ end
206
+
207
+ def xapian_operator(operator)
208
+ case operator
209
+ when :and then Xapian::Query::OP_AND
210
+ when :or then Xapian::Query::OP_OR
211
+ when :not then Xapian::Query::OP_AND_NOT
212
+ when :range then Xapian::Query::OP_VALUE_RANGE
213
+ else raise "Unknown Xapian operator #{operator}"
214
+ end
215
+ end
216
+
217
+ def xapian_parser
218
+ @xapian_parser ||= build_xapian_parser
219
+ end
220
+
221
+ def build_xapian_parser
222
+ parser = Xapian::QueryParser.new
223
+ parser.database = Xapit.database.xapian_database
224
+ if Xapit.config[:stemming]
225
+ parser.stemmer = Xapian::Stem.new(Xapit.config[:stemming])
226
+ parser.stemming_strategy = Xapian::QueryParser::STEM_SOME
227
+ end
228
+ parser.default_op = xapian_operator(:and)
229
+ @clauses.each do |clause|
230
+ if clause[:search]
231
+ clause[:search].scan(/([a-z0-9_]+)\:/i).each do
232
+ parser.add_prefix($1, "X#{$1}-")
233
+ end
234
+ end
235
+ end
236
+ parser
237
+ end
238
+ end
239
+ end
240
+ end
File without changes
@@ -0,0 +1 @@
1
+ IAmChert�� ���pn�N3��gf�]
File without changes
File without changes
File without changes
@@ -0,0 +1,13 @@
1
+ require "rubygems"
2
+
3
+ # Add lib directory so we can include xapit, this isn't necessary when a gem is available
4
+ root = File.expand_path('../../..', __FILE__)
5
+ $:.unshift "#{root}/lib"
6
+ require "xapit"
7
+
8
+
9
+ FileUtils.rm_rf("#{root}/tmp/testdb") # quick hack to start with a fresh database every time
10
+ Xapit.config[:database_path] = "#{root}/tmp/testdb"
11
+ Xapit.config[:template_path] = "#{root}/spec/fixtures/blankdb"
12
+
13
+ run Xapit::Server::App.new
@@ -0,0 +1,4 @@
1
+ development:
2
+ database_path: development_database
3
+ production:
4
+ database_path: production_database
@@ -1,15 +1,14 @@
1
1
  require 'rubygems'
2
- require 'spec'
3
- require 'active_support'
4
- require 'fileutils'
5
- require File.dirname(__FILE__) + '/../lib/xapit'
6
- require File.dirname(__FILE__) + '/xapit_member'
2
+ require 'bundler/setup'
3
+ Bundler.require(:default)
7
4
 
8
- Spec::Runner.configure do |config|
9
- config.mock_with :rr
5
+ require "support/spec_macros"
6
+ require "support/xapit_member"
7
+
8
+ RSpec.configure do |config|
9
+ config.include SpecMacros
10
10
  config.before(:each) do
11
- Xapit.setup(:database_path => File.dirname(__FILE__) + '/tmp/xapiandb')
12
- Xapit.remove_database
11
+ Xapit.reset_config
13
12
  XapitMember.delete_all
14
13
  end
15
14
  end
@@ -0,0 +1,6 @@
1
+ module SpecMacros
2
+ def load_xapit_database
3
+ Xapit.reset_config
4
+ Xapit.config[:spelling] = false
5
+ end
6
+ end
@@ -1,22 +1,17 @@
1
1
  class XapitMember
2
- include Xapit::Membership
3
-
2
+ include Xapit::Client::Membership
3
+ xapit { } # loads Xapit member methods
4
+
4
5
  attr_reader :id
5
-
6
- # Make it look like this inherits from ActiveRecord::Base
7
- # so it will use the ActiveRecord adapter.
8
- def self.ancestors
9
- ["ActiveRecord::Base"]
10
- end
11
-
6
+
12
7
  def self.find_each(&block)
13
8
  @@records.each(&block) if @@records
14
9
  end
15
-
10
+
16
11
  def self.delete_all
17
12
  @@records = []
18
13
  end
19
-
14
+
20
15
  def self.find(ids)
21
16
  if ids.kind_of? Array
22
17
  # change the order to mimic database where we can't predict the order
@@ -25,18 +20,21 @@ class XapitMember
25
20
  @@records.detect { |r| r.id == ids.to_i }
26
21
  end
27
22
  end
28
-
23
+
29
24
  def self.find_by_id(id)
30
25
  find(id)
31
26
  end
32
-
27
+
33
28
  def initialize(attributes = {})
34
29
  @@records ||= []
35
30
  @id = @@records.size + 1
36
- @attributes = attributes
31
+ @attributes = {}
32
+ attributes.each do |key, value|
33
+ @attributes[key.to_sym] = value
34
+ end
37
35
  @@records << self
38
36
  end
39
-
37
+
40
38
  def method_missing(name, *args)
41
39
  if @attributes.has_key? name
42
40
  @attributes[name]
@@ -44,7 +42,7 @@ class XapitMember
44
42
  super
45
43
  end
46
44
  end
47
-
45
+
48
46
  def update_attribute(name, value)
49
47
  @attributes[name] = value
50
48
  end
@@ -0,0 +1,63 @@
1
+ require "spec_helper"
2
+
3
+ describe Xapit::Client::Collection do
4
+ it "builds up clauses with in_classes, search, where, order calls" do
5
+ collection1 = Xapit::Client::Collection.new([:initial])
6
+ collection2 = collection1.in_classes(String).search("hello").where(:foo => "bar").order(:bar)
7
+ collection1.clauses.should eq([:initial])
8
+ collection2.clauses.should eq([:initial, {:in_classes => [String]}, {:search => "hello"}, {:where => {:foo => "bar"}}, {:order => [:bar, :asc]}])
9
+ end
10
+
11
+ it "returns same collection when searching nil or empty string" do
12
+ collection1 = Xapit::Client::Collection.new
13
+ collection1.search("").should eq(collection1)
14
+ collection1.search(nil).should eq(collection1)
15
+ collection1.search.should eq(collection1)
16
+ end
17
+
18
+ it "returns indexed records and delegates array methods to it" do
19
+ load_xapit_database
20
+ member = XapitMember.new
21
+ member.class.xapit_index_builder.add_document(member)
22
+ collection = Xapit::Client::Collection.new
23
+ collection.records.should eq([member])
24
+ collection.should respond_to(:flatten)
25
+ collection.flatten.should eq([member])
26
+ end
27
+
28
+ it "splits up matching facets into an array" do
29
+ collection = Xapit::Client::Collection.new.with_facets("foo-bar")
30
+ collection.clauses.should eq([{:with_facets => %w[foo bar]}])
31
+ end
32
+
33
+ it "splits range into from/to hash" do
34
+ collection = Xapit::Client::Collection.new.where(:priority => 3..5)
35
+ collection.clauses.should eq([{:where => {:priority => {:from => 3, :to => 5}}}])
36
+ end
37
+
38
+ it "does not raise an exception when passing nil to with_facets" do
39
+ lambda {
40
+ Xapit::Client::Collection.new.with_facets(nil).should be_kind_of(Xapit::Client::Collection)
41
+ }.should_not raise_exception
42
+ end
43
+
44
+ it "defaults to 20 per page and page 1" do
45
+ Xapit::Client::Collection.new.limit_value.should eq(20)
46
+ Xapit::Client::Collection.new.current_page.should eq(1)
47
+ end
48
+
49
+ it "supports kaminari pagination" do
50
+ collection = Xapit::Client::Collection.new.page("2").per("10")
51
+ collection.stub(:total_entries) { 29 }
52
+ collection.current_page.should eq(2)
53
+ collection.num_pages.should eq(3)
54
+ collection.limit_value.should eq(10)
55
+ end
56
+
57
+ it "supports will_paginate pagination" do
58
+ collection = Xapit::Client::Collection.new.page("2").per("10")
59
+ collection.stub(:total_entries) { 29 }
60
+ collection.current_page.should eq(2)
61
+ collection.total_pages.should eq(3)
62
+ end
63
+ end
@@ -0,0 +1,26 @@
1
+ require "spec_helper"
2
+
3
+ describe Xapit::Client::FacetOption do
4
+ it "has an identifier using attribute and value" do
5
+ option = Xapit::Client::FacetOption.new("greeting", :value => "Hello")
6
+ option.identifier.should eq(Xapit.facet_identifier("greeting", "Hello"))
7
+ end
8
+
9
+ it "has a name and count matching passed options" do
10
+ option = Xapit::Client::FacetOption.new("greeting", :value => "Hello", :count => "3")
11
+ option.name.should eq("Hello")
12
+ option.count.should eq(3)
13
+ end
14
+
15
+ it "combines previous identifiers with current one on to_param" do
16
+ id = Xapit.facet_identifier("greeting", "Hello")
17
+ option = Xapit::Client::FacetOption.new("greeting", {:value => "Hello"}, %w[abc 123])
18
+ option.to_param.should == "abc-123-#{id}"
19
+ end
20
+
21
+ it "removes current identifier from previous identifiers if it exists" do
22
+ id = Xapit.facet_identifier("greeting", "Hello")
23
+ option = Xapit::Client::FacetOption.new("greeting", {:value => "Hello"}, %w[abc 123] + [id])
24
+ option.to_param.should == "abc-123"
25
+ end
26
+ end