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