mongoid-giza 0.1.0

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