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