datamapper 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (131) hide show
  1. data/CHANGELOG +65 -0
  2. data/README +193 -1
  3. data/do_performance.rb +153 -0
  4. data/environment.rb +45 -0
  5. data/example.rb +119 -22
  6. data/lib/data_mapper.rb +36 -16
  7. data/lib/data_mapper/adapters/abstract_adapter.rb +8 -0
  8. data/lib/data_mapper/adapters/data_object_adapter.rb +360 -0
  9. data/lib/data_mapper/adapters/mysql_adapter.rb +30 -179
  10. data/lib/data_mapper/adapters/postgresql_adapter.rb +90 -199
  11. data/lib/data_mapper/adapters/sql/coersion.rb +32 -3
  12. data/lib/data_mapper/adapters/sql/commands/conditions.rb +97 -128
  13. data/lib/data_mapper/adapters/sql/commands/load_command.rb +234 -231
  14. data/lib/data_mapper/adapters/sql/commands/loader.rb +99 -0
  15. data/lib/data_mapper/adapters/sql/mappings/associations_set.rb +30 -0
  16. data/lib/data_mapper/adapters/sql/mappings/column.rb +68 -6
  17. data/lib/data_mapper/adapters/sql/mappings/schema.rb +6 -3
  18. data/lib/data_mapper/adapters/sql/mappings/table.rb +71 -42
  19. data/lib/data_mapper/adapters/sql/quoting.rb +8 -2
  20. data/lib/data_mapper/adapters/sqlite3_adapter.rb +32 -201
  21. data/lib/data_mapper/associations.rb +21 -7
  22. data/lib/data_mapper/associations/belongs_to_association.rb +96 -80
  23. data/lib/data_mapper/associations/has_and_belongs_to_many_association.rb +158 -67
  24. data/lib/data_mapper/associations/has_many_association.rb +96 -78
  25. data/lib/data_mapper/associations/has_n_association.rb +64 -0
  26. data/lib/data_mapper/associations/has_one_association.rb +49 -79
  27. data/lib/data_mapper/associations/reference.rb +47 -0
  28. data/lib/data_mapper/base.rb +216 -50
  29. data/lib/data_mapper/callbacks.rb +71 -24
  30. data/lib/data_mapper/{session.rb → context.rb} +20 -8
  31. data/lib/data_mapper/database.rb +176 -45
  32. data/lib/data_mapper/embedded_value.rb +65 -0
  33. data/lib/data_mapper/identity_map.rb +12 -4
  34. data/lib/data_mapper/support/active_record_impersonation.rb +12 -8
  35. data/lib/data_mapper/support/enumerable.rb +8 -0
  36. data/lib/data_mapper/support/serialization.rb +13 -0
  37. data/lib/data_mapper/support/string.rb +1 -12
  38. data/lib/data_mapper/support/symbol.rb +3 -0
  39. data/lib/data_mapper/validations/unique_validator.rb +1 -2
  40. data/lib/data_mapper/validations/validation_helper.rb +18 -1
  41. data/performance.rb +109 -34
  42. data/plugins/can_has_sphinx/LICENSE +23 -0
  43. data/plugins/can_has_sphinx/README +4 -0
  44. data/plugins/can_has_sphinx/REVISION +1 -0
  45. data/plugins/can_has_sphinx/Rakefile +22 -0
  46. data/plugins/can_has_sphinx/init.rb +1 -0
  47. data/plugins/can_has_sphinx/install.rb +1 -0
  48. data/plugins/can_has_sphinx/lib/acts_as_sphinx.rb +123 -0
  49. data/plugins/can_has_sphinx/lib/sphinx.rb +460 -0
  50. data/plugins/can_has_sphinx/scripts/sphinx.sh +47 -0
  51. data/plugins/can_has_sphinx/tasks/acts_as_sphinx_tasks.rake +41 -0
  52. data/plugins/dataobjects/REVISION +1 -0
  53. data/plugins/dataobjects/Rakefile +7 -0
  54. data/plugins/dataobjects/do.rb +246 -0
  55. data/plugins/dataobjects/do_mysql.rb +179 -0
  56. data/plugins/dataobjects/do_postgres.rb +181 -0
  57. data/plugins/dataobjects/do_sqlite3.rb +153 -0
  58. data/plugins/dataobjects/spec/do_spec.rb +150 -0
  59. data/plugins/dataobjects/spec/spec_helper.rb +81 -0
  60. data/plugins/dataobjects/swig_mysql/do_mysql.bundle +0 -0
  61. data/plugins/dataobjects/swig_mysql/extconf.rb +33 -0
  62. data/plugins/dataobjects/swig_mysql/mysql_c.c +18800 -0
  63. data/plugins/dataobjects/swig_mysql/mysql_c.i +8 -0
  64. data/plugins/dataobjects/swig_mysql/mysql_supp.i +46 -0
  65. data/plugins/dataobjects/swig_postgres/Makefile +146 -0
  66. data/plugins/dataobjects/swig_postgres/extconf.rb +29 -0
  67. data/plugins/dataobjects/swig_postgres/postgres_c.bundle +0 -0
  68. data/plugins/dataobjects/swig_postgres/postgres_c.c +8185 -0
  69. data/plugins/dataobjects/swig_postgres/postgres_c.i +73 -0
  70. data/plugins/dataobjects/swig_sqlite/db +0 -0
  71. data/plugins/dataobjects/swig_sqlite/extconf.rb +9 -0
  72. data/plugins/dataobjects/swig_sqlite/sqlite3_c.c +4725 -0
  73. data/plugins/dataobjects/swig_sqlite/sqlite_c.i +168 -0
  74. data/rakefile.rb +45 -23
  75. data/spec/acts_as_tree_spec.rb +39 -0
  76. data/spec/associations_spec.rb +220 -0
  77. data/spec/attributes_spec.rb +15 -0
  78. data/spec/base_spec.rb +44 -0
  79. data/spec/callbacks_spec.rb +45 -0
  80. data/spec/can_has_sphinx.rb +6 -0
  81. data/spec/coersion_spec.rb +34 -0
  82. data/spec/conditions_spec.rb +49 -0
  83. data/spec/conversions_to_yaml_spec.rb +17 -0
  84. data/spec/count_command_spec.rb +11 -0
  85. data/spec/delete_command_spec.rb +1 -1
  86. data/spec/embedded_value_spec.rb +23 -0
  87. data/spec/fixtures/animals_exhibits.yaml +2 -0
  88. data/spec/fixtures/people.yaml +18 -1
  89. data/spec/{legacy.rb → legacy_spec.rb} +3 -3
  90. data/spec/load_command_spec.rb +157 -20
  91. data/spec/magic_columns_spec.rb +9 -0
  92. data/spec/mock_adapter.rb +20 -0
  93. data/spec/models/animal.rb +1 -1
  94. data/spec/models/animals_exhibit.rb +6 -0
  95. data/spec/models/exhibit.rb +2 -0
  96. data/spec/models/person.rb +26 -1
  97. data/spec/models/project.rb +19 -0
  98. data/spec/models/sales_person.rb +1 -0
  99. data/spec/models/section.rb +6 -0
  100. data/spec/models/zoo.rb +3 -1
  101. data/spec/query_spec.rb +9 -0
  102. data/spec/save_command_spec.rb +65 -1
  103. data/spec/schema_spec.rb +89 -0
  104. data/spec/single_table_inheritance_spec.rb +27 -0
  105. data/spec/spec_helper.rb +9 -55
  106. data/spec/{symbolic_operators.rb → symbolic_operators_spec.rb} +9 -5
  107. data/spec/{validates_confirmation_of.rb → validates_confirmation_of_spec.rb} +4 -3
  108. data/spec/{validates_format_of.rb → validates_format_of_spec.rb} +5 -4
  109. data/spec/{validates_length_of.rb → validates_length_of_spec.rb} +8 -7
  110. data/spec/{validates_uniqueness_of.rb → validates_uniqueness_of_spec.rb} +7 -10
  111. data/spec/{validations.rb → validations_spec.rb} +24 -6
  112. data/tasks/drivers.rb +20 -0
  113. data/tasks/fixtures.rb +42 -0
  114. metadata +181 -42
  115. data/lib/data_mapper/adapters/sql/commands/advanced_load_command.rb +0 -140
  116. data/lib/data_mapper/adapters/sql/commands/delete_command.rb +0 -113
  117. data/lib/data_mapper/adapters/sql/commands/save_command.rb +0 -141
  118. data/lib/data_mapper/adapters/sql/commands/table_exists_command.rb +0 -33
  119. data/lib/data_mapper/adapters/sql_adapter.rb +0 -163
  120. data/lib/data_mapper/associations/advanced_has_many_association.rb +0 -55
  121. data/lib/data_mapper/support/blank_slate.rb +0 -3
  122. data/lib/data_mapper/support/proc.rb +0 -69
  123. data/lib/data_mapper/support/struct.rb +0 -26
  124. data/lib/data_mapper/unit_of_work.rb +0 -38
  125. data/spec/basic_finder.rb +0 -67
  126. data/spec/belongs_to.rb +0 -47
  127. data/spec/has_and_belongs_to_many.rb +0 -25
  128. data/spec/has_many.rb +0 -34
  129. data/spec/new_record.rb +0 -24
  130. data/spec/sub_select.rb +0 -16
  131. data/spec/support/string_spec.rb +0 -7
