mongoid-giza 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|