elastic_queue 0.0.2

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: 5fba6995c9fb76fcb3b2f7e0e204bf5c955e243b
4
+ data.tar.gz: a47b1f04bf521b2a4c42f1fee5f2be4feee51256
5
+ SHA512:
6
+ metadata.gz: f7e36b7ac21298dd00e897b1f7c4e16f02a0b20c3bd21b33d4e68fded773acb9ebd712dba768bd73ea216f2f3b52ccc85989168997c27f57dd821f6b8044866e
7
+ data.tar.gz: 16409af91a7ae6020d5844a68b4ceddfbe0a53283d1bb3e7cb066210d400c40e0963ba7082a0ecaa17700a46cade6ecaa404ee585f6063dd39675c8895b23b1c
data/.gitignore ADDED
@@ -0,0 +1,19 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .DS_Store
6
+ coverage
7
+ InstalledFiles
8
+ lib/bundler/man
9
+ pkg
10
+ rdoc
11
+ spec/reports
12
+ test/tmp
13
+ test/version_tmp
14
+ tmp
15
+
16
+ # YARD artifacts
17
+ .yardoc
18
+ _yardoc
19
+ doc/
data/.rubocop.yml ADDED
@@ -0,0 +1,20 @@
1
+ # This is the configuration rubocop, a ruby source code linter and checker.
2
+ Documentation:
3
+ Description: 'Document classes and non-namespace modules.'
4
+ Enabled: false
5
+
6
+ FinalNewline:
7
+ Description: 'Checks for a final newline in a source file.'
8
+ Enabled: false
9
+
10
+ LineLength:
11
+ Description: 'Limit lines to 79 characters.'
12
+ Enabled: false
13
+
14
+ MethodLength:
15
+ Description: 'Avoid methods longer than 10 lines of code.'
16
+ Enabled: false
17
+
18
+ RescueModifier:
19
+ Description: 'Avoid using rescue in its modifier form.'
20
+ Enabled: false
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # dependencies in elastic-queue.gemspec
4
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,56 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ elastic-queue (0.0.2)
5
+ activesupport
6
+ elasticsearch
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ activesupport (4.0.1)
12
+ i18n (~> 0.6, >= 0.6.4)
13
+ minitest (~> 4.2)
14
+ multi_json (~> 1.3)
15
+ thread_safe (~> 0.1)
16
+ tzinfo (~> 0.3.37)
17
+ atomic (1.1.14)
18
+ diff-lcs (1.2.5)
19
+ elasticsearch (0.4.1)
20
+ elasticsearch-api (= 0.4.1)
21
+ elasticsearch-transport (= 0.4.1)
22
+ elasticsearch-api (0.4.1)
23
+ multi_json
24
+ elasticsearch-transport (0.4.1)
25
+ faraday
26
+ multi_json
27
+ factory_girl (4.2.0)
28
+ activesupport (>= 3.0.0)
29
+ faraday (0.8.8)
30
+ multipart-post (~> 1.2.0)
31
+ i18n (0.6.9)
32
+ minitest (4.7.5)
33
+ multi_json (1.8.2)
34
+ multipart-post (1.2.0)
35
+ rake (10.1.0)
36
+ rspec (2.14.1)
37
+ rspec-core (~> 2.14.0)
38
+ rspec-expectations (~> 2.14.0)
39
+ rspec-mocks (~> 2.14.0)
40
+ rspec-core (2.14.7)
41
+ rspec-expectations (2.14.4)
42
+ diff-lcs (>= 1.1.3, < 2.0)
43
+ rspec-mocks (2.14.4)
44
+ thread_safe (0.1.3)
45
+ atomic
46
+ tzinfo (0.3.38)
47
+
48
+ PLATFORMS
49
+ ruby
50
+
51
+ DEPENDENCIES
52
+ bundler (>= 1.0.0)
53
+ elastic-queue!
54
+ factory_girl
55
+ rake
56
+ rspec (~> 2.6)
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2014 RuthThompson
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the "Software"), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9
+ the Software, and to permit persons to whom the Software is furnished to do so,
10
+ subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,12 @@
1
+ # ElasticQueue
2
+
3
+ [![Code Climate](https://codeclimate.com/github/RuthThompson/elastic_queue.png)](https://codeclimate.com/github/RuthThompson/elastic_queue)
4
+
5
+ A queueing system built on top of elasticsearch.
6
+
7
+ ### Usage
8
+ TODO
9
+
10
+ ### How it Works
11
+
12
+ TODO
data/Rakefile ADDED
@@ -0,0 +1,25 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ require 'rake'
4
+ require 'rspec/core/rake_task'
5
+ Bundler::GemHelper.install_tasks
6
+
7
+ desc 'Default: run the specs and features.'
8
+ task :default do
9
+ system('bundle exec rake -s appraisal spec:unit spec:acceptance features;')
10
+ end
11
+
12
+ namespace :spec do
13
+ desc 'Run unit specs'
14
+ RSpec::Core::RakeTask.new('unit') do |t|
15
+ t.pattern = 'spec/{*_spec.rb,factory_girl/**/*_spec.rb}'
16
+ end
17
+
18
+ desc 'Run acceptance specs'
19
+ RSpec::Core::RakeTask.new('acceptance') do |t|
20
+ t.pattern = 'spec/acceptance/**/*_spec.rb'
21
+ end
22
+ end
23
+
24
+ desc 'Run the unit and acceptance specs'
25
+ task :spec => ['spec:unit', 'spec:acceptance']
@@ -0,0 +1,28 @@
1
+ require './lib/elastic_queue/version'
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = 'elastic_queue'
5
+ s.version = ElasticQueue::VERSION
6
+ s.platform = Gem::Platform::RUBY
7
+ s.summary = 'A queueing system built on top of elasticsearch.'
8
+ s.description = 'A library for storing and filtering documents on elastic search with a queue paradigm for retrieval.'
9
+ s.license = 'MIT'
10
+
11
+ s.required_ruby_version = '>= 1.9.2'
12
+
13
+ s.authors = ['Ruth Thompson', 'Rob Law']
14
+ s.email = %w[ ruth@flywheelnetworks.com rob@flywheelnetworks.com ]
15
+ s.homepage = 'https://github.com/RuthThompson/elastic_queue'
16
+
17
+ s.require_paths = %w[ lib ]
18
+ s.files = `git ls-files`.split("\n")
19
+ s.test_files = Dir['spec/**/*.rb']
20
+
21
+ s.add_dependency 'activesupport'
22
+ s.add_dependency 'elasticsearch'
23
+ s.add_dependency 'will_paginate'
24
+ s.add_development_dependency 'bundler', '>= 1.0.0'
25
+ s.add_development_dependency 'rspec', '~> 2.6'
26
+ s.add_development_dependency 'factory_girl'
27
+ s.add_development_dependency 'rake'
28
+ end
@@ -0,0 +1,52 @@
1
+ require 'elasticsearch'
2
+ require 'elastic_queue/persistence'
3
+ require 'elastic_queue/query'
4
+
5
+ module ElasticQueue
6
+ class Base
7
+ include Persistence
8
+ # include Percolation
9
+
10
+ def self.search_client
11
+ Elasticsearch::Client.new
12
+ end
13
+
14
+ def self.models(*models)
15
+ @models = models
16
+ end
17
+
18
+ def self.model_names
19
+ raise NotImplementedError, "No models defined in #{self.class}" unless defined?(@models)
20
+ @models
21
+ end
22
+
23
+ def self.model_classes
24
+ model_names.map { |s| s.to_s.camelize.constantize }
25
+ end
26
+
27
+ def self.index_name
28
+ @index_name ||= to_s.underscore
29
+ end
30
+
31
+ def self.eager_load(includes)
32
+ @eager_loads = includes
33
+ end
34
+
35
+ def self.eager_loads
36
+ @eager_loads
37
+ end
38
+
39
+ def self.query(options = {})
40
+ Query.new(self, options)
41
+ end
42
+
43
+ def self.filter(options)
44
+ query.filter(options)
45
+ end
46
+
47
+ def self.count
48
+ query.count
49
+ end
50
+
51
+ end
52
+ end
@@ -0,0 +1,47 @@
1
+ module ElasticQueue
2
+ module Filters
3
+ extend ActiveSupport::Concern
4
+
5
+ module ClassMethods
6
+ end
7
+
8
+ def options_to_filters(options)
9
+ options.map { |k, v| option_to_filter(k, v) }.flatten
10
+ end
11
+
12
+ private
13
+
14
+ def option_to_filter(key, value)
15
+ if value.is_a? Array
16
+ or_filter(key, value)
17
+ elsif value.is_a? Hash
18
+ # date?
19
+ time_filter(key, value)
20
+ else
21
+ term_filter(key, value)
22
+ end
23
+ end
24
+
25
+ def or_filter(term, values)
26
+ { or: values.map { |v| term_filter(term, v) } }
27
+ end
28
+
29
+ def term_filter(term, value)
30
+ { term: { term => value } }
31
+ end
32
+
33
+ # take something like follow_up: { before: 'hii', after: 'low' }
34
+ def time_filter(term, value)
35
+ value.map do |k, v|
36
+ comparator = k.to_sym.in?([:after, :greater_than, :gt]) ? :gt : :lt
37
+ range_filter(term, v, comparator)
38
+ end
39
+ end
40
+
41
+ # like term filter but for comparison queries
42
+ def range_filter(term, value, comparator)
43
+ { range: { term => { comparator => value } } }
44
+ end
45
+
46
+ end
47
+ end
@@ -0,0 +1,42 @@
1
+ module ElasticQueue
2
+ module Percolation
3
+ extend ActiveSupport::Concern
4
+
5
+ module ClassMethods
6
+
7
+ # def percolator_queries
8
+ # queries = {}
9
+ # query = { 'query' => {'match_all' => {}} }
10
+ # search = search_client.search index: '_percolator', body: query.to_json, size: 1000
11
+ # debugger
12
+ # search['hits']['hits'].each { |hit| queries[hit['_id']] = hit['_source'] }
13
+ # queries
14
+ # end
15
+
16
+ end
17
+
18
+ # def reverse_search(instance)
19
+ # body = { 'doc' => instance.indexed_for_queue }
20
+ # search_client.percolate index: index_name, body: body.to_json
21
+ # end
22
+
23
+ # def register_percolator_query(name, opts)
24
+ # search_client.index index: '_percolator', type: index_name, id: name, body: translate_opts_to_query_for_percolator(opts)
25
+ # end
26
+
27
+ # def unregister_percolator_query(name)
28
+ # SEARCH_CLIENT.delete index: '_percolator', type: index_name, id: name
29
+ # end
30
+
31
+ # def model_in_queue?(model, queue_opts)
32
+ # return false unless model_names.include?(model.class.to_s.underscore.to_sym)
33
+ # search_id = SecureRandom.uuid
34
+ # body = { 'doc' => model.indexed_for_queue, 'query' => {'term' => {'_id' => search_id}} }
35
+ # SEARCH_CLIENT.index index: '_percolator', type: 'dynamic_percolator', id: search_id, body: translate_opts_to_query_for_percolator(queue_opts), refresh: true
36
+ # search = SEARCH_CLIENT.percolate index: 'dynamic_percolator', body: body.to_json
37
+ # SEARCH_CLIENT.delete index: '_percolator', type: 'dynamic_percolator', id: search_id
38
+ # search['matches'].length == 1
39
+ # end
40
+
41
+ end
42
+ end
@@ -0,0 +1,68 @@
1
+ # TODO: might want to move these to an Index class
2
+ module ElasticQueue
3
+ module Persistence
4
+ extend ActiveSupport::Concern
5
+ module ClassMethods
6
+
7
+ def index_exists?
8
+ search_client.indices.exists index: index_name
9
+ end
10
+
11
+ def reset_index
12
+ delete_index if index_exists?
13
+ create_index
14
+ end
15
+
16
+ def create_index
17
+ search_client.indices.create index: index_name
18
+ add_mappings
19
+ end
20
+
21
+ def delete_index
22
+ search_client.indices.delete index: index_name
23
+ end
24
+
25
+ # not using it, but it is nice for debugging
26
+ def refresh_index
27
+ search_client.indices.refresh index: index_name
28
+ end
29
+
30
+ def bulk_index(batch_size = 10_000)
31
+ create_index unless index_exists?
32
+ model_classes.each do |klass|
33
+ # modelclass(model).includes(associations_for_index(model)).
34
+ index_type = klass.to_s.underscore
35
+ klass.find_in_batches(batch_size: batch_size) do |batch|
36
+ body = []
37
+ batch.each do |instance|
38
+ body << { index: { _index: index_name, _id: instance.id, _type: index_type, data: instance.indexed_for_queue } }
39
+ end
40
+ search_client.bulk body: body
41
+ end
42
+ end
43
+ end
44
+
45
+ def add_mappings
46
+ model_classes.each do |klass|
47
+ search_client.indices.put_mapping index: index_name, type: klass.to_s.underscore, body: klass.queue_mapping
48
+ end
49
+ end
50
+
51
+ # TODO: move these to an instance?
52
+ def index_model(instance)
53
+ search_client.index index: index_name, id: instance.id, type: instance.class.to_s.underscore, body: instance.indexed_for_queue
54
+ end
55
+
56
+ def upsert_model(instance)
57
+ body = { doc: instance.indexed_for_queue, doc_as_upsert: true }
58
+ search_client.update index: index_name, id: instance.id, type: instance.class.to_s.underscore, body: body, refresh: true
59
+ end
60
+
61
+ def remove_model(instance)
62
+ search_client.delete index: index_name, id: instance.id, type: instance.class.to_s.underscore
63
+ end
64
+
65
+ end
66
+
67
+ end
68
+ end
@@ -0,0 +1,62 @@
1
+ require 'elastic_queue/query_options'
2
+ require 'elastic_queue/results'
3
+
4
+ module ElasticQueue
5
+ class Query
6
+
7
+ def initialize(queue, options = {})
8
+ @queue = queue
9
+ @options = QueryOptions.new(options)
10
+ end
11
+
12
+ def filter(options)
13
+ @options.add_filter(options)
14
+ self
15
+ end
16
+
17
+ def filters
18
+ @options.filters
19
+ end
20
+
21
+ def sort(options)
22
+ @options.add_sort(options)
23
+ self
24
+ end
25
+
26
+ def sorts
27
+ @options.sorts
28
+ end
29
+
30
+ def paginate(options = {})
31
+ options.each { |k, v| @options.send("#{k}=", v) }
32
+ all.paginate
33
+ end
34
+
35
+ def all
36
+ @results ||= Results.new(@queue, execute, @options)
37
+ end
38
+
39
+ def count
40
+ res = execute(count: true)
41
+ res[:hits][:total].to_i
42
+ end
43
+
44
+ def execute(count: false)
45
+ search_type = count ? 'count' : 'query_then_fetch'
46
+ begin
47
+ search = @queue.search_client.search index: @queue.index_name, body: @options.body, search_type: search_type, from: @options.from, size: @options.per_page
48
+ # search[:page] = @page
49
+ # search = substitute_page(opts, search) if !count && opts[:page_substitution_ok] && search['hits']['hits'].length == 0 && search['hits']['total'] != 0
50
+ rescue Elasticsearch::Transport::Transport::Errors::BadRequest
51
+ search = failed_search
52
+ end
53
+ search.with_indifferent_access
54
+ end
55
+
56
+ def failed_search
57
+ { page: 0, hits: { hits: [], total: 0 } }
58
+ end
59
+
60
+ end
61
+
62
+ end
@@ -0,0 +1,48 @@
1
+ require 'elastic_queue/filters'
2
+ require 'elastic_queue/sorts'
3
+
4
+ module ElasticQueue
5
+ class QueryOptions
6
+ include Filters
7
+ include Sorts
8
+
9
+ attr_reader :filters, :sorts
10
+ attr_accessor :per_page
11
+
12
+ def initialize(options = {})
13
+ @options = { per_page: 30, page: 1 }.merge(options)
14
+ @filters = { and: [] }.with_indifferent_access
15
+ @sorts = []
16
+ self.per_page = @options[:per_page]
17
+ self.page = @options[:page]
18
+ end
19
+
20
+ def add_filter(options)
21
+ @filters[:and] += options_to_filters(options)
22
+ end
23
+
24
+ def add_sort(options)
25
+ @sorts += options_to_sorts(options)
26
+ end
27
+
28
+ def from
29
+ (page - 1) * per_page
30
+ end
31
+
32
+ def page=(num)
33
+ @page = num.to_i unless num.blank?
34
+ end
35
+
36
+ def page
37
+ @page
38
+ end
39
+
40
+ def body
41
+ b = {}
42
+ b[:filter] = @filters unless @filters[:and].blank?
43
+ b[:sort] = @sorts unless @sorts.blank?
44
+ b
45
+ end
46
+
47
+ end
48
+ end
@@ -0,0 +1,66 @@
1
+ module ElasticQueue
2
+ module Queueable
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ after_commit :index_for_queues, if: :persisted?
7
+ after_touch :index_for_queues, if: :persisted?
8
+ before_destroy :remove_from_queue_indices
9
+ end
10
+
11
+ module ClassMethods
12
+
13
+ def queues(*queues)
14
+ @queues ||= queues
15
+ end
16
+
17
+ def queue_classes
18
+ queues.map { |q| q.to_s.camelize.constantize }
19
+ end
20
+
21
+ def queue_attributes(*attributes)
22
+ @queue_attributes ||= attributes
23
+ end
24
+
25
+ alias_method :analyzed_queue_attributes, :queue_attributes
26
+
27
+ def not_analyzed_queue_attributes(*attributes)
28
+ @not_analyzed_queue_attributes ||= attributes
29
+ end
30
+
31
+ # the union of analyzed and not_analyzed attributes
32
+ def all_queue_attributes
33
+ @queue_attributes.to_a | @not_analyzed_queue_attributes.to_a
34
+ end
35
+
36
+ def queue_mapping
37
+ return if @not_analyzed_queue_attributes.blank?
38
+ properties = {}
39
+ @not_analyzed_queue_attributes.each do |a|
40
+ properties[a.to_sym] = { type: :string, index: :not_analyzed }
41
+ end
42
+ { self.to_s.underscore.to_sym => { properties: properties } }
43
+ end
44
+
45
+ end
46
+
47
+ def indexed_for_queue
48
+ index = { id: id, model: self.class.to_s.underscore }
49
+ self.class.all_queue_attributes.each do |attr|
50
+ val = send(attr)
51
+ val = val.to_s(:db) if val.is_a? Date
52
+ index[attr] = val
53
+ end
54
+ index
55
+ end
56
+
57
+ def index_for_queues
58
+ self.class.queue_classes.each { |q| q.send(:upsert_model, self) }
59
+ end
60
+
61
+ def remove_from_queue_indices
62
+ self.class.queue_classes.each { |q| q.send(:remove_model, self) }
63
+ end
64
+
65
+ end
66
+ end
@@ -0,0 +1,67 @@
1
+ require 'will_paginate/collection'
2
+
3
+ module ElasticQueue
4
+
5
+ class Results
6
+
7
+ attr_reader :results
8
+
9
+ def initialize(queue, search_results, query_options)
10
+ @queue = queue
11
+ @instantiated_queue_items = instantiate_queue_items(search_results)
12
+ @start = query_options.page
13
+ @per_page = query_options.per_page
14
+ @total = search_results[:hits][:total]
15
+ @results = WillPaginate::Collection.create(@start, @per_page, @total) do |pager|
16
+ pager.replace(@instantiated_queue_items)
17
+ end
18
+ end
19
+
20
+ def paginate
21
+ @results
22
+ end
23
+
24
+ def instantiate_queue_items(search_results)
25
+ grouped_results, sort_order = group_sorted_results(search_results)
26
+ records = fetch_records(grouped_results)
27
+ sort_records(records, sort_order)
28
+ end
29
+
30
+ private
31
+
32
+ # group the results by { class_name: [ids] } and save their sorted order
33
+ def group_sorted_results(search_results)
34
+ grouped_results = {}
35
+ sort_order = {}
36
+ search_results[:hits][:hits].each_with_index do |result, index|
37
+ model = result[:_source][:model].to_sym
38
+ model_id = result[:_source][:id]
39
+ sort_order["#{model}_#{model_id}"] = index # save the sort order
40
+ grouped_results[model] ||= []
41
+ grouped_results[model] << model_id
42
+ end
43
+ [grouped_results, sort_order]
44
+ end
45
+
46
+ # take a hash of { model_name: [ids] } and return a list of records
47
+ def fetch_records(grouped_results)
48
+ records = []
49
+ grouped_results.each do |model, ids|
50
+ klass = model.to_s.camelize.constantize
51
+ if @queue.eager_loads && @queue.eager_loads[model]
52
+ records += klass.includes(@queue.eager_loads[model]).find_all_by_id(ids)
53
+ else
54
+ records += klass.find_all_by_id(ids)
55
+ end
56
+ end
57
+ records
58
+ end
59
+
60
+ def sort_records(records, sort_order)
61
+ records.sort do |a, b|
62
+ sort_order["#{a.class.name.underscore}_#{a.id}"] <=> sort_order["#{b.class.name.underscore}_#{b.id}"]
63
+ end
64
+ end
65
+
66
+ end
67
+ end
@@ -0,0 +1,5 @@
1
+ module ElasticQueue
2
+ class Search
3
+ # TODO
4
+ end
5
+ end
@@ -0,0 +1,23 @@
1
+ module ElasticQueue
2
+ module Sorts
3
+ extend ActiveSupport::Concern
4
+
5
+ module ClassMethods
6
+ end
7
+
8
+ def options_to_sorts(options)
9
+ options.map { |k, v| option_to_sort(k, v) }
10
+ end
11
+
12
+ private
13
+
14
+ def option_to_sort(key, value)
15
+ single_sort(key, value)
16
+ end
17
+
18
+ def single_sort(order_by, order)
19
+ { order_by => { order: order, ignore_unmapped: true } }
20
+ end
21
+
22
+ end
23
+ end
@@ -0,0 +1,3 @@
1
+ module ElasticQueue
2
+ VERSION = '0.0.2'
3
+ end