datamapper 0.1.1 → 0.2.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 (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