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.
- data/CHANGELOG +65 -0
- data/README +193 -1
- data/do_performance.rb +153 -0
- data/environment.rb +45 -0
- data/example.rb +119 -22
- data/lib/data_mapper.rb +36 -16
- data/lib/data_mapper/adapters/abstract_adapter.rb +8 -0
- data/lib/data_mapper/adapters/data_object_adapter.rb +360 -0
- data/lib/data_mapper/adapters/mysql_adapter.rb +30 -179
- data/lib/data_mapper/adapters/postgresql_adapter.rb +90 -199
- data/lib/data_mapper/adapters/sql/coersion.rb +32 -3
- data/lib/data_mapper/adapters/sql/commands/conditions.rb +97 -128
- data/lib/data_mapper/adapters/sql/commands/load_command.rb +234 -231
- data/lib/data_mapper/adapters/sql/commands/loader.rb +99 -0
- data/lib/data_mapper/adapters/sql/mappings/associations_set.rb +30 -0
- data/lib/data_mapper/adapters/sql/mappings/column.rb +68 -6
- data/lib/data_mapper/adapters/sql/mappings/schema.rb +6 -3
- data/lib/data_mapper/adapters/sql/mappings/table.rb +71 -42
- data/lib/data_mapper/adapters/sql/quoting.rb +8 -2
- data/lib/data_mapper/adapters/sqlite3_adapter.rb +32 -201
- data/lib/data_mapper/associations.rb +21 -7
- data/lib/data_mapper/associations/belongs_to_association.rb +96 -80
- data/lib/data_mapper/associations/has_and_belongs_to_many_association.rb +158 -67
- data/lib/data_mapper/associations/has_many_association.rb +96 -78
- data/lib/data_mapper/associations/has_n_association.rb +64 -0
- data/lib/data_mapper/associations/has_one_association.rb +49 -79
- data/lib/data_mapper/associations/reference.rb +47 -0
- data/lib/data_mapper/base.rb +216 -50
- data/lib/data_mapper/callbacks.rb +71 -24
- data/lib/data_mapper/{session.rb → context.rb} +20 -8
- data/lib/data_mapper/database.rb +176 -45
- data/lib/data_mapper/embedded_value.rb +65 -0
- data/lib/data_mapper/identity_map.rb +12 -4
- data/lib/data_mapper/support/active_record_impersonation.rb +12 -8
- data/lib/data_mapper/support/enumerable.rb +8 -0
- data/lib/data_mapper/support/serialization.rb +13 -0
- data/lib/data_mapper/support/string.rb +1 -12
- data/lib/data_mapper/support/symbol.rb +3 -0
- data/lib/data_mapper/validations/unique_validator.rb +1 -2
- data/lib/data_mapper/validations/validation_helper.rb +18 -1
- data/performance.rb +109 -34
- data/plugins/can_has_sphinx/LICENSE +23 -0
- data/plugins/can_has_sphinx/README +4 -0
- data/plugins/can_has_sphinx/REVISION +1 -0
- data/plugins/can_has_sphinx/Rakefile +22 -0
- data/plugins/can_has_sphinx/init.rb +1 -0
- data/plugins/can_has_sphinx/install.rb +1 -0
- data/plugins/can_has_sphinx/lib/acts_as_sphinx.rb +123 -0
- data/plugins/can_has_sphinx/lib/sphinx.rb +460 -0
- data/plugins/can_has_sphinx/scripts/sphinx.sh +47 -0
- data/plugins/can_has_sphinx/tasks/acts_as_sphinx_tasks.rake +41 -0
- data/plugins/dataobjects/REVISION +1 -0
- data/plugins/dataobjects/Rakefile +7 -0
- data/plugins/dataobjects/do.rb +246 -0
- data/plugins/dataobjects/do_mysql.rb +179 -0
- data/plugins/dataobjects/do_postgres.rb +181 -0
- data/plugins/dataobjects/do_sqlite3.rb +153 -0
- data/plugins/dataobjects/spec/do_spec.rb +150 -0
- data/plugins/dataobjects/spec/spec_helper.rb +81 -0
- data/plugins/dataobjects/swig_mysql/do_mysql.bundle +0 -0
- data/plugins/dataobjects/swig_mysql/extconf.rb +33 -0
- data/plugins/dataobjects/swig_mysql/mysql_c.c +18800 -0
- data/plugins/dataobjects/swig_mysql/mysql_c.i +8 -0
- data/plugins/dataobjects/swig_mysql/mysql_supp.i +46 -0
- data/plugins/dataobjects/swig_postgres/Makefile +146 -0
- data/plugins/dataobjects/swig_postgres/extconf.rb +29 -0
- data/plugins/dataobjects/swig_postgres/postgres_c.bundle +0 -0
- data/plugins/dataobjects/swig_postgres/postgres_c.c +8185 -0
- data/plugins/dataobjects/swig_postgres/postgres_c.i +73 -0
- data/plugins/dataobjects/swig_sqlite/db +0 -0
- data/plugins/dataobjects/swig_sqlite/extconf.rb +9 -0
- data/plugins/dataobjects/swig_sqlite/sqlite3_c.c +4725 -0
- data/plugins/dataobjects/swig_sqlite/sqlite_c.i +168 -0
- data/rakefile.rb +45 -23
- data/spec/acts_as_tree_spec.rb +39 -0
- data/spec/associations_spec.rb +220 -0
- data/spec/attributes_spec.rb +15 -0
- data/spec/base_spec.rb +44 -0
- data/spec/callbacks_spec.rb +45 -0
- data/spec/can_has_sphinx.rb +6 -0
- data/spec/coersion_spec.rb +34 -0
- data/spec/conditions_spec.rb +49 -0
- data/spec/conversions_to_yaml_spec.rb +17 -0
- data/spec/count_command_spec.rb +11 -0
- data/spec/delete_command_spec.rb +1 -1
- data/spec/embedded_value_spec.rb +23 -0
- data/spec/fixtures/animals_exhibits.yaml +2 -0
- data/spec/fixtures/people.yaml +18 -1
- data/spec/{legacy.rb → legacy_spec.rb} +3 -3
- data/spec/load_command_spec.rb +157 -20
- data/spec/magic_columns_spec.rb +9 -0
- data/spec/mock_adapter.rb +20 -0
- data/spec/models/animal.rb +1 -1
- data/spec/models/animals_exhibit.rb +6 -0
- data/spec/models/exhibit.rb +2 -0
- data/spec/models/person.rb +26 -1
- data/spec/models/project.rb +19 -0
- data/spec/models/sales_person.rb +1 -0
- data/spec/models/section.rb +6 -0
- data/spec/models/zoo.rb +3 -1
- data/spec/query_spec.rb +9 -0
- data/spec/save_command_spec.rb +65 -1
- data/spec/schema_spec.rb +89 -0
- data/spec/single_table_inheritance_spec.rb +27 -0
- data/spec/spec_helper.rb +9 -55
- data/spec/{symbolic_operators.rb → symbolic_operators_spec.rb} +9 -5
- data/spec/{validates_confirmation_of.rb → validates_confirmation_of_spec.rb} +4 -3
- data/spec/{validates_format_of.rb → validates_format_of_spec.rb} +5 -4
- data/spec/{validates_length_of.rb → validates_length_of_spec.rb} +8 -7
- data/spec/{validates_uniqueness_of.rb → validates_uniqueness_of_spec.rb} +7 -10
- data/spec/{validations.rb → validations_spec.rb} +24 -6
- data/tasks/drivers.rb +20 -0
- data/tasks/fixtures.rb +42 -0
- metadata +181 -42
- data/lib/data_mapper/adapters/sql/commands/advanced_load_command.rb +0 -140
- data/lib/data_mapper/adapters/sql/commands/delete_command.rb +0 -113
- data/lib/data_mapper/adapters/sql/commands/save_command.rb +0 -141
- data/lib/data_mapper/adapters/sql/commands/table_exists_command.rb +0 -33
- data/lib/data_mapper/adapters/sql_adapter.rb +0 -163
- data/lib/data_mapper/associations/advanced_has_many_association.rb +0 -55
- data/lib/data_mapper/support/blank_slate.rb +0 -3
- data/lib/data_mapper/support/proc.rb +0 -69
- data/lib/data_mapper/support/struct.rb +0 -26
- data/lib/data_mapper/unit_of_work.rb +0 -38
- data/spec/basic_finder.rb +0 -67
- data/spec/belongs_to.rb +0 -47
- data/spec/has_and_belongs_to_many.rb +0 -25
- data/spec/has_many.rb +0 -34
- data/spec/new_record.rb +0 -24
- data/spec/sub_select.rb +0 -16
- 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 @@
|
|
|
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
|