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,11 @@
1
+ module Xapit
2
+ module Client
3
+ class Facet
4
+ attr_reader :name, :options
5
+ def initialize(attribute, options, applied_facets = [])
6
+ @name = attribute.to_s.gsub("_", " ").gsub(/\b([a-z])/) { $1.to_s.upcase }
7
+ @options = options.map { |option| FacetOption.new(attribute, option, applied_facets) }
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,29 @@
1
+ module Xapit
2
+ module Client
3
+ class FacetOption
4
+ attr_reader :count, :attribute
5
+ def initialize(attribute, option, applied_facets = [])
6
+ @attribute = attribute
7
+ @value = option[:value]
8
+ @count = option[:count].to_i
9
+ @applied_facets = applied_facets
10
+ end
11
+
12
+ def identifier
13
+ Xapit.facet_identifier(@attribute, @value)
14
+ end
15
+
16
+ def name
17
+ @value
18
+ end
19
+
20
+ def to_param
21
+ if @applied_facets.include? identifier
22
+ (@applied_facets - [identifier]).join('-')
23
+ else
24
+ (@applied_facets + [identifier]).join("-")
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,67 @@
1
+ module Xapit
2
+ module Client
3
+ class IndexBuilder
4
+ attr_reader :attributes
5
+ def initialize
6
+ @attributes = {}
7
+ end
8
+
9
+ def text(*args, &block)
10
+ add_attribute(:text, *args, &block)
11
+ end
12
+
13
+ def field(*args, &block)
14
+ add_attribute(:field, *args, &block)
15
+ end
16
+
17
+ def sortable(*args, &block)
18
+ add_attribute(:sortable, *args, &block)
19
+ end
20
+
21
+ def facet(name, custom_name = nil, &block)
22
+ options = {}
23
+ options[:name] = custom_name if custom_name
24
+ add_attribute(:facet, name, options, &block)
25
+ end
26
+
27
+ def add_document(member)
28
+ Xapit.database.add_document(document_data(member))
29
+ end
30
+
31
+ def remove_document(member)
32
+ Xapit.database.remove_document(document_data(member))
33
+ end
34
+
35
+ def update_document(member)
36
+ Xapit.database.update_document(document_data(member))
37
+ end
38
+
39
+ def document_data(member)
40
+ data = {:class => member.class.name, :id => member.id, :attributes => {}}
41
+ attributes.each do |name, options|
42
+ value = member.send(name)
43
+ value = options[:_block].call(value) if options[:_block]
44
+ data[:attributes][name] = options.merge(:value => value)
45
+ end
46
+ data
47
+ end
48
+
49
+ def facets
50
+ attributes.keys.select do |attribute|
51
+ attributes[attribute][:facet]
52
+ end
53
+ end
54
+
55
+ private
56
+
57
+ def add_attribute(type, *args, &block)
58
+ options = args.last.kind_of?(Hash) ? args.pop : {}
59
+ args.each do |attribute|
60
+ @attributes[attribute] ||= {}
61
+ @attributes[attribute][type] = options
62
+ @attributes[attribute][:_block] = block if block
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,46 @@
1
+ module Xapit
2
+ module Client
3
+ module Membership
4
+ def self.included(base)
5
+ base.extend ClassMethods
6
+ end
7
+
8
+ module ClassMethods
9
+ def xapit(&block)
10
+ @xapit_index_builder = IndexBuilder.new
11
+ @xapit_index_builder.instance_eval(&block)
12
+ include AdditionalMethods unless include?(AdditionalMethods)
13
+ end
14
+ end
15
+
16
+ module AdditionalMethods
17
+ def self.included(base)
18
+ base.extend ClassMethods
19
+ base.xapit_model_adapter.setup
20
+ end
21
+
22
+ module ClassMethods
23
+ def xapit_model_adapter
24
+ @xapit_model_adapter ||= Xapit::Client::AbstractModelAdapter.adapter_class(self).new(self)
25
+ end
26
+
27
+ def xapit_index_builder
28
+ @xapit_index_builder
29
+ end
30
+
31
+ def xapit_search(*args)
32
+ Collection.new.in_classes(self).include_facets(*xapit_index_builder.facets).search(*args)
33
+ end
34
+
35
+ def search(*args)
36
+ xapit_search(*args)
37
+ end
38
+ end
39
+
40
+ def search_similar(*args)
41
+ self.class.search(*args).similar_to(self)
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,30 @@
1
+ module Xapit
2
+ module Client
3
+ class AbstractModelAdapter
4
+ def self.inherited(subclass)
5
+ @@subclasses ||= []
6
+ @@subclasses << subclass
7
+ end
8
+
9
+ def self.adapter_class(model_class)
10
+ @@subclasses.detect { |subclass| subclass.for_class?(model_class) } || DefaultModelAdapter
11
+ end
12
+
13
+ def self.for_class?(model_class)
14
+ false # override in subclass
15
+ end
16
+
17
+ def setup
18
+ # override in subclass
19
+ end
20
+
21
+ def index_all
22
+ # override in subclass
23
+ end
24
+
25
+ def initialize(model_class)
26
+ @model_class = model_class
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,27 @@
1
+ module Xapit
2
+ module Client
3
+ class ActiveRecordAdapter < AbstractModelAdapter
4
+ def self.for_class?(model_class)
5
+ model_class <= ActiveRecord::Base
6
+ end
7
+
8
+ def setup
9
+ @model_class.after_create do |member|
10
+ member.class.xapit_index_builder.add_document(member) if Xapit.config[:enabled]
11
+ end
12
+ @model_class.after_update do |member|
13
+ member.class.xapit_index_builder.update_document(member) if Xapit.config[:enabled]
14
+ end
15
+ @model_class.after_destroy do |member|
16
+ member.class.xapit_index_builder.remove_document(member) if Xapit.config[:enabled]
17
+ end
18
+ end
19
+
20
+ def index_all
21
+ @model_class.find_each do |member|
22
+ member.class.xapit_index_builder.add_document(member)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,7 @@
1
+ module Xapit
2
+ module Client
3
+ class DefaultModelAdapter < AbstractModelAdapter
4
+ # This adapter is used when no matching adapter is found
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,18 @@
1
+ module Xapit
2
+ module Client
3
+ class Railtie < Rails::Railtie
4
+ initializer "xapit.config" do
5
+ path = Rails.root.join("config/xapit.yml")
6
+ Xapit.load_config(path, Rails.env) if path.exist?
7
+ end
8
+
9
+ initializer "xapit.membership" do
10
+ ActiveRecord::Base.send(:include, Xapit::Client::Membership) if defined? ActiveRecord
11
+ end
12
+
13
+ rake_tasks do
14
+ load File.expand_path("../tasks.rb", __FILE__)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,21 @@
1
+ module Xapit
2
+ module Client
3
+ class RemoteDatabase
4
+ def initialize(url)
5
+ @url = url
6
+ end
7
+
8
+ Xapit::Server::Database::COMMANDS.each do |command|
9
+ define_method(command) do |options|
10
+ request(command, options)
11
+ end
12
+ end
13
+
14
+ def request(command, options)
15
+ uri = URI.parse("#{@url}/xapit/#{command}")
16
+ response = Net::HTTP.start(uri.host, uri.port) { |http| http.request_post(uri.path, options.to_json) }
17
+ Xapit.symbolize_keys(JSON.parse("[#{response.body}]").first) # terrible hack for handling simple objects
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,18 @@
1
+ require "rack"
2
+ require "fileutils"
3
+
4
+ namespace :xapit do
5
+ desc "Index all models for Xapit search"
6
+ task :index => :environment do
7
+ raise "No Xapian database specified in config." if Xapit.config[:database_path].blank?
8
+ FileUtils.rm_rf("tmp/xapit") if File.exist? "tmp/xapit"
9
+ FileUtils.mv(Xapit.config[:database_path], "tmp/xapit") if File.exist? Xapit.config[:database_path]
10
+ models = ActiveRecord::Base.subclasses
11
+ Dir[Rails.root.join("app", "models", "**", "*.rb")].each do |file|
12
+ # I hate to rescue nil, maybe there's a better way to handle unknown constants
13
+ models << File.basename(file, ".*").classify.constantize rescue nil
14
+ end
15
+ xapit_models = models.compact.uniq.select { |m| m.respond_to? :xapit_model_adapter }
16
+ Xapit.index(*xapit_models)
17
+ end
18
+ end
@@ -0,0 +1,27 @@
1
+ module Xapit
2
+ module Server
3
+ class App
4
+ def call(env)
5
+ request = Rack::Request.new(env)
6
+ command = request.path[%r</xapit/(.+)>, 1]
7
+ if Database::COMMANDS.include? command
8
+ action(command, request.body.gets)
9
+ else
10
+ render :status => 404
11
+ end
12
+ end
13
+
14
+ def action(command, json)
15
+ data = Xapit.symbolize_keys(JSON.parse(json))
16
+ render :content => Xapit.database.send(command, data).to_json
17
+ end
18
+
19
+ def render(options = {})
20
+ options[:status] ||= 200
21
+ options[:content] ||= ""
22
+ options[:content_type] ||= "text/html"
23
+ [options[:status], {"Content-Type" => options[:content_type]}, [options[:content]]]
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,47 @@
1
+ module Xapit
2
+ module Server
3
+ class Database
4
+ COMMANDS = %w[query add_document remove_document update_document spelling_suggestion]
5
+
6
+ def initialize(path)
7
+ @path = path
8
+ end
9
+
10
+ def xapian_database
11
+ @xapian_database ||= load_database
12
+ end
13
+
14
+ def add_document(data)
15
+ xapian_database.add_document(Indexer.new(data).document)
16
+ end
17
+
18
+ def remove_document(data)
19
+ xapian_database.delete_document(Indexer.new(data).id_term)
20
+ end
21
+
22
+ def update_document(data)
23
+ indexer = Indexer.new(data)
24
+ xapian_database.replace_document(indexer.id_term, indexer.document)
25
+ end
26
+
27
+ def query(data)
28
+ Query.new(data).data
29
+ end
30
+
31
+ def spelling_suggestion(data)
32
+ Query.new(data).spelling_suggestion
33
+ end
34
+
35
+ private
36
+
37
+ def load_database
38
+ if @path
39
+ FileUtils.mkdir_p(File.dirname(@path)) unless File.exist?(File.dirname(@path))
40
+ Xapian::WritableDatabase.new(@path, Xapian::DB_CREATE_OR_OPEN)
41
+ else
42
+ Xapian.inmemory_open
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,138 @@
1
+ module Xapit
2
+ module Server
3
+ class Indexer
4
+ def initialize(data)
5
+ @data = data
6
+ end
7
+
8
+ def database
9
+ Xapit.database.xapian_database
10
+ end
11
+
12
+ def document
13
+ document = Xapian::Document.new
14
+ document.data = id
15
+ terms.each { |term, weight| document.add_term(term, weight) }
16
+ text_terms.each { |term, weight| database.add_spelling(term, weight) } if Xapit.config[:spelling]
17
+ values.each { |index, value| document.add_value(index, value) }
18
+ save_facets
19
+ document
20
+ end
21
+
22
+ def id
23
+ "#{@data[:class]}-#{@data[:id]}"
24
+ end
25
+
26
+ def id_term
27
+ "Q#{id}"
28
+ end
29
+
30
+ def terms
31
+ base_terms + text_terms + stemmed_text_terms + field_terms + facet_terms
32
+ end
33
+
34
+ def values
35
+ values = {}
36
+ each_value do |index, value|
37
+ if values[index]
38
+ values[index] += "\3#{value}" # multiple values are split back out on the query side
39
+ else
40
+ values[index] = value
41
+ end
42
+ end
43
+ values
44
+ end
45
+
46
+ def text_terms
47
+ each_attribute(:text) do |name, value, options|
48
+ value.to_s.split(/\s+/u).map { |w| w.gsub(/[^\w]/u, "") }.map(&:downcase).map do |term|
49
+ [term, options[:weight] || 1]
50
+ end
51
+ end.flatten(1)
52
+ end
53
+
54
+ def stemmed_text_terms
55
+ if stemmer
56
+ each_attribute(:text) do |name, value, options|
57
+ value.to_s.split(/\s+/u).map { |w| w.gsub(/[^\w]/u, "") }.map(&:downcase).map do |term|
58
+ ["Z#{stemmer.call(term)}", options[:weight] || 1]
59
+ end
60
+ end.flatten(1)
61
+ else
62
+ []
63
+ end
64
+ end
65
+
66
+ def field_terms
67
+ each_attribute(:field) do |name, value, options|
68
+ ["X#{name}-#{parse_field(value)}", 1]
69
+ end
70
+ end
71
+
72
+ def facet_terms
73
+ each_attribute(:facet) do |name, value, options|
74
+ ["F#{Xapit.facet_identifier(name, value)}", 1]
75
+ end
76
+ end
77
+
78
+ def save_facets
79
+ each_attribute(:facet) do |name, value, options|
80
+ id = Xapit.facet_identifier(name, value)
81
+ unless database.term_exists("Xid-#{id}")
82
+ document = Xapian::Document.new
83
+ document.data = "#{name}|||#{value}"
84
+ document.add_term("CFacetOption")
85
+ document.add_term("Xid-#{id}")
86
+ database.add_document(document)
87
+ end
88
+ end
89
+ end
90
+
91
+ private
92
+
93
+ def stemmer
94
+ @stemmer ||= Xapian::Stem.new(Xapit.config[:stemming]) if Xapit.config[:stemming]
95
+ end
96
+
97
+ def base_terms
98
+ [["C#{@data[:class]}", 1], [id_term, 1]]
99
+ end
100
+
101
+ def parse_field(value)
102
+ if value.kind_of? Time
103
+ value.to_i
104
+ else
105
+ value.to_s.downcase
106
+ end
107
+ end
108
+
109
+ def each_value
110
+ each_attribute(:field) do |name, value, options|
111
+ yield(Xapit.value_index(:field, name), Xapit.serialize_value(value))
112
+ end
113
+ each_attribute(:sortable) do |name, value, options|
114
+ yield(Xapit.value_index(:sortable, name), Xapit.serialize_value(value))
115
+ end
116
+ each_attribute(:facet) do |name, value, options|
117
+ yield(Xapit.value_index(:facet, name), value)
118
+ end
119
+ end
120
+
121
+ def each_attribute(type)
122
+ if @data[:attributes]
123
+ @data[:attributes].map do |name, options|
124
+ if options.has_key? type
125
+ if options[:value].kind_of? Array
126
+ options[:value].map { |value| yield(name, value, options[type]) }
127
+ else
128
+ [yield(name, options[:value], options[type])]
129
+ end
130
+ end
131
+ end.compact.flatten(1)
132
+ else
133
+ []
134
+ end
135
+ end
136
+ end
137
+ end
138
+ end