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
@@ -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