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
@@ -1,17 +1,23 @@
1
- Background:
2
- Given an empty database at "tmp/xapiandatabase"
1
+ Feature: Suggestions
3
2
 
4
- Scenario: Spelling suggestion
5
- Given indexed records named "Zebra, Apple, Bike"
6
- When I query for "zerba bike aple"
7
- Then I should have "zebra bike apple" as a spelling suggestion
3
+ Scenario: Spelling suggestion
4
+ Given an empty database at "tmp/spellingdb"
5
+ And spelling is enabled
6
+ And indexed records named "Zebra, Apple, Bike"
7
+ When I query for "zerba bike aple"
8
+ Then I should have "zebra bike apple" as a spelling suggestion
8
9
 
9
- Scenario: Match similar words with stemming
10
- Given indexed records named "flies, fly, glider"
11
- When I query for "flying"
12
- Then I should find records named "flies, fly"
10
+ Scenario: No spelling suggestion
11
+ Given indexed records named "Zebra, Apple, Bike"
12
+ When I query for "zerba bike aple"
13
+ Then I should have "" as a spelling suggestion
13
14
 
14
- Scenario: Find similar records
15
- Given indexed records named "Jason John Smith, John Doe, Jason Smith, Jacob Johnson"
16
- When I query for similar records for "Jason John Smith"
17
- Then I should find records named "Jason Smith, John Doe"
15
+ Scenario: Find similar records
16
+ Given indexed records named "Jason John Smith, John Doe, Jason Smith, Jacob Johnson"
17
+ When I query for similar records for "Jason John Smith"
18
+ Then I should find records named "Jason Smith, John Doe"
19
+
20
+ Scenario: Match similar words with stemming
21
+ Given indexed records named "flies, fly, glider"
22
+ When I query for "flying"
23
+ Then I should find records named "flies, fly"
@@ -1,7 +1,14 @@
1
- require 'cucumber'
2
- require 'spec'
3
- require 'active_support'
4
- require 'fileutils'
1
+ require 'bundler/setup'
5
2
 
6
- require File.dirname(__FILE__) + '/../../lib/xapit'
7
- require File.dirname(__FILE__) + '/../../spec/xapit_member'
3
+ Bundler.require(:default)
4
+
5
+ require File.expand_path('../../../spec/support/xapit_member', __FILE__)
6
+
7
+ Before do
8
+ Xapit.reset_config
9
+ Xapit.config[:spelling] = false
10
+ end
11
+
12
+ at_exit do
13
+ $server.close if $server
14
+ end
@@ -1,16 +1,15 @@
1
1
  module XapitHelpers
2
2
  def create_records(records, perform_index = true)
3
- Xapit.remove_database
4
3
  XapitMember.delete_all
5
- XapitMember.xapit do |index|
4
+ XapitMember.xapit do
6
5
  records.first.keys.each do |attribute|
7
6
  if block_given?
8
- yield(index, attribute)
7
+ yield(self, attribute)
9
8
  else
10
- index.text attribute
11
- index.field attribute
12
- index.facet attribute
13
- index.sortable attribute
9
+ text attribute
10
+ field attribute
11
+ facet attribute
12
+ sortable attribute
14
13
  end
15
14
  end
16
15
  end
@@ -18,9 +17,9 @@ module XapitHelpers
18
17
  attributes.each do |key, value|
19
18
  attributes[key] = value.split(', ') if value.include? ', '
20
19
  end
21
- XapitMember.new(attributes.symbolize_keys)
20
+ member = XapitMember.new(attributes)
21
+ member.class.xapit_index_builder.add_document(member) if perform_index
22
22
  end
23
- Xapit.index_all if perform_index
24
23
  end
25
24
  end
26
25
 