@@ -0,0 +1,23 @@
1
+ Copyright (C) 2005 Kent Sibilev <ksibilev@yahoo.com>
2
+ All rights reserved.
3
+ *
4
+ Redistribution and use in source and binary forms, with or without
5
+ modification, are permitted provided that the following conditions
6
+ are met:
7
+ 1. Redistributions of source code must retain the above copyright
8
+ notice, this list of conditions and the following disclaimer.
9
+ 2. Redistributions in binary form must reproduce the above copyright
10
+ notice, this list of conditions and the following disclaimer in the
11
+ documentation and/or other materials provided with the distribution.
12
+ *
13
+ THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
14
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
16
+ ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
17
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
18
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
19
+ OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
20
+ HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
21
+ LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
22
+ OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
23
+ SUCH DAMAGE.
@@ -0,0 +1,4 @@
1
+ ActsAsSphinx
2
+ ============
3
+
4
+ Description goes here
@@ -0,0 +1 @@
1
+ 21
@@ -0,0 +1,22 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rake/rdoctask'
4
+
5
+ desc 'Default: run unit tests.'
6
+ task :default => :test
7
+
8
+ desc 'Test the acts_as_sphinx plugin.'
9
+ Rake::TestTask.new(:test) do |t|
10
+ t.libs << 'lib'
11
+ t.pattern = 'test/**/*_test.rb'
12
+ t.verbose = true
13
+ end
14
+
15
+ desc 'Generate documentation for the acts_as_sphinx plugin.'
16
+ Rake::RDocTask.new(:rdoc) do |rdoc|
17
+ rdoc.rdoc_dir = 'rdoc'
18
+ rdoc.title = 'ActsAsSphinx'
19
+ rdoc.options << '--line-numbers' << '--inline-source'
20
+ rdoc.rdoc_files.include('README')
21
+ rdoc.rdoc_files.include('lib/**/*.rb')
22
+ end
@@ -0,0 +1 @@
1
+ require "acts_as_sphinx"
@@ -0,0 +1 @@
1
+ # Install hook code here
@@ -0,0 +1,123 @@
1
+ require "sphinx"
2
+
3
+ module ActsAsSphinx
4
+ module ClassMethods
5
+ # Associates the model class with a sphinx index, which will be used by find_with_sphinx method.
6
+ # You can pass the following options:
7
+ #
8
+ # :host is the host name or an IP address where searchd daemon is running, default is localhost
9
+ # :port is the port number of the searchd process, default is 3312
10
+ # :index is the name of the index to be used, default is the name of the table for the current model class.
11
+ def acts_as_sphinx(options = {})
12
+ options.assert_valid_keys(SphinxClassMethods::VALID_OPTIONS)
13
+
14
+ default_options = {:host => 'localhost', :port => 3312, :index => name.tableize}
15
+ write_inheritable_attribute 'sphinx_options', options.reverse_merge(default_options)
16
+ extend SphinxClassMethods
17
+ end
18
+ end
19
+
20
+ def self.included(receiver)
21
+ receiver.extend(ClassMethods)
22
+ end
23
+
24
+ module SphinxClassMethods
25
+ VALID_OPTIONS = %w[mode offset page limit index weights host
26
+ port range filter filter_range group_by sort_mode].map(&:to_sym)
27
+
28
+ def sphinx_index
29
+ read_inheritable_attribute('sphinx_options')[:index]
30
+ end
31
+
32
+ def sphinx_options
33
+ read_inheritable_attribute 'sphinx_options'
34
+ end
35
+
36
+ # Performs a sphinx search and returns a hash object as defined by Sphinx#query method.
37
+ # This methods accepts the same set of options as :sphinx option of find_with_sphinx method.
38
+ def ask_sphinx(query, options = {})
39
+ options.assert_valid_keys(VALID_OPTIONS)
40
+
41
+ default_options = {:offset => 0, :limit => 20}
42
+ default_options.merge! sphinx_options
43
+ options.reverse_merge! default_options
44
+
45
+ if options[:page] && options[:limit]
46
+ options[:offset] = options[:limit] * (options[:page].to_i - 1)
47
+ options[:offset] = 0 if options[:offset] < 0
48
+ end
49
+
50
+ sphinx = Sphinx.new
51
+ sphinx.set_server options[:host], options[:port]
52
+ sphinx.set_limits options[:offset], options[:limit]
53
+ sphinx.set_weights options[:weights] if options[:weights]
54
+ sphinx.set_id_range options[:range] if options[:range]
55
+
56
+ options[:filter].each do |attr, values|
57
+ sphinx.set_filter attr, [*values]
58
+ end if options[:filter]
59
+
60
+ options[:filter_range].each do |attr, (min, max)|
61
+ sphinx.set_filter_range attr, min, max
62
+ end if options[:filter_range]
63
+
64
+ options[:group_by].each do |attr, func|
65
+ funcion = Sphinx.const_get("SPH_GROUPBY_#{func.to_s.upcase}") \
66
+ rescue raise("Unknown group by function #{func}")
67
+ sphinx.set_group_by attr, funcion
68
+ end if options[:group_by]
69
+
70
+ if options[:mode]
71
+ match_mode = Sphinx.const_get("SPH_MATCH_#{options[:mode].to_s.upcase}") \
72
+ rescue raise("Unknown search mode #{options[:mode]}")
73
+ sphinx.set_match_mode match_mode
74
+ end
75
+
76
+ if options[:sort_mode]
77
+ sort_mode, sort_expr = options[:sort_mode]
78
+ sort_mode = Sphinx.const_get("SPH_SORT_#{sort_mode.to_s.upcase}") \
79
+ rescue raise("Unknown sort mode #{sort_mode}")
80
+ sphinx.set_sort_mode sort_mode, sort_expr
81
+ end
82
+
83
+ sphinx.query query, options[:index]
84
+ end
85
+
86
+ # Find all model objects using sphinx index.
87
+ # Besides regular ActiveRecord::Base#find method's options, you can specify
88
+ # :sphinx key that points to a hash with the following sphinx specific parameters:
89
+ #
90
+ # :mode defines the search mode (:all, :any, :boolean, :extended)
91
+ # :sort_mode defines the sort mode (:relevance, :attr_desc, :attr_asc, :time_segments, :extended),
92
+ # for example :sort_mode => [:attr_desc, 'myattr']
93
+ # :limit restricts result to a specified number of objects, default is 20
94
+ # :offset make this method return from a specific offset, default is 0
95
+ # :page can be used instead of :offset option to specify the page number
96
+ # :host overrides the default value of this option, see acts_as_sphinx method
97
+ # :port overrides the default value of this option, see acts_as_sphinx method
98
+ # :index overrides the default index name
99
+ # :weight is an array of weights for each index component (used in the relevance algorithm)
100
+ # :range is an array that defines the range document ids to be used, e.g. :range => [min, max]
101
+ # :fiter and :filter_range
102
+ # options define a search filter by an attribute
103
+ # :group_by makes the search result to be grouped by an attribute, e.g. :group_by => [attr, function],
104
+ # where function is :day, :week, :month, :year, or :attr
105
+ #
106
+ # The returned array has three special attributes:
107
+ #
108
+ # ary.total returns a total hits retrieved for this search
109
+ # ary.total_found returns a total number of hits found while scanning indexes.
110
+ # ary.time returns a time spent performing the search.
111
+ def find_with_sphinx(query, options = {})
112
+ result = ask_sphinx(query, options.delete(:sphinx) || {})
113
+ records = result[:matches].empty? ? [] : find(result[:matches].keys, options)
114
+ records = records.sort_by{|r| -result[:matches][r.id][:weight] }
115
+ %w[total total_found time].map(&:to_sym).each do |method|
116
+ class << records; self end.send(:define_method, method) {result[method]}
117
+ end
118
+ records
119
+ end
120
+ end
121
+ end
122
+
123
+ ActiveRecord::Base.send :include, ActsAsSphinx
@@ -0,0 +1,460 @@
1
+ require "socket"
2
+ # = sphinx.rb - Sphinx Client Library
3
+ #
4
+ # Author:: Dmytro Shteflyuk <mailto:kpumuk@kpumuk.info>.
5
+ # Copyright:: Copyright (c) 2006 Wildbit, LLC
6
+ # License:: Distributes under the same terms as Ruby
7
+ #
8
+ # This library is distributed under the terms of the Ruby license.
9
+ # You can freely distribute/modify this library.
10
+
11
+ # ==Sphinx Client Library
12
+ #
13
+ # The Sphinx Client Library is used to communicate with <tt>searchd</tt>
14
+ # daemon and get search results from Sphinx.
15
+ #
16
+ # ===Usage
17
+ #
18
+ # sphinx = Sphinx.new
19
+ # result = sphinx.query('test')
20
+ # ids = result[:matches].map { |id, value| id }.join(',')
21
+ # posts = Post.find :all, :conditions => "id IN (#{ids})"
22
+ #
23
+ # docs = posts.map { |post| post.body }
24
+ # excerpts = sphinx.build_excerpts(docs, 'index', 'test')
25
+ #
26
+ class Sphinx
27
+
28
+ # :stopdoc:
29
+ class SphinxError < StandardError; end
30
+ class SphinxConnectError < SphinxError; end
31
+ class SphinxResponseError < SphinxError; end
32
+ class SphinxInternalError < SphinxError; end
33
+ class SphinxTemporaryError < SphinxError; end
34
+ class SphinxUnknownError < SphinxError; end
35
+ # :startdoc:
36
+
37
+ # known searchd commands
38
+ SEARCHD_COMMAND_SEARCH = 0
39
+ SEARCHD_COMMAND_EXCERPT = 1
40
+
41
+ # current client-side command implementation versions
42
+ VER_COMMAND_SEARCH = 0x104
43
+ VER_COMMAND_EXCERPT = 0x100
44
+
45
+ # known searchd status codes
46
+ SEARCHD_OK = 0
47
+ SEARCHD_ERROR = 1
48
+ SEARCHD_RETRY = 2
49
+
50
+ # known match modes
51
+ SPH_MATCH_ALL = 0
52
+ SPH_MATCH_ANY = 1
53
+ SPH_MATCH_PHRASE = 2
54
+ SPH_MATCH_BOOLEAN = 3
55
+ SPH_MATCH_EXTENDED = 4
56
+
57
+ # known sort modes
58
+ SPH_SORT_RELEVANCE = 0
59
+ SPH_SORT_ATTR_DESC = 1
60
+ SPH_SORT_ATTR_ASC = 2
61
+ SPH_SORT_TIME_SEGMENTS = 3
62
+ SPH_SORT_EXTENDED = 4
63
+
64
+ # known attribute types
65
+ SPH_ATTR_INTEGER = 1
66
+ SPH_ATTR_TIMESTAMP = 2
67
+
68
+ # known grouping functions
69
+ SPH_GROUPBY_DAY = 0
70
+ SPH_GROUPBY_WEEK = 1
71
+ SPH_GROUPBY_MONTH = 2
72
+ SPH_GROUPBY_YEAR = 3
73
+ SPH_GROUPBY_ATTR = 4
74
+
75
+ # Constructs the Sphinx object and sets options to their default values.
76
+ def initialize
77
+ @host = 'localhost' # searchd host (default is "localhost")
78
+ @port = 3312 # searchd port (default is 3312)
79
+ @offset = 0 # how much records to seek from result-set start (default is 0)
80
+ @limit = 20 # how much records to return from result-set starting at offset (default is 20)
81
+ @mode = SPH_MATCH_ALL # query matching mode (default is SPH_MATCH_ALL)
82
+ @weights = [] # per-field weights (default is 1 for all fields)
83
+ @sort = SPH_SORT_RELEVANCE # match sorting mode (default is SPH_SORT_RELEVANCE)
84
+ @sortby = '' # attribute to sort by (defualt is "")
85
+ @min_id = 0 # min ID to match (default is 0)
86
+ @max_id = 0xFFFFFFFF # max ID to match (default is UINT_MAX)
87
+ @min = {} # attribute name to min-value hash (for range filters)
88
+ @max = {} # attribute name to max-value hash (for range filters)
89
+ @filter = {} # attribute name to values set hash (for values-set filters)
90
+ @groupby = '' # group-by attribute name
91
+ @groupfunc = SPH_GROUPBY_DAY # function to pre-process group-by attribute value with
92
+ @maxmatches = 1000 # max matches to retrieve
93
+
94
+ @error = '' # last error message
95
+ @warning = '' # last warning message
96
+ end
97
+
98
+ # Get last error message.
99
+ def last_error
100
+ @error
101
+ end
102
+
103
+ # Get last warning message.
104
+ def last_warning
105
+ @warning
106
+ end
107
+
108
+ # Set searchd server.
109
+ def set_server(host, port)
110
+ @host = host
111
+ @port = port
112
+ end
113
+
114
+ # Set match offset, count, and max number to retrieve.
115
+ def set_limits(offset, limit, max = 0)
116
+ @offset = offset
117
+ @limit = limit
118
+ @maxmatches = max if max > 0
119
+ end
120
+
121
+ # Set match mode.
122
+ def set_match_mode(mode)
123
+ @mode = mode
124
+ end
125
+
126
+ # Set sort mode.
127
+ def set_sort_mode(mode, sortby = '')
128
+ @sort = mode
129
+ @sortby = sortby
130
+ end
131
+
132
+ # Set per-field weights.
133
+ def set_weights(weights)
134
+ @weights = weights
135
+ end
136
+
137
+ # Set IDs range to match.
138
+ #
139
+ # Only match those records where document ID is beetwen <tt>min_id</tt> and <tt>max_id</tt>
140
+ # (including <tt>min_id</tt> and <tt>max_id</tt>).
141
+ def set_id_range(min_id, max_id)
142
+ @min_id = min_id
143
+ @max_id = max_id
144
+ end
145
+
146
+ # Set values filter.
147
+ #
148
+ # Only match those records where <tt>attr</tt> column values
149
+ # are in specified set.
150
+ def set_filter(attr, values)
151
+ @filter[attr] = values
152
+ end
153
+
154
+ # Set range filter.
155
+ #
156
+ # Only match those records where <tt>attr</tt> column value
157
+ # is beetwen <tt>min</tt> and <tt>max</tt> (including <tt>min</tt> and <tt>max</tt>).
158
+ def set_filter_range(attr, min, max)
159
+ @min[attr] = min
160
+ @max[attr] = max
161
+ end
162
+
163
+ # Set grouping.
164
+ #
165
+ # if grouping
166
+ def set_group_by(attr, func)
167
+ @groupby = attr
168
+ @groupfunc = func
169
+ end
170
+
171
+ # Connect to searchd server and run given search query.
172
+ #
173
+ # * <tt>query</tt> -- query string
174
+ # * <tt>index</tt> -- index name to query, default is "*" which means to query all indexes
175
+ #
176
+ # returns hash which has the following keys on success:
177
+ #
178
+ # * <tt>:matches</tt> -- hash which maps found document_id to ( "weight", "group" ) hash
179
+ # * <tt>:total</tt> -- total amount of matches retrieved (upto SPH_MAX_MATCHES, see sphinx.h)
180
+ # * <tt>:total_found</tt> -- total amount of matching documents in index
181
+ # * <tt>:time</tt> -- search time
182
+ # * <tt>:words</tt> -- hash which maps query terms (stemmed!) to ( :docs, :hits ) hash
183
+ def query(query, index = '*')
184
+ sock = connect
185
+
186
+ # build request
187
+
188
+ # mode and limits
189
+ req = [@offset, @limit, @mode, @sort].pack('NNNN')
190
+ req << [@sortby.length].pack('N')
191
+ req << @sortby
192
+ # query itself
193
+ req << [query.length].pack('N')
194
+ req << query
195
+ # weights
196
+ req << [@weights.length].pack('N')
197
+ req << @weights.pack('N' * @weights.length)
198
+ # indexes
199
+ req << [index.length].pack('N')
200
+ req << index
201
+ # id range
202
+ req << [@min_id.to_i, @max_id.to_i].pack('NN')
203
+
204
+ # filters
205
+ req << [@min.length + @filter.length].pack('N')
206
+ @min.each do |attribute, min|
207
+ req << [attribute.length].pack('N')
208
+ req << attribute
209
+ req << [0, min, @max[attribute]].pack('NNN')
210
+ end
211
+
212
+ @filter.each do |attribute, values|
213
+ req << [attribute.length].pack('N')
214
+ req << attribute
215
+ req << [values.length].pack('N')
216
+ req << values.pack('N' * values.length)
217
+ end
218
+
219
+ # group-by
220
+ req << [@groupfunc, @groupby.length].pack('NN')
221
+ req << @groupby
222
+
223
+ # max matches to retrieve
224
+ req << [@maxmatches].pack('N')
225
+
226
+ # send query, get response
227
+ len = req.length
228
+ # add header
229
+ req = [SEARCHD_COMMAND_SEARCH, VER_COMMAND_SEARCH, len].pack('nnN') + req
230
+ sock.send(req, 0)
231
+
232
+ response = get_response(sock, VER_COMMAND_SEARCH)
233
+
234
+ # parse response
235
+ result = {}
236
+ max = response.length # protection from broken response
237
+
238
+ #read schema
239
+ p = 0
240
+ fields = []
241
+ attrs = {}
242
+
243
+ nfields = response[p, 4].unpack('N*').first
244
+ p += 4
245
+ while nfields > 0 and p < max
246
+ nfields -= 1
247
+ len = response[p, 4].unpack('N*').first
248
+ p += 4
249
+ fields << response[p, len]
250
+ p += len
251
+ end
252
+ result[:fields] = fields
253
+
254
+ nattrs = response[p, 4].unpack('N*').first
255
+ p += 4
256
+ while nattrs > 0 && p < max
257
+ nattrs -= 1
258
+ len = response[p, 4].unpack('N*').first
259
+ p += 4
260
+ attr = response[p, len]
261
+ p += len
262
+ type = response[p, 4].unpack('N*').first
263
+ p += 4
264
+ attrs[attr.to_sym] = type;
265
+ end
266
+ result[:attrs] = attrs
267
+
268
+ # read match count
269
+ count = response[p, 4].unpack('N*').first
270
+ p += 4
271
+
272
+ # read matches
273
+ result[:matches] = {}
274
+ while count > 0 and p < max
275
+ count -= 1
276
+ doc, weight = response[p, 8].unpack('N*N*')
277
+ p += 8
278
+
279
+ result[:matches][doc] ||= {}
280
+ result[:matches][doc][:weight] = weight
281
+ attrs.each do |attr, type|
282
+ val = response[p, 4].unpack('N*').first
283
+ p += 4
284
+ result[:matches][doc][:attrs] ||= {}
285
+ result[:matches][doc][:attrs][attr] = val
286
+ end
287
+ end
288
+ result[:total], result[:total_found], result[:time], words = \
289
+ response[p, 16].unpack('N*N*N*N*')
290
+ result[:time] = '%.3f' % (result[:time] / 1000)
291
+ p += 16
292
+
293
+ result[:words] = {}
294
+ while words > 0 and p < max
295
+ words -= 1
296
+ len = response[p, 4].unpack('N*').first
297
+ p += 4
298
+ word = response[p, len]
299
+ p += len
300
+ docs, hits = response[p, 8].unpack('N*N*')
301
+ p += 8
302
+ result[:words][word] = {:docs => docs, :hits => hits}
303
+ end
304
+
305
+ result
306
+ end
307
+
308
+ # Connect to searchd server and generate exceprts from given documents.
309
+ #
310
+ # * <tt>index</tt> -- a string specifiying the index which settings will be used
311
+ # for stemming, lexing and case folding
312
+ # * <tt>docs</tt> -- an array of strings which represent the documents' contents
313
+ # * <tt>words</tt> -- a string which contains the words to highlight
314
+ # * <tt>opts</tt> is a hash which contains additional optional highlighting parameters.
315
+ #
316
+ # You can use following parameters:
317
+ # * <tt>:before_match</tt> -- a string to insert before a set of matching words, default is "<b>"
318
+ # * <tt>:after_match</tt> -- a string to insert after a set of matching words, default is "<b>"
319
+ # * <tt>:chunk_separator</tt> -- a string to insert between excerpts chunks, default is " ... "
320
+ # * <tt>:limit</tt> -- max excerpt size in symbols (codepoints), default is 256
321
+ # * <tt>:around</tt> -- how much words to highlight around each match, default is 5
322
+ #
323
+ # Returns an array of string excerpts on success.
324
+ def build_excerpts(docs, index, words, opts = {})
325
+ sock = connect
326
+
327
+ # fixup options
328
+ opts[:before_match] ||= '<b>';
329
+ opts[:after_match] ||= '</b>';
330
+ opts[:chunk_separator] ||= ' ... ';
331
+ opts[:limit] ||= 256;
332
+ opts[:around] ||= 5;
333
+
334
+ # build request
335
+
336
+ # v.1.0 req
337
+ req = [0, 1].pack('N2'); # mode=0, flags=1 (remove spaces)
338
+ # req index
339
+ req << [index.length].pack('N')
340
+ req << index
341
+ # req words
342
+ req << [words.length].pack('N')
343
+ req << words
344
+
345
+ # options
346
+ req << [opts[:before_match].length].pack('N')
347
+ req << opts[:before_match]
348
+ req << [opts[:after_match].length].pack('N')
349
+ req << opts[:after_match]
350
+ req << [opts[:chunk_separator].length].pack('N')
351
+ req << opts[:chunk_separator]
352
+ req << [opts[:limit].to_i, opts[:around].to_i].pack('NN')
353
+
354
+ # documents
355
+ req << [docs.size].pack('N');
356
+ docs.each do |doc|
357
+ req << [doc.length].pack('N')
358
+ req << doc
359
+ end
360
+
361
+ # send query, get response
362
+ len = req.length
363
+ # add header
364
+ req = [SEARCHD_COMMAND_EXCERPT, VER_COMMAND_EXCERPT, len].pack('nnN') + req
365
+ sock.send(req, 0)
366
+
367
+ response = get_response(sock, VER_COMMAND_EXCERPT)
368
+
369
+ # parse response
370
+ p = 0
371
+ res = []
372
+ rlen = response.length
373
+ docs.each do |doc|
374
+ len = response[p, 4].unpack('N*').first;
375
+ p += 4
376
+ if p + len > rlen
377
+ @error = 'incomplete reply'
378
+ raise SphinxResponseError, @error
379
+ end
380
+ res << response[p, len]
381
+ p += len
382
+ end
383
+ return res;
384
+ end
385
+
386
+ # Connect to searchd server.
387
+ def connect
388
+ begin
389
+ sock = TCPSocket.new(@host, @port)
390
+ rescue
391
+ @error = "connection to #{@host}:#{@port} failed"
392
+ raise SphinxConnectError, @error
393
+ end
394
+
395
+ v = sock.recv(4).unpack('N*').first
396
+ if v < 1
397
+ sock.close
398
+ @error = "expected searchd protocol version 1+, got version '#{v}'"
399
+ raise SphinxConnectError, @error
400
+ end
401
+
402
+ sock.send([1].pack('N'), 0)
403
+ sock
404
+ end
405
+ private :connect
406
+
407
+ # get and check response packet from searchd server
408
+ def get_response(sock, client_version)
409
+ header = sock.recv(8)
410
+ status, ver, len = header.unpack('n2N')
411
+ response = ''
412
+ left = len
413
+ while left > 0 do
414
+ begin
415
+ chunk = sock.recv(left)
416
+ if chunk
417
+ response << chunk
418
+ left -= chunk.length
419
+ end
420
+ rescue EOFError
421
+ end
422
+ end if left
423
+ sock.close
424
+
425
+ # check response
426
+ read = response.length
427
+ if not response or read != len
428
+ @error = len \
429
+ ? "failed to read searchd response (status=#{status}, ver=#{ver}, len=#{len}, read=#{read})" \
430
+ : "received zero-sized searchd response"
431
+ raise SphinxResponseError, @error
432
+ end
433
+
434
+ # check status
435
+ if status == SEARCHD_ERROR
436
+ @error = "searchd error: " + response[4,].to_s
437
+ raise SphinxInternalError, @error
438
+ end
439
+
440
+ if status == SEARCHD_RETRY
441
+ @error = "temporary searchd error: " + response[4,]
442
+ raise SphinxTemporaryError, @error
443
+ end
444
+
445
+ unless status == SEARCHD_OK
446
+ @error = "unknown status code '#{status}'"
447
+ raise SphinxUnknownError, @error
448
+ end
449
+
450
+ # check version
451
+ if ver < client_version
452
+ @warning = "searchd command v.%d.%d older than client's v.%d.%d, some options might not work" % \
453
+ ver >> 8, ver & 0xff, client_ver >> 8, client_ver & 0xff
454
+ end
455
+
456
+ return response
457
+ end
458
+ private :get_response
459
+
460
+ end