mongoid-giza 0.1.0

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