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.
@@ -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,5 @@
1
+ module Mongoid
2
+ module Giza
3
+ VERSION = "0.1.0"
4
+ end
5
+ 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