mongoid-giza 0.1.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.
- checksums.yaml +7 -0
- data/.gitignore +21 -0
- data/.travis.yml +6 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +22 -0
- data/README.md +152 -0
- data/Rakefile +11 -0
- data/lib/mongoid/giza.rb +160 -0
- data/lib/mongoid/giza/configuration.rb +136 -0
- data/lib/mongoid/giza/dynamic_index.rb +37 -0
- data/lib/mongoid/giza/index.rb +101 -0
- data/lib/mongoid/giza/index/attribute.rb +39 -0
- data/lib/mongoid/giza/index/field.rb +27 -0
- data/lib/mongoid/giza/indexer.rb +33 -0
- data/lib/mongoid/giza/models/giza_id.rb +27 -0
- data/lib/mongoid/giza/railtie.rb +17 -0
- data/lib/mongoid/giza/search.rb +83 -0
- data/lib/mongoid/giza/version.rb +5 -0
- data/lib/mongoid/giza/xml_pipe2.rb +67 -0
- data/mongoid-giza.gemspec +32 -0
- data/spec/mongoid/giza/configuration_spec.rb +340 -0
- data/spec/mongoid/giza/dynamic_index_spec.rb +32 -0
- data/spec/mongoid/giza/index/attribute_spec.rb +36 -0
- data/spec/mongoid/giza/index/field_spec.rb +25 -0
- data/spec/mongoid/giza/index_spec.rb +162 -0
- data/spec/mongoid/giza/indexer_spec.rb +87 -0
- data/spec/mongoid/giza/models/giza_id_spec.rb +30 -0
- data/spec/mongoid/giza/search_spec.rb +100 -0
- data/spec/mongoid/giza/xml_pipe2_spec.rb +98 -0
- data/spec/mongoid/giza_spec.rb +282 -0
- data/spec/spec_helper.rb +51 -0
- metadata +227 -0
@@ -0,0 +1,37 @@
|
|
1
|
+
module Mongoid
|
2
|
+
module Giza
|
3
|
+
|
4
|
+
# Defines a dynamic index which is used to generate a index for each object of the class
|
5
|
+
class DynamicIndex
|
6
|
+
attr_reader :klass, :settings, :block
|
7
|
+
|
8
|
+
# Creates a new dynamic index for the supplied class
|
9
|
+
#
|
10
|
+
# @param klass [Class] a class which each object will generate an {Mongoid::Giza::Index}
|
11
|
+
# after the evaluation of the block
|
12
|
+
# @param settings [Hash] a hash of settings to be defined on every generated index
|
13
|
+
# @param block [Proc] the routine that will be evaluated for each object from the class
|
14
|
+
def initialize(klass, settings, block)
|
15
|
+
@klass = klass
|
16
|
+
@settings = settings
|
17
|
+
@block = block
|
18
|
+
end
|
19
|
+
|
20
|
+
# Generates indexes for every object of the class.
|
21
|
+
# The name of the index is unique so in case of a name collision,
|
22
|
+
# the last index to be generated is the one that will persist
|
23
|
+
#
|
24
|
+
# @return [Hash<Symbol, Mongoid::Giza::Index] an hash with every key being the index name
|
25
|
+
# and the value the index itself
|
26
|
+
def generate!
|
27
|
+
indexes = {}
|
28
|
+
klass.all.each do |object|
|
29
|
+
index = Mongoid::Giza::Index.new(klass, settings)
|
30
|
+
Docile.dsl_eval(index, object, &block)
|
31
|
+
indexes[index.name] = index
|
32
|
+
end
|
33
|
+
indexes
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
module Mongoid
|
2
|
+
module Giza
|
3
|
+
|
4
|
+
# Represents a Sphinx index
|
5
|
+
class Index
|
6
|
+
|
7
|
+
# Hash in which each key is a class accepted by +Mongoid+
|
8
|
+
# and its value is a compatible Sphix attribute type
|
9
|
+
TYPES_MAP = {
|
10
|
+
Regexp => :string,
|
11
|
+
String => :string,
|
12
|
+
Symbol => :string,
|
13
|
+
Boolean => :bool,
|
14
|
+
Integer => :bigint,
|
15
|
+
Date => :timestamp,
|
16
|
+
DateTime => :timestamp,
|
17
|
+
Time => :timestamp,
|
18
|
+
BigDecimal => :float,
|
19
|
+
Float => :float,
|
20
|
+
Array => :multi,
|
21
|
+
Range => :multi,
|
22
|
+
Hash => :json,
|
23
|
+
Moped::BSON::ObjectId => :string,
|
24
|
+
ActiveSupport::TimeWithZone => :timestamp,
|
25
|
+
}
|
26
|
+
|
27
|
+
attr_accessor :klass, :settings, :fields, :attributes
|
28
|
+
|
29
|
+
# Creates a new index with a class, which should include Mongoid::Document, and an optional settings hash.
|
30
|
+
#
|
31
|
+
# Note that no validations are made on class, so classes that behave like Mongoid::Document should be fine.
|
32
|
+
#
|
33
|
+
# @param klass [Class] the class whose objects will be indexed
|
34
|
+
# @param settings [Hash] an optional settings hash to be forwarded to Riddle
|
35
|
+
def initialize(klass, settings = {})
|
36
|
+
@klass = klass
|
37
|
+
@settings = settings
|
38
|
+
@name = @klass.name.to_sym
|
39
|
+
@criteria = klass.all
|
40
|
+
@fields = []
|
41
|
+
@attributes = []
|
42
|
+
end
|
43
|
+
|
44
|
+
# Adds a full-text field to the index with the corresponding name
|
45
|
+
#
|
46
|
+
# If a block is given then it will be evaluated for each instance of the class being indexed
|
47
|
+
# and the resulting string will be the field value.
|
48
|
+
# Otherwise the field value will be the value of the corresponding object field
|
49
|
+
#
|
50
|
+
# @param name [Symbol] the name of the field
|
51
|
+
# @param options [Hash] an optional hash of options.
|
52
|
+
# Currently only the boolean option :attribute is avaiable (see {Mongoid::Giza::Index::Field#initialize})
|
53
|
+
# @param block [Proc] an optional block to be evaluated at the scope of the document on index creation
|
54
|
+
def field(name, options = {}, &block)
|
55
|
+
attribute = options[:attribute].nil? ? false : true
|
56
|
+
@fields << Mongoid::Giza::Index::Field.new(name, attribute, &block)
|
57
|
+
end
|
58
|
+
|
59
|
+
# Adds an attribute to the index with the corresponding name.
|
60
|
+
#
|
61
|
+
# If a type is not given then it will try to fetch the type of the corresponding class field,
|
62
|
+
# falling back to :string
|
63
|
+
#
|
64
|
+
# If a block is given then it will be evaluated for each instance of the class being indexed
|
65
|
+
# and the resulting value will be the attribute value.
|
66
|
+
# Otherwise the attribute value will be the value of the corresponding object field
|
67
|
+
#
|
68
|
+
# @param name [Symbol] the name of the attribute
|
69
|
+
# @param type [Symbol] an optional attribute type
|
70
|
+
# @param block [Proc] an optional block to be evaluated at the scope of the document on index creation
|
71
|
+
def attribute(name, type = nil, &block)
|
72
|
+
if type.nil?
|
73
|
+
field = @klass.fields[name.to_s]
|
74
|
+
type = field.nil? ? Mongoid::Giza::Index::TYPES_MAP.values.first :
|
75
|
+
Mongoid::Giza::Index::TYPES_MAP[field.type] || Mongoid::Giza::Index::TYPES_MAP.values.first
|
76
|
+
end
|
77
|
+
@attributes << Mongoid::Giza::Index::Attribute.new(name, type, &block)
|
78
|
+
end
|
79
|
+
|
80
|
+
# Retrieves and optionally sets the index name
|
81
|
+
#
|
82
|
+
# @param new_name [Symbol, String] an optional new name for the index
|
83
|
+
#
|
84
|
+
# @return [Symbol] The name of the index
|
85
|
+
def name(new_name = nil)
|
86
|
+
@name = new_name || @name.to_sym
|
87
|
+
end
|
88
|
+
|
89
|
+
def criteria(new_criteria = nil)
|
90
|
+
@criteria = new_criteria || @criteria
|
91
|
+
end
|
92
|
+
|
93
|
+
# Generates a XML document according to the XMLPipe2 specification from Sphinx
|
94
|
+
#
|
95
|
+
# @param buffer [#<<] any IO object that supports appending content using <<
|
96
|
+
def generate_xmlpipe2(buffer)
|
97
|
+
Mongoid::Giza::XMLPipe2.new(self, buffer).generate!
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Mongoid
|
2
|
+
module Giza
|
3
|
+
class Index
|
4
|
+
|
5
|
+
# Represents an Sphinx index {http://sphinxsearch.com/docs/current.html#attributes attribute}
|
6
|
+
class Attribute
|
7
|
+
|
8
|
+
# Defines the array of currently supported Sphix attribute types
|
9
|
+
TYPES = [
|
10
|
+
:uint, :bool, :bigint, :timestamp, :str2ordinal,
|
11
|
+
:float, :multi, :string, :json, :str2wordcount
|
12
|
+
]
|
13
|
+
|
14
|
+
attr_accessor :name, :type, :block
|
15
|
+
|
16
|
+
# Creates a new attribute with name, type and an optional block
|
17
|
+
#
|
18
|
+
# If a block is given then it will be evaluated for each instance of the class being indexed
|
19
|
+
# and the resulting value will be the attribute value.
|
20
|
+
# Otherwise the attribute value will be the value of the corresponding object field
|
21
|
+
#
|
22
|
+
# @param name [Symbol] the name of the attribute
|
23
|
+
# @param type [Symbol] the type of the attribute. Must be one of the types defined in {Mongoid::Giza::Index::Attribute::TYPES}
|
24
|
+
# @param block [Proc] an optional block to be evaluated at the scope of the document on index creation
|
25
|
+
#
|
26
|
+
# @raise [TypeError] if the type is not valid. (see {Mongoid::Giza::Index::Attribute::TYPES})
|
27
|
+
def initialize(name, type, &block)
|
28
|
+
raise TypeError,
|
29
|
+
"attribute type not supported. " \
|
30
|
+
"It must be one of the following: " \
|
31
|
+
"#{Mongoid::Giza::Index::Attribute::TYPES.join(", ")}" unless Mongoid::Giza::Index::Attribute::TYPES.include? type
|
32
|
+
@name = name
|
33
|
+
@type = type
|
34
|
+
@block = block
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Mongoid
|
2
|
+
module Giza
|
3
|
+
class Index
|
4
|
+
|
5
|
+
# Represents a Sphinx index {http://sphinxsearch.com/docs/current.html#fields full-text field}
|
6
|
+
class Field
|
7
|
+
attr_accessor :name, :attribute, :block
|
8
|
+
|
9
|
+
# Creates a full-text field with a name and an optional block
|
10
|
+
#
|
11
|
+
# If a block is given then it will be evaluated for each instance of the class being indexed
|
12
|
+
# and the resulting string will be the field value.
|
13
|
+
# Otherwise the field value will be the value of the corresponding object field
|
14
|
+
#
|
15
|
+
# @param name [Symbol] the name of the field
|
16
|
+
# @param attribute [TrueClass, FalseClass] whether this field will also be stored as an string attribute
|
17
|
+
# (see {http://sphinxsearch.com/docs/current.html#conf-xmlpipe-field-string})
|
18
|
+
# @param block [Proc] an optional block to be evaluated at the scope of the document on index creation
|
19
|
+
def initialize(name, attribute = false, &block)
|
20
|
+
@name = name
|
21
|
+
@attribute = attribute
|
22
|
+
@block = block
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Mongoid
|
2
|
+
module Giza
|
3
|
+
|
4
|
+
# Routines related to creating the defined indexes in sphinx
|
5
|
+
class Indexer
|
6
|
+
include Singleton
|
7
|
+
|
8
|
+
# Creates the Indexer instance
|
9
|
+
def initialize
|
10
|
+
@configuration = Mongoid::Giza::Configuration.instance
|
11
|
+
@controller = Riddle::Controller.new(@configuration, @configuration.file.output_path)
|
12
|
+
end
|
13
|
+
|
14
|
+
# Index everything, regenerating all dynamic indexes from all classes
|
15
|
+
def full_index
|
16
|
+
@configuration.clear_generated_indexes
|
17
|
+
giza_classes.each { |klass| klass.regenerate_dynamic_sphinx_indexes }
|
18
|
+
index!
|
19
|
+
end
|
20
|
+
|
21
|
+
# Creates the sphinx configuration file then executes the indexer on it
|
22
|
+
def index!(*indexes)
|
23
|
+
@configuration.render
|
24
|
+
@controller.index(*indexes, verbose: true)
|
25
|
+
end
|
26
|
+
|
27
|
+
# @return [Array<Class>] all Mongoid models that include the {Mongoid::Giza} module
|
28
|
+
def giza_classes
|
29
|
+
Mongoid.models.select { |model| model.include?(Mongoid::Giza) }
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Mongoid
|
2
|
+
module Giza
|
3
|
+
|
4
|
+
# MongoDB counter collection to generate ids compatible with sphinx
|
5
|
+
class GizaID
|
6
|
+
include Mongoid::Document
|
7
|
+
|
8
|
+
field :_id, type: Symbol
|
9
|
+
field :seq, type: Integer, default: 0
|
10
|
+
|
11
|
+
attr_accessible :id
|
12
|
+
|
13
|
+
class << self
|
14
|
+
|
15
|
+
# Gets the next id in the sequence to assign to an object
|
16
|
+
#
|
17
|
+
# @param klass [Symbol] the name of the class which next id will be retrived for
|
18
|
+
#
|
19
|
+
# @return [Integer] the next id in the sequence
|
20
|
+
def next_id(klass)
|
21
|
+
giza_id = where(id: klass).find_and_modify({"$inc" => {seq: 1}}, new: true)
|
22
|
+
giza_id.seq
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require "rails"
|
2
|
+
|
3
|
+
module Mongoid
|
4
|
+
module Giza
|
5
|
+
class Railtie < Rails::Railtie
|
6
|
+
configuration = Mongoid::Giza::Configuration.instance
|
7
|
+
|
8
|
+
initializer "mongoid-giza.load-configuration" do
|
9
|
+
# Sets the default xmlpipe_command
|
10
|
+
configuration.source.xmlpipe_command = "rails r '<%= index.klass %>.sphinx_indexes[:<%= index.name %>].generate_xmlpipe2(STDOUT)'"
|
11
|
+
# Loads the configuration file
|
12
|
+
file = Rails.root.join("config", "giza.yml")
|
13
|
+
configuration.load(file, Rails.env) if file.file?
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
module Mongoid
|
2
|
+
module Giza
|
3
|
+
|
4
|
+
# Executes queries on Sphinx
|
5
|
+
class Search
|
6
|
+
attr_accessor :indexes
|
7
|
+
attr_reader :client
|
8
|
+
|
9
|
+
# Creates a new search
|
10
|
+
#
|
11
|
+
# @param host [String] the host address of sphinxd
|
12
|
+
# @param port [Fixnum] the TCP port of sphinxd
|
13
|
+
# @param indexes [String] an optional string define the indexes that the search will run on.
|
14
|
+
# Defaults to "*" which means all indexes
|
15
|
+
def initialize(host, port, *indexes)
|
16
|
+
@client = Riddle::Client.new(host, port)
|
17
|
+
@indexes = indexes
|
18
|
+
end
|
19
|
+
|
20
|
+
# Sets the search criteria on full-text fields
|
21
|
+
#
|
22
|
+
# @param query [String] a sphinx query string based on the current {http://sphinxsearch.com/docs/current.html#matching-modes matching mode}
|
23
|
+
def fulltext(query)
|
24
|
+
index = indexes.length > 0 ? indexes.join(" ") : "*"
|
25
|
+
@client.append_query(query, index)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Sets a filter based on an attribute.
|
29
|
+
# Only documents that the attribute value matches will be returned from the search
|
30
|
+
#
|
31
|
+
# @param attribute [Symbol] the attribute name to set the filter
|
32
|
+
# @param value [Fixnum, Float, Range] the value (or values) that the attribute must match
|
33
|
+
def with(attribute, value)
|
34
|
+
@client.filters << Riddle::Client::Filter.new(attribute.to_s, value, false)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Excludes from the search documents that the attribute value matches
|
38
|
+
#
|
39
|
+
# @param attribute [Symbol] the attribute name
|
40
|
+
# @param value [Fixnum, Float, Range] the value (or values) that the attribute must match
|
41
|
+
def without(attribute, value)
|
42
|
+
@client.filters << Riddle::Client::Filter.new(attribute.to_s, value, true)
|
43
|
+
end
|
44
|
+
|
45
|
+
# Sets the order in which the results will be returned
|
46
|
+
#
|
47
|
+
# @param attribute [Symbol] the attribute used for sorting
|
48
|
+
# @param order [Symbol] the order of the sorting. Valid values are :asc and :desc
|
49
|
+
def order_by(attribute, order)
|
50
|
+
@client.sort_by = "#{attribute} #{order.to_s.upcase}"
|
51
|
+
end
|
52
|
+
|
53
|
+
# Executes the configured queries
|
54
|
+
#
|
55
|
+
# @return [Array] an Array of Hashes as specified by Riddle::Response
|
56
|
+
def run
|
57
|
+
@client.run
|
58
|
+
end
|
59
|
+
|
60
|
+
# Checks for methods on Riddle::Client
|
61
|
+
#
|
62
|
+
# @param method [Symbol, String] the method name that will be checked on Riddle::Client
|
63
|
+
#
|
64
|
+
# @return [TrueClass, FalseClass] true if either Riddle::Client or Mongoid::Giza::Search respond to the method
|
65
|
+
def respond_to?(method)
|
66
|
+
@client.respond_to?("#{method}=") || super
|
67
|
+
end
|
68
|
+
|
69
|
+
# Dynamically dispatches the method call to Riddle::Client if the method is defined in it
|
70
|
+
#
|
71
|
+
# @param method [Symbol, String] the method name that will be called on Riddle::Client
|
72
|
+
# @param args [Array] an argument list that will also be forwarded to the Riddle::Client method
|
73
|
+
#
|
74
|
+
# @return [Object] the return value of the Riddle::Client method
|
75
|
+
#
|
76
|
+
# @raise [NoMethodError] if the method is also missing on Riddle::Client
|
77
|
+
def method_missing(method, *args)
|
78
|
+
super if !respond_to?(method)
|
79
|
+
@client.send "#{method}=", *args
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require "builder"
|
2
|
+
|
3
|
+
module Mongoid
|
4
|
+
module Giza
|
5
|
+
class XMLPipe2
|
6
|
+
|
7
|
+
# Creates a new XMLPipe2 object based on the specified index and that will write to the specified buffer.
|
8
|
+
# Note that the actual XML will be generated only when {#generate!} is called
|
9
|
+
#
|
10
|
+
# @param index [Mongoid::Giza::Index] the index which will be used to generate the data
|
11
|
+
# @param buffer any object that supports the method <<
|
12
|
+
def initialize(index, buffer)
|
13
|
+
@index = index
|
14
|
+
@xml = Builder::XmlMarkup.new(target: buffer)
|
15
|
+
end
|
16
|
+
|
17
|
+
# Generates a XML document with the {http://sphinxsearch.com/docs/current.html#xmlpipe2 xmlpipe2 specification}.
|
18
|
+
# The buffer passed on object creation will contain the XML
|
19
|
+
def generate!
|
20
|
+
@xml.instruct! :xml, version: "1.0", encoding: "utf-8"
|
21
|
+
@xml.sphinx :docset do
|
22
|
+
generate_schema
|
23
|
+
generate_docset
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Generates the schema part of the XML document.
|
28
|
+
# Used internally by {#generate!} so you should never need to call it directly
|
29
|
+
def generate_schema
|
30
|
+
@xml.sphinx :schema do |schema|
|
31
|
+
@index.fields.each do |field|
|
32
|
+
attrs = {name: field.name}
|
33
|
+
attrs[:attr] = :string if field.attribute
|
34
|
+
schema.sphinx :field, attrs
|
35
|
+
end
|
36
|
+
@index.attributes.each { |attribute| schema.sphinx :attr, name: attribute.name, type: attribute.type }
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Generates the content part of the XML document.
|
41
|
+
# Used internally by {#generate!} so you should never need to call it directly
|
42
|
+
def generate_docset
|
43
|
+
@index.criteria.each do |object|
|
44
|
+
@xml.sphinx :document, id: object.giza_id do
|
45
|
+
generate_doc_tags(@index.fields, object)
|
46
|
+
generate_doc_tags(@index.attributes, object)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Generates the tags with the content to be indexed of every field or attribute.
|
52
|
+
# Used internally by {#generate_docset} so you should never need to call it directly
|
53
|
+
#
|
54
|
+
# @param contents [Array] list of fields or attributes to generate the tags for
|
55
|
+
# @param object [Object] the object being indexed
|
56
|
+
def generate_doc_tags(contents, object)
|
57
|
+
contents.each do |content|
|
58
|
+
if content.block.nil?
|
59
|
+
@xml.tag! content.name, object[content.name]
|
60
|
+
else
|
61
|
+
@xml.tag! content.name, content.block.call(object)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|