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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 150e88a193d198b7ee59e633753808457f01f9d0
4
+ data.tar.gz: dd2c1cc23492df7aa5e1c3e9c3fd6b13a578486a
5
+ SHA512:
6
+ metadata.gz: 632f8929ab2a00803101594e1dc0e0bdd98164ef984797bb889eaaa4d90ebfef1250e7c94ad3f00f2eadab1313b4b36b49dd9d2969010b585df34a6b10f2bff1
7
+ data.tar.gz: 5eadf9661bc8697db39111932e5677ed1df5199269cef298fc2cf0788a95de779d1c8a63e3f783bb9877556ce9e3f9d553ecfd07f24ef6f8ef3602fcb5dc6d7e
data/.gitignore ADDED
@@ -0,0 +1,21 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ .ruby-gemset
19
+ .ruby-version
20
+ .rspec
21
+ turbulence
data/.travis.yml ADDED
@@ -0,0 +1,6 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ - 2.0.0
5
+ services:
6
+ - mongodb
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'coveralls', require: false
4
+
5
+ # Specify your gem's dependencies in mongoid-giza.gemspec
6
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 YADEV Team
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,152 @@
1
+ # Mongoid::Giza [![Build Status](https://travis-ci.org/yadevteam/mongoid-giza.png)](https://travis-ci.org/yadevteam/mongoid-giza) [![Code Climate](https://codeclimate.com/github/yadevteam/mongoid-giza.png)](https://codeclimate.com/github/yadevteam/mongoid-giza) [![Coverage Status](https://coveralls.io/repos/yadevteam/mongoid-giza/badge.png)](https://coveralls.io/r/yadevteam/mongoid-giza)
2
+
3
+ Mongoid layer for the Sphinx fulltext search server that supports block fields and dynamic indexes
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem "mongoid-giza"
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install mongoid-giza
18
+
19
+ ## Usage
20
+
21
+ :warning: **Before proceeding is extremely recommended to read the [Sphinx documentation](http://sphinxsearch.com/docs/current.html) if you are not yet familiar with it. Reading up to chapter 5 is enought to get you going.**
22
+
23
+ ### Setting up indexes on models
24
+
25
+ Use a `sphinx_index` block to create a new index.
26
+
27
+ The `sphinx_index` method may receive optional settings that will be set in this index's section or in its source section on the generated sphinx configuration file.
28
+ These settings take precedence to the defaults defined in `giza.yml`.
29
+
30
+ A model may have more than one index, but they need to have different names.
31
+ If two or more indexes have the same name the last one to be defined is the one which will exist.
32
+
33
+ An index name is the name of the class it's defined on unless overwritten by the `name` method inside the index definition block.
34
+
35
+ Besides `name`, `field`, `attribute` and `criteria` are the methods avaible inside the index definition block.
36
+
37
+ Both `field` and `attribute` take a name as first parameter that may match with a Mongoid field. In this case the value of the field will be used when indexing the objects.
38
+ The `attribute` method may receive a second paramenter that defines the type of the attribute. If it is ommited, than the type of the Mongoid field will be used.
39
+
40
+ At last, both methods may take an block with the object as parameter. The return of the block will be used as the value of the field or attribute when indexing.
41
+
42
+ The `criteria` method receives a `Mongoid::Criteria` that will be used to select the objects that will be indexed.
43
+ It's `Class.all` by default.
44
+
45
+ **Example:** Creating a index on the person model
46
+
47
+ ```
48
+ class Person
49
+ include Mongoid::Document
50
+ include Mongoid::Giza
51
+
52
+ field :name
53
+ field :age, type: Integer
54
+
55
+ sphinx_index(enable_star: 1) do
56
+ field :name
57
+ field :bio do |person|
58
+ "#{person.name.capitalize} was born #{person.age.years.days} ago"
59
+ end
60
+ attribute :age
61
+ end
62
+ end
63
+ ```
64
+
65
+ #### Dynamic Indexes
66
+
67
+ Because of the schemaless nature of MongoDB, sometimes you may find problems mapping your mongo models to sphinx indexes.
68
+ To circunvent this limitation Mongoid::Giza supports dynamic indexes.
69
+
70
+ When you define a dynamic index, it will generate a regular index based on your definition for each object of the class.
71
+ This allows the creation of different indexes for objects of the same model that have different dynamic fields.
72
+
73
+ Although it's not necessary, dynamic indexes are better used together with a `criteria`,
74
+ so it's possible to control which objects of the class will be indexed on each determined index.
75
+
76
+ To create a dynamic index all that needs to be done is pass the object to the `sphin_index` block.
77
+
78
+ **Example:** Creating a dynamic index on the person model.
79
+ This dynamic index will generate one index for each job that is associated to a person.
80
+ On each index only the people that have that job will be indexed.
81
+ Finally each dynamic attribute of the job will be a field on its index.
82
+
83
+ ```
84
+ class Job
85
+ include Mongoid::Document
86
+
87
+ field :name
88
+ # each job object has specific dynamic fields
89
+
90
+ has_many :people
91
+ end
92
+
93
+ class Person
94
+ include Mongoid::Document
95
+ include Mongoid::Giza
96
+
97
+ field :name
98
+ field :age, type: Integer
99
+
100
+ belongs_to :job
101
+
102
+ sphinx_index do |person|
103
+ name person.job.name
104
+ criteria Person.where(job: person.job)
105
+ person.job.attributes.except("name").each do |attr, val|
106
+ field attr.to_sym
107
+ end
108
+ end
109
+ end
110
+ ```
111
+
112
+ ### Searching
113
+
114
+ Use the `search` block on the class that have the indexes where the search should run.
115
+ It returns a result array, where each position of the array is a [riddle result hash](http://rdoc.info/github/pat/riddle/Riddle/Client#query-instance_method), plus a key with the class name, that has the `Mongoid::Criteria` that selects the matching objects from the mongo database.
116
+
117
+ Inside the `search` block use the `fulltext` method to perform a fulltext search.
118
+ If multiple `fulltext` are called inside a `search` block, then each one will generate a separated query and will return a new position o the results array.
119
+
120
+ To filter your search using the attributes defined on the index creation, use the `with` and `without` methods, that accept the name of the attribute and the value or range.
121
+
122
+ To order the results, use the `order_by` method, that receives the *attribute* used for sorting and a Symbol, that can be either `:asc` or `:desc`.
123
+
124
+ Every other [Riddle::Client](http://rdoc.info/github/pat/riddle/Riddle/Client) setter is avaible without the **=**, to maintain the DSL syntax consistent.
125
+
126
+ **Example:** Searching on the person class
127
+
128
+ ```
129
+ results = Person.search do
130
+ fulltext "john"
131
+ with :age 18..40
132
+ order_by :age :asc
133
+ end
134
+
135
+ results.first[:Person].each do |person|
136
+ puts "#{person.name} is #{person.age} years old"
137
+ end
138
+ ```
139
+
140
+ ## TODO
141
+
142
+ * Support delta indexing
143
+ * Support RT indexes
144
+ * Support distributed indexes
145
+
146
+ ## Contributing
147
+
148
+ 1. Fork it
149
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
150
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
151
+ 4. Push to the branch (`git push origin my-new-feature`)
152
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ require "bundler"
2
+ Bundler.setup
3
+
4
+ require "bundler/gem_tasks"
5
+ require "rspec/core/rake_task"
6
+
7
+ RSpec::Core::RakeTask.new("spec") do |spec|
8
+ spec.pattern = "spec/**/*_spec.rb"
9
+ end
10
+
11
+ task default: :spec
@@ -0,0 +1,160 @@
1
+ require "docile"
2
+ require "mongoid"
3
+ require "riddle"
4
+ require "mongoid/giza/configuration"
5
+ require "mongoid/giza/dynamic_index"
6
+ require "mongoid/giza/index"
7
+ require "mongoid/giza/index/field"
8
+ require "mongoid/giza/index/attribute"
9
+ require "mongoid/giza/indexer"
10
+ require "mongoid/giza/models/giza_id"
11
+ require "mongoid/giza/railtie" if defined?(Rails)
12
+ require "mongoid/giza/search"
13
+ require "mongoid/giza/version"
14
+ require "mongoid/giza/xml_pipe2"
15
+
16
+ module Mongoid
17
+
18
+ # Module that should be included in a Mongoid::Document in order to
19
+ # index fields of documents of this class
20
+ #
21
+ # @example Creating a simple index with a full-text field (named fts) and an attribute (named attr)
22
+ # class Person
23
+ # include Mongoid::Document
24
+ # include Mongoid::Giza
25
+ #
26
+ # field :name
27
+ # field :age, type: Integer
28
+ #
29
+ # sphinx_index do
30
+ # field :name
31
+ # attribute :age
32
+ # end
33
+ # end
34
+ #
35
+ # @example Searching the previously defined index for people named John between 18 and 59 years old
36
+ # results = Person.search do
37
+ # fulltext "john"
38
+ # with age: 18..59
39
+ # end
40
+ #
41
+ # results.first[:Person].first # => First object that matched
42
+ module Giza
43
+ extend ActiveSupport::Concern
44
+
45
+ included do
46
+ Mongoid::Giza::GizaID.create(id: name.to_sym)
47
+ @giza_configuration = Configuration.instance
48
+ @static_sphinx_indexes = {}
49
+ @generated_sphinx_indexes = {}
50
+ @dynamic_sphinx_indexes = []
51
+ end
52
+
53
+ # Retrives the sphinx compatible id of the object.
54
+ # If the id does not exists yet, it will be generated
55
+ #
56
+ # @return [Integer] the object's integer id generated by Giza
57
+ def giza_id
58
+ set(:giza_id, GizaID.next_id(self.class.name.to_sym)) if self[:giza_id].nil?
59
+ self[:giza_id]
60
+ end
61
+
62
+ module ClassMethods
63
+ attr_reader :static_sphinx_indexes, :generated_sphinx_indexes, :dynamic_sphinx_indexes
64
+
65
+ # Class method that defines a index relative to the current class' objects.
66
+ # If an argument is given in the block then a dynamic index will be created.
67
+ # Otherwise it will create a static index.
68
+ #
69
+ # @param settings [Hash] optional settings for the index and it's source
70
+ # @param block [Proc] a block that will be evaluated on an {Mongoid::Giza::Index}
71
+ def sphinx_index(settings = {}, &block)
72
+ if block_given?
73
+ if block.arity > 0
74
+ add_dynamic_sphinx_index(settings, block)
75
+ else
76
+ add_static_sphinx_index(settings, block)
77
+ end
78
+ end
79
+ end
80
+
81
+ # Adds an dynamic index to the class.
82
+ # Will also automatically generate the dynamic index for each object of the class
83
+ #
84
+ # @param settings [Hash] settings for the index and it's source
85
+ # @param block [Proc] a block that will be evaluated on an {Mongoid::Giza::Index}.
86
+ # The block receives one argument that is the current object of the class for which the index will be generated
87
+ def add_dynamic_sphinx_index(settings, block)
88
+ dynamic_index = DynamicIndex.new(self, settings, block)
89
+ dynamic_sphinx_indexes << dynamic_index
90
+ process_dynamic_sphinx_index(dynamic_index)
91
+ end
92
+
93
+ # Adds an static index to the class
94
+ #
95
+ # @param settings [Hash] settings for the index and it's source
96
+ # @param block [Proc] a block that will be evaluated on an {Mongoid::Giza::Index}.
97
+ def add_static_sphinx_index(settings, block)
98
+ index = Index.new(self, settings)
99
+ Docile.dsl_eval(index, &block)
100
+ static_sphinx_indexes[index.name] = index
101
+ @giza_configuration.add_index(index)
102
+ end
103
+
104
+ # Generates the indexes from the dynamic index and
105
+ # registers them on the class and on the configuration
106
+ #
107
+ # @param dynamic_index [Mongoid::Giza::DynamicIndex] the dynamic index which will generate the static indexes from
108
+ def process_dynamic_sphinx_index(dynamic_index)
109
+ generated = dynamic_index.generate!
110
+ generated_sphinx_indexes.merge!(generated)
111
+ generated.each { |name, index| @giza_configuration.add_index(index, true) }
112
+ end
113
+
114
+ # Class method that implements a search DSL using a {Mongoid::Giza::Search} object.
115
+ # The search will run on indexes defined on the class unless it's overwritten using {Mongoid::Giza::Search#indexes=}
116
+ #
117
+ # @param block [Proc] a block that will be evaluated on a {Mongoid::Giza::Search}
118
+ #
119
+ # @return [Array] an Array with Riddle result hashes containing an additional key with the name of the class.
120
+ # The value of this aditional key is a Mongoid::Criteria that return the actual objects of the match
121
+ def search(&block)
122
+ search = Mongoid::Giza::Search.new(@giza_configuration.searchd.address, @giza_configuration.searchd.port, *sphinx_indexes_names)
123
+ Docile.dsl_eval(search, &block)
124
+ results = search.run
125
+ results.each { |result| result[name.to_sym] = self.in(giza_id: result[:matches].map { |match| match[:doc] }) }
126
+ end
127
+
128
+ # Regenerates all dynamic indexes of the class
129
+ def regenerate_dynamic_sphinx_indexes
130
+ generated_sphinx_indexes.clear
131
+ dynamic_sphinx_indexes.each { |dynamic_index| process_dynamic_sphinx_index(dynamic_index) }
132
+ end
133
+
134
+ # Execute the indexing routines of the indexes defined on the class.
135
+ # This means (re)create the sphinx configuration file and then execute the indexer program on it.
136
+ # If no index names are supplied than all indexes defined on the class will be indexed.
137
+ # If none of the index names supplied are on this class then nothing is indexed
138
+ #
139
+ # @param names [Array] a list of index names of this class that will be indexed
140
+ def sphinx_indexer!(*names)
141
+ indexes_names = names.length > 0 ? sphinx_indexes_names.select { |name| names.include? name } : sphinx_indexes_names
142
+ Mongoid::Giza::Indexer.instance.index!(*indexes_names) if indexes_names.length > 0
143
+ end
144
+
145
+ # Retrieves all the sphinx indexes defined on this class, static and dynamic
146
+ #
147
+ # @return [Array] an Array of indexes from the current class {Mongoid::Giza::Index}
148
+ def sphinx_indexes
149
+ static_sphinx_indexes.merge(generated_sphinx_indexes)
150
+ end
151
+
152
+ # Retrieves all the names of sphinx indexes defined on this class, static and dynamic
153
+ #
154
+ # @return [Array] an Array of names of indexes from the current class {Mongoid::Giza::Index}
155
+ def sphinx_indexes_names
156
+ static_sphinx_indexes.merge(generated_sphinx_indexes).keys
157
+ end
158
+ end
159
+ end
160
+ end
@@ -0,0 +1,136 @@
1
+ require "erb"
2
+ require "ostruct"
3
+ require "yaml"
4
+
5
+ module Mongoid
6
+ module Giza
7
+
8
+ # Holds the configuration of the module
9
+ class Configuration < Riddle::Configuration
10
+ include Singleton
11
+
12
+ attr_reader :source, :index, :file
13
+
14
+ # Creates the configuration instance
15
+ def initialize
16
+ super
17
+ @source = Riddle::Configuration::XMLSource.new(:source, :xmlpipe2)
18
+ @index = Riddle::Configuration::Index.new(:index, @source)
19
+ @file = OpenStruct.new(output_path: "./sphinx.conf")
20
+ @static_indexes = {}
21
+ @generated_indexes = {}
22
+ end
23
+
24
+ # Loads a YAML file with settings defined.
25
+ # Settings that are not recognized are ignored
26
+ #
27
+ # @param path [String] path to the YAML file which contains the settings defined
28
+ # @param env [String] environment whoose settings will be loaded
29
+ def load(path, env)
30
+ YAML.load(File.open(path).read)[env].each do |section_name, settings|
31
+ section = instance_variable_get("@#{section_name}")
32
+ if !section.nil?
33
+ settings.each do |setting, value|
34
+ method = "#{setting}="
35
+ section.send(method, value) if section.respond_to?(method)
36
+ end
37
+ end
38
+ end
39
+ end
40
+
41
+ # Adds an index to the sphinx configuration,
42
+ # so this index can be rendered on the configuration file
43
+ #
44
+ # @param index [Mongoid::Giza::Index] the index that will be added to the configuration
45
+ # @param generated [TrueClass, FalseClass] determines if this index was generated from a {Mongoid::Giza::DynamicIndex}
46
+ def add_index(index, generated = false)
47
+ riddle_index = create_index(index)
48
+ if generated
49
+ position = register_index(riddle_index, @generated_indexes)
50
+ else
51
+ position = register_index(riddle_index, @static_indexes)
52
+ end
53
+ indices[position] = riddle_index
54
+ end
55
+
56
+ # Creates a new Riddle::Index based on the given {Mongoid::Giza::Index}
57
+ #
58
+ # @param index [Mongoid::Giza::Index] the index to generate the configuration from
59
+ #
60
+ # @return [Riddle::Configuration::Index] the created riddle index
61
+ def create_index(index)
62
+ source = Riddle::Configuration::XMLSource.new(index.name, :xmlpipe2)
63
+ riddle_index = Riddle::Configuration::Index.new(index.name, source)
64
+ apply_default_settings(@index, riddle_index, index)
65
+ apply_default_settings(@source, source, index)
66
+ apply_user_settings(index, riddle_index)
67
+ apply_user_settings(index, source)
68
+ riddle_index.path = File.join(riddle_index.path, index.name.to_s)
69
+ riddle_index.charset_type = "utf-8"
70
+ riddle_index
71
+ end
72
+
73
+ # Adds the riddle index to it's respective collection
74
+ #
75
+ # @param index [Riddle::Configuration::Index] the index that will be registrated
76
+ # @param indexes [Hash] the collection which will hold this index
77
+ #
78
+ # @return [Integer] the position where this index should be inserted on the configuration indices array
79
+ def register_index(index, indexes)
80
+ position = indexes.has_key?(index.name) ? indices.index(indexes[index.name]) : indices.length
81
+ indexes[index.name] = index
82
+ position
83
+ end
84
+
85
+ # Applies the settings defined on an object loaded from the configuration to a Riddle::Configuration::Index or Riddle::Configuration::XMLSource instance.
86
+ # Used internally by {#add_index} so you should never need to call it directly
87
+ #
88
+ # @param default [Riddle::Configuration::Index, Riddle::Configuration::XMLSource] the object that holds the global settings values
89
+ # @param section [Riddle::Configuration::Index, Riddle::Configuration::XMLSource] the object that settings are being set
90
+ def apply_default_settings(default, section, index)
91
+ default.class.settings.each do |setting|
92
+ method = "#{setting}="
93
+ value = interpolate_string(default.send("#{setting}"), index)
94
+ section.send(method, value) if !value.nil? and section.respond_to?(method)
95
+ end
96
+ end
97
+
98
+ # Applies the settings defined on a {Mongoid::Giza::Index} to the Riddle::Configuration::Index or Riddle::Configuration::XMLSource.
99
+ # Used internally by {#add_index} so you should never need to call it directly
100
+ #
101
+ # @param index [Mongoid::Giza::Index] the index where the settings were defined
102
+ # @param section [Riddle::Configuration::Index, Riddle::Configuration::XMLSource] where the settings will be applied
103
+ def apply_user_settings(index, section)
104
+ index.settings.each do |setting, value|
105
+ method = "#{setting}="
106
+ section.send(method, interpolate_string(value, index)) if section.respond_to?(method)
107
+ end
108
+ end
109
+
110
+ # Interpolates a value if it's a String using ERB.
111
+ # Useful for defining dynamic settings.
112
+ # The ERB template may reference to the current {Mongoid::Giza::Index} and it's methods
113
+ #
114
+ # @param value [String] the ERB template that will be interpolated
115
+ # @param index [Mongoid::Giza::Index] the index that will be accessible from the template
116
+ #
117
+ # @return [Object] if value was a String and contains ERB syntax than it will beinterpolated and returned.
118
+ # Otherwise it will return the original value
119
+ def interpolate_string(value, index)
120
+ namespace = OpenStruct.new(index: index)
121
+ value.is_a?(String) ? ERB.new(value).result(namespace.instance_eval { binding }) : value
122
+ end
123
+
124
+ # Renders the configuration to the output_path
125
+ def render
126
+ File.open(@file.output_path, "w") { |file| file.write(super) }
127
+ end
128
+
129
+ # Removes all Riddle::Index from the indices Array that where created from a generated {Mongoid::Giza::Index}
130
+ def clear_generated_indexes
131
+ @generated_indexes.each { |name, index| indices.delete(index) }
132
+ @generated_indexes = {}
133
+ end
134
+ end
135
+ end
136
+ end