├── .gitignore ├── .rspec ├── .travis.yml ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── active_elastic.gemspec ├── bin ├── console └── setup ├── lib ├── active_elastic.rb ├── active_elastic │ ├── callbacks.rb │ ├── config.rb │ ├── elastic_schema.rb │ ├── filter_parser.rb │ ├── indexable.rb │ ├── model.rb │ ├── model_importer.rb │ ├── query │ │ ├── base.rb │ │ ├── builder.rb │ │ └── query_methods.rb │ ├── railtie.rb │ ├── record_not_found.rb │ ├── scopable.rb │ ├── version.rb │ └── workers │ │ ├── importer.rb │ │ └── indexer.rb └── tasks │ └── active_elastic_schema.rake └── spec ├── active_elastic └── model_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.1.4 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in active_elastic.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Pixel Perfect Tree 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ActiveElastic 2 | =============== 3 | 4 | ActiveElastic grants to an ActiveRecord models to query and index documents easily to ElasticSearch. 5 | 6 | Requirements 7 | ---------- 8 | 9 | ActiveElastic uses the following gems to work properly: 10 | 11 | * **ActiveRecord** 12 | * **elasticsearch-model** 13 | * **elasticsearch-persitence** 14 | * **Sidekiq**: For document indexing in the background. (Future version will use ActiveJob) 15 | 16 | Installation 17 | ----------- 18 | Add this line to your application's Gemfile: 19 | 20 | ```ruby 21 | gem 'active_elastic', git: 'https://github.com/PixelPerfectTree/active_elastic' 22 | ``` 23 | 24 | And then execute: 25 | 26 | $ bundle install 27 | 28 | 29 | Getting Started 30 | ------------ 31 | ActiveElastic is supposed to work with an ActiveRecord model. 32 | 33 | Include `ActiveElastic::Model` to ActiveRecord models to power it up!. 34 | 35 | class Post < ActiveRecord::Base 36 | include ActiveElastic::Model 37 | end 38 | 39 | Configuration 40 | ------------- 41 | ActiveElastic has a few configuration methods that can be used in an initializer. 42 | 43 | ActiveElastic::Config.configure do |config| 44 | config.index_prefix = nil # Prefix to be use with indexes name. By default is empty. 45 | config.prepend_env_in_index = true # Prepend the current enviroment in document index name. 46 | config.index_document_after_save = false # Auto index the document when the model is modified. 47 | config.use_background_jobs = false # Use Background Jobs for indexing documents (Sidekiq) 48 | config.schema_models = [ # Models to be imported by the schema importer. 49 | :Posts, 50 | :Comments 51 | ] 52 | end 53 | 54 | Sidekiq Queues 55 | ---------- 56 | To use Sidekiq make sure you have this queues in the sidekiq worker: `elastic_search_indexer_worker` and 57 | `elatic_model_importer` 58 | 59 | Query Interface 60 | --------- 61 | ActiveElastic adds methods to build queries for ElasticSeach. 62 | To begin to build this queries there is a `elastic_find` method we need to use. 63 | 64 | ### .all 65 | Execute the current query. By default returns all the documents. 66 | 67 | Post.elastic_find.all 68 | 69 | ### .where(field: value) 70 | Filters documents by a condition. 71 | 72 | Post.elastic_find.where(active: true) 73 | 74 | We also can use a array. This is an alias to `.in` 75 | 76 | Post.elastic_find.where(tags: ['tag1', 'tag2']) 77 | 78 | ### .where_not 79 | Same has `where` but negated. 80 | 81 | ### .first 82 | Return the first document 83 | 84 | ### .find_by(field: value) 85 | Find a document by a single conditions. Raises `ActiveElastic::RecordNotFound` if document is not found. 86 | 87 | ### .find(id) 88 | Find a document by ID. Raises `ActiveElastic::RecordNotFound` if document is not found. 89 | 90 | ### .order(:field) 91 | Add an order condition to the current query. 92 | 93 | ### limit(number) 94 | Add a limit condition to the current query. By default is 10 95 | 96 | ### page(number) 97 | Search document with the offset number useing the current limit. 98 | 99 | ### is_null(:field) 100 | Check if a field is null or missing. 101 | 102 | ### not_null(:field) 103 | Check if a field is not null or missing. 104 | 105 | 106 | ### included_in(field: values) 107 | Add a conditions where a field has to have all values 108 | 109 | Post.elastic_find.inclued_in(tags: ['tag1', 'tags2']) # Find Post that are tagged with tag1 and tag2 110 | 111 | ### not_inclued_in(field: values) 112 | Same as `inclued_in` but negated. 113 | 114 | ### filter_using(hash) 115 | Execute all the query methods inside a hash. 116 | 117 | conditions = { 118 | where: { title "Hello World" }, 119 | where: { active: true }, 120 | order: title 121 | } 122 | 123 | Post.elaastic_find.filter_using(conditions).all 124 | 125 | 126 | Scopes 127 | ------ 128 | We also can define scopes inside the model. 129 | 130 | class Post < ActiveRecord::Base 131 | include ActiveElastic::Model 132 | 133 | elastic_scope :started, -> { where(started: true) } 134 | default_scope, -> { where(active: true) } 135 | end 136 | 137 | Post.elastic_find.started.all 138 | 139 | If we have to do a query without the default scope we can use `unscoped` method to have a query without default scope. 140 | 141 | Post.elastic_find.unscoped.started.all 142 | 143 | Indexing 144 | --------- 145 | 146 | ### Indexing documents 147 | 148 | The `index_document` method calls the indexer worker. 149 | By default the worker will index the document without using a backgorund job. 150 | If `ActiveElastic::Config.use_background_jobs` is true, it will use Sidekiq the enqueue the document for indexation in the `elastic_search_indexer_worker`. 151 | 152 | The indexstion use the `as_indexed_json` method to serialize the object. 153 | 154 | **Note:** Background Jobs are disable in the test enviroment. 155 | 156 | ### Indexing relations 157 | 158 | If we want to include relations inside the indexed document. We need to define which relations will be included. 159 | 160 | To do this we need to define a `index_relations` method inside the model and use `index_relation` method. 161 | 162 | class Post < ActiveRecord::Base 163 | include ActiveElastic::Model 164 | 165 | def index_relations(exlucluded_relations=[]) 166 | index_relation(:comments) unless exclude_relations.include? :comments 167 | end 168 | end 169 | 170 | The indexer will call the `comments` method and serialize its result. 171 | 172 | # Importing Commands 173 | ActiveElastic provides some Rake tasks to manipulate schemas and import DB data to ElasticSearch. 174 | Mosts of this tasks uses the models defined in `ActiveElastic::Config.schema_models`. 175 | 176 | ### rake active_elastic_schema:create_schema 177 | Create the models schema for ElasticSearch 178 | 179 | ### rake active_elastic_schema:drop_schema 180 | Drop all the ElasticSearch schemas. 181 | 182 | ### rake active_elastic_schema:force_create 183 | Will drop and then create all the model schemas. 184 | 185 | ### rake active_elastic_schema:migrate 186 | Imports all the models's data to ElasticSearch. 187 | This job uses Sidekiq's `elatic_model_importer` queue for background Job. 188 | 189 | ### rake active_elastic_schema:import[ModelName] 190 | Imports the data for a single model to ElasticSearch. 191 | This job uses Sidekiq's `elatic_model_importer` queue for background Job. 192 | 193 | ### rake active_elastic_schema:import_now[ModelName] 194 | Imports the data for a single model to ElasticSearch without using a background job. 195 | 196 | ## Testing with ActiveElastic 197 | 198 | ### Using a record cleaner for ElasticSearch. 199 | You can use this helper module for Rspec to reset ElasticSearch for each test. 200 | 201 | # spec/support/elastic_helper.rb 202 | module ElasticHelper 203 | def elastic_cleanable 204 | before :each do 205 | ActiveElastic::ElasticSchema.force_create 206 | end 207 | end 208 | end 209 | 210 | Usage: 211 | 212 | ### spec/rails_helper.rb 213 | .... 214 | RSpec.configure do |config| 215 | config.extend ElasticHelper 216 | ... 217 | 218 | ### /spec/some_spec.rb 219 | describe SomeSpec do 220 | elastic_cleanble 221 | 222 | it do 223 | ... 224 | end 225 | end 226 | 227 | ### Refresh ElasticSearch index when creating updting a document. 228 | 229 | Make sure to use `Model.refresh_index!` to access documents when created in test. Not using this will return nil when retreaving the created document. 230 | 231 | .... 232 | it "this is a example" do 233 | FactoryGirl.create(:post) 234 | Post.refresh_index! # Without this command, the next search would return an empty list of posts. 235 | 236 | expect(Post.elastic_find.all.total).to eq(1) 237 | end 238 | 239 | ## Contributing 240 | 241 | 1. Fork it ( https://github.com/PixelPerfectTree/active_elastic' ) 242 | 2. Create your feature branch (`git checkout -b my-new-feature`) 243 | 3. Commit your changes (`git commit -am 'Add some feature'`) 244 | 4. Push to the branch (`git push origin my-new-feature`) 245 | 5. Create a new Pull Request 246 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | 3 | -------------------------------------------------------------------------------- /active_elastic.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'active_elastic/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "active_elastic" 8 | spec.version = ActiveElastic::VERSION 9 | spec.authors = ["Briam Santiago", "Marcos Mercedes"] 10 | spec.email = ["briam@pixept.com", "marcos@pixelpt.com"] 11 | 12 | if spec.respond_to?(:metadata) 13 | spec.metadata['allowed_push_host'] = "TODO: Set to 'http://mygemserver.com' to prevent pushes to rubygems.org, or delete to allow pushes to any server." 14 | end 15 | 16 | spec.summary = %q{ActiveElastic grants to an ActiveRecord models to query and index documents easily to ElasticSearch.} 17 | spec.description = %q{ActiveElastic grants to an ActiveRecord models to query and index documents easily to ElasticSearch.} 18 | spec.homepage = "TODO: Put your gem's website or public repo URL here." 19 | spec.license = "MIT" 20 | 21 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 22 | spec.bindir = "exe" 23 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 24 | spec.require_paths = ["lib"] 25 | 26 | spec.add_dependency 'elasticsearch-model' 27 | spec.add_dependency 'elasticsearch-rails' 28 | spec.add_dependency 'elasticsearch-persistence' 29 | spec.add_dependency 'sidekiq' 30 | 31 | spec.add_development_dependency "activemodel", "~> 4" 32 | spec.add_development_dependency "activesupport", "~> 4" 33 | spec.add_development_dependency "bundler", "~> 1.8" 34 | spec.add_development_dependency "rake", "~> 10.0" 35 | spec.add_development_dependency "rspec", "~> 3.0" 36 | end 37 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "active_elastic" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start 15 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | 5 | bundle install 6 | 7 | # Do any other automated setup that you need to do here 8 | -------------------------------------------------------------------------------- /lib/active_elastic.rb: -------------------------------------------------------------------------------- 1 | require 'elasticsearch/model' 2 | require 'elasticsearch/persistence' 3 | 4 | require "active_elastic/scopable.rb" 5 | require "active_elastic/query/query_methods.rb" 6 | require "active_elastic/query/builder.rb" 7 | require "active_elastic/query/base.rb" 8 | require "active_elastic/filter_parser.rb" 9 | require "active_elastic/model.rb" 10 | require "active_elastic/config.rb" 11 | require "active_elastic/callbacks.rb" 12 | require "active_elastic/elastic_schema.rb" 13 | require "active_elastic/indexable.rb" 14 | require "active_elastic/model_importer.rb" 15 | require "active_elastic/record_not_found.rb" 16 | require "active_elastic/version.rb" 17 | require "active_elastic/workers/importer.rb" 18 | require "active_elastic/workers/indexer.rb" 19 | require "active_elastic/railtie.rb" if defined?(Rails.version) 20 | -------------------------------------------------------------------------------- /lib/active_elastic/callbacks.rb: -------------------------------------------------------------------------------- 1 | module ActiveElastic 2 | module Callbacks 3 | def self.included(base) 4 | base.class_eval do 5 | after_save lambda { index_document if ActiveElastic::Config.index_document_after_save? } 6 | after_destroy lambda { __elasticsearch__.delete_document rescue nil } 7 | end 8 | end 9 | end 10 | end -------------------------------------------------------------------------------- /lib/active_elastic/config.rb: -------------------------------------------------------------------------------- 1 | module ActiveElastic 2 | class Config 3 | 4 | cattr_writer :index_prefix, :prepend_env_in_index, :index_document_after_save, :use_background_jobs, :schema_models 5 | 6 | class << self 7 | 8 | def configure 9 | yield self if block_given? 10 | end 11 | 12 | def index_prefix 13 | "#{@@index_prefix}_" if @@index_prefix.present? 14 | end 15 | 16 | def prepend_env 17 | "#{Rails.env}_" if prepend_env_in_index? 18 | end 19 | 20 | def prepend_env_in_index? 21 | !!@@prepend_env_in_index 22 | end 23 | 24 | def index_document_after_save? 25 | !!@@index_document_after_save 26 | end 27 | 28 | def use_background_jobs? 29 | !!@@use_background_jobs 30 | end 31 | 32 | def schema_models 33 | @@schema_models 34 | end 35 | 36 | 37 | end 38 | end 39 | 40 | ActiveElastic::Config.prepend_env_in_index = true 41 | ActiveElastic::Config.index_document_after_save = false 42 | ActiveElastic::Config.use_background_jobs = false 43 | ActiveElastic::Config.schema_models = [] 44 | end -------------------------------------------------------------------------------- /lib/active_elastic/elastic_schema.rb: -------------------------------------------------------------------------------- 1 | module ActiveElastic 2 | class ElasticSchema 3 | 4 | class << self 5 | 6 | def create(force = false) 7 | ActiveElastic::Config.schema_models.each { |model| model.to_s.constantize.__elasticsearch__.create_index!(force: force) } 8 | end 9 | 10 | def force_create 11 | create(true) 12 | end 13 | 14 | def drop 15 | ActiveElastic::Config.schema_models.each { |model| model.to_s.constantize.__elasticsearch__.delete_index! } 16 | end 17 | 18 | end 19 | 20 | end 21 | end -------------------------------------------------------------------------------- /lib/active_elastic/filter_parser.rb: -------------------------------------------------------------------------------- 1 | module ActiveElastic 2 | class FilterParser 3 | attr_reader :string 4 | 5 | def initialize(string) 6 | @string = string 7 | end 8 | 9 | def query_conditions 10 | @query_conditions ||= string.split(';') 11 | end 12 | 13 | def conditions 14 | @condition ||= query_conditions.map { |c| Condition.new(c) } 15 | end 16 | 17 | def must_queries 18 | conditions.map { |c| c.to_h if c.positive? }.compact 19 | end 20 | 21 | def must_not_queries 22 | conditions.map { |c| c.to_h if c.negative? }.compact 23 | end 24 | 25 | class Condition 26 | attr_reader :operation 27 | 28 | def initialize(operation) 29 | @operation = operation 30 | end 31 | 32 | def field 33 | @field ||= operation.split(operator).first 34 | end 35 | 36 | def value 37 | @value ||= operation.split(operator).last 38 | end 39 | 40 | def operator 41 | regex = /\w(=|!=|<=|>=|<|>)\w/ 42 | match = operation.gsub(regex) 43 | 44 | return if match.to_a.size != 1 45 | 46 | @operator ||= operation.match(regex)[1] 47 | end 48 | 49 | def to_h 50 | equals = ["=", "!="] 51 | range = ["=>", "<=", "<", ">"] 52 | 53 | if equals.include?(operator) 54 | h_equals 55 | elsif range.include?(operator) 56 | h_range 57 | end 58 | end 59 | 60 | def positive? 61 | !["!="].include?(operator) 62 | end 63 | 64 | def negative? 65 | !positive? 66 | end 67 | 68 | def valid? 69 | operation.present? 70 | end 71 | 72 | def field_value 73 | h = Hash.new 74 | h[field] = value 75 | h 76 | end 77 | 78 | def range_value 79 | range_map = { 80 | "<" => :lt, 81 | "<=" => :lte, 82 | ">" => :gt, 83 | ">=" => :gte, 84 | } 85 | 86 | return if !range_map.has_key?(operator) 87 | 88 | h = Hash.new 89 | h[field] = {} 90 | h[field][range_map[operator]] = value 91 | h 92 | end 93 | 94 | private 95 | 96 | def h_equals 97 | { 98 | query: { 99 | term: field_value 100 | } 101 | } 102 | end 103 | 104 | def h_range 105 | { 106 | range: range_value 107 | } 108 | end 109 | 110 | end 111 | end 112 | end -------------------------------------------------------------------------------- /lib/active_elastic/indexable.rb: -------------------------------------------------------------------------------- 1 | module ActiveElastic 2 | module Indexable 3 | 4 | def self.included(base) 5 | base.extend ClassMethods 6 | end 7 | 8 | # Relations to be indexed after updating the model. 9 | # 10 | # Usage: 11 | # 12 | # def index_relations(exclude_relations: []) 13 | # index_relation(:users) unless exclude_relations.include? :users 14 | # index_relation(:comments) unless exclude_relations.include? :comments 15 | # end 16 | # 17 | 18 | def index_relations(exclude_relations: []); end; 19 | 20 | def index_relation(relation, exclude_relations: []) 21 | relation = self.send(relation) 22 | if relation.respond_to? :each 23 | relation.each { |r| r.index_document(exclude_relations: exclude_relations) } 24 | else 25 | relation.index_document(exclude_relations: exclude_relations) 26 | end 27 | end 28 | 29 | def index_document(exclude_relations: []) 30 | ActiveElastic::Workers::Indexer.index_record(self, exclude_relations: exclude_relations) 31 | end 32 | 33 | module ClassMethods 34 | def refresh_index! 35 | __elasticsearch__.refresh_index! 36 | end 37 | 38 | def import_async 39 | ActiveElastic::Workers::Importer.perform_async(self) 40 | end 41 | 42 | # Relations to be eager loaded when object is imported via ElasticSeach::ModelImporter 43 | def elastic_relations(relations = []) 44 | @elastic_relations ||= relations 45 | end 46 | end 47 | end 48 | end -------------------------------------------------------------------------------- /lib/active_elastic/model.rb: -------------------------------------------------------------------------------- 1 | module ActiveElastic 2 | module Model 3 | 4 | def self.included(base) 5 | base.class_eval do 6 | include Elasticsearch::Model 7 | include Elasticsearch::Model::Indexing 8 | 9 | include ActiveElastic::Callbacks 10 | include ActiveElastic::Indexable 11 | include ActiveElastic::Scopable 12 | extend ClassMethods 13 | 14 | index_name default_index_name 15 | end 16 | end 17 | 18 | module ClassMethods 19 | def default_index_name 20 | "#{ActiveElastic::Config.prepend_env}#{ActiveElastic::Config.index_prefix}#{table_name}" 21 | end 22 | 23 | def elastic_field_for(field) 24 | if self::ELASTIC_RAW_FIELDS.include?(field) 25 | "#{field}_raw.raw" 26 | else 27 | field if column_names.include?(field) 28 | end 29 | end 30 | end 31 | end 32 | end -------------------------------------------------------------------------------- /lib/active_elastic/model_importer.rb: -------------------------------------------------------------------------------- 1 | module ActiveElastic 2 | class ModelImporter 3 | 4 | attr_reader :model 5 | def initialize(model) 6 | @model = model 7 | end 8 | 9 | def import 10 | model.unscoped.order(:id).includes(model.elastic_relations).import(batch_size: 100) 11 | end 12 | 13 | end 14 | end -------------------------------------------------------------------------------- /lib/active_elastic/query/base.rb: -------------------------------------------------------------------------------- 1 | module ActiveElastic 2 | module Query 3 | class Base 4 | 5 | include ActiveElastic::Query::QueryMethods 6 | 7 | def initialize(model, scope, execute_default_scope=false) 8 | @model = model 9 | @scope = scope.new(self) 10 | initialize_defaults 11 | @scope.default_elastic_scope if execute_default_scope && @scope.respond_to?(:default_elastic_scope) 12 | end 13 | 14 | def initialize_defaults 15 | @query = {} 16 | @filtered_query = {} 17 | @filtered_filter = {must: [], must_not: []} 18 | @like = {} 19 | @order = [] 20 | @min_score = nil 21 | end 22 | 23 | def unscoped 24 | initialize_defaults 25 | self 26 | end 27 | 28 | def related(model, fields, options = {}) 29 | options[:min_term_freq] ||= 1 30 | options[:max_query_terms] ||= 12 31 | 32 | @min_score = options[:min_score] || 4 33 | @filtered_query = { 34 | more_like_this: { 35 | docs: [ 36 | { 37 | _index: model.class.index_name, 38 | _type: model.class.to_s.downcase, 39 | _id: model.id 40 | } 41 | ], 42 | fields: fields, 43 | min_term_freq: options[:min_term_freq], 44 | max_query_terms: options[:max_query_terms] 45 | } 46 | } 47 | 48 | self 49 | end 50 | 51 | def find(id) 52 | find_by(:id, id) 53 | end 54 | 55 | def find_by(field, id) 56 | where(field, id).first or raise ActiveElastic::RecordNotFound 57 | end 58 | 59 | def method_missing(method, *args, &block) 60 | scope.send(method, *args, &block) 61 | end 62 | 63 | def filter_using(filters) 64 | filters.compact.each { |filter| self.send(filter.first, filter.last) if filter.last.present? } 65 | self 66 | end 67 | 68 | def paginate(options = {page: 1, per_page: 10}) 69 | per(options[:per_page] || 10) 70 | page(options[:page] || 1) 71 | end 72 | 73 | def first 74 | per(1).page(1).all.first 75 | end 76 | 77 | def all 78 | execute.results 79 | end 80 | 81 | def execute 82 | model.search(build) 83 | end 84 | 85 | def filter(string) 86 | string = string.to_s 87 | must_queries = ActiveElastic::FilterParser.new(string).must_queries 88 | must_not_queries = ActiveElastic::FilterParser.new(string).must_not_queries 89 | 90 | @filtered_filter[:must].concat must_queries 91 | @filtered_filter[:must_not].concat must_not_queries 92 | 93 | self 94 | end 95 | 96 | 97 | def build_query_body 98 | if @filtered_filter || @filtered_query 99 | query_body = { filtered: {} } 100 | query_body[:filtered][:filter] = { bool: @filtered_filter } if @filtered_filter.any? 101 | query_body[:filtered][:query] = @filtered_query if @filtered_query.any? 102 | query_body 103 | end 104 | end 105 | 106 | def build 107 | body = build_query_body 108 | @query[:min_score] = @min_score if @min_score.present? 109 | @query[:query] = body if body.any? 110 | @query[:sort] = @order if @order.any? 111 | @query[:size] = @per if @per 112 | @query[:from] = @offset if @offset 113 | @query 114 | end 115 | 116 | def add_query 117 | 118 | end 119 | 120 | def multi_match(term, fields, options = {}) 121 | if options[:order].present? 122 | order(options[:order]) 123 | else 124 | @order = [] 125 | end 126 | 127 | @min_score = options[:min_score] || 1 128 | 129 | @filtered_query[:multi_match] = { 130 | query: term, 131 | fields: fields, 132 | minimum_should_match: "90%" 133 | } 134 | 135 | @filtered_query[:multi_match][:type] = options[:type] if options[:type].present? 136 | 137 | self 138 | end 139 | 140 | def offset(value) 141 | @offset = value 142 | self 143 | end 144 | 145 | def per(value) 146 | @per = value 147 | self 148 | end 149 | 150 | def limit(value) 151 | per(value) 152 | end 153 | 154 | def page(value) 155 | offset((value - 1) * @per) 156 | self 157 | end 158 | 159 | def order(value) 160 | if value.is_a? Array 161 | value.each do |item| 162 | parse_order_condition(item) 163 | end 164 | else 165 | parse_order_condition(value) 166 | end 167 | self 168 | end 169 | 170 | def where(field, value, condition = true) 171 | if value.is_a? Array 172 | self.in(field, value, condition) 173 | else 174 | add_query_condition({match_phrase: {"#{field}" => value}}, condition) 175 | end 176 | end 177 | 178 | def where_not(field, value, condition = true) 179 | if value.is_a? Array 180 | self.not_in(field, value, condition) 181 | else 182 | add_not_query_condition({match_phrase: {"#{field}" => value}}, condition) 183 | end 184 | end 185 | 186 | def not_null(field, condition = true) 187 | add_condition({exists: {field: "#{field}"}}, condition) 188 | end 189 | 190 | def is_null(field, condition = true) 191 | add_condition({missing: {field: "#{field}"}}, condition) 192 | end 193 | 194 | def in(field, values, condition = true) 195 | add_query_condition( {terms: {"#{field}" => values}}, condition) 196 | end 197 | 198 | def not_in(field, values, condition = true) 199 | add_not_query_condition( {terms: {"#{field}" => values}}, condition) 200 | end 201 | 202 | 203 | def included_in(field, values, condition = true) 204 | add_query_condition({terms: {"#{field}" => values, execution: :and}}, condition) 205 | end 206 | 207 | def not_included_in(field, values, condition = true) 208 | add_not_query_condition({terms: {"#{field}" => values, execution: :and}}, condition) 209 | end 210 | 211 | 212 | def nested_where(relation, field, value, condition = true) 213 | @filtered_filter[:must].push({nested: { 214 | path: "#{relation}", 215 | query: { 216 | bool: { 217 | must: [ 218 | { match: { "#{relation}.#{field}" => value }} 219 | ] 220 | } 221 | } 222 | } 223 | }) 224 | self 225 | end 226 | 227 | def nested_in(relation, field, value, condition = true) 228 | @filtered_filter[:must].push({nested: { 229 | path: "#{relation}", 230 | query: { 231 | bool: { 232 | must: [ 233 | { terms: { "#{relation}.#{field}" => value }} 234 | ] 235 | } 236 | } 237 | } 238 | }) 239 | self 240 | end 241 | 242 | def range(field, range_type, value, condition = true) 243 | add_condition({range: {"#{field}" => { "#{range_type}" => value }}}, condition) 244 | end 245 | 246 | private 247 | 248 | attr_reader :model, :scope 249 | 250 | def add_order_condition(field, direction = :asc) 251 | @order.push("#{field}" => {order: direction}) 252 | end 253 | 254 | def parse_order_condition(condition) 255 | if condition.is_a? Hash 256 | add_order_condition(condition.to_a.first.first, condition.to_a.first.last) 257 | else 258 | add_order_condition(condition) 259 | end 260 | end 261 | 262 | def add_query(hash) 263 | {query: hash} 264 | end 265 | 266 | def add_query_condition(hash, condition) 267 | add_condition(add_query(hash), condition) 268 | end 269 | 270 | def add_condition(hash, condition) 271 | @filtered_filter[:must].push( hash ) if condition 272 | self 273 | end 274 | 275 | def add_not_query_condition(hash, condition) 276 | add_not_condition(add_query(hash), condition) 277 | end 278 | 279 | def add_not_condition(hash, condition) 280 | @filtered_filter[:must_not].push( hash ) if condition 281 | self 282 | end 283 | 284 | end 285 | end 286 | end -------------------------------------------------------------------------------- /lib/active_elastic/query/builder.rb: -------------------------------------------------------------------------------- 1 | module ActiveElastic 2 | module Query 3 | class Builder 4 | def initialize 5 | @query = {} 6 | @filtered = {must: []} 7 | @order = [] 8 | end 9 | 10 | def build 11 | query = {} 12 | query_body = {} 13 | query_body[:bool] = @filtered if @filtered[:must].any? 14 | query[:query] = query_body if query_body.any? 15 | query[:sort] = @order if @order.any? 16 | query[:size] = @per if @per 17 | query[:from] = @offset if @offset 18 | query 19 | end 20 | 21 | def offset(value) 22 | @offset = value 23 | self 24 | end 25 | 26 | def per(value) 27 | @per = value 28 | self 29 | end 30 | 31 | def page(value) 32 | offset((value - 1) * @per) 33 | self 34 | end 35 | 36 | def order(value) 37 | if value.is_a? Array 38 | value.each do |item| 39 | parse_order_condition(item) 40 | end 41 | else 42 | parse_order_condition(value) 43 | end 44 | self 45 | end 46 | 47 | def where(field, value, condition = true) 48 | if value.is_a? Array 49 | self.in(field, value, condition) 50 | else 51 | @filtered[:must].push({match_phrase: {"#{field}" => value}}) if condition 52 | self 53 | end 54 | end 55 | 56 | def in(field, values, condition = true) 57 | @filtered[:must].push({terms: {"#{field}" => values}}) if condition 58 | self 59 | end 60 | 61 | def included_in(field, values, condition = true) 62 | @filtered[:must].push({terms: {"#{field}" => values, execution: :and}}) if condition 63 | self 64 | end 65 | 66 | def nested_where(relation, field, value, condition = true) 67 | @filtered[:must].push({nested: { 68 | path: "#{relation}", 69 | query: { 70 | bool: { 71 | must: [ 72 | { match: { "#{relation}.#{field}" => value }} 73 | ] 74 | } 75 | } 76 | } 77 | }) 78 | self 79 | end 80 | 81 | def nested_in(relation, field, value, condition = true) 82 | @filtered[:must].push({nested: { 83 | path: "#{relation}", 84 | query: { 85 | bool: { 86 | must: [ 87 | { terms: { "#{relation}.#{field}" => value }} 88 | ] 89 | } 90 | } 91 | } 92 | }) 93 | self 94 | end 95 | 96 | def range(field, range_type, value, condition = true) 97 | range_clause = { 98 | "#{range_type}" => value 99 | } 100 | @filtered[:must].push({range: {"#{field}" => range_clause}}) if condition 101 | self 102 | end 103 | 104 | private 105 | 106 | def add_order_condition(field, direction = :asc) 107 | @order.push("#{field}" => {order: direction}) 108 | end 109 | 110 | def parse_order_condition(condition) 111 | if condition.is_a? Hash 112 | add_order_condition(condition.to_a.first.first, condition.to_a.first.last) 113 | else 114 | add_order_condition(condition) 115 | end 116 | end 117 | 118 | end 119 | end 120 | end -------------------------------------------------------------------------------- /lib/active_elastic/query/query_methods.rb: -------------------------------------------------------------------------------- 1 | module ActiveElastic 2 | module Query 3 | module QueryMethods 4 | end 5 | end 6 | end -------------------------------------------------------------------------------- /lib/active_elastic/railtie.rb: -------------------------------------------------------------------------------- 1 | require 'rails' 2 | module ActiceElastic 3 | class Railtie < Rails::Railtie 4 | initializer "active_elastic.configure_rails_initialization" do 5 | 6 | if(defined? ActiveModel::Serializers) 7 | class Elasticsearch::Model::Response::Results 8 | include ActiveModel::ArraySerializerSupport 9 | alias_method :read_attribute_for_serialization, :send 10 | alias_method :length, :size 11 | alias_method :total_entries, :total 12 | end 13 | 14 | class Elasticsearch::Model::Response::Result 15 | include ActiveModel::SerializerSupport 16 | alias_method :read_attribute_for_serialization, :send 17 | end 18 | 19 | class Hashie::Mash 20 | include ActiveModel::SerializerSupport 21 | alias_method :read_attribute_for_serialization, :send 22 | end 23 | end 24 | 25 | end 26 | 27 | rake_tasks do 28 | load "#{File.dirname(__FILE__)}/../tasks/active_elastic_schema.rake" 29 | end 30 | end 31 | end -------------------------------------------------------------------------------- /lib/active_elastic/record_not_found.rb: -------------------------------------------------------------------------------- 1 | class ActiveElastic::RecordNotFound < StandardError 2 | end 3 | -------------------------------------------------------------------------------- /lib/active_elastic/scopable.rb: -------------------------------------------------------------------------------- 1 | module ActiveElastic 2 | module Scopable 3 | def self.included(base) 4 | base.extend ClassMethods 5 | base.class_eval do 6 | 7 | class << self 8 | attr_accessor :query_scope_class 9 | attr_accessor :elastic_query_scope 10 | end 11 | 12 | self.query_scope_class = Class.new do 13 | attr_accessor :query_scope 14 | 15 | def initialize(query_scope) 16 | @query_scope = query_scope 17 | end 18 | 19 | def self.define_scope(name, body) 20 | self.send(:define_method, name) do |*args| 21 | self.instance_exec(*args) do 22 | query_scope.instance_exec(*args, &body) 23 | end 24 | end 25 | end 26 | end 27 | 28 | end 29 | end 30 | 31 | module ClassMethods 32 | def elastic_find 33 | self.elastic_query_scope = ActiveElastic::Query::Base.new(self, self.query_scope_class, true) 34 | end 35 | 36 | def elastic_scope(name, body) 37 | query_scope_class.define_scope(name, body) 38 | end 39 | 40 | def default_elastic_scope(body) 41 | query_scope_class.define_scope(:default_elastic_scope, body) 42 | end 43 | 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/active_elastic/version.rb: -------------------------------------------------------------------------------- 1 | module ActiveElastic 2 | VERSION = "0.1.9" 3 | end 4 | -------------------------------------------------------------------------------- /lib/active_elastic/workers/importer.rb: -------------------------------------------------------------------------------- 1 | require 'sidekiq' 2 | 3 | module ActiveElastic 4 | module Workers 5 | class Importer 6 | 7 | include Sidekiq::Worker 8 | sidekiq_options queue: :elatic_model_importer 9 | 10 | def perform(class_name) 11 | ActiveElastic::ModelImporter.new(class_name.constantize).import 12 | end 13 | 14 | end 15 | end 16 | end -------------------------------------------------------------------------------- /lib/active_elastic/workers/indexer.rb: -------------------------------------------------------------------------------- 1 | require 'sidekiq' 2 | 3 | module ActiveElastic 4 | module Workers 5 | class Indexer 6 | 7 | if defined? Sidekiq 8 | include Sidekiq::Worker 9 | sidekiq_options queue: :elastic_search_indexer_worker 10 | end 11 | 12 | def self.index_record(record, exclude_relations: []) 13 | if (use_background_job?) 14 | self.perform_async(record.id, record.class, exclude_relations) 15 | else 16 | self.index!(record, exclude_relations: exclude_relations) 17 | end 18 | end 19 | 20 | def perform(id, klass_name, exclude_relations=[]) 21 | record = Module.const_get(klass_name).unscoped.find(id) 22 | self.class.index! record, exclude_relations: exclude_relations 23 | end 24 | 25 | def self.index!(record, exclude_relations: []) 26 | record.__elasticsearch__.index_document 27 | record.index_relations(exclude_relations: exclude_relations) 28 | end 29 | 30 | private 31 | def self.use_background_job? 32 | defined?(Sidekiq) && ActiveElastic::Config.use_background_jobs? && (Rails.env.production? || Rails.env.development?) 33 | end 34 | end 35 | end 36 | end -------------------------------------------------------------------------------- /lib/tasks/active_elastic_schema.rake: -------------------------------------------------------------------------------- 1 | namespace :active_elastic_schema do 2 | 3 | desc "Creates the Schema for Elastic Search" 4 | task create_schema: :environment do 5 | ActiveElastic::ElasticSchema.create 6 | end 7 | 8 | desc "Drops the Schema for Elastic Search" 9 | task drop_schema: :environment do 10 | ActiveElastic::ElasticSchema.drop 11 | end 12 | 13 | desc "Drops and Creates the Schema for Elastic Search" 14 | task recreate_schema: :environment do 15 | ActiveElastic::ElasticSchema.force_create 16 | end 17 | 18 | desc "Imports all the models in the schema" 19 | task migrate: :environment do 20 | ActiveElastic::Config.schema_models.each{ |model| model.to_s.constantize.import_async } 21 | end 22 | 23 | desc "Imports data from a given model" 24 | task :import, [:model_klass] => :environment do |t, args| 25 | args[:model_klass].to_s.constantize.import_async 26 | end 27 | 28 | desc "Imports data from a given model" 29 | task :import_now, [:model_klass] => :environment do |t, args| 30 | ActiveElastic::ModelImporter.new(args[:model_klass].to_s.constantize).import 31 | end 32 | 33 | end -------------------------------------------------------------------------------- /spec/active_elastic/model_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe ActiveElastic::Model do 4 | 5 | before(:all) do 6 | ActiveElastic::Config.configure do |c| 7 | c.index_prefix = nil 8 | c.index_document_after_save = true 9 | end 10 | 11 | class FakeModel 12 | 13 | class << self 14 | attr_accessor :prc_save, :prc_destroy 15 | end 16 | 17 | ELASTIC_RAW_FIELDS = [:extra] 18 | 19 | def self.table_name 20 | "fake_models" 21 | end 22 | 23 | def self.model_name 24 | ActiveModel::Name.new(self) 25 | end 26 | 27 | def self.column_names 28 | [:id, :name, :date, :age, :all_star, :extra] 29 | end 30 | 31 | attr_accessor :id, :name, :date, :age, :all_star, :extra, :hobbies, :friends 32 | 33 | def initialize(attrs) 34 | @id = attrs[:id] 35 | @name = attrs[:name] 36 | @date = attrs[:date] 37 | @age = attrs[:age] 38 | @all_star = attrs[:all_star] || false 39 | @extra = attrs[:extra] 40 | @hobbies = attrs[:hobbies] || [] 41 | @friends = attrs[:friends] || [] 42 | end 43 | 44 | def as_indexed_json(options = {}) 45 | { 46 | id: id, 47 | name: name, 48 | date: date, 49 | age: age, 50 | all_star: all_star, 51 | extra: extra, 52 | raw_extra: extra && extra.parameterize(' '), 53 | hobbies: hobbies, 54 | friends: friends 55 | } 56 | end 57 | 58 | def save 59 | self.class.prc_save.each{|p| self.instance_exec(&p) } 60 | end 61 | 62 | def destroy 63 | self.class.prc_destroy.each{|p| self.instance_exec(&p) } 64 | end 65 | 66 | def self.index_document; end; 67 | 68 | def self.delete_document; end; 69 | 70 | def self.after_save(prc) 71 | @prc_save ||= [] 72 | @prc_save.push(prc) 73 | end 74 | 75 | def self.after_destroy(prc) 76 | @prc_destroy ||= [] 77 | self.prc_destroy.push(prc) 78 | end 79 | 80 | include ActiveElastic::Model 81 | 82 | settings do 83 | mappings dynamic: true do 84 | indexes :extra_raw, type: :string, analyzer: :english, fields: { raw: { type: :string, index: :not_analyzed } } 85 | end 86 | end 87 | 88 | elastic_scope :all_stars, -> { where(:all_star, true) } 89 | 90 | # Every query method receives an extra boolean parameters which indicates if this condition should be included in the query 91 | elastic_scope :only_all_stars, -> (valid = true) { where(:all_star, true, valid) } 92 | 93 | default_elastic_scope -> { range(:age, :gt, 18) } 94 | end 95 | 96 | end 97 | 98 | before(:each) do 99 | FakeModel.__elasticsearch__.create_index!(force: true) 100 | end 101 | 102 | let(:model) { FakeModel.new(id: 1, name: "Dude", date: Date.today, age: 9000) } 103 | 104 | let(:model_list) { 105 | [ 106 | FakeModel.new(id: 1, name: "I'm the number one Dude", date: Date.today, age: 7000, all_star: true, extra: 'Rand'), 107 | FakeModel.new(id: 2, name: "Dude", date: Date.today, age: 7000, all_star: true, extra: 'rand'), 108 | FakeModel.new(id: 3, name: "Marcos", date: Date.today, age: 7000, all_star: true, extra: 'oil'), 109 | FakeModel.new(id: 4, name: "Briam", date: Date.today, age: 1000, all_star: true, extra: 'gas'), 110 | FakeModel.new(id: 5, name: "Dude", date: Date.today, age: 5000, all_star: true, extra: 'ruby'), 111 | FakeModel.new(id: 6, name: "Dude", date: Date.today, age: 2000, all_star: false, extra: 'Rails'), 112 | FakeModel.new(id: 7, name: "I'm the lucky one", date: Date.today, age: 5000, all_star: false, extra: 'Elastic Search'), 113 | FakeModel.new(id: 8, name: "Dude", date: Date.today, age: 9000, all_star: false), 114 | FakeModel.new(id: 9, name: "Dude", date: Date.today, age: 5000, all_star: false), 115 | FakeModel.new(id: 10, name: "Dude", date: Date.today, age: 9000, all_star: false) 116 | 117 | ] 118 | } 119 | 120 | let(:full_model_list) { 121 | FakeModel.new(id: 1, name: "nested", date: Date.today, age: 7000, all_star: true, extra: 'rand', 122 | hobbies: [ 123 | { id: 1, name: 'baseball'}, { id: 2, name: 'madden'}, 124 | ], 125 | friends: [ 126 | { id: 1, name: 'ruby'}, { id: 2, name: 'rails'}, 127 | ]) 128 | } 129 | 130 | it "can has instances" do 131 | expect( model ).not_to be_nil 132 | end 133 | 134 | context "query methods" do 135 | before(:each) do 136 | model_list.each{|m| m.save } 137 | FakeModel.refresh_index! 138 | end 139 | 140 | describe "filter_using" do 141 | it "applies dynamic scopes to the query from a given hash" do 142 | filter = { 143 | paginate: {page: 1, per_page: 3}, 144 | only_all_stars: true, 145 | } 146 | 147 | documents = FakeModel.elastic_find.filter_using(filter).all 148 | 149 | expect(documents.size).to eq 3 150 | expect(documents.total).to eq 5 151 | end 152 | end 153 | 154 | context "pagination" do 155 | describe "paginate" do 156 | it "paginates results by indicating how many items should be per page and items from which page should be returned" do 157 | expect( FakeModel.elastic_find.order(:id).paginate({page: 2, per_page: 3}).all.size ).to eq 3 158 | end 159 | end 160 | 161 | describe "total" do 162 | it "indicates the total number of documents that the matching the query" do 163 | expect( FakeModel.elastic_find.order(:id).paginate({page: 2, per_page: 3}).all.total ).to eq 10 164 | end 165 | end 166 | 167 | describe "limit" do 168 | it "limits the number of documents matching the query criteria" do 169 | expect( FakeModel.elastic_find.order(:id).limit(7).all.size ).to eq 7 170 | end 171 | end 172 | 173 | describe "offset" do 174 | it "skips some documents from the beginning of the resultset" do 175 | expect( FakeModel.elastic_find.order(:id).offset(3).all.size ).to eq 7 176 | end 177 | end 178 | 179 | describe "per / page" do 180 | it "paginates results by indicating how many items should be per page and items from which page should be returned" do 181 | expect( FakeModel.elastic_find.order(:id).per(4).page(3).all.size ).to eq 2 182 | end 183 | end 184 | end 185 | 186 | describe "all" do 187 | it "returns a list of documents" do 188 | expect( FakeModel.elastic_find.all.size ).to eq 10 189 | end 190 | end 191 | 192 | describe "first" do 193 | it "returns the first item from the query" do 194 | expect( FakeModel.elastic_find.first ).not_to be_nil 195 | end 196 | end 197 | 198 | describe "find" do 199 | it "finds a record by it's id" do 200 | expect( FakeModel.elastic_find.find(7).name ).to eq "I'm the lucky one" 201 | end 202 | end 203 | 204 | describe "find_by" do 205 | it do 206 | expect( FakeModel.elastic_find.find_by(:name, 'Marcos').name ).to eq "Marcos" 207 | expect( FakeModel.elastic_find.find_by(:name, 'Briam').name ).to eq "Briam" 208 | end 209 | end 210 | 211 | describe "order" do 212 | context "defaault sorting" do 213 | it do 214 | orderded_list = FakeModel.elastic_find.order(:id).all 215 | expect( orderded_list.first._source.id ).to eq 1 216 | expect( orderded_list.to_a.last._source.id ).to eq 10 217 | end 218 | end 219 | 220 | context "with sorting direction" do 221 | it do 222 | orderded_list = FakeModel.elastic_find.order(id: :desc).all 223 | expect( orderded_list.first._source.id ).to eq 10 224 | expect( orderded_list.to_a.last._source.id ).to eq 1 225 | end 226 | end 227 | 228 | context "multiple sorting" do 229 | it do 230 | orderded_list = FakeModel.elastic_find.order([:age, :id]).all 231 | expect( orderded_list.first._source.id ).to eq 4 232 | expect( orderded_list.to_a.last._source.id ).to eq 10 233 | expect( orderded_list.size ).to eq 10 234 | end 235 | end 236 | end 237 | 238 | describe "where" do 239 | context "equals value" do 240 | it do 241 | expect( FakeModel.elastic_find.where(:all_star, false).all.size ).to eq 5 242 | end 243 | end 244 | 245 | context "in array of values" do 246 | it do 247 | expect( FakeModel.elastic_find.where(:age, [1000, 7000]).all.size ).to eq 4 248 | end 249 | end 250 | end 251 | 252 | describe "where_not" do 253 | context "not equals value" do 254 | it do 255 | expect( FakeModel.elastic_find.where_not(:all_star, false).all.size ).to eq 5 256 | end 257 | end 258 | 259 | context "not in array of values" do 260 | it do 261 | expect( FakeModel.elastic_find.where_not(:age, [1000, 7000]).all.size ).to eq 6 262 | end 263 | end 264 | end 265 | 266 | describe "not_null" do 267 | it do 268 | expect( FakeModel.elastic_find.not_null(:extra).all.size ).to eq 7 269 | end 270 | end 271 | 272 | describe "is_null" do 273 | it do 274 | expect( FakeModel.elastic_find.is_null(:extra).all.size ).to eq 3 275 | end 276 | end 277 | 278 | describe "in" do 279 | it do 280 | expect( FakeModel.elastic_find.in(:age, [1000, 2000, 9000]).all.size ).to eq 4 281 | end 282 | end 283 | 284 | describe "not_in" do 285 | it do 286 | expect( FakeModel.elastic_find.not_in(:age, [1000, 2000, 9000]).all.size ).to eq 6 287 | end 288 | end 289 | 290 | describe "included_in" do 291 | skip "TODO" 292 | end 293 | 294 | describe "not_included_in" do 295 | skip "TODO" 296 | end 297 | 298 | describe "nested_where" do 299 | skip "TODO" 300 | end 301 | 302 | describe "nested_in" do 303 | skip "TODO" 304 | end 305 | 306 | describe "range" do 307 | it do 308 | expect( FakeModel.elastic_find.range(:age, 'lt', 1000).all.size ).to eq 0 309 | expect( FakeModel.elastic_find.range(:age, 'lte', 2000).all.size ).to eq 2 310 | expect( FakeModel.elastic_find.range(:age, 'gt', 2000).all.size ).to eq 8 311 | expect( FakeModel.elastic_find.range(:age, 'gte', 2000).all.size ).to eq 9 312 | end 313 | end 314 | 315 | describe "query method chain" do 316 | it do 317 | expect( 318 | FakeModel.elastic_find.range(:age, 'gt', 2000).not_null(:extra).order(:id).all.size 319 | ).to eq 5 320 | end 321 | end 322 | end 323 | 324 | context "methods" do 325 | describe "elastic_field_for" do 326 | it do 327 | expect( FakeModel.elastic_field_for(:extra) ).to eq "extra_raw.raw" 328 | expect( FakeModel.elastic_field_for(:age).to_s ).to eq 'age' 329 | end 330 | end 331 | end 332 | 333 | context "configurations" do 334 | describe "index_name" do 335 | it do 336 | expect(FakeModel.index_name).to eq "test_fake_models" 337 | end 338 | end 339 | end 340 | 341 | context "indexing" do 342 | describe "index/update" do 343 | 344 | it "Indexes new documents and updates existing documents" do 345 | #index 346 | #"Indexes a document in ES" 347 | model.index_document 348 | FakeModel.refresh_index! 349 | expect( FakeModel.elastic_find.find(model.id).name ).to eq "Dude" 350 | 351 | #Update 352 | #Update the metadate of an existing document in ES 353 | model.name = "Big Fella" 354 | model.index_document 355 | FakeModel.refresh_index! 356 | expect( FakeModel.elastic_find.find(model.id).name ).to eq "Big Fella" 357 | end 358 | end 359 | 360 | describe "delete_document" do 361 | it "Deletes an indexed document" do 362 | model.save 363 | FakeModel.refresh_index! 364 | expect(FakeModel.elastic_find.all.size).to eq 1 365 | model.destroy 366 | FakeModel.refresh_index! 367 | expect(FakeModel.elastic_find.all.size).to eq 0 368 | end 369 | end 370 | end 371 | 372 | context "scopes" do 373 | describe "elastic_find/default scope" do 374 | it do 375 | model.save 376 | FakeModel.refresh_index! 377 | expect(FakeModel.elastic_find.all.size).to eq 1 378 | end 379 | end 380 | 381 | context "custom scopes" do 382 | it do 383 | model.save 384 | FakeModel.refresh_index! 385 | expect( FakeModel.elastic_find.all_stars.all.size ).to eq 0 386 | model.all_star = true 387 | model.save 388 | FakeModel.refresh_index! 389 | expect( FakeModel.elastic_find.all_stars.all.size ).to eq 1 390 | end 391 | end 392 | 393 | context "unscoped" do 394 | it "finds documents ignoring the default scope" do 395 | FakeModel.new(id: 2, name: "Young dude", date: Date.today, age: 15).save 396 | FakeModel.refresh_index! 397 | expect( FakeModel.elastic_find.unscoped.all.size ).to eq 1 398 | end 399 | end 400 | end 401 | 402 | end -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 2 | 3 | require 'bundler/setup' 4 | Bundler.setup 5 | 6 | 7 | module Rails 8 | def self.env 9 | :test 10 | end 11 | 12 | def self.development? 13 | false 14 | end 15 | 16 | def self.production? 17 | false 18 | end 19 | 20 | def self.test? 21 | true 22 | end 23 | end 24 | 25 | require 'active_support/all' 26 | require 'active_model' 27 | require 'elasticsearch/model' 28 | require 'elasticsearch/persistence' 29 | require 'active_elastic' 30 | --------------------------------------------------------------------------------