@@ -0,0 +1,14 @@
1
+ module Xapit
2
+ module Generators
3
+ class InstallGenerator < Rails::Generators::Base
4
+ def self.source_root
5
+ File.dirname(__FILE__) + "/templates"
6
+ end
7
+
8
+ def copy_files
9
+ copy_file "xapit.yml", "config/xapit.yml"
10
+ copy_file "xapit.ru", "xapit.ru"
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,6 @@
1
+ require "rubygems"
2
+ require "xapit"
3
+
4
+ Xapit.load_config(File.expand_path('../config/xapit.yml', __FILE__), "production")
5
+
6
+ run Xapit::Server::App.new
@@ -0,0 +1,11 @@
1
+ # See https://github.com/ryanb/xapit/wiki/Configuration
2
+ development:
3
+ database_path: "db/xapit"
4
+
5
+ test:
6
+ enabled: false
7
+ spelling: false
8
+
9
+ production:
10
+ database_path: "db/xapit"
11
+ server: "http://localhost:9292"
@@ -1,71 +1,113 @@
1
- require 'digest/sha1'
2
- require 'rubygems'
3
1
  require 'xapian'
2
+ require 'digest/sha1'
3
+ require 'rack'
4
+ require 'json'
5
+ require 'net/http'
4
6
 
5
- # Looking for more documentation? A good place to start is Xapit::Membership
6
7
  module Xapit
7
- # Index all membership classes with xapit defined. Delegates to Xapit::IndexBlueprint.
8
- # You will likely want to call Xapit.remove_database before this.
9
- def self.index_all(*args, &block)
10
- IndexBlueprint.index_all(*args, &block)
11
- end
12
-
13
- # Used to perform a search on all indexed models. The returned collection can
14
- # contain instances of different classes which were indexed.
15
- #
16
- # # perform a simple full text search
17
- # @records = Xapit.search("phone")
18
- #
19
- # See Xapit::Membership for details on search options.
20
- def self.search(*args)
21
- Collection.new(nil, *args)
22
- end
23
-
24
- # Setup configuration options. The following options are supported.
25
- #
26
- # <tt>:database_path</tt>: Where the database is stored.
27
- # <tt>:stemming</tt>: The language to use for stemming, defaults to "english".
28
- # <tt>:spelling</tt>: True or false to enable/disable spelling, defaults to true.
29
- # <tt>:indexer</tt>: Class to handle the indexing, defaults to SimpleIndexer.
30
- # <tt>:query_parser</tt>: Class to handle the parsing, defaults to ClassicQueryParser.
31
- # <tt>:breadcrumb_facets</tt>: Use breadcrumb mode for applied facets. See Collection#applied_facet_options for details.
32
- #
33
- def self.setup(*args)
34
- Config.setup(*args)
35
- end
36
-
37
- # Removes the configured database file and clears the stored one in memory.
38
- def self.remove_database
39
- Config.remove_database
40
- end
41
-
42
- def self.serialize_value(value)
43
- if value.kind_of?(Date)
44
- Xapian.sortable_serialise(value.to_time.to_i)
45
- elsif value.kind_of?(Time)
46
- Xapian.sortable_serialise(value.to_i)
47
- elsif value.kind_of?(Numeric) || value.to_s =~ /^[0-9]+$/
48
- Xapian.sortable_serialise(value.to_f)
49
- else
50
- value.to_s.downcase
8
+ # A general Xapit exception
9
+ class Error < StandardError; end
10
+
11
+ # Raised when accessing the database when Xapit is disabled
12
+ class Disabled < Error; end
13
+
14
+ class << self
15
+ attr_reader :config
16
+
17
+ def reset_config
18
+ @database = nil
19
+ @config = {
20
+ :enabled => true,
21
+ :spelling => true,
22
+ :stemming => "english"
23
+ }
24
+ end
25
+
26
+ def reload
27
+ reset_config
28
+ @config.merge!(@loaded_config) if @loaded_config
29
+ end
30
+
31
+ def database
32
+ raise Disabled, "Unable to access Xapit database because it is disabled in configuration." unless Xapit.config[:enabled]
33
+ if config[:server]
34
+ @database ||= Xapit::Client::RemoteDatabase.new(config[:server])
35
+ else
36
+ @database ||= Xapit::Server::Database.new(config[:database_path])
37
+ end
38
+ end
39
+
40
+ def load_config(filename, environment)
41
+ @loaded_config = symbolize_keys(YAML.load_file(filename)[environment.to_s])
42
+ raise ArgumentError, "The #{environment} environment does not exist in #{filename}" if @loaded_config.nil?
43
+ @config.merge!(@loaded_config)
44
+ end
45
+
46
+ def value_index(type, attribute)
47
+ Zlib.crc32(["xapit", type, attribute].join) % 99999999 # TODO: Figure out the true max of a xapian value index
48
+ end
49
+
50
+ def facet_identifier(attribute, value)
51
+ Digest::SHA1.hexdigest(["xapit", attribute, value].join)[0..6]
52
+ end
53
+
54
+ def search(*args)
55
+ Xapit::Client::Collection.new.not_in_classes("FacetOption").search(*args)
56
+ end
57
+
58
+ def serialize_value(value)
59
+ if value.kind_of?(Time)
60
+ Xapian.sortable_serialise(value.to_i)
61
+ elsif value.kind_of?(Numeric) || value.to_s =~ /^[0-9]+$/
62
+ Xapian.sortable_serialise(value.to_f)
63
+ else
64
+ value.to_s.downcase
65
+ end
66
+ end
67
+
68
+ def enable
69
+ config[:enabled] = true
70
+ end
71
+
72
+ def index(*models)
73
+ models.each do |model|
74
+ model.xapit_model_adapter.index_all
75
+ end
76
+ end
77
+
78
+ # from http://snippets.dzone.com/posts/show/11121
79
+ # could use some refactoring
80
+ def symbolize_keys(arg)
81
+ case arg
82
+ when Array
83
+ arg.map { |elem| symbolize_keys(elem) }
84
+ when Hash
85
+ Hash[
86
+ arg.map { |key, value|
87
+ k = key.is_a?(String) ? key.to_sym : key
88
+ v = symbolize_keys(value)
89
+ [k,v]
90
+ }]
91
+ else
92
+ arg
93
+ end
51
94
  end
52
95
  end
96
+
97
+ reset_config
53
98
  end
54
99
 
55
- require File.dirname(__FILE__) + '/xapit/membership'
56
- require File.dirname(__FILE__) + '/xapit/index_blueprint'
57
- require File.dirname(__FILE__) + '/xapit/collection'
58
- require File.dirname(__FILE__) + '/xapit/config'
59
- require File.dirname(__FILE__) + '/xapit/facet_blueprint'
60
- require File.dirname(__FILE__) + '/xapit/facet'
61
- require File.dirname(__FILE__) + '/xapit/facet_option'
62
- require File.dirname(__FILE__) + '/xapit/query'
63
- require File.dirname(__FILE__) + '/xapit/query_parsers/abstract_query_parser'
64
- require File.dirname(__FILE__) + '/xapit/query_parsers/simple_query_parser'
65
- require File.dirname(__FILE__) + '/xapit/query_parsers/classic_query_parser'
66
- require File.dirname(__FILE__) + '/xapit/indexers/abstract_indexer'
67
- require File.dirname(__FILE__) + '/xapit/indexers/simple_indexer'
68
- require File.dirname(__FILE__) + '/xapit/indexers/classic_indexer'
69
- require File.dirname(__FILE__) + '/xapit/adapters/abstract_adapter'
70
- require File.dirname(__FILE__) + '/xapit/adapters/active_record_adapter'
71
- require File.dirname(__FILE__) + '/xapit/adapters/data_mapper_adapter'
100
+ require 'xapit/server/database'
101
+ require 'xapit/server/query'
102
+ require 'xapit/server/indexer'
103
+ require 'xapit/server/app'
104
+ require 'xapit/client/membership'
105
+ require 'xapit/client/index_builder'
106
+ require 'xapit/client/collection'
107
+ require 'xapit/client/facet'
108
+ require 'xapit/client/facet_option'
109
+ require 'xapit/client/remote_database'
110
+ require 'xapit/client/railtie' if defined? Rails
111
+ require 'xapit/client/model_adapters/abstract_model_adapter'
112
+ require 'xapit/client/model_adapters/default_model_adapter'
113
+ require 'xapit/client/model_adapters/active_record_adapter'
@@ -0,0 +1,150 @@
1
+ module Xapit
2
+ module Client
3
+ class Collection
4
+ DEFAULT_PER_PAGE = 20
5
+
6
+ attr_reader :clauses
7
+
8
+ def initialize(clauses = [])
9
+ @clauses = clauses
10
+ end
11
+
12
+ def in_classes(*classes)
13
+ scope(:in_classes, classes)
14
+ end
15
+
16
+ def not_in_classes(*classes)
17
+ scope(:not_in_classes, classes)
18
+ end
19
+
20
+ def search(phrase = nil)
21
+ if phrase && !phrase.empty?
22
+ scope(:search, phrase)
23
+ else
24
+ self
25
+ end
26
+ end
27
+
28
+ def where(conditions)
29
+ scope(:where, where_conditions(conditions))
30
+ end
31
+
32
+ def not_where(conditions)
33
+ scope(:not_where, where_conditions(conditions))
34
+ end
35
+
36
+ def or_where(conditions)
37
+ scope(:or_where, where_conditions(conditions))
38
+ end
39
+
40
+ def order(column, direction = :asc)
41
+ scope(:order, [column, direction])
42
+ end
43
+
44
+ def page(page_num)
45
+ scope(:page, page_num)
46
+ end
47
+
48
+ def per(per_page)
49
+ scope(:per_page, per_page)
50
+ end
51
+
52
+ def similar_to(member)
53
+ scope(:similar_to, member.class.xapit_index_builder.document_data(member))
54
+ end
55
+
56
+ def with_facets(facets)
57
+ facets.to_s.length.zero? ? self : scope(:with_facets, facets.split("-"))
58
+ end
59
+
60
+ def include_facets(*facets)
61
+ facets.empty? ? self : scope(:include_facets, facets)
62
+ end
63
+
64
+ def records
65
+ @records ||= query[:records].map { |record| Kernel.const_get(record[:class]).find(record[:id]) }
66
+ end
67
+
68
+ def total_entries
69
+ query[:total].to_i
70
+ end
71
+
72
+ def current_page
73
+ (clause_value(:page) || 1).to_i
74
+ end
75
+
76
+ def limit_value
77
+ (clause_value(:per_page) || DEFAULT_PER_PAGE).to_i
78
+ end
79
+
80
+ def num_pages
81
+ (total_entries.to_f / limit_value).ceil
82
+ end
83
+ alias_method :total_pages, :num_pages
84
+
85
+ def applied_facet_options
86
+ query[:applied_facet_options].map do |option|
87
+ FacetOption.new(option[:name], {:value => option[:value]}, applied_facet_identifiers)
88
+ end
89
+ end
90
+
91
+ def applied_facet_identifiers
92
+ query[:applied_facet_options].map { |option| option[:id] }
93
+ end
94
+
95
+ def facets
96
+ @facets ||= fetch_facets.select { |f| f.options.size > 0 }
97
+ end
98
+
99
+ def spelling_suggestion
100
+ @spelling_suggestion ||= Xapit.database.spelling_suggestion(@clauses)
101
+ end
102
+
103
+ def respond_to?(method, include_private = false)
104
+ Array.method_defined?(method) || super
105
+ end
106
+
107
+ private
108
+
109
+ def where_conditions(conditions)
110
+ conditions.keys.each do |key|
111
+ if conditions[key].kind_of? Range
112
+ conditions[key] = {:from => conditions[key].begin, :to => conditions[key].end}
113
+ end
114
+ end
115
+ conditions
116
+ end
117
+
118
+ def fetch_facets
119
+ applied_facets = applied_facet_options.map(&:identifier)
120
+ query[:facets].map { |attribute, options| Facet.new(attribute, filter_facet_options(options), applied_facets) }
121
+ end
122
+
123
+ def filter_facet_options(options)
124
+ options.select do |option|
125
+ option[:count].to_i < total_entries
126
+ end
127
+ end
128
+
129
+ def clause_value(key)
130
+ clauses.map { |clause| clause[key] }.compact.first
131
+ end
132
+
133
+ def query
134
+ @query ||= Xapit.database.query(@clauses)
135
+ end
136
+
137
+ def scope(type, args)
138
+ Collection.new(@clauses + [{type => args}])
139
+ end
140
+
141
+ def method_missing(method, *args, &block)
142
+ if Array.method_defined?(method)
143
+ records.send(method, *args, &block)
144
+ else
145
+ super
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end